Jste zde

Programování v jazyce C - 5. FUNKCE

Pokračování seriálu o výuce programování v jazyce C.
Jak na funkce v C. Probereme deklarace a definice funkcí, návratové hodnoty, předávání argumentů funkcí, proměnný

počet argumentů a rekurzi.

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:

  1. FA int -> SA int, char, short
  2. FA double -> SA double, float
  3. FA pole -> předá se adresa 1. prvku (obsah celého pole nelze předat hodnotou)
  4. Struktury (struct, union) se předávají hodnotou jako celek.
  5. 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.     */
/****************************************/
#include 
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 : %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 */
/************************/
#include 
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! = %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()!

RNDr. Petr Šaloun, Ph.D.
petr.saloun@ vsb.cz

DOWNLOAD & Odkazy

 

 

 

Hodnocení článku: