Predchozi kapitola

Obsah

Konec

Nasledujici kapitola

5. FUNKCE

5.1. Deklarace a definice funkce.
5.2.
Navratova hodnota funkce.
5.3.
Argumenty funkci a zpusob jejich predavani.
5.4.
Funkce s promennym poctem argumentu.
5.5.
Rekurse.


V teto kapitole si podrobneji rozebereme zakladni stavebni kamen jazyka C - funkci. Z predchoziho textu vime, ze kazdy C program obsahuje alespon jednu funkci - main(). A mame co vysvetlovat. Proc je za main napsana dvojice zavorek? Tak pozname funkci od identifikatoru jineho typu (nebo od vyrazu predstavujiciho adresu vstupniho bodu funkce). Z ANSI C vyplyva jeste jedna povinnost, funkce main() vraci hodnotu typu int.

Funkce v sobe zahrnuje takove prikazy, ktere se v programu casto opakuji a proto se vyplati je vyclenit, pojmenovat a volat. Takto byla funkce chapana zejmena v dobach, kdy byla pamet pocitacu mala a muselo se s nizachazet velmi hospodarne. I dnes muzeme pouzivat funkce ze stejnych duvodu. Nicmene se na funkce divame ponekud jinak.

Predevsim rozlisujme standardni funkce, uzivatelske funkce a podpurne a nadstavbove funkce1.

Standardni funkce jsou definovany normou jazyka a vyrobce prekladace je dodava jako soucast programoveho baliku tvoriciho prekladac a jeho podpurne programy a soubory. Tyto standardni funkce zpravidla dostavame jako soucast standardnich knihoven (proto se jim nekdy rika knihovni funkce) a jejich deklarace je popsana v hlavickovych souborech. Nemame k dispozici jejich zdrojovy kod. Ostatne nas ani moc nezajima. Tyto funkce se preci maji chovat tak, jak definuje norma.

Uzivatelske funkce jsou ty funkce, ktere jsme napsali a mame jejich zdrojove texty. Pokud jsme profesionaly, vyplati se nam nase funkce precizne dokumentovat a archivovat. Kdyz uz jsme je jednou napsali a odladili, muzemeje priste s duverou jiz jen pouzivat. Muze byt ucelne sdruzovat vice uzivatelskych funkci do jednoho, pripadne nekolika, souboru pripadne z nich vytvorit knihovnu, abychom je pri kazdem pouziti nemuseli znovu prekladat. Hlavickove soubory (prototypy funkci, uzivatelske typy, definice maker, ...) jsou opet nezbytnosti.

Podpurne a nadstavbove funkce jako polozka v rozdeleni funkci nam takove pekne rozdeleni okamzite znici. Sem totiz zaradime nejen funkce od tzv. tretich vyrobcu (podpora pro spolupraci s databazemi, volani modemu, faxu, uzivatelske rozhrani, ...) ale i rozsireni prekladace o funkce nedefinovane normou jazyka, funkce implementacne zavisle a podobne. Nakonec sem patri i nase funkce, ktere pouzivame jako podpurne. Jako posledni bod si muzeme predstavit tymovou praci, kdy pouzivame funkce kolegu. O jejich zdrojovy text se zajimame az v pripade, kdy se funkce nechovaji tak, jak maji2.

Vratme se ale k poslani funkci. Funkce odpovidaji strukturovanemu programovani. Predstavuji zakladni jednotku, ktera resi nejaky problem. Pokud je problem prilis slozity, vola na pomoc jinou (jine) funkce. Z toho plyne, ze by funkce nemela byt prilis rozsahla. Pokud tomu tak neni, nejenze se stava neprehlednou, ale je i obtizne modifikovatelnou. Jak se napriklad dotkne zmena casti rozsahle funkce jeji jine casti?

Pokud chceme jit na uroven modularniho programovani, je zde soubor. Ten muze obsahovat vice funkci, ktere navzajem spolupracuji. To co ze souboru chceme dat k dispozici jinym jednotkam, umistime jako deklarace do hlavickoveho souboru.

Definovane funkce maji status extern. Jsou pripojitelne do jinych jednotek, at chceme nebo ne. ANSI C vyzaduje prototyp kazde funkce, kterou chceme pouzit3. Tento pozadavek vyrazne zvysuje bezpecnost - umoznuje typovou kontrolu.

5.1. Deklarace a definice funkce.zpet

