Funkcie

Alebo: Ako neprogramovať jednu vec viackrát

Doteraz, keď sme v programovaní chceli niečo zopakovať viackrát, tak sme použili cyklus. Niekedy však potrebujeme danú vec robiť na rôznych miestach. Napríklad vypisovať čísla 1 až 10 v rôznych častiach programu.

int main() {
    // radi by sme napisali desat cisel na zaciatku programu
    for (int i = 0; i < 10; ++i)
        printf("%d ", i + 1);
    printf("\n");

    int n;
    scanf("%d", &n);
    if (n < 0) {
        // potom raz v pripade, ze uzivatel zada zaporne cislo
        for (int i = 0; i < 10; ++i)
            printf("%d ", i + 1);
        printf("\n");
    }

    for (int j = 0; j < n; ++j) {
        // alebo n krat, ked uzivatel zada kladne n
        for (int i = 0; i < 10; ++i)
            printf("%d ", i + 1);
        printf("\n");
    }
}

Čo je na vyššie uvedenom programe zle?

Veľakrát používa rovnaký kód. A to máme ešte štastie, že opakujeme len trikrát a opakujúci sa kód je dlhý len tri riadky. Aj tak, keď si zmyslíme, že nechceme vypisovať 10 čísel ale 11, tak to treba všade prepísať. Fuj.

Teraz si predstavme, že robíme niečo podstatne zložitejšie a na 200 miestach by sme mali rovnaký 10 riadkový blok. Keby sme tam chceli niečo zmeniť, tak sa zbláznime. A určite pri nejakom prepise spravíme chybu. A nemáme šancu tú chybu nájsť.

Proste copy-pastovanie kódu je zlo a chceme sa mu vyhnúť, lebo tvorí neprehľadné a chybné programy. Ale ako sa mu vyhneme?

Celkom dobrý spôsob je zabaliť kus kódu do funkcie ­— nejako si ten kus kódu nazvať. Funkciu môžeme chápať ako samostatný program. Proste postupnosť príkazov, ktorá niečo robí. Konieckoncov všetky naše doterajšie programy boli len funkcia int main().

Funkciu vyrobíme tým, že ju zadeklarujeme v tvare <typ> <meno>(<argumenty>){<príkazy>} Spúšťame (voláme, po anglicky call) funkcie <meno>(<hodnoty-argumentov>)

Podivný program zhora vieme prepísať napríklad takto:

// nasledujuca funkcia je typu void, vola sa vypis_desat_cisel a nema ziadne argumenty

void vypis_desat_cisel() {
    for (int i = 0; i < 10; ++i)
        printf("%d ", i + 1);
    printf("\n"); 
}

// s funkciou main uz sme sa stretli
int main() {
    // zavolame funkciu
    vypis_desat_cisel()

    int n;
    scanf("%d", &n);
    if (n < 0)
        vypis_desat_cisel()

    for(int j = 0; j<n; ++j) 
        vypis_desat_cisel()
}

Dostali sme oveľa prehľadnejší kód a ak chceme napríklad zmeniť počet vypisovaných čísel, stačí to spraviť na jednom mieste.

Čo je to void a vôbec ten typ?

Funkcia môže vracať nejakú hodnotu a ten typ je typ hodnoty, akú funkcia vracia. void znamená, že funkcia nevracia nič. Funkcia, ktorá niečo vracia, môže byť napríklad funkcia osem() z nasledujúceho príkladu. Isto si všimnete, že na vrátenie hodnoty slúži príkaz return. Príkaz return zároveň skončí vykonávanie funkcie, preto všetko, čo je za ním, sa už nevykoná.

int osem() {
    return 8;
}

int devat() {
    return 9;
    printf("tento prikaz sa nevykona\n");
}

void nic_nerob() {
    return; // pri void funkcii nemame co vratit, tak piseme return; bez hodnoty
    printf("ani tento\n");
}

