Funkce

Funkce jsou základním stavebním kamenem jazyka C. Zatím jste se setkali s funkcí main() a s funkcemi printf() a scanf(). V této kapitole se především naučíte vytvářet vlastní funkce. K popisu funkcí, definovaných normou ANSI C, které již překladač obsahuje, se dostanu později.

Standardní funkce jsou obsaženy ve standardních knihovnách (například printf() a scanf() jsou deklarovány v souboru <stdio.h>).

Překladač je obvykle nainstalován i se spoustou dalších funkcí, jež nejsou v normě ANSI C. Jejich použití si musíte nastudovat v dokumentaci. Jsou to funkce závislé jak na překladači, tak mnohdy na operačním systému, takže jejich použitím se značně snižuje přenositelnost kódu.

Funkce se při spuštění programu usídlí kdesi v paměti. Při její volání program přeskočí do funkce a po jejím provedení se vrátí za místo, kde byla funkce volána. Pokud voláte funkci v programu vícekrát, znamená to jen to, že přeskakujete vícekrát na to samé místo v paměti. Vytvářením funkcí tak šetříte paměť počítače, ale šetříte i sobě práci, protože tělo funkce napíšete jen jednou a v programu použijete kolikrát potřebujete. Na druhou stranu, každé volání funkce stojí nějaký (velmi malý) čas na „odskočení si“. Pokud funkci voláte v cyklu tisíckrát, už je to 1000x malý čas, a to je trochu znát.

Například:

for (x = 0; x < 1000; x++)
{
       printf("Cyklus cislo: ");
       printf("%i\n",x);
}

… je pomalejší než …

for (x = 0; x < 1000; x++)
{
       printf("Cyklus cislo: %i\n",x);
}

Úspora místa a zpřehlednění programu je mnohem důležitější, než časová režie spojená s voláním funkce. Navíc platí, že při změně je daleko snazší a bezpečnější (z hlediska možných programátorských chyb) změnit jednu funkci, než několik míst v programu. Pokud však vytváříte malou funkci, kde je její volání v některých místech kritické, je někdy výhodné ji nahradit pomocí maker (ovšem se všemi jejich záludnostmi). Změna makra pak znamená změnu ve všech místech programu, kde je makro použito, stejně jako změna funkce znamená změnu chování programu ve všech místech, kde je funkce volána.

Jazyk C++ tento problém řeší elegantněji pomocí tzv. inline funkcí. Jak už jsem psal, makra se v C++ moc nenosí.

Definice funkce

Definice funkce vypadá takto:

navratovy_typ jmeno ([parametry, ...])
{
       telo funkce 
}
Funkce

Funkce v C/C++

Jméno funkce slouží k jejímu identifikování. Při volání funkce v programu musíme uvést za jménem funkce kulaté závorky, a to i tehdy, když funkce nemá žádné argumenty. Samotné jméno funkce bez závorek totiž reprezentuje adresu v paměti, kde funkce začíná (kam si program odskočí, když je funkce volána). Toho se dá využít v odkazech na funkci (o tom ale až později).

Parametry funkce popisují očekávaná data, která bude funkce zpracovávat. Každý parametr má své jméno a musí být určen jeho datový typ. Pokud funkce nemá žádné parametry, uvádí se v závorkách slůvko void (ale není to povinné). Pokud je jich více než jeden, oddělují se čárkou. Existují i funkce s proměnlivým počtem argumentů.
Parametrem může být celé nebo racionální číslo, znak, struktura, nebo ukazatele. Nemůže to být například pole, které obsahuje x prvků, ale může to být ukazatel na začátek pole.

Funkce má jednu návratovou hodnotu. Její typ se uvádí před jménem funkce. Například funkce main() má vždy návratovou hodnotu typu int. Pokud nechcete, aby funkce vracela nějaká data, jako návratovou hodnotu uveďte void.

Funkci lze kdykoliv ukončit pomocí příkazu return, za který se uvádí hodnota nebo výraz, jehož výsledek se stane návratovou hodnotou funkce. Pokud funkce žádnou návratovou hodnotu nemá, uvádí se return bez hodnoty nebo výrazu (ukončený středníkem). Pokud funkce má návratovou hodnotu, je použití return na konci funkce povinné a za return musí být výraz, jenž po vyhodnocení musí mít stejný datový typ, jako je datový typ návratové hodnoty funkce.

Po ukončení funkce, ať již příkazem return, nebo tím že se vykonají všechny příkazy v jejím těle, se pokračuje v provádění kódu za místem kde byla funkce volána (program skočí zpět z těla funkce na místo, odkud byla funkce volána). Pokud však příkazem return ukončíte funkci main(), ukončíte program.

