Image
12.6.2016 0 Comments

C++ / Deklarácie III / 13. časť

Späť na úvod >> Späť na programovanie >> Späť na seriál

Dnešná časť je svojím spôsobom mimoriadna. Predovšetkým, verte či neverte, už uplynul rok odvtedy, čo vyšla prvá časť seriálu C++ pod lupou. Nemal som vtedy ani najmenšie tušenie, či a ako bude moja práca akceptovaná, nevedel som s istotou, ako dlho bude seriál vychádzať. Dnes vidím, že moje rozhodnutie pustiť sa do náročnej a tak trochu i nevďačnej úlohy bolo správne – svedčí o tom množstvo e-mailov, ktoré som od vás za ten rok dostal. Príjemne ma prekvapilo, že z tohto množstva boli možno dva či tri výrazne kritické; napriek tomu si však nemyslím, že je moje dielo dokonalé a (prevažne vďaka mojej perfekcionistickej povahe) práve pri retrospektívnom pohľade zisťujem, že veľa vecí by som dnes spravil ináč. Žiaľ, jednotlivé časti seriálu sú už „in statu quo“ a nedá sa spraviť nijaké „undo“. (Mimochodom, aj vás občas štve, že v živote neexistuje táto možnosť, taká bežná vo väčšine dnešného softvéru?) Chcem sa poďakovať všetkým tým, ktorí si našli chvíľku času a vyjadrili svoj názor na obsah i formu seriálu. Verím, že vás rovnako uspokoja časti, ktoré len vyjdú, a že vydržíte až do konca.

Druhá vec, pre mňa oveľa významnejšia ako pre vás, je fakt, že vo chvíli, keď čítate tieto riadky, mal by som mať za sebou štátnu skúšku na Fakulte elektrotechniky a informatiky STU. Vďaka zvyčajnému časovému posunu medzi uzávierkou a vyjdením PC REVUE vám nemôžem povedať, s akým výsledkom, ale pevne verím, že všetko dopadne dobre. Našťastie sa mi podarilo bez výrazných stresových situácií zvládnuť prácu na diplomovke aj písanie seriálu. Dúfam, že ste nepostrehli nijaké zníženie kvality v posledných dvoch či troch častiach. Ak áno, nebojte sa, už to bude zase v poriadku.

Nezachádzajme však do filozofických úvah. Vráťme sa po tretíkrát k problematike deklarácií. Dnes výklad na túto tému na veľkú radosť (moju a zrejme i vašu) dokončíme. Už nám chýba len veľmi málo, aby sme sa prehupli do trochu atraktívnejšej objektovej polovice seriálu.

Pokračujeme v deklarátoroch

Minule sme si začali hovoriť o druhej zložke deklarácií – o deklarátoroch. Vieme, že pomocou deklarátorov určujeme názov novo deklarovaných objektov a môžeme modifikovať typ, určený pomocou špecifikátorov. Dve z týchto modifikácií sme si aj podrobne opísali: deklaráciu ukazovateľov a deklaráciu referencií. Modifikácie typu, ktoré sú súčasťou deklarátorov, možno za dodržania určitých pravidiel rôzne reťaziť a získavať tak prakticky neobmedzenú množinu nových, často relatívne zložitých typov (ako polia ukazovateľov na funkcie a pod.). Teraz si objasníme problematiku deklarácie polí, deklarácie a definície funkcií a inicializácie deklarovaných objektov.

Deklarácia polí

Deklarácia poľa prvkov ľubovoľného typu má nasledujúci tvar:

T D1 [konštantný-výraz]

Meno, ktoré deklarujeme (a ktorého identifikátor je súčasťou deklarátora D1, prípadne je s ním totožný), bude mať typ „pole … prvkov typu T“. To, akého typu budú prvky poľa, určuje tvar deklarátora D1. Konštantný výraz, ktorý je súčasťou deklarátora, udáva počet prvkov deklarovaného poľa. V určitých prípadoch ho môžeme vynechať – ak ide o deklaráciu, a nie definíciu (t. j. poľu je prideľovaná pamäť niekde inde), ďalej pri deklarácii formálnych argumentov funkcií a konečne vtedy, keď je súčasťou deklarácie zoznam počiatočných hodnôt prvkov poľa (vtedy si prekladač rozmer poľa dokáže zistiť sám).

Typ prvkov poľa môže byť prakticky ľubovoľný: od primitívnych typov cez ukazovatele, štruktúry/triedy, enumerácie až po iné polia. Vieme už, že pole, ktorého prvkami sú opäť polia, svojím správaním navodzuje dojem viacrozmerného poľa. Nebudeme sa už vracať k jednotlivým špecifikám, ak máte chuť osviežiť si vedomosti, pohľadajte si príslušné časti seriálu v starších číslach. Upozorním len na jeden drobný detail – pri deklarácii viacrozmerného poľa môžeme vynechať (samozrejme, len v uvedených prípadoch) najviac jeden rozmer, a to ten, ktorý je najbližšie k identifikátoru (teda prvý), napríklad:

int m[][2][3];

Tento fakt vyplýva z nutnosti poznania rozmeru vnorených polí pri výpočte umiestnenia ich prvkov v pamäti (a navyše použitie týchto vnorených polí nespadá ani pod jeden uvedený prípad!).

