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:
… je pomalejší než …
Ú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:
{
telo funkce
}
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í :-).
- /*------------------------------------------------*/
- /* c13/rekurz.c */
- #include <stdio.h>
- { /* definice */
- }
- { /* definice */
- return f;
- }
- {
- }
- /*------------------------------------------------*/
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).
- /*------------------------------------------------*/
- /* c13/faktor.c */
- #include <stdio.h>
- {
- }
- {
- int i;
- f = faktorial(i);
- }
- }
- /*------------------------------------------------*/
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:
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.
- /*------------------------------------------------*/
- /* c13/fce2.c */
- #include <stdio.h>
- #include <stdarg.h>
- #include <stdbool.h>
- {
- va_list ap;
- int i;
- /* znamenko je posledni deklarovana promenna */
- maxS = (maxS < dalsiS) ? dalsiS : maxS;
- maxU = (maxU < dalsiU) ? dalsiU : maxU;
- }
- }
- return maxS;
- }
- {
- }
- /*------------------------------------------------*/
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.