Návratová hodnota funkce main() se vrací operačnímu systému. Zaběhnutá praxe je, že návratová hodnota 0 znamená úspěšné ukončení programu, jakákoliv jiná hodnota určuje číslo chyby (to už je čistě na programátorovi programu, aby vymyslel různým chybám různá čísla).

/*------------------------------------------------*/
/* c13/fce1.c                                     */
#include <stdio.h>

void tecka(int pocet)
{
    if (pocet <= 0)
        return;        /* kdyby byl pocet unsigned int,
                          bylo by tohle zbytecne */

    /* kdyz se podivate na podminku v cyklu tak
     * zjistite, ze je to zbytecne stejne :-) */

    for (; pocet > 0; pocet--)
        printf(". ");
}

int mocnina(int x)
{
    return x * x;
}

int main(void)
{
    tecka(10);
    printf("-5^2 = %i\n", mocnina(-5));
    return 0;
}

/*------------------------------------------------*/

Výstup programu:

. . . . . . . . . . -5^2 = 25

Na řádku 23 se volá funkce tecka(), které se předá do argumentu pocet číslo 10. Program si odskočí na řádku 5. Pokud by byl pocet záporný, tak na řádce 8 vyskočí z funkce a pokračuje na řádku 24. Protože pocet nebyl záporný, funkce pokračovala ve svém těle. Když ukončila cyklus a dostala se na konec svého těla (řádek 14), vrátilo se provádění programu na řádku 24.

Programu je při spuštěný vyhrazena určitá část paměti, kam si poznamenává, kde v provádění svého kódu skončil, než si odskočí do funkce, aby věděl, kam se zase vrátit. Navíc do této paměti uloží hodnoty argumentů funkce a funkce do této paměti ukládá svůj výsledek (návratovou hodnotu). Této paměti se říká zásobník (stack).
Když ve funkci zavoláte další funkci a v té zase další funkci …, zásobník se pomalu zaplňuje. Když z funkce vyskočíte, zásobník se zase uvolňuje. (Podobně jako když do zásobníku strkáte patrony. Poslední vložená patrona do zásobníku se z něj vystřelí jako první).

Deklarace funkce

Při deklaraci funkce se uvádí pouze datový typ návratové hodnoty, jméno funkce a typy argumentů. To je vše, co potřebuje překladač k tomu, aby její volání mohl do programu zapsat. Mohou se uvést i názvy argumentů, ale to není nutné. Než je funkce v programu volána, musí být deklarována, ale definována může být až později.

Pokud funkci předem nedeklarujete, ale rovnou definujete, je definice zároveň i deklarací. Pokud se deklarace s pozdější definicí neshodují, oznámí překladač chybu.

V následujícím programu se funkci nejdříve deklaruje, použije se v jiné funkci a pak se teprve definujeme.

/*------------------------------------------------*/
/* c13/deklar.c                                   */
#include <stdio.h>


float vypocet(int, float, float *);     /* deklarace funkce */

int main(void)
{
    float a, b;

    a = vypocet(5, (float) 0.3, &b);
    printf("Soucet = %5.2f\nNasobek = %5.2f\n", a, b);

    return 0;
}

float vypocet(int a, float b, float *c)
{
    float f;
    *c = (float) a *b;
    f = (float) a + b;
    b = 55.5;                   /* menim lokalni promenou b,
                                   to nema s promennou b ve funkci
                                   main nic spolecneho */

    return f;
}

/*------------------------------------------------*/

Výstup z programu:

Soucet =  5.30
Nasobek =  1.50

Tak malý příklad a tolik toho ukazuje. Platnost lokálních proměnných jen uvnitř funkce, využití návratové hodnoty funkce, dopřednou deklaraci funkce a použití ukazatele. Funkci jsem jako třetí argument předal adresu proměnné b a díky tomu mohla funkce na tuto adresu uložit nějakou hodnotu (v příkladě násobek čísel a a b).
Přiřazení na řádku 23 je zbytečné, protože se s proměnnou b v těle funkce už dál nepracuje a po skončení funkce se hodnota „ztratí“ (příští volání funkce vypocet() nastaví zase b dle druhého přiřazeného argumentu).

Příklad taky ukazuje, jak je blbé pojmenovávat proměnné a,b,c … Alespoň by stálo za to přemenovat f třeba na soucet a c na nasobek. Oč by byl pak program čitelnější.

Rekurzívní funkce

