Black Friday je tu! Využij jedinečnou příležitost a získej až 80 % znalostí navíc zdarma! Více zde
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 13 - Funkce v jazyce C

V předešlém cvičení, Řešené úlohy k 11. a 12. lekci Céčka, jsme si procvičili nabyté zkušenosti z předchozích lekcí.

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:

pozdrav(); // zavolání funkce

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:

pozdrav("Karle"); // zavolání funkce

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:

pozdrav("Karle");
pozdrav("Davide");
pozdrav("Mařenko");

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:

printf("Obsah obdélníku je: %d cm^2", obsah_obdelniku(10, 20));

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:

int obsah = obsah_obdelniku(10, 20);
printf("Obsah obdélníku je: %d cm^2", obsah);

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ů:

int celkovy_obsah = obsah_obdelniku(10, 20) + obsah_obdelniku(20, 40);
printf("Součet obsahů obdélníku je: %d cm^2", celkovy_obsah);

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:

printf("%d", cyklus(0, 10, 0)); // začátek rekurze

To samé můžeme zapsat pomocí cyklu for:

// ekvivalentní zápis s for
int suma = 0;
int a;
for (a = 0; a < 10; a++)
    suma += a;
printf("%d", suma);

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:

printf("%d", faktorial(10));

A alternativa pomocí cyklu:

int vysledek = 1;
int x = 10;
int i;
for (i = 2; i <= x; i++)
    vysledek *= i;
printf("%d", vysledek);

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 13. lekci Cečka, si procvičíme nabyté zkušenosti z předchozích lekcí.


 

Předchozí článek
Řešené úlohy k 11. a 12. lekci Céčka
Všechny články v sekci
Základní konstrukce jazyka C
Přeskočit článek
(nedoporučujeme)
Řešené úlohy k 13. lekci Cečka
Článek pro vás napsal David Čápka
Avatar
Uživatelské hodnocení:
17 hlasů
David je zakladatelem ITnetwork a programování se profesionálně věnuje 13 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David se informační technologie naučil na Unicorn University - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity

 

 

Komentáře
Zobrazit starší komentáře (6)

Avatar
studnicka.ji
Člen
Avatar
studnicka.ji:10.12.2016 15:53
int cyklus(int aktualni_index, int konecny_index,int suma)
{
     if(aktualni_index == konecny_index)
          return suma;
     cyklus(aktualni_index+1,konecny_index,suma+aktualni_index);
}
cyklus(0,10,0); //začátek rekurze

tento kod mi hází vysledek 45... to je divné

 
Odpovědět
10.12.2016 15:53
Avatar
Libor Šimo (libcosenior):10.12.2016 16:31

1+2+3+4+5+6+7+8+9

Odpovědět
10.12.2016 16:31
Aj tisícmíľová cesta musí začať jednoduchým krokom.
Avatar
Jack Brendl
Člen
Avatar
Jack Brendl:25.1.2019 22:09

Dobrý den, mohl bych se zeptat co znamena tohle: %s!\n ? Moc dekuji.

 
Odpovědět
25.1.2019 22:09
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Jack Brendl
DarkCoder:25.1.2019 23:08

% ve funkci printf() představuje část tzv. specifikace formátu. Specifikace formátu udává typ dat které se budou v místě, kde je tato specifikace uvedena, zobrazovat. Druh zobrazovaného typu je dán písmenem za znakem %. Konkrétně %s říká, že funkce printf() má očekávat řetězec znaků (pole typu char ukončené nulovým znakem).

Vykřičník ve formátovacím řetězci nemá smysl negace. Je to znak který se prostě zobrazí.

\ ve funkci printf() představuje část tzv. escape sekvence. Escape sekvence je sekvence řídících znaků, které umožňuje pozměnit chování terminálu. To co se má provést určuje znak za zpětným lomítkem. konkrétně v tomto případě \n říká, že funkce printf() má v místě, kde je uvedena tato escape sekvence, odřádkovat.

Odpovědět
25.1.2019 23:08
"Chceš-li předávat své znalosti, měj kvalitní podklady."
Avatar
Martin Russin:22.6.2021 16:30

Dobrý deň, chcel som si vyskúšať načítať meno pomocou príkazu scanf, no neúspešne. Neviete mi prosím poradiť, ako na to? Neviem skĺbiť načítanie mena pomocou príkazu scanf("%s", jmeno); s príkazom na vyvolanie funkcie pozdrav -> pozdrav("").

#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)
{
    scanf("%s", jmeno);
    pozdrav(.......);
    return (EXIT_SUCCESS);
}
 
Odpovědět
22.6.2021 16:30
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Martin Russin
DarkCoder:22.6.2021 17:57

Nejprve si musíš vyhradit paměťový prostor pro jméno. Tedy alokovat pole takové délky, aby se Ti do něj vešlo požadované jméno a to včetně nulového znaku.

Např.

#define NAME_LIMIT 20
char szName[NAME_LIMIT + 1];

Poté načteš jméno pomocí scanf

scanf("%s", szName);

A nakonec zavoláš funkci pozdrav

pozdrav(szName);
Odpovědět
22.6.2021 17:57
"Chceš-li předávat své znalosti, měj kvalitní podklady."
Avatar
Odpovídá na DarkCoder
Martin Russin:22.6.2021 18:21