Ukážme si príklad zložitejších deklarácií polí. Ak chceme deklarovať napríklad pole konštantných, ne-volatile ukazovateľov (ktorých deklarátor, ako už vieme, vyzerá takto: * const D), použijeme zápis: T * const D [konštantný-výraz]. Medzery medzi jednotlivými zložkami nie sú povinné. Tu je deklarácia poľa cal desiatich konštantných ukazovateľov na typ long:

long * const cal[10];

Ak chceme deklarovať pole neznámej veľkosti, ktorého prvkami budú konštantné reťazce (teda ukazovatele na konštantný char), použijeme takýto zápis:

const char * msgs[];

V predchádzajúcej časti sme si ukazovali deklaráciu poľa ukazovateľov na funkcie. Hoci ešte nevieme deklarovať ukazovateľ na funkciu, dokážeme zapísať deklaráciu poľa takýchto ukazovateľov. Ak vám prezradím, že ukazovateľ na najjednoduchší typ funkcie – bez argumentov, bez návratovej hodnoty, teda typu void f(), deklarujeme ako:

void (*f)();

mali by ste byť schopní bez problémov zapísať deklaráciu poľa napr. dvanástich takýchto ukazovateľov:

void (*f[12])();

Prakticky teda deklarácia poľa spočíva v pripojení hranatých zátvoriek s uvedeným rozmerom poľa za príslušný deklarátor. Umenie však spočíva v nájdení správneho miesta, t. j. v rozpoznaní správneho deklarátora. Na tomto mieste by som rád ukázal odlišný spôsob deklarácie poľa ukazovateľov a deklarácie ukazovateľa na pole. V oboch prípadoch použijeme ako základ typ int. Najprv si ukážeme výsledok prvého príkladu. Pole ukazovateľov na int deklarujeme takto:

int * api[];

Tu je dôležité si uvedomiť, že deklarujeme api ako pole „nejakých“ prvkov, ktorých typ je zhodou okolností ukazovateľ na int. Dôraz sa tu kladie na hierarchiu typov v rámci deklarácie. Najprv napíšeme deklarátor pre pole:

api[]

Sme na začiatku hierarchie, použijeme preto priamo identifikátor výsledného objektu api. Ďalej potrebujeme deklarátor, ktorý bude vyjadrovať typ ukazovateľ. Vieme, že musíme použiť zápis:

* D1

Deklarátor D1 predstavuje niečo, čo sa bude v budúcnosti používať ako ukazovateľ. Deklarujeme pole ukazovateľov, za D1 teda dosadíme vyššie zapísaný deklarátor poľa. Keď totiž sprístupníme nejaký prvok tohto poľa, dostaneme objekt, ktorý má zmysel dereferencovať (predradením operátora *). Všimnite si úzku spojitosť deklarácie s následným spôsobom použitia. Keď je niečo deklarované ako „int xxx“, potom všade tam, kde prekladač bude očakávať typ int, môžeme použiť deklarované „xxx“. Takisto ak deklarátor ukazovateľa má tvar „* D1“, môže byť D1 hocičo, ale musí sa to vyhodnotiť na typ ukazovateľ. Spojenie našich dvoch deklarátorov vykonáme jednoducho použitím substitúcie a výsledok doplníme príslušným špecifikátorom základného typu, čím dostaneme žiadaný tvar:

int *api[];

To, či a kam budeme dávať medzery, závisí iba od nás, ibaže nesmieme vynechať medzeru tam, kde by mohlo dôjsť k dvojznačnosti pri interpretácii prekladačom.

V druhom prípade deklarujeme objekt pai ako ukazovateľ na pole prvkov typu int:

int (*pai)[];

Tentoraz deklarujeme pai ako ukazovateľ na „nejaký“ typ. Týmto typom je čírou náhodou pole celých čísel. Deklaráciu začneme zápisom deklarátora pre ukazovateľ:

* pai

Okrem neho potrebujeme ešte deklarátor, ktorý bude vyjadrovať pole prvkov. Nič jednoduchšie – použijeme zápis:

D1 []

Tentoraz ako D1 musíme uviesť niečo, čo bude v budúcnosti predstavovať pole. Keďže deklarujeme ukazovateľ na pole, zrejme týmto poľom bude náš dereferencovaný ukazovateľ. Zápis takejto dereferencie je (náhodou?) zhodný s uvedeným deklarátorom ukazovateľa. Nie, to nie je náhoda, to je úmysel. Za D1 naozaj musíme dosadiť deklarátor ukazovateľa. Lenže pozor, teraz oba deklarátory nemôžeme spojiť jednoducho pomocou substitúcie! Dôvodom je skutočnosť, že operátor [] má vyššiu prioritu ako * a my potrebujeme najprv ukazovateľ dereferencovať a až potom výsledok indexovať. Pomôžeme si rovnako ako vo výrazoch použitím okrúhlych zátvoriek. Do nich uzavrieme deklarátor ukazovateľa *pai a až teraz ho substituujeme za D1, čím dostaneme to, čo potrebujeme:

int (*pai)[];

