Kurz C++ (5.)


Vítám Vás na dalším pokračování našeho kurzu o C++. Dnes si povíme něco o funkcích.

Základy

Funkce nám umožňují napsat kód, který provádí nějakou operaci, pouze jednou, a pak se na něj odkazovat z jakéhokoli místa v programu (programátorsky se tomu říká "zavolat funkci"). Můžeme tak provádět stejnou činnost na různých místech v programu aniž bychom pokaždé museli napsat stejný kód. Funkce také vnášejí do kódu modularitu a řád a dělají program přehlednějším.

Funkce může získat od volajícího programu nějaké informace (říká se jim "parametry funkce"), a může mu nějakou informaci vrátit (to je "návratová hodnota"). Jak parametry, tak návratová hodnota mohou být jakéhokoli datového typu.

Před použitím funkcí si musíme ujasnit pojmy deklarace a definice. Deklarací říkáme kompilátoru, že někde dále v programu voláme takovou funkci, a dáváme mu k dispozici příslušné údaje o ní (jméno, seznam parametrů, typ návratové hodnoty). Příklad dvou deklarací:

int min(int a, int b);    // počítá minimum dvou čísel
void vypisCopyright();    // vypisuje údaje o autorovi

Na prvním místě je typ návratové hodnoty, v našem případě funkce min vrací celé číslo, funkce vypisCopyright nevrací vůbec nic - to je význam klíčového slova void. Dále následuje jméno funkce (min, vypisCopyright) a pak v kulatých závorkách seznam parametrů (uvádí se datový typ a jméno parametru - funkce min má tedy dva celočíselné parametry (a, b), funkce vypisCopyright nemá žádný parametr. Takovému řádku se říká "hlavička funkce".


Poznámka: V některých programech se můžete setkat s deklarací, která vypadá následovně:

void vypisCopyright(void);

tedy místo prázdného seznamu parametrů je klíčové slovo void. Je to naprosto totéž jako příklad vypisCopyright výše (funkce bez parametrů), ale v jazyce C (předchůdci jazyka C++).



Všimněte si, že deklarací ještě neuvádíme, co vlastně bude funkce provádět. To kompilátoru říkáme až definicí:
int min(int a, int b) {   // vrací nejmenší ze dvou čísel
    if (a < b)
        return a;         // a < b, vracíme tedy jako výsledek a
    return b;             // pokud jsme se dostali az sem, 
                          // tak a >= b, vracime tedy b
}
    
void vypisCopyright() {
    cout << "(c) Andrei Badea, 2001\n";
}

Definice tedy začíná opětovným uvedením hlavičky, ale místo středníku na konci již píšeme kód, který se ve funkci vykonává, uzavřen do složených závorek ("tělo" funkce). V příkladu jsme použili další klíčové slovo jazyka C++, a sice příkaz return. Tímto příkazem říkáme: tady skonči provádění funkce, vrať hodnotu a předej řízení nadřazenému programu (který funkci zavolal). Proběhne-li příkaz return, další příkazy, které za ním mohou následovat, se neprovedou. Návrat do nadřazeného programu také probíhá samovolně na konci funkce. Pokud funkce nic nevrací, return tam ani nemusí být (viz funkci vypisCopyright). V opačném případě je použití return i s nějakou návratovou hodnotu povinné (funkce min).

Použítí funkce v programu vypadá takto:

#include <iostream.h>
// deklarace
int min(int a, int b);

// definice
void vypisCopyright() {
    cout << "(c) Andrei Badea, 2001\n";
}

// funkce main - to je také definice
void main() {
    int cislo1 = 2, cislo2 = 3;
    int vysledek;

    vypisCopyright();     // voláme funkci
    vysledek = min(cislo1, cislo2);    // voláme funkci - parametry uvedeny v hlavičce funkce a 
                                       // ty, které předáváme, nemusí mít stejné jméno
    cout << vysledek;
}

// definice
int min(int a, int b) {
    if (a < b) {
        return a;
    return b;
}

Proměnné výsledek jsme přiřadili návratovou hodnotu funkce min. Funkci můžeme použít na všech místech, kde kompilátor očekává výraz stejného datového typu, jako je její návratová hodnota, přičemž funguje implicitní i explicitní konverze. Funkci s návratovým typem void můžeme používat jako příkaz, (v jazyce Pascal existuje poměrně výstižný termín "procedura") ale nesmíme zapomenout na závorky. Vlastně jakoukoli funkci můžeme používat jako proceduru, tedy ignorujeme návratovou hodnotu. V minulém díle jsme mluvili o funkci strcpy. Ta je deklarována takto:

char *strcpy(char *str1, char *str2);

Funkce spojí oba řetězce do řetězce str1, který pak vrací. Nás pravděpodobně nezajímá návratová hodnota, protože řetězec str1 máme někde deklarovaný, takže ji můžeme ignorovat a zavolat funkci takto:

strcpy(str1, str2);

Dále si všimněte, že funkci můžeme deklarovat i definovat kdekoli v programu (ale mimo jinou funkci - deklarovat například ve funkci main jinou funkci by nešlo).


Poznámka: z tohoto příkladu je vidět, že veškerý kód programu je uzavřen v nějaké funkci, mimo ni nemůžete psát kód, pouze deklarovat proměnné a konstanty. Takže tvrzením "program volá funkci" jsem se dopustil malé nepřesnosti (z důvodu jednoduchosti výkladu): ve skutečnosti je vždy funkce zavolána jinou funkci (až na funkci main, která je zavolána operačním systémem).

Výše uvedený příklad na používání funkce je možná trochu zarážející, protože chybí deklarace funkce vypisCopyright. Deklarace funkcí jsou ve skutečnosti nepovinné. Pokud funkci definujeme před tím, než ji poprvé zavoláme, tak deklarace je vlastně zbytečná, protože v okamžiku zavoláni funkce kompilátor již má všechny údaje o ní k dispozici. Kdybychom posunuli funkci min před funkci main, mohli bychom vynechat i její deklaraci. Nicméně existují i případy, kdy se bez deklarací neobejdeme. Typickým příkladem jsou dvě funkce, z nichž každá volá tou druhou:

void a(bool stop) {
    cout << "Ve funkci a\n";
    if (stop == false)
        b();
}

void b() {
    cout << "Ve funkci b\n";
    a(true);
}

void main() {
    a(false);
}

Pokusíte-li se zkompilovat tento program, kompilátor ohlásí chybu ve funkci a na řádku b(), protože o funkci b zatím neví vůbec nic (postupuje shora dolů).


Poznámka: pokud Vám není jasný význam parametru stop, ten tam je pouze kvůli tomu, aby vzájemné volání funkcí a a b vůbec skončilo. Jinak by a stále volala b, následně b volala a a nikdy by to neskončilo (tedy ano: "program způsobil neplatnou operaci a bude ukončen", ale to asi nechcete).

Aby nám příklad výše fungoval, musíme ho vylepšit o deklaraci funkce b před funkcí a:

void b();

void a(bool stop) {
    cout << "Ve funkci a\n";
    if (stop == false)
        b();
}

void b() {
    cout << "Ve funkci b\n";
    a(true);
}

void main() {
    a(false);
}

Takové případy, kdy budete potřebovat deklarace, ovšem nejsou příliš časté, takže se deklaracemi nemusíte obtěžovat. Stačí, když budete vědět, že existují.

 

Předávání parametrů

Představte si, že byste potřebovali funkci počítající aritmetický a geometrický průměr (obě najednou). To by znamenalo, že funkce bude muset vracet dvě hodnoty, a přitom máme k dispozici pouze jednu návratovou hodnotu. Nabízí se možnost použít k vracení výsledků parametry funkce. Deklarace funkce by vypadala takto:

void prumery(double a, double b, double aritm, double geom);

Má tedy 2 vstupní parametry (čísla a, b), a dva výstupní (do parametru aritm budeme ukládat spočtený aritmetický průměr, do geom geometrický):

void prumery(double a, double b, double aritm, double geom) {
    aritm = (a + b) / 2;
    geom = sqrt(a * b);            // sqrt počítá druhou odmocninu
}

Pokusíte-li se funkci použít, zjistíte, že se vypisují nějaké nesmyslné hodnoty (a také dostanete při překladu upozornění že používáte neinicializované proměnné aritm a geom):

#include <math.h>
#include <iostream.h>

void prumery(double a, double b, double aritm, double geom) {
    aritm = (a + b) / 2;
    geom = sqrt(a * b);            // sqrt počítá druhou odmocninu
}

void main() {
    double a = 2, b = 3;
    double aritm, geom;

    prumery(a, b, aritm, geom);

    cout << aritm << "\n";
    cout << geom << "\n";
}

Je to tím, že existují dva způsoby předávání parametrů: hodnotou a odkazem. Předávání odkazem se používá standardně, a znamená to, že hodnoty parametrů se zkopírují a funkci se předají právě kopie. Tím pádem jakékoli změny, které funkce provede, že ve skutečnosti provedou na kopiích, které se při návratu zahazují. Další možností je předávání odkazem. V tomto případě se funkci předají odkazy na parametry, takže se veškeré změny do hodnot parametrů promítnou. U parametrů aritm a geom potřebujeme tedy předávání odkazem, to se udává operátorem & (nazývá se operátor reference) u těchto dvou parametrů:

void prumery(double a, double b, double &aritm, double &geom) {
    aritm = (a + b) / 2;
    geom = sqrt(a * b);            // sqrt počítá druhou odmocninu
}

Přetěžování funkcí

Funkce min, kterou jsme si napsali, je poměrně užitečná a možná se Vám bude hodit i v dalším programování. Má ale jednu nevýhodu: umí počítat nejmenší hodnotu jen pro celá čísla. Chceme-li funkce min i pro datový typ double, char atd., musíme si je napsat. Ve starém C muselo jméno funkce být unikátní, z čehož vyplývá, že např. funkce min pro double by se vlastně nemohla jmenovat min, ale třeba min_double. C++ je vyspělejší a tak v něm existuje mechanismus, který umožňuje deklarovat více funkcí se stejným jménem - je to přetěžování funkcí (angl. function overloading), například:

int min(int a, int b) {
    if (a < b)
        return a;
    return b;
}

double min(double a, double b) {
    if (a < b)
        return a;
    return b;
}

void main() {
    double dbl_a = 1.0, dbl_b = 1.5;
    int int_a = 1, int_b = 2;

    cout << min(int_a, int_b) << "\n";
    cout << min(dbl_a, dbl_b) << "\n";
}

Spustíte-li tento program, vypíše se nejdřív 1, pak 1.5. Kompilátor poznal jakou funkci chceme volat podle typů parametrů. Pravě takhle to funguje - dvě funkce se stejným jménem se musí lišit alespoň typem jednoho parametru. Není možné přetěžovat funkce pouze na základě rozdílného typu návratové hodnoty. Představte si tento příklad:

void min(int a, int b);
int min(int a, int b);

// definici těchto funkcí vynechávám, nejsou pro tento příklad důležité

void main() {
    min();
}

Když voláte funkci min jako proceduru, kompilátor by nevěděl, kterou funkci zavolat: tu bez parametru, nebo tu vracející int.

Inline funkce

Každé volání funkce spotřebuje nějaký čas procesoru. Není to moc, ale voláte-li funkci často (například v nějakém cyklu) a záleží-li na každém taktu, může to být poznat. Právě u takových funkcí může být výhodné je definovat jako inline. To znamená, že funkce se nezavolá, ale celé její tělo se vloží na místo, odkud ji voláme (inline funkce fungují prakticky stejně jako makra). Režie spojená se zavoláním odpadne a program tak poběží rychleji:

inline int main(int a, int b) {
    if (a < b)
        return a;
    return b;
}

Pozor, funkci deklaruje jako inline pouze tehdy, když máte jistotu, že to potřebujete. V převážné většině případů to potřebovat nebudete, a program se nezrychlí, ale bude vetší.

Příklad

Na závěr tohoto dílu si ukážeme rozsáhlejší příklad. Bude se jednat o program počítající, kolika způsoby je možno vybrat ze skupiny n prvků skupinu k prkvů, bez opakování (počet k-členných variací bez opakování z n prvků). Například je-li n = 4 a k = 2 jedná se o počet možných výběrů dvou prvků ze ctyř, přičemž záleží na pořadí - (a,b) je něco jiného než (b,a):

 

a,b a,c a,d
b,a b,c b,d
c,a c,b c,d
d,a d,b d,c


Pro výpočet variací existuje matematický vzorec:



kde

(faktoriál z čísla n). Také platí 0! = 1.

 

Vybaveni těmito znalostmi se můžeme pustit do práce: nejdřív budeme potřebovat přečíst parametry z příkazové řádky a připravit je pro výpočet:

#include <stdlib.h>
#include <iostream.h>

// deklarace řetězců s chybovými hlášeními
char *szMaloParametru = "Parametry programu musí být dvě čísla.";
char *szNeplatne = "Parametry nejsou platné.";

// další řetězce použité v programu
char *szVysledek = "Výsledek: ";

void main(int argc, char **argv) {
    int n, k;

    // program musí mít alespoň 3 parametry (cesta k programu (ta tam je vzdy), n a k)
    if (argc < 3) {
        cout << szMaloParametru;
        return;
    }

    // převod parametru uložených jako řetězce na čísla funkcí atoi (v hlavičkovém souboru stdlib.h)
    // argv[1] je k (první parametr)
    // argv[2] je n (druhý parametr)
    k = atoi(argv[1]);
    n = atoi(argv[2]);

    // kontrola správnosti parametrů
    if (n <= 0 || k <= 0 || n < k) {
        cout << szNeplatne;
        return;
    }

    // výpis výsledku
    cout << szVysledek << variace(k, n);
}

Na konci programu vypisujeme výsledek funkcí variace, která spočte požadovaný počet variací na základě parametrů n a k. Tuto funkci si musíme definovat (před funkci main, abychom nemuseli psát i deklaraci), podle matematického vzorce:

unsigned int variace(unsigned int k, unsigned int n) {
    // výsledek počítáme rovnou, nemusíme pro něj deklarovat proměnnou
    return faktorial(n) / faktorial(n - k);
}

Ve funkci variace používáme funkci faktorial, takže ji přidáme:

unsigned int faktorial(unsigned int n) {
    unsigned int i, vysledek = 1;

    if (n == 0)
        return 1;
		
    for (i = 2; i <= n; i++)
        vysledek *= i;    // vysledek = vysledek * i6
    return vysledek;
}

A to je všechno. Zajímavé je snad jen to, že funkce variace a faktoriál dostávají parametry typu unsigned int (celé číslo bez znaménka) a také takovou hodnotu vracejí. To je v pořádku, variaci ani faktoriál ze záporných čísel počítat nelze. Ale při čtení parametrů z příkazové řádky ve funkci main používáme proměnné typu int, abychom mohli zachytit pokus uživatele o zadání záporných čísel (kdybychom místo int použili i tady unsigned int došlo by při zadání záporného čísla k přetečení a např. místo čísla -1 by se do proměnné uložilo 4294967295).

 

Děkuji za pozornost a těším se na další díl.

 Andrei Badea