5.1. Deklarace a definice
funkce
5.2. Návratová hodnota funkce
5.3. Argumenty funkcí a způsob jejich
předávání
5.4. Funkce s proměnným počtem argumentů
5.5. Rekurse
V této kapitole si podrobněji rozebereme základní stavební kámen jazyka C -
funkci. Z předchozího textu víme, že každý C program obsahuje alespoň jednu funkci -
main()
. A máme co vysvětlovat. Proč je za main
napsána dvojice
závorek? Tak poznáme funkci od identifikátoru jiného typu (nebo od výrazu
představujícího adresu vstupního bodu funkce). Z ANSI C vyplývá ješte
jedna povinnost, funkce main()
vrací hodnotu typu int
.
Funkce v sobě zahrnuje takové příkazy, které se v programu často opakují, a proto se vyplatí je vyčlenit, pojmenovat a volat. Takto byla funkce chápána zejména v dobách, kdy byla paměť počítačů malá a muselo se s ní zacházet velmi hospodárně. I dnes můžeme používat funkce ze stejných důvodů, nicméně se na funkce díváme poněkud jinak.
Především rozlišujme standardní funkce, uživatelské funkce a podpůrné a nadstavbové funkce1.
Standardní funkce jsou definovány normou jazyka a výrobce překladače je dodává jako součást programového balíku tvořícího překladač a jeho podpůrné programy a soubory. Tyto standardní funkce zpravidla dostáváme jako součást standardních knihoven (proto se jim někdy říká knihovní funkce) a jejich deklarace je popsána v hlavičkových souborech. Nemáme k dispozici jejich zdrojový kód. Ostatně nás ani moc nezajímá. Tyto funkce se přece mají chovat tak, jak definuje norma.
Uživatelské funkce jsou ty funkce, které jsme napsali a máme jejich zdrojové texty. Pokud jsme profesionály, vyplatí se nám naše funkce precizně dokumentovat a archivovat. Když už jsme je jednou napsali a odladili, můžeme je příště s důvěrou již jen používat. Může být účelné sdružovat více uživatelských funkcí do jednoho, případně několika souborů, případně z nich vytvořit knihovnu, abychom je při každém použití nemuseli znovu překládat. Hlavičkové soubory (prototypy funkcí, uživatelské typy, definice maker, ...) jsou opět nezbytností.
Podpůrné a nadstavbové funkce jako položka v rozdělení funkcí nám takové pěkné rozdělení okamžitě zničí. Sem totiž zařadíme nejen funkce od tzv. třetích výrobců (podpora pro spolupráci s databázemi, volání modemu, faxu, uživatelské rozhraní ...), ale i rozšíření překladače o funkce nedefinované normou jazyka, funkce implementačně závislé a podobně. Nakonec sem patří i naše funkce, které používáme jako podpůrné. Jako poslední bod si můžeme představit týmovou práci, kdy používáme funkce kolegů. O jejich zdrojový text se zajímáme až v případě, kdy se funkce nechovají tak, jak mají2.
Vraťme se ale k poslání funkcí. Funkce odpovídají strukturovanému programování. Představují základní jednotku, která řeší nějaký problém. Pokud je problém příliš složitý, volá na pomoc jinou (jiné) funkce. Z toho plyne, že by funkce neměla být příliš rozsáhlá. Pokud tomu tak není, nejenže se stává nepřehlednou, ale je i obtížně modifikovatelnou. Jak se například dotkne změna části rozsáhlé funkce její jiné části?
Pokud chceme jít na úroveň modulárního programování, je zde soubor. Ten může obsahovat více funkcí, které navzájem spolupracují. To, co ze souboru chceme dát k dispozici jiným jednotkám, umístíme jako deklarace do hlavičkového souboru.
Definované funkce mají status extern
. Jsou připojitelné do jiných
jednotek, ať chceme, nebo ne. ANSI C vyžaduje prototyp každé funkce, kterou chceme použít3. Tento požadavek výrazně
zvyšuje bezpečnost - umožňuje typovou kontrolu.
5.1.Deklarace a definice funkce
Po obecném úvodu, v němž jsme si shrnuli význam funkcí, se podívejme, jak se prakticky s funkcemi pracuje. Začněme klasicky, uvedením syntaktického zápisu deklarace funkce:
typ jméno([seznam argumentů]);
kde
- typ
představuje typ návratové hodnoty funkce
- jméno
je identifikátor, který funkci dáváme
- ()
je povinná dvojice omezující deklaraci argumentů (v tomto případě
prázdnou)
seznam argumentů
je nepovinný - funkce nemusí mít žádné
argumenty, může mít jeden nebo více argumentů, nebo také můžeme určit, že funkce má
proměnný počet argumentů4
Než se v jednotlivých podkapitolách budeme podrobněji věnovat každé položce z deklarace funkce, popišme si jméno funkce hned.
Identifikátor funkce je prostě jméno
, pod kterým se budeme na funkci
odvolávat. Závorkami za identifikátorem (toto se týká výskytu
identifikátoru funkce mimo deklaraci či definici) dáváme najevo, že funkci voláme. Pokud
závorky neuvedeme, jde o adresu funkce. Adresou funkce rozumíme adresu vstupního bodu funkce.
V této kapitole jsme zatím popsali jen deklaraci funkce. Ta ovšem patří do hlavičkového souboru. Ostatně středník, který ji ukončuje, nám nedává prostor pro samotnou definici seznamu deklarací a příkazů, které tělo funkce obsahuje.
Definice funkce, na rozdíl od její deklarace, nemá za závorkou ukončující seznam argumentů středník, ale blok. Tento blok se nazývá tělo funkce. V úvodu může, jako jakýkoliv jiný blok, obsahovat definice proměnných (jsou v rámci bloku lokální, neuvedeme-li jinak, jsou umístěny v zásobníku). Pak následuje posloupnost příkazů. Ty definují chování (vlastnosti) funkce. Při definici funkce vytváříme (definujeme) její kód, který obsazuje paměť.
Deklarace funkce popisuje vstupy a výstupy, které funkce poskytuje5. Funkce nemá provádět akce s jinými daty, než která jí předáme jako argumenty. Současně výstupy z funkce mají probíhat jen jako její návratová hodnota a nebo (myšleno případně i současně) prostřednictvím argumentů funkce. Pokud se funkce nechová uvedeným způsobem, říkáme, že má vedlejší účinky. Těch se vyvarujme. Často vedou k chybám6.
Pokud uvedeme pouze definici funkce, na kterou se později v souboru odvoláváme, slouží tato definice současně jako deklarace.
V případě neshody deklarace a definice funkce ohlásí překladač chybu.
5.2. Návratová hodnota funkce.
Pojem datový typ intuitivně známe. Chceme-li získat návratovou hodnotu funkce,
musíme určit, jakého datového typu tato hodnota je. Na typ funkce nejsou kladena
žádná omezení. Pokud nás návratová hodnota funkce nezajímá,
tak ji prostě nepoužijeme7. Pokud
ovšem chceme deklarovat, že funkce nevrací žádnou hodnotu, pak použijeme klíčové
slovo void
, které je určeno právě pro tento účel.
Kromě určení datového typu musíme také určit hodnotu, která bude
návratovou. Je to snadné, tato hodnota musí být výsledkem výrazu
uvedeného jako argument příkazu return
. Typ return
výrazu se
pochopitelně musí shodovat, nebo být alespoň slučitelný, s deklarovaným typem
návratové hodnoty funkce.
Výraz následující za return
může být uzavřen v
závorkách. To je klasický K&R styl, ANSI C norma závorky nevyžaduje:
return výraz_vhodného_typu;
Spojeno s definicí funkce, může funkce celočíselně vracející druhou mocninu svého celočíselného argumentu vypadat takto:
int isqr(int i) { return i * i; }
Příkaz return
se v těle funkce nemusí vyskytovat pouze jedenkrát. Pokud to
odpovídá větvení ve funkci, může se vyskytovat na konci každé větve. Rozhodně
však příkaz return
ukončí činnost funkce, umístí návratovou
hodnotu na specifikované místo8 a předá řízení programu bezprostředně za místem, z něhož byla funkce
volána.
5.3. Argumenty funkcí a způsob jejich předávání
Deklarace všech formálních argumentů funkcí musí obsahovat datový typ. Není možné uvést společný typ pro více následujících identifikátorů.
Korespondence mezi skutečnými a formálními argumenty odpovídá jejich pořadí při volání a v definici. To je ostatně stejné i v ostatních vyšších programovacích jazycích.
Pro slučitelnost formálních argumentů se skutečnými musíme dodržet následující pravidla (říkají nám způsob předávání argumentů). Pro zkrácení použijeme FA pro formální a SA pro skutečný argument:
- FA
int
-> SAint
,char
,short
- FA
double
-> SAdouble
,float
- FA pole -> předá se adresa 1. prvku (obsah celého pole nelze předat hodnotou)
- Struktury (
struct
,union
) se předávají hodnotou jako celek. - Funkce nemohou být předávány hodnotou.
Podstatný je způsob předávání argumentů funkci. Hodnoty skutečných argumentů jsou předány do zásobníku. Formální argumenty se odkazují na odpovídající místa v zásobníku (kopie skutečných argumentů). Změna hodnoty formálního argumentu se nepromítne do změny hodnoty argumentu skutečného! Tomuto způsobu předávání argumentů se říká předávání hodnotou. Problém vzniká v okamžiku, kdy potřebujeme, aby funkce vrátila více než jednu hodnotu9. Řešení pomocí globální proměnné jsme již dříve odmítli (vedlejší účinky) a tvorba nového datového typu jako návratové hodnoty nemusí být vždy přirozeným řešením.
Řešení je předávání nikoli hodnot skutečných argumentů, ale jejich adres. Formální argumenty pak sice budou ukazateli na příslušný datový typ, ale s tím si poradíme. Podstatnější je možnost změny hodnot skutečných argumentů, způsob nazývaný třeba v uascalu volání odkazem. V C se hovoří o volání adresou, což je zcela jasné a vystihující.
Ukažme si jednoduchý program, který volá různé funkce jak hodnotou, tak adresou.
/****************************************/ /* soubor fn_argho.c */ /* pro procviceni predavani argumentu */ /* hodnotou i "odkazem" v jazyce C. */ /****************************************/ #includeint 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 : %fn", dej_podil(c1, c2)); return 0; } /* main */
Funkce nacti()
musí vracet dvě načtené hodnoty. Předává proto argumenty
adresou10. Funkce
dej_podil()
naopak skutečné argumenty změnit nesmí (nemá to ostatně smysl). Proto
jsou jí předávány hodnotou. Povšimněme si, že návratový výraz je v
ní uzavřen v závorkách. Nevadí to, i když jsou závorky nadbytečné.
Výraz je přece ukončen středníkem.
Povšimněme si ještě jednou pozorněji funkce nacti()
. Její
návratová hodnota slouží k tomu, aby potvrdila platnost (respektive neplatnost) argumentů,
které předává adresou. To je v C velmi praktikovaný obrat (viz
scanf()
).
5.4. Funkce s proměnným počtem argumentů.
Určíme-li typ a identifikátor argumentů funkce, je vše jednoduché. Co však v
okamžiku, kdy potřebujeme funkci s proměnným počtem argumentů. Možné to jistě je, neboť jsou
standardní funkce, které si s takovou skutečností umí poradit. Například
oblíbené I/O funkce scanf()
/printf()
.
Dosud víme, že hodnoty skutečných argumentů jsou umisťovány do zásobníku. Pokud tedy dokážeme to, co díky deklaraci formálních argumentů překladač umí, tedy provést jejich jednoznačné přiřazení, máme vyhráno.
Problémem však je, že díky deklaraci i volající místo ví, jakého typu očekává volaná funkce argumenty. Podle toho je předá. Při proměnném počtu argumentů je předání nesnadné. Jak v místě volání (skutečné argumenty), tak v místě přijetí (formální).
ANSI C pamatuje na popsanou situaci několika makry a pravidly, Díky nim si téměř nemusíme
uvědomit, že pracujeme se zásobníkem (ukazatelem na něj). Jediné, co k tomu potřebujeme, je
mít mezi argumenty funkce nějaký pevný bod (poslední pevný argument pro
získání správného ukazatele do zásobníku). Další
argumenty budeme deklarovat jako proměnné (tři tečky). Pak použijeme standardní makra. Pro
jednoduchost zpracování jsou typy hodnot předávaných skutečných argumentů
konvertovány na int
a double
.
Makra pro proměnný počet argumentů (viděli jsme i pojem výpustky, ale my přece argumenty
nevypouštíme, my pouze dopředu neznáme jejich počet) jsou tři, va_start
,
va_arg
a va_end
. Co představují? Nejprve uvedeme jejich deklarace, poté
popíšeme význam:
void va_start(va_list ap, lastfix); type va_arg(va_list ap, type); void va_end(va_list ap);
kde
- va_list
formálně představuje pole proměnných argumentů
- va_start
nastaví ap
na první z proměnných argumentů
předaných funkci (probíhá fixace na poslední neproměnný deklarovaný a
předaný argument lastfix
)
- va_arg
se jako makro rozvine ve výraz stejného typu a hodnoty, jako je
další očekávaný argument (type
)
- va_end
umožní volané funkci provést případné operace pro
bezproblémový návrat zpět - return
Poznamenejme, že při použití těchto maker musí být volány v pořadí
va_start
- před prvním voláním va_arg
nebo va_end
. Po
ukončení načítání by měl být volán va_end
.
Dále poznamenejme, že va_end
může ap
změnit tak, že při jeho
dalším použití musíme znovu volat va_start
.
Počet skutečně předaných hodnot může být předán jako poslední neproměnný argument, nebo technikou nastavení zarážky.
Program vyhodnocující pro konkrétní nezávislou proměnnou polynom určený
svými koeficienty následuje. x
by mohlo být předáno jako poslední
(fixační) argument, pak by nebyl třeba nepotřebný řetězec. Šlo nám však o to
ukázat převzetí argumentů různého typu. Komentář k programu je tentokrát obsažen
ve zdrojovém textu. Proto nebudeme program dále rozebírat.
/****************************************/ /* soubor fn_argx.c */ /* ukazuje pouziti funkce s promennym */ /* poctem argumentu ruznych typu */ /****************************************/ #include#include 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 = %lfn", s, f); s = "dalsi polynom"; f = polynom(s, 3.0, 1, 0, 0, 0, konec); /* x^3 ,x=3 -> 27 */ printf("%s = %lfn", s, f); return 0; }
5.5. Rekurse
Díky umístění kopií skutečných argumentů na zásobník a umístění lokálních proměnných tamtéž se nám nabízí možnost rekurse.
Rekurse znamená volání funkce ze sebe samé, ať přímo, nebo prostřednictvím konečné posloupnosti voláním funkcí dalších. K tomu je nutné, aby programovací jazyk měl výše uvedené vlastnosti.
Předností rekurse je přirozené řešení problémů takovým způsobem, jaký odpovídá realitě či použitému algoritmu. Nevýhodou jsou často vysoké paměťové nároky na zásobník. S předáváním hodnot do zásobníku a následným obnovením jeho stavu souvisí i značná, zejména časová, režie rekurse.
Je ponecháno na naší úvaze, použijeme-li rekursi, či problém vyřešíme klasickými postupy. Platí totiž věta, že každá rekurse jde převést na iteraci.
Častým příkladem rekurse je výpočet hodnoty faktoriálu přirozeného
čísla či určení hodnoty zvoleného členu Fibonacciho posloupnosti. Podívejme se na
první z nich. V úloze je použit úmyslně jiný datový typ pro argument
(long
) a pro výsledek (double
). Rozšíření rozsahu hodnot, pro
něž faktoriál počítáme, je vedlejší efekt. Chtěli jsme zejména
ukázat použití různých datových typů.
/************************/ /* soubor fact_r.c */ /* faktorial rekursivne */ /************************/ #includedouble 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! = %lfn", n, fact(n)); return 0; }
Za povšimnutí stojí skutečnost, že je použita jediná proměnná. Ta určuje
hodnotu, pro kterou faktoriál počítáme. Při tisku hodnoty výsledku je použito
přímo umístění volání fact()
jako jednoho z argumentů
printf()
. Rekursivní funkce fact()
má pochopitelně formální
argument.
Pokud při rekursi vyžadujeme sdílení nějaké proměnné všemi stejnými
rekursivními funkcemi, můžeme pro takovou proměnnou deklarovat paměťovou třídu static
.
Proměnná pak není umístěna v zásobníku, ale v datovém segmentu programu,
kde má vyhrazeno pevné místo. Toto místo je společné všem vnořeným
rekursím funkce. Nejjednodušší možností ukázky použití
statické lokální proměnné je počítání hloubky rekurse.
Můžeme například rozšířit funkci fact()
o statickou celočíselnou
proměnnou h
. Při každém volání funkce fact()
budeme zobrazovat jak
hodnotu h
, tak i hodnotu n
. Počáteční inicializace není explicitně
nutná. Statické proměnné inicializuje nulou sám překladač. Uvádíme jen
změněnou funkci fact()
a výstup programu pro zadanou hodnotu 4
. Ostatní je
oproti předchozímu příkladu nezměněno.
double fact(long n) { static int h; double navrat; printf("hloubka=%4dtn=%4ldn", ++h, n); if (n == 0l) navrat = 1.0F; else navrat = n * fact(n-1); printf("hloubka=%4dtn=%4ldtnavrat=%.0lfn", 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 */
1 Nejjednodušší možné členění zní - standardní funkce a
ostatní funkce.
2 Nebo jak jsme z dokumentace usoudili, že se chovat mají, a kolega je zrovna nedosažitelný,
aby nám nejasnost vysvětlil nebo funkci opravil.
3 To se pochopitelně týká těch funkcí, které dosud nebyly v souboru,
obsahujícím zdrojový text, definovány. Přesto nevadí, zahrneme-li shodnou
deklaraci funkce vícekrát.
4 Později v této kapitole.
5 Souhrnně je nazýváme rozhraní funkce.
6 Navíc k chybám těžce odhalitelným.
7 Děláme to často, například I/O funkce scanf()
/printf()
hodnotu
vrací.
8 Do zásobníku, programátor se o umístění obvykle nestará.
9 Obdobný problém vzniká při předávání rozsáhlého
pole. Představa kopírování jeho obsahu do zásobníku je strašná (viz
pravidlo 3. slučitelnosti FA a SA).
10 Pozor na scanf()
!
petr.saloun@ vsb.cz
DOWNLOAD & Odkazy
- Domovská stránka autora - http://www.cs.vsb.cz/saloun/publik/c4zelenace/
- Další užitečné zdroje týkající se tématu - index.html
- Obsah kurzu - index.html