Ak máte momentálne v hlave tak trochu „mišung“, tak vás upozorňujem, že dokonalé porozumenie predchádzajúceho príkladu je absolútne nevyhnutné na neskoršie úspešné ovládnutie umenia deklarácie objektu akéhokoľvek, ľubovoľne zložitého typu. Je veľmi dôležité uvedomiť si hierarchiu typov a podľa nej pri deklarácii postupovať. Nie je problém pomýliť sa, musíte byť preto maximálne pozorní – odmenou vám obyčajne bude nulový výskyt výnimiek typu GPF pri behu programu (vo Windows; v DOS-e to znamená, že program nebude prepisovať pamäť tam, kde nemá čo robiť).

Prejdeme si spolu ešte jeden príklad, ktorý bude dosť náročný. Ukážem vám však, že pri dodržaní správneho postupu deklarácie dôjdeme aj k správnemu výsledku. Deklarujeme si (a teraz dávajte pozor) ukazovateľ na pole desiatich ukazovateľov, z ktorých každý bude ukazovať na dvadsaťprvkové pole ukazovateľov na typ const char. Mohli by ste namietať, že také niečo sa v praxi hádam ani nedá použiť, ale to by ste sa mýlili – predstavte si, že máme program, ktorý používa pri rôznych operáciách dvadsať textových reťazcov. Ukazovatele na tieto reťazce máme uložené v nejakej tabuľke a reťazce sprístupňujeme z tabuľky použitím výrazu typu *tab[i]. Mohli by sme ich mať síce natvrdo zapísané v kóde programu všade tam, kde ten - ktorý reťazec používame, ale to by sme nemali možnosť realizácie nasledujúceho kroku: zmeny všetkých reťazcov napr. do iného jazyka. Veľmi jednoduchým a efektívnym spôsobom môžeme túto zmenu zabezpečiť výmenou používanej tabuľky. Znamená to, že budeme mať v programe viacero (minimálne teda dve) tabuliek s ukazovateľmi na príslušné reťazce. Samozrejme, najpriamočiarejším spôsobom výmeny tabuliek je zmena hodnoty nejakého všeobecne prístupného ukazovateľa, ktorý bude v každom okamihu ukazovať na práve používanú tabuľku. No ak máme  viac tabuliek (dohodli sme sa, že ich máme desať), treba si pamätať adresu každej z nich – najjednoduchšie opäť vo forme nejakej tabuľky, indexovanej napr. pomocou kódu aktuálneho „jazyka“. Potom budeme mať vo všeobecne prístupnej premennej uložený nie ukazovateľ na aktuálnu tabuľku, ale tento kód a ukazovateľ získame vyhľadaním v tabuľke. Ukazovateľ na takúto tabuľku bude mať práve ten spomínaný komplikovaný typ a použijeme ho napríklad pri odovzdávaní adresy tabuľky do nejakej funkcie.

Na začiatok si uvedieme, k čomu chceme dospieť. Uvedený ukazovateľ s názvom p deklarujeme ako:

const char *(*(*p)[10])[20];

Na pohľad to nevyzerá až tak zložito, čo poviete? Poďme si teda ukázať jednotlivé kroky. Budeme postupovať podľa hierarchie (sledujte súčasne opis typu). V prvom rade deklarujeme ukazovateľ (na niečo). Teda napíšeme:

*p

Tento ukazovateľ ukazuje na typ „pole desiatich prvkov“:

D1[10]

Každý prvok poľa je opäť ukazovateľom:

*D2

A každý z týchto ukazovateľov ukazuje na pole dvadsiatich prvkov:

D3[20]

Nakoniec každý z týchto dvadsiatich prvkov je ukazovateľom (na typ const char):

*D4

Teraz len jednoducho postupujeme zhora nadol a každý riadok dosadíme do nasledujúceho riadka za príslušný deklarátor (pozor na prioritu, tam, kde treba, doplníme zátvorky!). Uvedieme si tu jednotlivé fázy vytvárania deklarácie, v ktorých bude substituovaná časť vždy obalená zátvorkami a zvýraznená kurzívou:

*p

(*p)[10]

*((*p)[10])

(*((*p)[10]))[20]

*((*((*p)[10]))[20])

Nakoniec odstránime nadbytočné páry zátvoriek, doplníme špecifikátor základného typu (const char) a dostávame želaný výsledok:

const char *(*(*p)[10])[20];

Verím, že ste celý postup bez problémov zvládli. Ukážem vám ešte jeden zo spôsobov kontroly, či to, čo ste napísali, je správne. Celý výraz predstavuje deklaráciu objektu nejakého typu. Keď sa naň pozrieme ako na deklaráciu v tvare „const char xxx“, znamená to, že časť xxx musí byť typu const char. Špecifikátor const char odstránime a ďalej postupujeme viac-menej rekurzívne. Časť xxx vyzerá takto: *(*(*p)[10])[20]. Vieme, že [] má vyššiu prioritu ako *, preto najprv oddelíme operátor * (postupujeme v opačnom poradí, ako určuje priorita!). Časť xxx sa nám transformuje do podoby „* yyy“, kde yyy je logicky typu „ukazovateľ na (už odstránený) const char“. V ďalšom kroku z časti yyy (ktorá teraz vyzerá ako (*(*p)[10])[20]) oddelíme [20], čím dostaneme výraz „zzz [20]“, kde zzz je typu „pole dvadsiatich ukazovateľov na const char“. Časť zzz je teraz ekvivalentná (*(*p)[10]). Zátvorky nepotrebujeme, preto ich jednoducho odstránime a ďalej pokračujeme úplne rovnako. Na konci sa dopracujeme k vnorenému identifikátoru p, ktorého typ by nám mal vyjsť zhodný s naším zamýšľaným typom. Ak nám vyšlo niečo iné, v deklarácii je chyba.

