12. diel - Funkcie v jazyku C
V predchádzajúcom cvičení, Riešené úlohy k 10. a 11. lekcii Céčka, sme si precvičili získané skúsenosti z predchádzajúcich lekcií.
V minulej lekcii, Riešené úlohy k 10. a 11. lekcii Céčka , sme sa zoznámili s matematickou knižnicou
math.h
. Dnešné tutoriál o programovacom jazyku C je venovaný
veľmi dôležitej téme, ktorým je využívanie funkcií. My sme už
zoznámenie s tým, že kód programu píšeme do funkcie main()
.
To pre naše učebné programy, ktoré vedeli vykonávať len jednu jednoduchú
vec, zatiaľ stačilo. Predstavte si však, že píšete program, ktorý je
dlhý niekoľko sto tisíc riadkov. Určite uznáte, že v takej rezance kóde v
jednom súbore a v jednej funkcii by sa orientovalo veľmi zle. Navyše, ak by
sme chceli vykonať nejakú rovnakú postupnosť príkazov na viacerých
miestach, museli by sme ju buď stále opisovať alebo v kóde skákať z miesta
na miesto. Obe dve možnosti sú opäť veľmi neprehľadné.
Funkcionálne dekompozícia
O rozdelení aplikácie do funkcií sa niekedy hovorí ako o tzv. Funkcionálne dekompozícii. Neľakajte sa termíne, jednoducho si rozmyslíme, čo má naša aplikácia vedieť a pre rôzne užívateľské funkcie zvyčajne vytvoríme jednotlivé funkcie v zdrojovom kóde. V praxi sa nám bude často stávať, že si budeme tvoriť okrem týchto funkcií ešte nejaké pomocné, napr. Môžeme mať funkciu pre výpis menu aplikácie alebo rozdelíme nejaký zložitý výpočet do viacerých menších funkcií kvôli prehľadnosti.
Funkciám sa niekedy hovorí podprogramy alebo subrutiny. Ak funkcia nevracia
žiadnu hodnotu (viď ďalej), môže sa ju v niektorých jazykoch hovoriť
procedúra. U väčších aplikácií, ktoré majú mnoho funkcií, sa funkcia
združujú do tzv. Modulov. Tie vy dobre poznáte napr. V podobe
#include <stdio.h>
, ktorým načítame knižnicu (modul) pre
prácu so štandardným vstupom a výstupom (teda pre nás s konzolou). Podobne
sú matematické funkcie sústredené v systémovom module math.h
.
Tieto moduly alebo ak knižnice sa tiež naučíme vytvárať.
Tvorba funkcií
Funkcia je logický blok kódu, ktorý raz napíšeme a potom ho môžeme
ľubovoľne volať bez toho, aby sme ho písali znovu a opakovali sa. Funkciu
deklarujeme v globálnom priestore, niekde nad funkciou main()
.
Bude vyzerať podobne. Pridajme do nášho zdrojového kódu funkciu, ktorá do
konzoly vypíše " Ahoj, vrelo ťa tu vítam! "
. Pre
názornosť si prvýkrát uveďme kompletný zdrojový kód programu:
#include <stdio.h> #include <stdlib.h> void pozdrav(void) { printf("Ahoj, vrelo ťa tu vítam!\n"); } int main(int argc, char** argv) { return (EXIT_SUCCESS); }
Prvé slovo void
v definícii funkcie udáva, že funkcia
nevracia žiadnu hodnotu. Druhé void
má podobný význam,
určuje, že funkcia nemá žiadne vstupné parametre. Funkciu teraz musíme
zavolať, aby sa spustila. Musíme to samozrejme urobiť až potom, čo ju
deklarujeme, inak by ju kompilátor nepoznal (preto sme ju písali nad funkciu
main()
). Do main()
napíšeme tento riadok:
#include <stdio.h>
#include <stdlib.h>
void pozdrav(void)
{
printf("Ahoj, vrelo ťa tu vítam!\n");
}
int main(int argc, char** argv)
{
pozdrav(); // zavolanie funkcie
return (EXIT_SUCCESS);
}
výsledok:
Konzolová aplikácia
Ahoj, vrelo ťa tu vítam!
Funkcie s parametrami
Funkcia môže mať tiež ľubovoľný počet vstupných parametrov (niekedy
sa im hovorí argumenty), ktoré píšeme do zátvorky v jej definícii.
Parametre ovplyvňujeme správanie funkcie. Majte situácii, keď chceme
pozdraviť nášho používateľa podľa mena. Rozšírime teda existujúce
funkciu o parameter jmeno
a ten potom pridáme s konkrétnou
hodnotou do volanie funkcie:
void pozdrav(char jmeno[]) { printf("Ahoj, vrelo ťa tu vítam %s!\n", jmeno); }
Volanie funkcie v main()
následne upravíme takto:
#include <stdio.h>
#include <stdlib.h>
void pozdrav(char jmeno[])
{
printf("Ahoj, vrelo ťa tu vítam %s!\n", jmeno);
}
int main(int argc, char** argv)
{
pozdrav("Karle"); // zavolanie funkcie
return (EXIT_SUCCESS);
}
Keby sme teraz chceli pozdraviť niekoľko ľudí, nemusíme otrocky písať
znova a znova printf("Ahoj, vřele
..., stačí nám len viackrát
zavolať našej funkcii:
#include <stdio.h>
#include <stdlib.h>
void pozdrav(char jmeno[])
{
printf("Ahoj, vrelo ťa tu vítam %s!\n", jmeno);
}
int main(int argc, char** argv)
{
pozdrav("Karle");
pozdrav("Davide");
pozdrav("Mařenko");
return (EXIT_SUCCESS);
}
výsledok:
Konzolová aplikácia
Ahoj, vrelo ťa tu vítam Karle!
Ahoj, vrelo ťa tu vítam Davide!
Ahoj, vrelo ťa tu vítam Mařenko!
Návratová hodnota funkcie
Funkcia môže ďalej vracať nejakú hodnotu. Opusťme náš príklad s
pozdravom a vytvorme tentoraz funkciu, ktorá nám spočíta obsah obdĺžnika.
Tento obsah však nebudeme chcieť len vypísať, ale budeme ho chcieť použiť
v ďalších výpočtoch. Preto výsledok funkcie nevypíše, ale vrátime ako
návratovú hodnotu. Funkcia môže vracať práve jednu hodnotu pomocou
príkazu return
, ktorý zároveň funkciu aj ukončí, ďalší
kód za return
teda už nebude spustený. Dátový typ návratovej
hodnoty musíme uviesť pred definíciu funkcie. Pridajte si do programu
nasledujúcej funkcii:
int obsah_obdelniku(int sirka, int vyska) { int vysledek = sirka * vyska; return vysledek; }
V praxi by naše funkcie samozrejme počítala niečo zložitejšieho, aby sa
nám ju vyplatilo vôbec programovať. V príklade ale obdĺžnik poslúži
dobre. Funkcia pomenovávame malými písmenami, celými slovami a miesto
medzier používame podčiarknutia. Hoci céčko samotné je plné skrátené,
vy sa im vyhnite. Je totiž oveľa čitateľnejšie funkcie
datum_narozeni()
ako datnar()
, u ktorej nemusia byť
na prvý pohľad zrejmé čo že to vôbec robí.
Ak by sme teraz chceli vypísať obsah nejakého obdĺžnika, jednoducho
vložíme volanie funkcie do funkcie printf()
. Ako prvý sa
spočíta obsah obdĺžnika, funkcia túto hodnotu vráti a hodnota príde ako
vstupný parameter funkciu printf()
, ktorá ju vypíše. Ako
šírku a výšku zadajte napr. 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ĺžnika je: %d cm^2", obsah_obdelniku(10, 20));
{/C_MAIN_BLOCK}
Konzolová aplikácia
Obsah obdĺžnika je: 200 cm^2
Pokiaľ vám to príde zmätené, vždy môžete použiť ešte pomocnú premennú:
{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ĺžnika je: %d cm^2", obsah);
{/C_MAIN_BLOCK}
Návratovú hodnotu funkcie sme však nepoužili kvôli tomu, aby sme ju len vypisovali. Využime teraz toho, že je výpis na nás, a výpisy súčet obsahov dvoch obdĺžnikov:
{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("Súčet obsahov obdĺžnika je: %d cm^2", celkovy_obsah);
{/C_MAIN_BLOCK}
výsledok:
Konzolová aplikácia
Súčet obsahov obdĺžnika je: 1000 cm^2
Spomeňme si na minulé príklady, ktoré sme počas nášho seriálu vytvorili. Môžete si ich skúsiť prepísať tak, aby ste volali funkciu. V rámci návrhu by všetok kód mal byť rozdelený do funkcií (a ideálne do modulov, viď. Ďalšie diely) a to najmä kvôli prehľadnosti. My sme to zo začiatku kvôli jednoduchosti zanedbali, teraz to prosím berte na vedomie
Výhoda funkciou je teda v prehľadnosti a úspornosti (môžeme napísať nejakú vec raz a volať ju treba stokrát na rôznych miestach programu). Keď sa rozhodneme funkciu zmeniť, vykonáme zmenu len na jednom mieste a táto zmena sa prejaví všade, čo značne znižuje riziko chýb. V príklade, kde zdravíme Karla, Davida a Marienku nám stačí zmeniť text pozdravu vo funkcii a zmení sa vo všetkých troch volaniach. Nemať kód vo funkcii, museli by sme prepisovať 3 vety a v nejakej by sme mohli urobiť chybu.
Rekurzia
Na koniec si urobme ešte odbočku k pokročilejšiemu téme, ktorým je rekurzia. Rekurzívne funkcie je taká funkcia, ktorá v tele volá sama seba. Takáto funkcia potrebuje nejakú informáciu, podľa ktorej spozná, kedy má skončiť (tzv. Ukončenie rekurzia), inak by zavolala seba, tá zas seba a tak až do pádu programu na nedostatok pamäti. Rekurzia sa často používa v algoritmizácia.
Vo funkcionálnych jazykoch sa rekurzia používa namiesto cyklov. Zoberme si
napríklad cyklus for
, ktorý je súčtom čísla od 1
do 10
. Rovnaký výsledok môžeme docieliť aj rekurzia, funkcie
sa buď zavolá znovu s číslom o 1
vyšším alebo sa
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); }
Funkciu by sme 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čiatok rekurzia
{/C_MAIN_BLOCK}
To isté môžeme zapísať pomocou 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}
Ako môžete vidieť, čítanie rekurzia nie je tak jednoduché, ako je tomu
u cykle for
. Aby toho nebolo málo, použitie rekurzia sebou nesie
dodatočnú záťaž, pretože sa musí opakovane odovzdávať parametre (viac v
článku o kompiláciu). Všeobecne možno veľkú časť programov, ktoré
používajú rekurziu, prepísať do podoby bez rekurzia. Pre príklad si
napíšeme program, ktorý počíta faktoriál.
Predvedieme si verziu s rekurzia a verziu bez rekurzia.
int faktorial(int x) { if (x == 1) return 1; return x * faktorial(x - 1); }
Funkciu by sme 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 alternatíva pomocou 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 rekurzia sa môžete často stretnúť v už existujúcich programoch alebo na pohovoroch do práce. Avšak odporúčam sa rekurziu skôr vyhýbať, aspoň zo začiatku. Rekurzia tiež dokáže veľmi rýchlo zaplniť zásobník a ukončiť celý program. Naviac je zložitá na chápania, ak vás zmiatla, ešte sa s ňou stretnete minimálne u algoritmov, kde bude dostatok priestoru pre ďalšie vysvetlenie.
V budúcej lekcii, Riešené úlohy k 12. lekcii céčko , sa pozrieme na ďalšie sa základných aspektov C - na štruktúry.
V nasledujúcom cvičení, Riešené úlohy k 12. lekcii céčko, si precvičíme nadobudnuté skúsenosti z predchádzajúcich lekcií.