Lekce 14 - Funkce v jazyce C
V minulé lekci, Nejčastější chyby C začátečníků - Umíš pojmenovat proměnné?, jsme si ukázali nejčastější chyby začátečníků v C ohledně pojmenování proměnných.
Dnešní tutoriál o programovacím jazyce C je věnován velmi důležitému
tématu, kterým je využívání funkcí. My jsme již seznámení s tím, že
kód programu píšeme do funkce main()
. To pro naše učební
programy, které uměly vykonávat jen jednu jednoduchou věc, prozatím
stačilo. Představte si ovšem, že píšete program, který je dlouhý
několik set tisíc řádků. Určitě uznáte, že v takové nudli kódu v
jednom souboru a v jedné funkci by se orientovalo velmi špatně. Navíc, pokud
bychom chtěli provést nějakou stejnou posloupnost příkazů na více
místech, museli bychom ji buď stále opisovat nebo v kódu skákat z místa na
místo. Obě dvě možnosti jsou opět velmi nepřehledné.
Funkcionální dekompozice
O rozdělení aplikace do funkcí se někdy hovoří jako o tzv. funkcionální dekompozici. Nelekejte se termínu, jednoduše si rozmyslíme, co má naše aplikace umět a pro různé uživatelské funkce obvykle vytvoříme jednotlivé funkce ve zdrojovém kódu. V praxi se nám bude často stávat, že si budeme tvořit kromě těchto funkcí ještě nějaké pomocné, např. můžeme mít funkci pro výpis menu aplikace nebo rozdělíme nějaký složitý výpočet do více menších funkcí kvůli přehlednosti.
Funkcím se někdy říká podprogramy nebo subrutiny. Pokud funkce nevrací
žádnou hodnotu (viz dále), může se ji v některých jazycích říkat
procedura. U větších aplikací, které mají mnoho funkcí, se funkce
sdružují do tzv. modulů. Ty vy dobře znáte např. v podobě
#include <stdio.h>
, kterým načítáme knihovnu (modul) pro
práci se standardním vstupem a výstupem (tedy pro nás s konzolí). Podobně
jsou matematické funkce soustředěné v systémovém modulu
math.h
. Tyto moduly nebo-li knihovny se také naučíme
vytvářet.
Tvorba funkcí
Funkce je logický blok kódu, který jednou napíšeme a poté ho můžeme
libovolně volat bez toho, abychom ho psali znovu a opakovali se. Funkci
deklarujeme v globálním prostoru, někde nad funkcí main()
. Bude
vypadat podobně. Přidejme do našeho zdrojového kódu funkci, která do
konzole vypíše "Ahoj, vřele tě tu vítám!"
. Pro
názornost si poprvé uveďme kompletní zdrojový kód programu:
#include <stdio.h> #include <stdlib.h> void pozdrav(void) { printf("Ahoj, vřele tě tu vítám!\n"); } int main(int argc, char** argv) { return (EXIT_SUCCESS); }
První slovo void
v definici funkce udává, že funkce nevrací
žádnou hodnotu. Druhé void
má podobný význam, určuje, že
funkce nemá žádné vstupní parametry. Funkci nyní musíme zavolat, aby se
spustila. Musíme to samozřejmě udělat až potom, co ji deklarujeme, jinak by
ji kompilátor neznal (proto jsme ji psali nad funkci main()
). Do
main()
napíšeme tento řádek:
#include <stdio.h>
#include <stdlib.h>
void pozdrav(void)
{
printf("Ahoj, vřele tě tu vítám!\n");
}
int main(int argc, char** argv)
{
pozdrav(); // zavolání funkce
return (EXIT_SUCCESS);
}
Výsledek:
Konzolová aplikace
Ahoj, vřele tě tu vítám!
Funkce s parametry
Funkce může mít také libovolný počet vstupních parametrů (někdy se
jim říká argumenty), které píšeme do závorky v její definici. Parametry
ovlivňujeme chování funkce. Mějte situaci, kdy chceme pozdravit našeho
uživatele podle jména. Rozšíříme tedy stávající funkci o parametr
jmeno
a ten potom přidáme s konkrétní hodnotou do volání
funkce:
void pozdrav(char jmeno[]) { printf("Ahoj, vřele tě tu vítám %s!\n", jmeno); }
Volání funkce v main()
následně upravíme takto:
#include <stdio.h>
#include <stdlib.h>
void pozdrav(char jmeno[])
{
printf("Ahoj, vřele tě tu vítám %s!\n", jmeno);
}
int main(int argc, char** argv)
{
pozdrav("Karle"); // zavolání funkce
return (EXIT_SUCCESS);
}
Kdybychom nyní chtěli pozdravit několik lidí, nemusíme otrocky psát
znovu a znovu printf("Ahoj, vřele
..., stačí nám pouze
vícekrát zavolat naší funkci:
#include <stdio.h>
#include <stdlib.h>
void pozdrav(char jmeno[])
{
printf("Ahoj, vřele tě tu vítám %s!\n", jmeno);
}
int main(int argc, char** argv)
{
pozdrav("Karle");
pozdrav("Davide");
pozdrav("Mařenko");
return (EXIT_SUCCESS);
}
Výsledek:
Konzolová aplikace
Ahoj, vřele tě tu vítám Karle!
Ahoj, vřele tě tu vítám Davide!
Ahoj, vřele tě tu vítám Mařenko!
Návratová hodnota funkce
Funkce může dále vracet nějakou hodnotu. Opusťme náš příklad s
pozdravem a vytvořme tentokrát funkci, která nám spočítá obsah
obdélníku. Tento obsah ovšem nebudeme chtít pouze vypsat, ale budeme ho
chtít použít v dalších výpočtech. Proto výsledek funkce nevypíšeme,
ale vrátíme jako návratovou hodnotu. Funkce může vracet právě jednu
hodnotu pomocí příkazu return
, který zároveň funkci i
ukončí, další kód za return
tedy již nebude spuštěn.
Datový typ návratové hodnoty musíme uvést před definici funkce. Přidejte
si do programu následující funkci:
int obsah_obdelniku(int sirka, int vyska) { int vysledek = sirka * vyska; return vysledek; }
V praxi by naše funkce samozřejmě počítala něco složitějšího, aby
se nám ji vyplatilo vůbec programovat. V příkladu ale obdélník poslouží
dobře. Funkce pojmenováváme malými písmeny, celými slovy a místo mezer
používáme podtržítka. Ačkoli céčko samotné je plné zkrácenin, vy se
jim vyhněte. Je totiž mnohem čitelnější funkce
datum_narozeni()
než datnar()
, u které nemusí být
na první pohled zřejmé co že to vůbec dělá.
Pokud bychom nyní chtěli vypsat obsah nějakého obdélníku, jednoduše
vložíme volání funkce do funkce printf()
. Jako první se
spočítá obsah obdélníku, funkce tuto hodnotu vrátí a hodnota přijde jako
vstupní parametr funkci printf()
, která ji vypíše. Jako
šířku a výšku zadejme např. 10
a 20
cm:
{C_IMPORTS}
int obsah_obdelniku(int sirka, int vyska)
{
int vysledek = sirka * vyska;
return vysledek;
}
{C_MAIN_BLOCK}
printf("Obsah obdélníku je: %d cm^2", obsah_obdelniku(10, 20));
{/C_MAIN_BLOCK}
Konzolová aplikace
Obsah obdélníku je: 200 cm^2
Pokud vám to přijde zmatené, vždy můžete použít ještě pomocnou proměnnou:
{C_IMPORTS}
int obsah_obdelniku(int sirka, int vyska)
{
int vysledek = sirka * vyska;
return vysledek;
}
{C_MAIN_BLOCK}
int obsah = obsah_obdelniku(10, 20);
printf("Obsah obdélníku je: %d cm^2", obsah);
{/C_MAIN_BLOCK}
Návratovou hodnotu funkce jsme ovšem nepoužili kvůli tomu, abychom ji jen vypisovali. Využijme nyní toho, že je výpis na nás, a vypišme součet obsahů dvou obdélníků:
{C_IMPORTS}
int obsah_obdelniku(int sirka, int vyska)
{
int vysledek = sirka * vyska;
return vysledek;
}
{C_MAIN_BLOCK}
int celkovy_obsah = obsah_obdelniku(10, 20) + obsah_obdelniku(20, 40);
printf("Součet obsahů obdélníku je: %d cm^2", celkovy_obsah);
{/C_MAIN_BLOCK}
Výsledek:
Konzolová aplikace
Součet obsahů obdélníku je: 1000 cm^2
Vzpomeňme si na minulé příklady, které jsme během našeho seriálu vytvořili. Můžete si je zkusit přepsat tak, abyste volali funkci. V rámci návrhu by všechen kód měl být rozdělen do funkcí (a ideálně do modulů, viz. další díly) a to zejména kvůli přehlednosti. My jsme to ze začátku kvůli jednoduchosti zanedbali, nyní to prosím berte na vědomí
Výhoda funkcí je tedy v přehlednosti a úspornosti (můžeme napsat nějakou věc jednou a volat ji třeba stokrát na různých místech programu). Když se rozhodneme funkci změnit, provedeme změnu jen na jednom místě a tato změna se projeví všude, což značně snižuje riziko chyb. V příkladě, kde zdravíme Karla, Davida a Mařenku nám stačí změnit text pozdravu ve funkci a změní se ve všech třech voláních. Nemít kód ve funkci, museli bychom přepisovat 3 věty a v nějaké bychom mohli udělat chybu.
Rekurze
Na konec si udělejme ještě odbočku k pokročilejšímu tématu, kterým je rekurze. Rekurzivní funkce je taková funkce, která v těle volá sama sebe. Taková funkce potřebuje nějakou informaci, podle které pozná, kdy má skončit (tzv. ukončení rekurze), jinak by zavolala sebe, ta zas sebe a tak až do pádu programu na nedostatek paměti. Rekurze se často používá v algoritmizaci.
Ve funkcionálních jazycích se rekurze používá namísto cyklů. Vezměme
si například cyklus for
, který sčítá čísla od
1
do 10
. Stejného výsledku můžeme docílit i
rekurzí, funkce se buď zavolá znovu s číslem o 1
vyšším
nebo se ukončí.
int cyklus(int aktualni_index, int konecny_index, int suma) { if (aktualni_index == konecny_index) return suma; return cyklus(aktualni_index + 1, konecny_index, suma + aktualni_index); }
Funkci bychom zavolali takto:
{C_IMPORTS}
int cyklus(int aktualni_index, int konecny_index, int suma)
{
if (aktualni_index == konecny_index)
return suma;
return cyklus(aktualni_index + 1, konecny_index, suma + aktualni_index);
}
{C_MAIN_BLOCK}
printf("%d", cyklus(0, 10, 0)); // začátek rekurze
{/C_MAIN_BLOCK}
To samé můžeme zapsat pomocí cyklu for
:
{C_CONSOLE}
// ekvivalentní zápis s for
int suma = 0;
int a;
for (a = 0; a < 10; a++)
suma += a;
printf("%d", suma);
{/C_CONSOLE}
Jak můžete vidět, čtení rekurze není tak snadné, jak je tomu u cyklu
for
. Aby toho nebylo málo, použití rekurze sebou nese
dodatečnou zátěž, protože se musí opakovaně předávat parametry (více v
článku o kompilaci). Obecně lze velkou část programů, které používají
rekurzi, přepsat do podoby bez rekurze. Pro příklad si napíšeme program,
který počítá faktoriál.
Předvedeme si verzi s rekurzí a verzi bez rekurze.
int faktorial(int x) { if (x == 1) return 1; return x * faktorial(x - 1); }
Funkci bychom zavolali takto:
{C_IMPORTS}
int faktorial(int x)
{
if (x == 1)
return 1;
return x * faktorial(x - 1);
}
{C_MAIN_BLOCK}
printf("%d", faktorial(10));
{/C_MAIN_BLOCK}
A alternativa pomocí cyklu:
{C_CONSOLE}
int vysledek = 1;
int x = 10;
int i;
for (i = 2; i <= x; i++)
vysledek *= i;
printf("%d", vysledek);
{/C_CONSOLE}
S rekurzí se můžete často setkat v již existujících programech nebo na pohovorech do práce. Nicméně doporučuji se rekurzi spíše vyhýbat, alespoň ze začátku. Rekurze také dokáže velice rychle zaplnit zásobník a ukončit celý program. Navíc je složitá na chápání, pokud vás zmátla, ještě se s ní setkáte minimálně u algoritmů, kde bude dostatek prostoru pro další vysvětlení.
V následujícím cvičení, Řešené úlohy k 14. lekci Cečka, si procvičíme nabyté zkušenosti z předchozích lekcí.