Po obecnem uvodu, v nemz jsme si shrnuli vyznam funkci, se podivejme, jak se prakticky s funkcemi pracuje. Zacneme klasicky, uvedenim syntaktickeho zapisu deklarace funkce:

typ jmeno([seznam argumentu]);

kde

- typ predstavuje typ navratove hodnoty funkce
- jmeno je identifikator, ktery funkci davame
- () je povinna dvojice omezujici deklaraci argumentu (v tomto pripade prazdnou)

seznam argumentu je nepovinny - funkce nemusi mit zadne argumenty, muze mit jeden nebo vice argumentu, nebo take muzeme urcit, ze funkce ma promenny pocet argumentu4

Nez se v jednotlivych podkapitolach budeme podrobneji venovat kazde polozce z deklarace funkce, popisme si jmeno funkce hned.

Identifikator funkce je proste jmeno, pod kterym se budeme na funkci odvolavat. Zavorkami za identifikatorem (toto se tyka vyskytu identifikatoru funkce mimo deklaraci ci definici) davame najevo, ze funkci volame. Pokud zavorky neuvedeme, jde o adresu funkce. Adresou funkce rozumime adresu vstupniho bodu funkce.

V teto kapitole jsme zatim popsali jen deklaraci funkce. Ta ovsem patri do hlavickoveho souboru. Ostatne strednik, ktery ji ukoncuje, nam nedava prostor pro samotnou definici seznamu deklaraci a prikazu, ktere telo funkce obsahuje.

Definice funkce, na rozdil od jeji deklarace, nema za zavorkou ukoncujici seznam argumentu strednik, ale blok. Tento blok se nazyva telo funkce. V uvodu muze, jako jakykoliv jiny blok, obsahovat definice promennych (jsou v ramci bloku lokalni, neuvedeme-li jinak, jsou umisteny v zasobniku). Pak nasleduje posloupnost prikazu. Ty definuji chovani (vlastnosti) funkce. Pri definici funkce vytvarime (definujeme) jeji kod, ktery obsazuje pamet.

Deklarace funkce popisuje vstupy a vystupy, ktere funkce poskytuje5. Funkce nema provadet akce s jinymi daty, nez ktera ji predame jako argumenty. Soucasne vystupy z funkce maji probihat jen jako jeji navratova hodnota a nebo (mysleno pripadne i soucasne) prostrednictvim argumentu funkce. Pokud se funkce nechova uvedenym zpusobem rikame, ze ma vedlejsi ucinky. Tech se vyvarujme. Casto vedou k chybam6.

Pokud uvedeme pouze definici funkce, na kterou se pozdeji v souboru odvolavame, slouzi tato definice soucasne jako deklarace.

V pripade neshody deklarace a definice funkce ohlasi prekladac chybu.

5.2. Navratova hodnota funkce.zpet

Pojem datovy typ intuitivne zname. Chceme-li ziskat navratovou hodnotu funkce, musime urcit, jakeho datoveho typu tato hodnota je. Na typ funkce nejsou kladena zadna omezeni. Pokud nas navratova hodnota funkce nezajima, tak ji proste nepouzijeme7. Pokud ovsem chceme deklarovat, ze funkce nevraci zadnou hodnotu, pak pouzijeme klicove slovo void, ktere je urceno prave pro tento ucel.

Krome urceni datoveho typu musime take urcit hodnotu, ktera bude navratovou. Je to snadne, tato hodnota musi byt vysledkem vyrazu uvedeneho jako argument prikazu return. Typ return vyrazu se pochopitelne musi shodovat, nebo byt alespon slucitelny, s deklarovanym typem navratove hodnoty funkce.

Vyraz, nasledujici za return, muze byt uzavren v zavorkach. To je klasicky K&R styl. ANSIC norma zavorky nevyzaduje:

return vyraz_vhodneho_typu;

Spojeno s definici funkce, muze funkce celociselne vracejici druhou mocninu sveho celociselneho argumentu vypadat takto:

int isqr(int i)
{
return i * i;
}

Prikaz return se v tele funkce nemusi vyskytovat pouze jedenkrat. Pokud to odpovida vetveni ve funkci, muze se vyskytovat na konci kazde vetve. Rozhodne vsak prikaz return ukonci cinnost funkce, umisti navratovou hodnotu na specifikovane misto8 a preda rizeni programu bezprostredne za mistem, z nehoz byla funkce volana.

5.3. Argumenty funkci a zpusob jejich predavani.zpet

Deklarace vsech formalnich argumentu funkci musi obsahovat datovy typ. Neni mozne uvest spolecny typ pro vice nasledujicich identifikatoru.

Korespondence mezi skutecnymi a formalnimi argumenty odpovida jejich poradi pri volani a v definici. To je ostatne stejne i v ostatnich vyssich programovacich jazycich.

Pro slucitelnost formalnich argumentu se skutecnymi musime dodrzet nasledujici pravidla (rikaji nam zpusob predavani argumentu). Pro zkraceni pouzijeme FA pro formalni a SA pro skutecny argument:

  1. FA int -> SA int, char, short
  2. FA double -> SA double, float
  3. FA pole -> preda se adresa 1. prvku (obsah celeho pole nelze predat hodnotou)
  4. Struktury (struct, union) se predavaji hodnotou jako celek.
  5. Funkce nemohou byt predavany hodnotou.

Podstatny je zpusob predavani argumentu funkci. Hodnoty skutecnych argumentu jsou predany do zasobniku. Formalni argumenty se odkazuji na odpovidajici mista v zasobniku (kopie skutecnych argumentu). Zmena hodnoty formalniho argumentu se nepromitne do zmeny hodnoty argumentu skutecneho! Tomuto zpusobu predavani argumentu se rika predavani hodnotou. Problem vznika v okamziku, kdy potrebujeme, aby funkce vratila vice nez jednu hodnotu9. Reseni pomoci globalni promenne jsme jiz drive odmitli (vedlejsi ucinky) a tvorba noveho datoveho typu jako navratove hodnoty nemusi byt vzdy prirozenym resenim.

Reseni je predavani nikoli hodnot skutecnych argumentu, ale jejich adres. Formalni argumenty pak sice budou ukazateli na prislusny datovy typ, ale s tim si poradime. Podstatnejsi je moznost zmeny hodnot skutecnych argumentu, zpusob nazyvany treba v Pascalu volani odkazem. V C se hovori o volani adresou, coz je zcela jasne a vystihujici.

Ukazme si jednoduchy program, ktery vola ruzne funkce jak hodnotou, tak adresou.

/****************************************/
/* soubor fn_argho.c                    */
/* pro procviceni predavani argumentu   */
/* hodnotou i "odkazem" v jazyce C.     */
/****************************************/

#include <stdio.h>

int nacti(int *a, int *b)
{
 printf("\nzadej dve cela cisla:");
 return scanf("%d %d", a, b);
} /* void nacti(int *a, int *b) */
float dej_podil(int i, int j)
{
 return((float) i / (float) j);
} /* float dej_podil(int i, int j) */

int main(void)
{
 int c1, c2;
 if (nacti(&c1, &c2) == 2)
       printf("podil je : %f\n", dej_podil(c1, c2));
 return 0;
} /* main */

Funkce nacti() musi vracet dve nactene hodnoty. Predava proto argumenty adresou10. Funkce dej_podil() naopak skutecne argumenty zmenit nesmi (nema to ostatne smysl). Proto jsou ji predavany hodnotou. Povsimneme si, ze navratovy vyraz je v ni uzavren v zavorkach. Nevadi to, i kdyz jsou zavorky nadbytecne. Vyraz je preci ukoncen strednikem.

Povsimneme si jeste jednou pozorneji funkce nacti(). Jeji navratova hodnota slouzi k tomu, aby potvrdila platnost (respektive neplatnost) argumentu, ktere predava adresou. To je v C velmi praktikovany obrat (viz scanf()).

5.4. Funkce s promennym poctem argumentu.zpet

Urcime-li typ a identifikator argumentu funkce, je vse jednoduche. Co vsak v okamziku, kdy potrebujeme funkci s promennym poctem argumentu. Mozne to jiste je, nebot jsou standardni funkce, ktere si s takovou skutecnosti umi poradit. Napriklad oblibene I/O funkce scanf()/printf().

Dosud vime, ze hodnoty skutecnych argumentu jsou umistovany do zasobniku. Pokud tedy dokazeme to, co diky deklaraci formalnich argumentu prekladac umi, tedy provest jejich jednoznacne prirazeni, mame vyhrano.

Problemem vsak je, ze diky deklaraci i volajici misto vi, jakeho typu ocekava volana funkce argumenty. Podle toho je preda. Pri promennem poctu argumentu je predani nesnadne. Jak v miste volani (skutecne argumenty), tak v miste prijeti (formalni).