(Poznámka: Vzhľadom na to, že sme sa dosiaľ nevenovali deklarácii funkcií, je deklarácia v uvedenom príklade prakticky len striedavou kombináciou polí a ukazovateľov. V praxi sa používajú aj oveľa komplikovanejšie typy, v ktorých sa vyskytujú ukazovatele na funkcie, na triedy, polia týchto ukazovateľov a podobne. Každá deklarácia sa však dá postupne rozvinúť na sériu jednoduchých deklarátorov, na základe ktorých dokážeme deklarovaný typ identifikovať.)

Deklarácia a definícia funkcie

Aj pre deklaráciu funkcie existuje podobný recept ako pre deklaráciu predchádzajúcich objektov (pozor, ide o deklaráciu čiže prototyp funkcie, nie o jej definíciu – chýba telo funkcie):

T D1 ( zoznam-deklarácií-argumentov )

Meno, ktoré takto deklarujeme (určené identifikátorom, vnoreným v deklarátore D1), bude mať typ „… funkcia s argumentmi podľa zoznamu-deklarácií-argumentov, vracajúca typ T“. Návratovým typom T môže byť ľubovoľný typ (vrátane void) s výnimkou polí a funkcií (keď chceme vrátiť pole, definujeme ako návratový typ ukazovateľ na typ prvku poľa; vracanie funkcií sa realizuje aj pomocou ukazovateľov na funkcie, ktoré ako návratový typ použiť môžeme).

Zoznam deklarácií argumentov je zoznamom bežných deklarácií, oddelených čiarkou. Tieto deklarácie však neurčujú nové premenné, ale tzv. formálne argumenty funkcie. Formálne z toho dôvodu, že skutočné hodnoty argumentov funkcie sú známe až v okamihu jej volania. V tele funkcie však potrebujeme na jednotlivé argumenty odkazovať, formálne argumenty sú teda formálnymi zástupcami skutočných argumentov. V rámci definície funkcie (t. j. jej tela) sa formálne argumenty správajú ako bežné, automatické (lokálne) premenné, ktorých rozsah platnosti sa začína okamihom deklarácie a končí sa v okamihu ukončenia tela funkcie. Tieto lokálne premenné sa vytvárajú na zásobníku a inicializujú sa hodnotami skutočných argumentov v okamihu volania funkcie. Formálne argumenty môžeme v tele funkcie ľubovoľne meniť, tieto zmeny sa nijako neprejavia na hodnotách premenných, ktoré sme ako skutočné argumenty funkcii poskytli. Aby to bolo jasnejšie, ukážeme si krátky príklad:

#include <stdio.h>
 
void f(int a)
{
    a = 5;
}
 
void main()
{
    int b = 3;
    printf("b = %i\n", b);
    f(b);
    printf("b = %i\n", b);
}

V príklade máme definovanú funkciu f(), ktorá má jediný argument a typu int. V tele funkcie tomuto argumentu priraďujeme hodnotu 5. Z funkcie main(), kde sme deklarovali lokálnu premennú b, zavoláme funkciu f() a odovzdáme jej ako skutočný argument túto premennú b. V C++ sa argumenty vždy odovzdávajú hodnotou (by value), preto funkcia f() dostane iba obsah premennej b, ktorým inicializuje svoj formálny argument (a teda svoju lokálnu premennú) a. Ako uvidíme po spustení programu, hodnota premennej b sa volaním funkcie f() nijako nezmení. Funkcia f() po inicializácii argumentu a stratila akékoľvek spojenie s objektom (v našom prípade premennou b), ktorého hodnota bola použitá ako skutočný argument.

Hovorili sme si už o tom, ako vyriešiť požiadavku odovzdávania argumentov odkazom (by reference). V zásade sú možné dva spôsoby. Pri prvom funkcii odovzdáme ako argument ukazovateľ na premennú, ktorú chceme meniť. Nasledujúci príklad ukazuje notoricky známy typ funkcie na výmenu obsahov dvoch premenných (typu int):

void swap(int* pa, int* pb)
{
    int tmp = *pa;
    *pa = *pb;
    *pb = tmp;
}

Funkcia je natoľko triviálna, že ju nejdem ani vysvetľovať. Pri jej volaní musíme ako argumenty uviesť ukazovatele na premenné, ktorých obsahy vymieňame (čo je prakticky jej jediná nevýhoda).

Pri druhom spôsobe riešenia problému odovzdávania argumentov odkazom deklarujeme formálny argument ako referenciu na typ tejto premennej. Pri volaní sa (dosiaľ neinicializovaná!) referencia inicializuje odkazom na odovzdávanú premennú, ktorú tak v rámci funkcie môžeme meniť. Pozmeňme trochu predposledný príklad tak, aby funkcia f() dostala svoj argument ako odkaz:

#include <stdio.h>
void f(int& a)
{
    a = 5;
}
 
void main()
{
    int b = 3;
    printf("b = %i\n", b);
    f(b);
    printf("b = %i\n", b);
}