int main() {
    // navratove hotnoty mozeme vyuzivat vo vyrazoch:

    printf("funkcia osem vracia %d\n", osem());
    int a = devat();
    printf("a je %d\n", a);

    // V jazyku C sa musel na konci main() pisat aj tento riadok:
    return 0;
    // V C++ pokial nic nevratime, vrati sa nejaka defaultna hodnota, napr. 0,
    // takze ten riadok pisat nemusime
}

Zatiaľ tie funkcie nie sú veľmi úžasné, lebo robia stále to isté (asi ako programy bez vstupu). A stále nevieme, čo sú to tie argumenty.

Tak poďme obe veci vyriešiť zároveň. Argumenty sú akoby vstup pre funkciu. Už sme sa s tým stretli, keď sme napríklad funkcii printf() do zátvoriek zadávali, čo má vypísať. Analogicky namiesto funkcie vypis_desat_cisel() môžeme použiť všeobecnejšiu funkciu:

void vypis_cisla(int pocet) {
    for (int i = 0; i < pocet; ++i)
        printf("%d ", i+1);
    printf("\n");
}

int main(){
    vypis_cisla(10);

    // takto vieme pouzivat funkciu na rozne pocty vypisovanych cisel
    vypis_cisla(10 + 7);
    int n;
    scanf("%d", n);
    vypis_cisla(n);
}

Funkcia môže mať ľubovoľný počet argumetov, pri deklarácii(vyrábaní) funkcie ich jednoducho vymenujeme medzi zátvorky () v rovnakom tvare, ako pri deklarácii premenných, oddelené čiarkami. Následne ich môžeme používať ďalej vo funkcii, ako nové premenné (t.j. ich môžeme napríklad aj meniť).

Pri volaní funkcie píšeme medzi zátvorky hodnoty alebo výrazy, samozrejme musí sa zachovať typ.

int scitaj(int a, int b, int c) {
    // prehodíme hodnoty premennych, len tak pre srandu
    int pomocna = a;
    a = b;
    b = c;
    c = pomocna;
    return a + b + c;
}

int main() {
    int a = 5;
    printf("%d\n", scitaj(1 + 7, 4 * a, a));
}

Veľmi užitočná vlasnosť funkcií je rekurzívne volanie, teda, že funkcia sa može volať opakovne sama seba. Toto vieme využiť napríklad aj na počítanie všeliakých zaujímavých vecí bez toho aby sme museli nejak významne premýšľať.

Fibonacciho postupnosť, ${F_n}$, je veľmi zaujímavá číselná postupnosť, definovaná takto: $F_0 = 0$, $F_1 = 1$, ak $i>1$ potom $F_i = F_{i-1} + F_{i-2}$. Bez toho aby sme tejto postupnosti hlbšie porozumeli, môžeme ju zapísať ako funkciu.

int F(int n) {
    if (n < 2)
        return n;
    return F(n - 1) + F(n - 2);
}

Zjavne F(n) $= F_n$. Nie je to super? Jediný problém tejto funkcie je, že nie je veľmi rýchla, na vašich počítačoch sa rozumne rýchlo zvládajú čísla pre $n \leq 40$. Neskôr sa naučíme, ako sa tomuto neduhu pri funkciách vyhnúť.

Funkcia sa môžu volať aj navzájom, nie však takto:

void a() {
    b(); // ! chyba ! funkcia b nie je deklarovana.
}

void b() {
    a();
}

Volať funkciu môžeme až po deklarácií, ale naštastie môžeme deklarovať funkciu pred definovaním (napísaním tela) takto:

void b();

void a() {
    b();
}

void b() {
    a();
}

Samozrejme tieto funkcie a, b nemajú veľký úžitok, lebo po zavolaní jednej z funkcií sa program zacyklí až sa časom minie pamäť do ktorej sa zapísujú zavolané funkcie a program spadne.

Cvičenie: Napíšte funkciu, ktorá počíta $n! = 1\cdot 2\cdot\dots\cdot(n-1)\cdot n$ bez použitia cyklov.

Riešenie
int faktorial(int n) {
    if (n < 2) return 1;
    return n * faktorial(n - 1);
}