ANSI C pamatuje na popsanou situaci nekolika makry a pravidly, Diky nim si temer nemusime uvedomit, ze pracujeme se zasobnikem (ukazatelem na nej). Jedine, co k tomu potrebujeme, je mit mezi argumenty funkce nejaky pevny bod (posledni pevny argument pro ziskani spravneho ukazatele do zasobniku). Dalsi argumenty budeme deklarovat jako promenne (tri tecky). Pak pouzijeme standardni makra. Pro jednoduchost zpracovani jsou typy hodnot predavanych skutecnych argumentu konvertovany na int a double.

Makra pro promenny pocet argumentu (videli jsme i pojem vypustky, ale my prece argumenty nevypoustime, my pouze dopredu nezname jejich pocet) jsou tri, va_start, va_arg a va_end. Co predstavuji? Nejprve uvedeme jejich deklarace, pote popiseme vyznam:

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

kde

- va_list formalne predstavuje pole promennych argumentu
- va_start nastavi ap na prvni z promennych argumentu predanych funkci (probiha fixace na posledni nepromenny deklarovany a predany argument lastfix)
- va_arg se jako makro rozvine ve vyraz stejneho typu a hodnoty, jako je dalsi ocekavany argument (type)
- va_end umozni volane funkci provest pripadne operace pro bezproblemovy navrat zpet - return

Poznamenejme, ze pri pouziti techto maker musi byt volany v poradi va_start - pred prvnim volanim va_arg nebo va_end. Po ukonceni nacitani by mel byt volan va_end.

Dale poznamenejme, ze va_end muze ap zmenit tak, ze pri jeho dalsim pouziti musime znovu volat va_start.

Pocet skutecne predanych hodnot muze byt predan jako posledni nepromenny argument, nebo technikou nastaveni zarazky.

Program, vyhodnocujici pro konkretni nezavislou promennou polynom urceny svymi koeficienty, nasleduje. x by mohlo byt predano jako posledni (fixacni) argument, pak by nebyl treba nepotrebny retezec. Slo nam vsak o to, ukazat prevzeti argumentu ruzneho typu. Komentar k programu je tentokrat obsazen ve zdrojovem textu. Proto nebudeme program dale rozebirat.

/****************************************/
/* soubor FN_ARGX.C                     */
/* ukazuje pouziti funkce s promennym   */
/* poctem argumentu ruznych typu        */
/****************************************/

#include <stdio.h>
#include <stdarg.h>

const int konec = -12345;

double polynom(char *msg, ...)
/****************************************/
/* vycisli polynom zadany jako:         */
/* an*x^n + an-1*x^n-1 + ... a1*x + a0  */
/* nejprve je uvedeno x pak koeficienty */
/* kde                                  */
/*      koeficienty an jsou typu int    */
/*      hodnota x je typu double        */
/* pro ukonceni je pouzita zarazka konec*/
/****************************************/
{
 double hodnota = 0.0, x;
 va_list ap;
 int koef;
 va_start(ap, msg);
 x = va_arg(ap, double);
 koef = va_arg(ap, int);
 while (koef != konec)
 {
      hodnota = hodnota * x + koef;
      koef = va_arg(ap, int);
 }
 va_end(ap);
 return hodnota;
}

int main(void)
{
 double f;
 char *s;
 s = "polynom stupne 2";
f = polynom(s, 2.0, 2, 3, 4, konec);
      /* 2x^2 + 3x + 4,x=2 -> 18      */
 printf("%s = %lf\n", s, f);
 s = "dalsi polynom";
f = polynom(s, 3.0, 1, 0, 0, 0, konec);
      /* x^3          ,x=3 -> 27      */
 printf("%s = %lf\n", s, f);
 return 0;
}

5.5. Rekurse.zpet

Diky umisteni kopii skutecnych argumentu na zasobnik a umisteni lokalnich promennych tamtez se nam nabizi moznost rekurse.

Rekurse znamena volani funkce ze sebe same, at primo, nebo prostrednictvim konecne posloupnosti volanim funkci dalsich. K tomu je nutne, aby programovaci jazyk mel vyse uvedene vlastnosti.

Prednosti rekurse je prirozene reseni problemu takovym zpusobem, jaky odpovida realite ci pouzitemu algoritmu. Nevyhodou jsou casto vysoke pametove naroky na zasobnik. S predavanim hodnot do zasobniku a naslednym obnovenim jeho stavu souvisi i znacna, zejmena casova, rezie rekurse.

Je ponechano na nasi uvaze, pouzijeme-li rekursi ci problem vyresime klasickymi postupy. Plati totiz veta, ze kazda rekurse jde prevest na iteraci.