Pridali sme jediný znak ('&') do programu a jeho výstup je úplne iný – tentoraz funkcia f() dostane ako argument referenciu na objekt, ktorý bol použitý ako skutočný argument, takže každá zmena, ktorú táto funkcia nad svojím argumentom vykoná, sa prejaví aj na tomto skutočnom argumente.

Za domácu úlohu zmeňte funkciu swap() tak, aby jej argumentmi boli referencie. Je jasné, že sa zmení aj spôsob jej volania. Pre zaujímavosť tu uvádzam aj alternatívne spôsoby výmeny dvoch (celočíselných!) premenných a a b:

a += b;
b = a - b;
a -= b;

alebo aj:

a ^= b;
b ^= a;
a ^= b;

Porozmýšľajte nad tým, ako fungujú a ako je možné, že pri prvom z nich neprekáža, keď pri sčítaní/odčítaní dôjde k pretečeniu.

Aby sme sa však vrátili k funkciám – zoznam argumentov funkcie môže byť aj prázdny, to vtedy, keď funkciu nepotrebujeme volať s argumentmi. Ekvivalentom prázdneho zoznamu argumentov (ktorý zapisujeme ako ()) je zoznam s jediným kľúčovým slovom void. Inak toto kľúčové slovo ako typ argumentu nemôžeme použiť (samozrejme, s výnimkou ukazovateľov na void, ale to už vieme). Ďalej má C++ jednu špecifickú vlastnosť: umožňuje deklarovať funkciu s neznámym, resp. s premenným počtom argumentov. Zoznam deklarácií argumentov takejto funkcie musí vyzerať takto:

( zoznam-deklarácií-argumentov, ... )

alebo:

( zoznam-deklarácií-argumentov ... )

Za znakmi výpustky (elipsy) "..." už nesmie nasledovať nijaký ďalší argument. Zoznam deklarácií argumentov nachádzajúci sa pred výpustkou môže byť aj prázdny. Príkladom takejto funkcie je vám dobre známa funkcia printf(), ktorej prototyp si môžete vyhľadať v hlavičkovom súbore stdio.h. Nájdete tam niečo príbuzné tomuto:

int printf(const char*, ...);

Tento prototyp udáva, že prvým argumentom funkcie printf() je vždy reťazec. Čo uvedieme za ním, to sa nedá povedať dopredu, a preto prekladač ani nebude kontrolovať zhodu typu ďalších argumentov (ani nemá s čím). Funkcia sama, samozrejme, musí nejako vedieť, aké dostala tie zvyšné argumenty – konkrétne printf() si to zistí na základe obsahu prvého argumentu (formátovacieho reťazca). Iným spôsobom je použitie konvencie, že posledný argument, ktorý uvedieme, bude mať nejakú známu hodnotu (napr. 0). Spôsob, akým je možné pristupovať k nedeklarovaným argumentom, je implementačne závislý, a preto na tieto účely máme k dispozícii súbor makier definovaných v hlavičkovom súbore stdarg.h. Povieme si o nich však až pri rozprávaní o štandardnej knižnici.

Vráťme sa ešte na chvíľu k zoznamu argumentov. Jeho význam spočíva predovšetkým v tom, že prekladač, vidiac prototyp deklarovanej funkcie, môže pri každom jej volaní kontrolovať, či bola dodržaná (vzhľadom na štandardné konverzie) zhoda typov formálnych a skutočných argumentov. Inak povedané, ak deklarujeme funkciu s argumentom typu char* a voláme s argumentom typu double, prekladač nám pri preklade dôrazne vynadá. V tejto súvislosti treba spomenúť dôležitý fakt, že jazyk C++ povoľuje, aby dve funkcie s rôznymi typmi argumentov mali rovnaký názov. Tento zdanlivý detail je v skutočnosti obrovskou výhodou, ktorú oceníte pri návrhu rozsiahlejších systémov. Ukážeme si tu len jednoduchý (a značne otrepaný) príklad, z ktorého však bude zrejmé, o čo ide. Predstavme si, že potrebujeme funkciu na výpočet maxima z dvoch hodnôt. Nič ľahšie, definujeme funkciu max():

int max(int a, int b)
{
    return a > b ? a : b;
}

Lenže čo v prípade, ak potrebujeme počítať maximum z dvoch hodnôt, ktoré sú iného typu ako int? V jazyku C bolo treba definovať sériu funkcií s obskúrnymi menami, ako max_int(), max_long(), max_double() a pod. V C++ môžeme bez problémov definovať funkciu max() znova s iným typom argumentov:

double max(double a, double b)
{
    return a > b ? a : b;
}

(Tento príklad je zvláštny okrem iného aj tým, že telo funkcie je rovnaké pre oba prípady. V takej situácii sa obyčajne použije šablóna funkcie. Ale o tom až niekedy v budúcnosti.)