Pokud funkce volá ve svém těle samu sebe, nebo je volána funkcí, kterou volá ve svém těle, hovoříme o rekurzi. Data, která jsou funkci předávána, se ukládají do takzvaného zásobníku.
To je nějaké vyhrazené místo v paměti. Po skončení funkce se data ze zásobníku zase odstraní. Pokud však funkce během svého vykonávání zavolá samu sebe, pak se do zásobníku umístí další data a tak stále dokola. To je samozřejmě velice náročné na paměť a může vést až ke zhroucení programu (pokud dojde místo vyhrazené pro zásobník). Použití rekurze je sice efektní, ale ne vždy efektivní. Proto je třeba mít opravdu dobrý důvod pro používání rekurzí. Následuje ilustrativní příklad. V něm dobrý důvod pro použití rekurze určitě není :-).

  1. /*------------------------------------------------*/
  2. /* c13/rekurz.c                                   */
  3. #include <stdio.h>
  4.  
  5. float prvni_funkce(int, float); /* deklarace */
  6.  
  7. float druha_funkce(int a, float f)
  8. {                               /* definice */
  9.     printf("Vola se druha funkce\n");
  10.     return prvni_funkce(a, f * (a + 1));
  11. }
  12.  
  13. float prvni_funkce(int a, float f)
  14. {                               /* definice */
  15.     printf("Prvni funkce a= %2i, f=%5.2f\n", a, f);
  16.     if (a <= 0)
  17.         return f;
  18.     return druha_funkce(--a, f);
  19. }
  20.  
  21. int main(void)
  22. {
  23.     printf("Vysledek je %f\n", prvni_funkce(5, 5.0));
  24.     return 0;
  25. }
  26.  
  27. /*------------------------------------------------*/

Výstup z programu:

Prvni funkce a=  5, f= 5.00
Vola se druha funkce
Prvni funkce a=  4, f=25.00
Vola se druha funkce
Prvni funkce a=  3, f=100.00
Vola se druha funkce
Prvni funkce a=  2, f=300.00
Vola se druha funkce
Prvni funkce a=  1, f=600.00
Vola se druha funkce
Prvni funkce a=  0, f=600.00
Vysledek je 600.000000

V příkladě se počítá jakási číselná řada. Druhá funkce zde slouží jen k nějakému výpisu a k novému zavolání funkce prvni_funkce(). Berte to jenom jako příklad, jistě byste dokázali daný problém vyřešit rozuměji.

Bez dopředné deklarace prvni_funkce() byste tuto funkci nemohli použít v těle druhe_funkce() a naopak.

Mimochodem, napadlo vás, co by se stalo, kdyby se volání return druha_funkce(--a, f); změnilo na return druha_funkce(a--, f);?
V takovém případě by se volala druha_funkce() s hodnotou proměnné a, která by nebyla snížena o jedničku. Ta by se měla snížit až po zavolání funkce. Výsledkem by byl nekonečný cyklus vzájemného volání funkcí. Zkuste si to a uvidíte, jak rychle vám program klekne :-).

Na podobné problémy si musíte dávat pozor. Logické chyby programátora se odhalují mnohem hůře, než syntaktické chyby jazyka C (ty ostatně odhalí již překladač). Při programování v C/C++ prostě nemůžete být nikdy dost obezřetní.

Klasickým příkladem na rekurzi je výpočet faktoriálu (avšak i faktoriál lze vypočítat elegantně bez rekurze).

  1. /*------------------------------------------------*/
  2. /* c13/faktor.c                                   */
  3. #include <stdio.h>
  4.  
  5. long double faktorial(long double x)
  6. {
  7.     if (x == 0L)
  8.         return 1L;
  9.     return x * faktorial(x - 1L);
  10. }
  11.  
  12. int main(void)
  13. {
  14.     int i;
  15.     long double f;
  16.     for (i = 0; i <= 40; i++) {
  17.         f = faktorial(i);
  18.         printf("%2i! = %050.0Lf (%0.3Le)\n", i, f, f);
  19.     }
  20.     return 0;
  21. }
  22.  
  23. /*------------------------------------------------*/

Výstup je trochu zkrácen:

 0! = 00000000000000000000000000000000000000000000000001 (1.000e+00)
 1! = 00000000000000000000000000000000000000000000000001 (1.000e+00)
 2! = 00000000000000000000000000000000000000000000000002 (2.000e+00)
 3! = 00000000000000000000000000000000000000000000000006 (6.000e+00)
 4! = 00000000000000000000000000000000000000000000000024 (2.400e+01)
 5! = 00000000000000000000000000000000000000000000000120 (1.200e+02)
 6! = 00000000000000000000000000000000000000000000000720 (7.200e+02)
 7! = 00000000000000000000000000000000000000000000005040 (5.040e+03)
 8! = 00000000000000000000000000000000000000000000040320 (4.032e+04)