Castym prikladem rekurse je vypocet hodnoty faktorialu prirozeneho cisla ci urceni hodnoty zvoleneho clenu Fibonacciho posloupnosti. Podivejme se na prvni z nich. V uloze je pouzit umyslne jiny datovy typ pro argument (long) a pro vysledek (double). Rozsireni rozsahu hodnot, pro nez faktorial pocitame je vedlejsi efekt. Chteli jsme zejmena ukazat pouziti ruznych datovych typu.

/************************/
/* soubor FACT_R.C      */
/* faktorial rekursivne */
/************************/
#include <stdio.h>
double fact(long n)
{
 if (n == 0l)
       return 1.0F; /* 1 jako double konstanta */
 else
       return n * fact(n-1);
}
int main(void)
{
 static long n;
 printf("\nPro vypocet faktorialu zadej prirozene cislo n:");
 scanf("%ld", &n);
 printf("%ld! = %lf\n", n, fact(n));
 return 0;
}

Za povsimnuti stoji skutecnost, ze je pouzita jedina promenna. Ta urcuje hodnotu, pro kterou faktorial pocitame. Pri tisku hodnoty vysledku je pouzito primo umisteni volani fact() jako jednoho z argumentu printf(). Rekursivni funkce fact() ma pochopitelne formalni argument.

Pokud pri rekursi vyzadujeme sdileni nejake promenne vsemi stejnymi rekursivnimi funkcemi, muzeme pro takovou promennou deklarovat pametovou tridu static. Promenna pak neni umistena v zasobniku, ale v datovem segmentu programu, kde ma vyhrazeno pevne misto. Toto misto je spolecne vsem vnorenym rekursim funkce. Nejjednodussi moznosti ukazky pouziti staticke lokalni promenne je pocitani hloubky rekurse.

Muzeme napriklad rozsirit funkci fact() o statickou celociselnou promennou h. Pri kazdem volani funkce fact() budeme zobrazovat jak hodnotu h, tak i hodnotu n. Pocatecni inicializace neni explicitne nutna. Staticke promenne inicializuje nulou sam prekladac. Uvadime jen zmenenou funkci fact() a vystup programu pro zadanou hodnotu 4. Ostatni je oproti predchozimu prikladu nezmeneno.

double fact(long n)
{
 static int h;

double navrat;
 printf("hloubka=%4d\tn=%4ld\n", ++h, n);
 if (n == 0l)
       navrat = 1.0F;
 else
       navrat = n * fact(n-1);
 printf("hloubka=%4d\tn=%4ld\tnavrat=%.0lf\n", h--, n, navrat);
 return navrat;
}

/*
Pro vypocet faktorialu zadej prirozene cislo n:4
hloubka=	1	n=	4
hloubka=	2	n=	3
hloubka=	3	n=	2
hloubka=	4	n=	1
hloubka=	5	n=	0
hloubka=	5	n=	0	navrat=1
hloubka=	4	n=	1	navrat=1
hloubka=	3	n=	2	navrat=2
hloubka=	2	n=	3	navrat=6
hloubka=	1	n=	4	navrat=24
4! = 24.000000
*/

Vysvetlivky:

1 Nejjednodussi mozne cleneni zni - standardni funkce a ostatni funkce.
2 Nebo jak jsme z dokumentace usoudili, ze se chovat maji, a kolega je zrovna nedosazitelny, aby nam nejasnost vysvetlil nebo funkci opravil.
3 To se pochopitelne tyka tech funkci, ktere dosud nebyly v souboru, obsahujicim zdrojovy text, definovany. Presto nevadi, zahrneme-li shodnou deklaraci funkce vicekrat.
4 Pozdeji v teto kapitole.
5 Souhrnne je nazyvame rozhrani funkce.
6 Navic k chybam tezce odhalitelnym.
7 Delame to casto, napriklad I/O funkce scanf()/printf() hodnotu vraci.
8 Do zasobniku, programator se o umisteni obvykle nestara.
9 Obdobny problem vznika pri predavani rozsahleho pole. Predstava kopirovani jeho obsahu do zasobniku je strasna (viz pravidlo 3. slucitelnosti FA a SA).
10 Pozor na scanf()!


Predchozi kapitola

Obsah

Zacatek

Nasledujici kapitola


Nazev: Programovani v jazyce C
Autor: Petr Saloun
Do HTML prevedl: Kristian Wiglasz
Posledni uprava: 29.11.1996