Funkcie, ktoré majú rovnaký názov, sa musia líšiť v počte a/alebo type svojich argumentov, nestačí len odlišnosť v návratovom type! Inak by sa totiž prekladač nevedel rozhodnúť, ktorú z oboch funkcií použiť (v prípade, že návratovú hodnotu funkcie po jej zavolaní ignorujeme). Rozhodovanie prekladača, ktorú z viacerých funkcií použiť, je téma na samostatný článok, takže zatiaľ ju nenápadne preskočíme. Čo sa týka rozlišovania argumentov, typy definované pomocou typedef sa pokladajú za zhodné so svojimi ekvivalentmi (ehm, to asi nie je veľmi jasné – jednoducho ak máme v programe riadok typedef int bool;, je typ bool zhodný s typom int a funkcie f(int a) a f(bool a) nie sú odlišné). Počet a typ argumentov funkcie spolu s jej identifikátorom a typom návratovej hodnoty tvoria tzv. signatúru funkcie (ktorú v mierne prispôsobenej podobe v podstate používa linker) a v programe musí mať každá funkcia túto signatúru jedinečnú. Celý fenomén existencie viacerých funkcií s rovnakým názvom sa v angličtine nazýva function overloading. Toto slovné spojenie si preložte, ako sami uznáte za vhodné, ja sa prikláňam najviac k prekladu „prekrývanie funkcií“, oproti možno formálne správnemu, ale akosi násilnému a príliš doslovnému „preťažovanie funkcií“. Overload síce v angličtine znamená aj „preťažiť“, ale (to len tak na zamyslenie) slovo load sa používa v počítačovom slangu na opis úplne inej činnosti, ako je nakladanie či nabíjanie, čo vy na to?

Pri argumentoch funkcie ešte chvíľu zostaneme. C++ poskytuje pre začínajúceho programátora ešte jedno lákadlo, a tým sú implicitné argumenty funkcií. Pod implicitným argumentom si predstavte taký formálny argument, ktorý si pamätá svoju implicitnú hodnotu. Tou sa inicializuje, ak pri volaní funkcie neuvedieme jeho hodnotu explicitne. Zoberme si veľmi jednoduchý príklad – chceme vypočítať absolútnu hodnotu komplexného čísla pomocou funkcie cabs(). Prvým argumentom tejto funkcie nech je reálna zložka komplexného čísla, druhým imaginárna zložka. Aby sme funkciu využili aj na výpočet absolútnej hodnoty reálnych čísel (to sú vlastne všetky komplexné čísla s nulovou imaginárnou zložkou), povieme deklaráciou funkcii, že vždy, keď jej nedodáme druhý argument, má ho implicitne brať ako nulový. Prototyp takej funkcie vyzerá takto:

double cabs(double re, double im = 0.0);

Vidíme, že deklarácia implicitného argumentu vyzerá úplne rovnako ako deklarácia bežnej premennej spojená s inicializáciou. Musíme však mať na pamäti dôležitý fakt. Len čo vyhlásime niektorý z argumentov za implicitný, musíme ako implicitný deklarovať aj každý ďalší. To vyplýva aj zo spôsobu volania funkcie – keď raz nejaký argument vynecháme, musíme vynechať aj všetky „za ním“. Tým som vlastne naznačil, ako zabezpečíme použitie implicitnej hodnoty argumentu pri volaní funkcie: jednoducho príslušný argument vynecháme. Našu funkciu cabs()môžeme teda volať dvoma spôsobmi:

double a1 = cabs(1.23, -4.56);
double a2 = cabs(7.89);

V prvom prípade do premennej a1 ukladáme absolútnu hodnotu čísla 1,23 – 4,56i, v druhom do premennej a2 absolútnu hodnotu čísla 7,89. Z príkladu je zrejmé aj to, že môžeme implicitnú hodnotu argumentu zmeniť explicitným uvedením inej hodnoty (v našom prípade –4,56 namiesto nuly). Počet implicitných argumentov nie je okrem uvedeného pravidla nijako obmedzený, funkcia môže mať všetky argumenty implicitné alebo nemusí mať ani jeden. V prípade viacerých prototypov (resp. prototypu a skutočnej definície) funkcie nesmú byť implicitné argumenty predefinované (ani rovnakou hodnotou!). V praxi teda obyčajne uvediete implicitnú hodnotu do prototypov, ktoré máte uložené v hlavičkovom súbore.

Zhrňme si teraz stručne, čo sa deje, keď program dospeje do bodu, v ktorom má zavolať nejakú funkciu. Všetky skutočné argumenty, ktoré majú byť funkcii odovzdané, sú (resp. ich hodnoty) skopírované na zásobník. Poradie, v ktorom sa toto kopírovanie deje, je špecifické pre jazyk C++ – argumenty sa kopírujú sprava doľava (ako prvý sa uloží do zásobníka posledný argument funkcie a ako posledný sa uloží prvý argument) na rozdiel napríklad od Pascalu, kde sa argumenty kopírujú presne naopak, zľava doprava. Preto napríklad C++ podporuje premenný počet argumentov funkcie a Pascal nie. Obyčajne je totiž počet argumentov zakódovaný v prvom z nich a tento prvý argument je po zavolaní funkcie na vrchole zásobníka. Vieme, že zásobník je LIFO štruktúra, pristupovať teda (bežným spôsobom) môžeme iba k údajom na jeho vrchole. Po skopírovaní skutočných argumentov sa do zásobníka uloží ešte návratová adresa (aby procesor vedel, kam sa má vrátiť, keď sa funkcia skončí) a riadenie sa prenesie na začiatok volanej funkcie. Funkcia vykoná to, čo má, a eventuálne vráti volajúcemu kódu návratovú hodnotu. Tá je uložená v niektorom z registrov, prípadne aj na zásobníku. Po návrate z funkcie volajúca funkcia vyčistí zásobník od argumentov, ktoré tam vložila. Aj toto je špecifikom C++, v iných jazykoch (zase ma napadá len ten Pascal) zásobník čistí volaná funkcia. Kedysi dávno som mal v úmysle aj nakresliť, ako to vyzerá na zásobníku po zavolaní nejakej funkcie, ale predbehol ma jeden z kolegov v seriáli o programovaní v assembleri. Takže ak sa chcete poučiť, viete, kam sa máte obrátiť.