…

35! = 00000000010333147966386144929973846968255251480576 (1.033e+40)
36! = 00000000371993326789901217462530208167145295052800 (3.720e+41)
37! = 00000013763753091226345045735828383554804299857920 (1.376e+43)
38! = 00000523022617466601111725872220378936271647539200 (5.230e+44)
39! = 00020397882081197443356844789080046496991166857216 (2.040e+46)
40! = 00815915283247897734263888042887576837447481294848 (8.159e+47)

Možná jste si po prohlédnutí výsledků všimli, že na výpočtech není něco v pořádku. Nejsou totiž příliš přesné. Platí přeci, že 40! = 39!*40, ovšem 20397882081197443356844789080046496991166857216*40 =
815915283247897734273791563201859879646674288640
a ne
815915283247897734263888042887576837447481294848 (a že je to pořádná chyba).

Je to smutné, ale jazyk C neumí počítat s racionálními čísly moc přesně.

Je možné, že s vaším překladačem dostanete přesnější výsledky (například s Visual Studiem), zde popsaný problém se ale týká každého překladače - jen se chyba projeví trochu později (s většími čísly).

Je to dáno tím, jak jsou čísla v paměti uložena a jak se s nimi počítá. Když třeba vydělíte 10/3 a výsledek znovu vynásobíte třemi, vyjde vám 9.999 …

Čísla typu float/double mají zaručenou přesnost cca na 14 „nejvýznamnějších“ míst. (Záměrně neříkám desetinných míst, neboť to záleží na tom, s jak velkým/malým číslem pracujete). Vzhledem k velikosti čísla je chyba sice zanedbatelná, ovšem pokud budete programovat řekněme pro nějaký bankovní úřad, asi by z vás nikdo neměl radost.
Naštěstí lze přesně počítat s celočíselnými proměnnými, takže lze podobně velká čísla zpracovávat pomocí několika celočíselných proměnných (a složitých algoritmů). Existují knihovny funkcí, které jsou zaměřené na přesné výpočty s velkými čísly.

Za domácí úkol napište funkci faktorial() bez použití rekurze. Bude vám k tomu stačit pár lokálních proměnných a cyklus dle libosti.

Proměnlivý počet argumentů

Zatímco doteď jste při deklaraci nebo definici funkce určovali jaké bude mít argumenty, nyní se naučíte vytvořit funkci bez přesného počtu a typu argumentů. Vzpomeňte si například na funkce printf() a scanf(), které také mají proměnlivý počet argumentů.

Argumenty funkce se ukládají do zásobníku. Aby mohla funkce k těmto argumentům přistupovat, musíte ji předat alespoň místo, kde zásobník začíná. Proto musí mít funkce s proměnlivým počtem argumentů minimálně jeden argument napevno daný. To, že mohou následovat další argumenty, se při definici nebo deklaraci označí pomocí tří teček (tzv. výpustka).

Pro práci s nedeklarovanými argumenty existuje standardní knihovna <stdarg.h>. V ní jsou definovány tři makra. Zde jsou jejich deklarace:

void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);

Výraz va_list vnímejte jako datový typ proměnné ap. Je to proměnná, která reprezentuje nedeklarované argumenty (list všech argumentů). Proměnnou ap byste si mohli pojmenovat jakkoliv, ale je dobrým zvykem zůstat u ap. Když si na to zvyknete, je pak čtení takového kódu jednodušší.

Makro va_start inicializuje proměnnou ap pomocí poslední napevno deklarované proměnné ve funkci. Ta je předána jako druhý argument – last (díky tomu získá potřebnou adresu do zásobníku s argumenty).

Makro va_arg vrací hodnotu dalšího argumentu v řadě. Jakého typu má být se musí určit druhým argumentem type. Díky tomu bude makro vědět, kolik bytů načíst (a o kolik se posunout na začátek dalšího argumentu funkce).
Toto makro můžete volat vícekrát, maximálně však tolikrát, kolik je předáno funkci při volání nedeklarovaných argumentů. Jinak byste se pokoušeli přečíst data, která již nepatří k argumentům funkce.

Makro va_end zajišťuje bezproblémové ukončení práce s ap. Ačkoliv vám program poběží pravděpodobně i bez volání tohoto makra, nikdy na něj nezapomínejte. V určitých kritických situacích by se vám to vymstilo a po půl roce byste takovou chybu v programu jen těžko hledali. (va_end může například uvolňovat paměť o kterou požádalo va_start).