Ďakujem za odpoveď a pomoc.

Mám to prosím správne?

#include <stdio.h>
#include <stdlib.h>
#define VELKOST_POLE 20

void pozdrav(char jmeno[])
{
    printf("Ahoj, vřele tě tu vítám %s!\n", jmeno);
}

int main(int argc, char** argv)
{
    char jmeno[VELKOST_POLE];
    scanf("19%s", jmeno);
    pozdrav(jmeno);
    return (EXIT_SUCCESS);
}

Prečo si definoval veľkosť poľa týmto spôsobom?

char szName[NAME_LIMIT + 1];

Mám na myslí to +1. Viem, že veľkosť poľa musí byť väčšia o 1 ako počet znakov, ktoré sa do neho uložia (kvôli nulovému znaku '\0') ale nebolo by jednoduchšie len v príkaze scanf zadefinovať koľko znakov sa ma načítať?

Chcel by som sa prosím opýtať ešte na jednu vec. Spravil som chybu ak som vo funkcii main zadeklaroval pole s textovým reťazcom pod názvom jmeno a to iste som spravil aj vo fukncii pozdrav? Narobí to nejakú šarapatu alebo je potrebné použiť v týchto dvoch funkciách odlišné názvy polí?

 
Odpovědět
22.6.2021 18:21
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Martin Russin
DarkCoder:22.6.2021 19:09

Je to správně, pouze bych jako identifikátor konstanty zvolil významově jasnější jméno. viz můj předešlý příspěvek.

Deklaraci pole tímto způsobem jsem zvolil z důvodu přehlednosti. Jasně vidím, že délka pole je dána konstantou velikosti délky jména a nulovým znakem. Rovněž i identifikátor říká že proměnná je pole znaků ukončené nulovým znakem.

Jednoduší by to nebylo, neboť počet znaků které může funkce scanf() načíst není spárovaná s konstantou. Když pak budu chtít měnit délku pole, neměním hodnotu uvnitř scanf(), což je špatně. Mohlo by to vést k nesprávné funkcí programu když se na to zapomene. Vhodně zvolenou konstantou toto mohu vyřešit.

Používání totožných jmen není úplně správné, ikdyž jazyk C má jasná pravidla kterou proměnnou použít. V příkladu je to v pořádku, neboť obojí jsou lokální proměnné a každá je tak viditelná ve své funkci. Práce s polem ve funkci je dáno tím, že se toto pole předává jako argument funkce a pracuje se s ním prostřednictvím jména parametru.

Odpovědět
22.6.2021 19:09
"Chceš-li předávat své znalosti, měj kvalitní podklady."
Avatar
Odpovídá na DarkCoder
Martin Russin:22.6.2021 19:55

O tomto som nevedel: "Jednoduší by to nebylo, neboť počet znaků které může funkce scanf() načíst není spárovaná s konstantou."

Ako ale potom pri tvojom spôsobe ošetrím to, aby sa do poľa nenačítalo väčší počet znakov, než je jeho veľkosť (napr. ak by náhodou užívateľ mal dlhšie meno ako 20 písmen)?

 
Odpovědět
22.6.2021 19:55
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Martin Russin
DarkCoder:22.6.2021 21:19

Pokud je uvedena šířka u specifikace formátu pro řetězec ve funkci scanf(), může být načteno pouze uvedené množství znaků. Tato hodnota je součástí formátovacího řetězce funkce scanf(). Maximální délka pole je dána konstantou a s tou to je třeba spárovat.

Aby to bylo možné je třeba pokročilejších znalostí práce s řetězci. Konkrétně dvě věci:

  1. Dva sousední řetězce uzavřené mezi uvozovky jsou spojovány do jednoho.
  2. Převod číselné hodnoty na řetězec v čase kompilace.

První bod je jasný, netřeba více rozebírat. Snad jen pro ukázku:

"spojování" "řetězců"
// dává výsledek
"spojování řetězců"

Druhý bod je alfou a omegou celého řešení. Využívá se operátoru preprocesoru #, který převádí hodnotu na řetězec v čase kompilace.

Tedy se nejprve převede na řetězec hodnota makra a následně se řetězce spojí do jednoho, čímž vznikne dynamicky se měnící formátovací řetězec měnící hodnotu dle hodnoty definované konstanty zvenčí.

Tedy výsledek by mohl vypadat následovně:

scanf(#NAME_LIMIT "%s", jmeno);

což po zpracování preprocesorem přejde do následující podoby:

scanf("19%s", jmeno);

Hodnota ve formátovacím řetězci se mění dle hodnoty makra NAME_LIMIT.

Dále přesně vyjádřená hodnota makra NAME_LIMIT pomáhá snazšímu pochopení funkčnosti formátovacího řetězce funkce scanf(). Odpovídá počtu znaků které může tvořit jméno.

Tímto způsobem lze provázat hodnotu makra s částmi kódu, které využívají jeho hodnotu v určité podobě a vyhnout se tak chybám způsobených opomenutím změny kódu.

Ještě dodatek, pro práci s řetězci se než různé manipulování s možnostmi funkce scanf() používá funkce fgets(), jejíž jeden parametr je hodnota maximálního počtu načtených znaků.

Odpovědět
22.6.2021 21:19
"Chceš-li předávat své znalosti, měj kvalitní podklady."
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 16. Zobrazit vše