Ešte si povedzme niečo o definícii funkcií. To je, ako vieme, deklarácia, ktorá obsahuje aj telo funkcie (čiže samotný opis toho, čo funkcia robí). Ako vyzerá definícia, to hádam netreba osobitne ukazovať, je to jednoducho spojenie špecifikátorov funkcie, deklarátora opísaného v predchádzajúcich riadkoch (neukončujeme bodkočiarkou – nejde o deklaračný príkaz!!) a zloženého príkazu, predstavujúceho telo funkcie.

Na záver rozprávania o funkciách si ukážeme (podobne ako pri deklarácii polí) rozdiel medzi deklaráciou funkcie vracajúcej ukazovateľ a deklaráciou ukazovateľa na funkciu. Majme najprv funkciu f(), ktorá má dva celočíselné argumenty a ktorá vracia ukazovateľ na typ int. Pri zápise jej deklarátora postupujeme podobne, ako sme si to hovorili pri deklarácii polí, ale treba dávať pozor na to, že tu nie je až taká jasná hierarchia toho, čo deklarujeme. Chceme deklarovať „funkciu, ktorá vracia ukazovateľ“, začneme preto deklarátorom funkcie:

f(int a, int b)

Funkcia f() má vracať ukazovateľ, napíšeme teda ďalej deklarátor ukazovateľa:

* D1

Spojením oboch deklarátorov a pridaním typového špecifikátora dostaneme výsledok:

int *f(int a, int b);

Toto je naozaj deklarácia funkcie vracajúcej ukazovateľ na typ int. Druhým prípadom bude ukazovateľ pf na funkciu, ktorá má opäť dva celočíselné argumenty a vracia samotný typ int. Deklarujeme „ukazovateľ na funkciu“, tentoraz teda začneme deklarátorom ukazovateľa:

*pf

Ďalej napíšeme deklarátor požadovanej funkcie:

D1 (int a, int b)

Oba deklarátory spojíme. Aj tu však musíme dať pozor, lebo operátor volania funkcie () má vyššiu prioritu ako operátor *. Použijeme preto zátvorky a dostaneme takýto výsledok:

int (*pf)(int a, int b);

Toto je deklarácia ukazovateľa na funkciu s danými argumentmi a návratovým typom.

Zaujímavým príkladom je deklarácia funkcie, ktorá má ako argument aj ako návratovú hodnotu ukazovateľ na nejakú inú funkciu. Takouto funkciou je napríklad funkcia signal() zo štandardnej knižnice, ktorej prvým argumentom je typ int a druhým argumentom i návratovou hodnotou je ukazovateľ na funkciu typu void f(int n). Takýto ukazovateľ by sme vedeli deklarovať bez problémov, čo však so spomenutou funkciou signal()? Riešenie je dosť náročné, postup, ktorý sme si povedali, nie je taký názorný. Problém je totiž v tom, že funkcia signal() vracia ukazovateľ na inú funkciu. Normálne je návratový typ funkcie vyjadrený typovým špecifikátorom, tu však typovým špecifikátorom vyjadrujeme návratový typ funkcie, na ktorú ukazuje ukazovateľ, ktorý je návratovým typom našej funkcie signal(). Asi to bude jasnejšie z ukážky postupu. Začíname deklarátorom funkcie s požadovanými argumentmi:

signal(int i, void (*pf)(int s))

Nasleduje deklarátor ukazovateľa (všeobecného):

* D1

Ukazovateľ ukazuje na funkciu s jediným argumentom typu int:

D2 (int s)

Teraz deklarátory spojíme a pridáme k nim ešte spomínaný typový špecifikátor, v našom prípade void:

void (*signal(int i, void (*pf)(int s)))(int s)

Ak máte pocit, že ste tak trochu mimo, nič si z toho nerobte, toto patrí takpovediac medzi „vyššiu matematiku“ C++ a časom iste všetkému porozumiete. Na domácu úlohu si vyskúšajte napríklad deklaráciu poľa ukazovateľov na funkcie, deklaráciu referencie na funkciu a podobne – fantázii sa medze nekladú.

Abstraktné deklarátory

K úplnosti výkladu o deklarátoroch nám ešte čosi chýba. Tým čímsi sú tzv. abstraktné deklarátory. Abstraktný deklarátor vznikne z klasického odstránením identifikátora objektu, ktorý deklarujeme. Nasledujú príklady klasických deklarátorov objektov typu int, ukazovateľ na int, pole prvkov typu int, pole ukazovateľov na int, ukazovateľ na pole prvkov typu int, funkcia, vracajúca ukazovateľ na int a ukazovateľ na funkciu vracajúcu int:

int i
int *pi
int ai[]
int *api[]
int (*pai)[]
int *f()
int (*pf)()

a tu sú abstraktné deklarátory týchto objektov:

int
int *
int []
int *[]
int (*)[]
int *()
int (*)()

Abstraktné deklarátory sa v praxi aj naozaj používajú, a to pri deklarácii funkcií (pozor, nie pri definícii!). Vtedy totiž prekladač nepotrebuje vedieť názvy formálnych argumentov, zaujíma ho len ich typ. Môžeme teda v zozname argumentov funkcie vynechať ich identifikátory. Napríklad spomínanú funkciu signal() môžeme deklarovať aj takto (a v hlavičkovom súbore tak aj bude):

void (*signal(int, void (*)(int)))(int)

Okrem toho sa abstraktné deklarátory používajú na vyjadrenie typu pri explicitnom pretypovaní operátorom (typ).

Inicializácia premenných

O tom, ako sa inicializujú jednotlivé typy premenných, sme už písali. Zhrnieme si preto všetky vedomosti (a doplníme novými) o inicializácii, ktorá je zo svojej podstaty súčasťou deklarácie.

Inicializáciu v zásade môžeme zapísať dvoma spôsobmi. Prvý z nich je tento:

špecifikátory deklarátor = init-hodnota

Druhý z nich (používaný prevažne pri objektových premenných):

špecifikátory deklarátor ( init-hodnota )

Premenné môžeme inicializovať ľubovoľnými výrazmi (s ohľadom na kompatibilitu typov), s použitím už deklarovaných premenných či funkcií, prípadne konštánt. Objekty typu T môžeme inicializovať inými objektmi typu T bez ohľadu na const či volatile modifikátory. Ukazovateľ typu const T* môžeme inicializovať ukazovateľom typu T*, ale nie naopak! Statické premenné (z hľadiska ukladacej triedy) sú implicitne inicializované na nulu, konvertovanú na príslušný typ, automatické a registrové premenné majú implicitne nedefinovanú hodnotu.

Polia inicializujeme pomocou zoznamu hodnôt, uzavretého v krútených zátvorkách. Počet prvkov v tomto zozname nesmie presiahnuť deklarovanú dĺžku poľa, ak dĺžku neuvedieme, prekladač si ju dosadí automaticky podľa dĺžky inicializačného zoznamu (ktorý je v takom prípade povinný). Znakové polia (a rovnako aj ukazovatele na typ char) môžeme inicializovať pomocou reťazcových konštánt. Bližšie informácie nájdete v časti venovanej poliam.

Referencie typu T& musia byť inicializované povinne objektom typu T (alebo kompatibilným). Referencia na volatile T môže byť inicializovaná objektom typu volatile T alebo T, ale nie const T. Naopak, referencia na const T môže byť inicializovaná objektom typu const T alebo T, ale nie volatile T. Referencia na typ T môže byť inicializovaná iba objektom typu T. Ďalšie detaily opäť nájdete v časti venovanej referenciám.

Priraďovanie hodnoty formálnym argumentom funkcie a vytváranie návratovej hodnoty sa považuje za inicializáciu (preto referencie ako argumenty nemusia byť inicializované pri deklarácii funkcie).

A máme to za sebou

Tak sme sa úspešne dostali na koniec dlhočizného výkladu o deklaráciách. Je to naozaj náročná téma, ktorej dokonalé zvládnutie je podmienené hlavne praxou. Nezúfajte, ak nemáte vo všetkom jasno. A ako pravidelne pripomínam, vracajte sa k starším častiam seriálu a predovšetkým si všetko skúšajte na príkladoch. Odmenou vám budú funkčné programy.

V nasledujúcej časti sa budeme zaoberať o štandardnými konverziami, prácou preprocesora a ešte niekoľkými významnými maličkosťami.

C++

Nechajte si posielať prehľad najdôležitejších správ emailom

Mohlo by Vás zaujímať

Ako na to

Tipy a triky: Ako na snímku obrazovky na akomkoľvek počítači s Windows?

02.12.2016 00:13

Ak snímky obrazovky robíte často apotrebujete napríklad funkcie na posun stránok alebo snímanie zobrazenia pri vyššom rozlíšení displeja, zrejme používate nejakú špecializovanú aplikáciu. Väčšina použ ...

Ako na to 1

Tipy a triky: Ako aplikácii prednastaviť spúšťanie s administrátorskými právami?

30.11.2016 00:10

Väčšina aspoň trochu skúsenejších používateľov vie, že aj keď máte na operačnom systéme Windows vytvorený administrátorský účet, aplikácie pre bezpečnosť nefungujú vždy splnými administrátorskými práv ...

Ako na to 2

Tipy a triky: Ako vypnúť uzamykaciu obrazovku vo Windows 10?

29.11.2016 00:10

Rozčuľuje vás, že pred každým prihlásením doúčtu vášho počítača musíte prejsť uzamykacou obrazovkou? Windows 10 na tejto obrazovke ukazuje čas,dátum anejakú zaujímavú fotografiu zrôznych kútov sveta. ...

Žiadne komentáre

Vyhľadávanie

Kyocera - prve-zariadenia-formatu-a4-s-vykonom-a3

Najnovšie videá