Nyní se nabízí otázka, jak zjistí funkce s proměnlivým počtem argumentů, jaké jí byli argumenty předány (jakého typu) a také kolik. Možností je hned několik. Například známá funkce printf() má vždy jako první argument řetězec (přesněji řečeno ukazatel na řetězec; řetězec je znakové pole a pole nelze předávat jako argument funkce).
V tomto řetězci hledá speciální sekvence (%s,%c) a podle nich zjišťuje, že má mít další argument a také jakého typu. A tak je přesně dáno, o kolik argumentů a jakých typů si funkce řekne.
Další možností, jak určit počet předaných argumentů je, že při deklaraci funkce deklarujete jeden parametr (minimálně jeden stejně vždy musíte mít), který bude obsahovat počet proměnných. Další možností je například definování nějaké zarážky. Třeba že vždy jako poslední argument bude ukazatel s hodnotou NULL, nebo číslo nula (tak se pracuje s textovými řetězci) atp. To už je jen na vás.

  1. /*------------------------------------------------*/
  2. /* c13/fce2.c                                     */
  3. #include <stdio.h>
  4. #include <stdarg.h>
  5. #include <stdbool.h>
  6.  
  7. int maximum(const int pocet, const bool znamenko, ...)
  8. {
  9.     va_list ap;
  10.     int i;
  11.     int maxS, dalsiS;
  12.     unsigned int maxU, dalsiU;
  13.  
  14.     if (pocet <= 0)
  15.         return 0;
  16.  
  17.     /* znamenko je posledni deklarovana promenna */
  18.     va_start(ap, znamenko);
  19.  
  20.     if (znamenko)
  21.         maxS = va_arg(ap, int);
  22.     else
  23.         maxU = va_arg(ap, unsigned int);
  24.  
  25.     for (i = 0; i < pocet - 1; i++) {
  26.         if (znamenko) {
  27.             dalsiS = va_arg(ap, int);
  28.             maxS = (maxS < dalsiS) ? dalsiS : maxS;
  29.         } else {
  30.             dalsiU = va_arg(ap, unsigned int);
  31.             maxU = (maxU < dalsiU) ? dalsiU : maxU;
  32.         }
  33.     }
  34.  
  35.     va_end(ap);
  36.     if (znamenko)
  37.         return maxS;
  38.     else
  39.         return (signed int) maxU;
  40. }
  41.  
  42. int main(void)
  43. {
  44.     printf("Maximum = %i\n", maximum(3, true, 7, 9, -2));
  45.     printf("Maximum = %i\n", maximum(7, true, -5, 8, -2, 5, -6, -1, 4));
  46.     printf("Maximum = %u\n", (unsigned int) maximum(7, false, -5, 8, -2, 5, -6, -1, 4));
  47.     return 0;
  48. }
  49.  
  50. /*------------------------------------------------*/

Parametry pocet a znamenko jsem deklaroval jako konstantní, aby bylo jasné, že není vhodné jejich hodnotu ve funkci měnit.

Výstup z programu:

Maximum = 9
Maximum = 8
Maximum = 4294967295

Funkce maximum() je navržená tak, že zvládne najít maximální hodnotu pro čísla se znaménkem i bez. To je ale špatný návrh z několika důvodů. Za prvé, návratová hodnota je definována jako se znaménkem, cože je při výpočtu bez znaménka matoucí a výlsedek se musí přetypovávat. Za druhé, tělo funkce to dost zesložiťuje a znepřehledňuje. Taky se vám zvětšuje počet parametrů (kvůli parametru znamenko).
Mnohem lepší by bylo mít dvě funkce - jednu pro int a jednu pro unsinged int.
Funkce by měli, pokud možno, dělat jen jednu, jasně danou, činnost. Důvod je zase zlepšení čitelnosti programu. Jednou napsanou funkci můžete konec konců používat v mnoha programech. Čím je funkce jednodušší, tím je větší šance, že ji budete moci použít i jinde bez dodatečných úprav.

Komentář Hlášení chyby
Created: 29.8.2003
Last updated: 20.6.2014
Tato stánka používá ke svému běhu cookies, díky kterým je možné monitorovat, co tu provádíte (ne že bych to bez cookies nezvládl). Také vás tu bude špehovat google analytics. Jestli si myslíte, že je to problém, vypněte si cookies ve vašem prohlížeči, nebo odejděte a už se nevracejte :-). Prohlížením tohoto webu souhlasíte s používáním cookies. Dozvědět se více..