Image
12.6.2016 0 Comments

C++ / Deklarácie II. / 12. časť

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

Tak vás vítam v novom roku 1999. Dúfajme, že bude lepší ako tie doposiaľ (a horší ako tie, čo ešte len prídu). V každom prípade je posledným rokom, ktorý sa začína dvojčíslím 19. Zámerne nepíšem, že je posledným rokom tohto storočia, pretože podľa mňa (a podľa mnohých racionálne zmýšľajúcich ľudí) 21. storočie a súčasne tretie tisícročie sa začína až 1. januára 2001 o 0.00 hod. miestneho času. A to z toho dôvodu, že kresťanský letopočet, odvíjajúci sa od roku narodenia Krista, sa začal rokom 1, jeho prvé desaťročie sa skončilo rokom 10 (po desiatich rokoch od začiatku), prvé storočie rokom 100 a tak ďalej. Teraz máme 20. storočie, ktoré sa skončí v okamihu, keď sa skončí rok 2000 (= 20 × 100). Samozrejme, náš letopočet sa nezačal tak, že niekto vyhlásil: „Teraz je rok 1, začíname!“ Bol zavedený (ak sa nemýlim) o niečo neskôr, po spočítaní času, ktorý uplynul od cirkvou stanoveného počiatku, teda od roku narodenia Krista (nepýtajte sa ma, ktorý pápež bol vtedy hlavou cirkvi a kto výpočty realizoval, to vám veru nepoviem). Ale v každom prípade okamih, v ktorom sa rok 1999 zmení na rok 2000, bude asi veľmi vzrušujúci a môžeme ďakovať osudu, že sme sa narodili v správny čas a tento prechod zažijeme. S oslavami nového milénia však bude treba počkať ešte 365 dní.

Ak máme veriť všetkým katastrofickým scenárom, ktoré opisujú stav sveta 1. januára 2000 ako takmer apokalyptický, mali by sme sa už pomaly začali baliť, kopať si zemľanku niekde v záhrade, nakúpiť si zásoby jedla aspoň na dva roky a podobne. Alebo nie? Problém roku 2000 (Y2K, Year 2k problem) sa čoraz neodbytnejšie hlási k slovu a vďaka „zaručeným“ správam rôznych novinárov, ktorí citujú nemenej zaručené vyhlásenia anonymných analytikov, softvérových inžinierov, prípadne počítačových poradcov, je až neprimerane medializovaný, bohužiaľ, s presne opačným účinkom – priemerne (t. j. dosť málo) inteligentný Američan je schopný spomenuté prípravy realizovať a na tie dva roky sa naozaj niekde „zakopať“. A čo my? Zoženieme si lopatu alebo to riskneme a budeme všetky varovania ignorovať? Ako obyčajne riešenie je niekde uprostred. Rozhodne Y2K neovplyvní priemerného či občasného používateľa PC – keby mu náhodou jeho dosové účtovníctvo prestalo fungovať (čo by mal otestovať ešte pred Silvestrom), jednoducho si zaobstará nový program. Jeho firmu to pravdepodobne nepoloží a jeho samého takisto nie. Horšie je to s podnikmi, kde informačné systémy slúžia na zabezpečenie alebo realizáciu kritických operácií, od ktorých závisia ľudské životy, dodávka energie, plynulý chod spoločnosti – to sú napr. nemocnice, elektrárne, banky, dopravné spoločnosti a iné. Takéto podniky (za predpokladu, že ich IS sú ovplyvnené prechodom do nového tisícročia) budú musieť rozhodne celú záležitosť brať vážne a dovolím si tvrdiť, že väčšina (dúfam, že podstatná) z nich dnes nesedí so založenými rukami, ale realizuje príslušné opravy.

Priestupné roky a príkazový riadok

Verím, že mi prepáčite, že som takpovediac zneužil úvod svojho seriálu na krátky pohľad na problémy súvisiace s prechodom do roku 2000. Aby som nadviazal na hlavnú tému seriálu, uvedieme si krátky program, pomocou ktorého sa dá otestovať, či je zadaný rok priestupný, alebo nie. Tento test totiž niektoré programy realizujú nesprávne a rok 2000 napríklad označia za nepriestupný. Od zavedenia gregoriánskeho kalendára sa za priestupné roky považujú tie, ktoré spĺňajú nasledujúce kritériá:

  • ak je rok deliteľný číslom 4 a súčasne nie je deliteľný číslom 100, potom je priestupný;
  • ak je rok deliteľný číslom 100 a súčasne nie je deliteľný číslom 400, potom nie je priestupný;
  • ak je rok deliteľný číslom 400, potom je priestupný;
  • vo všetkých ostatných prípadoch rok nie je priestupný.

Podľa týchto pravidiel teda rok 2000 priestupný je, pretože hoci je deliteľný číslom 100, je súčasne deliteľný aj číslom 400. Ak si pravidlá zhrnieme, rok je priestupný len vtedy, ak je deliteľný číslom 400 alebo ak je deliteľný číslom 4 a súčasne nie číslom 100. Prv než budete pokračovať, skúste si príslušný test napísať pomocou operátorov jazyka C++. Je to jednoduché, použijeme operátor % (modulo) a logické operátory && a ||. Príklad je koncipovaný ako samostatný program:

#include
#include
 
int main(int argc, char* argv[])
{
    if (argc != 2)
        exit(1);
       
    int r = atoi(argv[1]);
    printf("Rok %i je ", r);
    int p = ( (r % 4 == 0)
           && (r % 100 != 0) )
           || (r % 400 == 0);
    printf(p ? "priestupný.\n"
             : "nepriestupný.\n");
    return 0;
}

Operátor % vráti nulovú hodnotu, ak jeho prvý argument je deliteľný druhým. Premennú r predstavujúcu rok testujeme trikrát – na deliteľnosť číslami 4, 100 a 400. Podmienka priestupnosti (uložená do premennej p) je splnená, ak r je deliteľné 4 (r % 4 == 0) a súčasne (&&) r nie je deliteľné 100 (r % 100 != 0) alebo (||) r je deliteľné 400 (r % 400 == 0). Namiesto r % 4 == 0 môžeme písať aj !(r % 4) a namiesto r % 100 != 0 iba r % 100, čo sa v praxi obyčajne aj robí, ale na ilustračné účely je náš zápis vhodnejší. Po vyhodnotení testu bude v p nenulová hodnota, ak rok r je priestupný, a nulová, ak je nepriestupný.

Rok, ktorý chceme otestovať, sa programu zadá ako voliteľný argument na príkazovom riadku (bohužiaľ, opäť musím nariekať nad nepružnosťou slovenčiny a uviesť výstižnejší anglický názov command-line argument). Čo je to argument, definuje prekladač a/alebo prostredie, v ktorom program beží, obyčajne je to časť príkazového riadka, ohraničená z oboch strán medzerami. Pokiaľ chceme medzeru (a prípadne rôzne „zakázané“ znaky) zahrnúť do argumentu, ohraničujeme ho úvodzovkami. Dosiaľ sme si nepovedali, akým spôsobom je príkazový riadok prístupný programátorovi, takže to teraz napravíme. Funkcia main(), ktorú sme si uvádzali vždy bez argumentov, v skutočnosti má minimálne dva argumenty (je tu menšia slovná kolízia – argumenty funkcie vs. argumenty príkazového riadka; význam by mal byť jasný z kontextu). Ak tieto argumenty nepoužívame, nemusíme ich deklarovať, ale ak s nimi chceme pracovať, musí byť prvý z nich typu int a druhý typu char**. Hoci ich názvy nie sú určené normou, podľa zaužívaných konvencií ich deklarujeme ako argc a argv. Prvý z nich, argc, vyjadruje počet argumentov (alebo parametrov?) zadaných na príkazovom riadku. Tento počet je však o jeden vyšší ako skutočný, pretože programu sa ako prvý z argumentov dodá názov jeho vykonateľného súboru (s cestou či bez nej, to opäť závisí od prekladača či prostredia – dokonca napríklad prekladač GCC pre DOS/Windows znaky '\' v ceste zmení podľa konvencií Unixu na znaky '/'). Druhý z argumentov predstavuje pole reťazcov reprezentujúcich jednotlivé zadané argumenty príkazového riadka. Vieme, že reťazce sú vlastne polia znakov, preto ide v skutočnosti o pole ukazovateľov na tieto reťazce. Ďalej vieme, že pole sa v prípade, ako je tento (teda odovzdávanie do funkcie), prevádza na ukazovateľ na jeho prvý prvok. Preto typ argumentu argv je „ukazovateľ na ukazovateľ na char“. Takýto ukazovateľ môžeme pri zápise formálneho argumentu funkcie deklarovať buď ako char** argv, alebo tak ako v našom príklade char* argv[]. Druhý spôsob je, myslím, zrozumiteľnejší a zreteľne vyjadruje, že ide o pole ukazovateľov na char.

Prvý z argumentov programu, ktorý je prístupný ako argv[0], obsahuje spomínaný názov súboru, v ktorom je program uložený. Nasledujú zvyšné argumenty v tom poradí, v akom boli zadané na príkazovom riadku. Navyše máme zaručené, že celé pole argv[] bude ukončené nulovým ukazovateľom, t. j. platí, že argv[argc] == 0. K jednotlivým argumentom môžeme pristupovať nielen pomocou indexov v rozsahu 0 až argc–1, ale aj prostredníctvom ukazovateľa na typ char*, ktorý inicializujeme hodnotou argv. Jeho postupnou inkrementáciou a následnou dereferenciou dostávame príslušné ukazovatele na ­argumenty. Na tento účel môžeme použiť aj priamo premennú argv, ktorá vďaka tomu, že je formálnym argumentom funkcie, správa sa ako bežná lokálna premenná, inicializovaná príslušným skutočným parametrom. Pri prípadných nejasnostiach sa vráťte o dve časti naspäť k obrázku, ktorý znázorňuje pole ukazovateľov.

Nasledujú tri verzie programu, ktorý vypíše všetky svoje argumenty, ale každý na samostatný riadok:

// echo1.cpp
int main(int argc, char* argv[])
{
    for (int i = 1; i < argc; i++)
        printf("%s\n", argv[i]);
    return 0;
}
 
// echo2.cpp
int main(int argc, char* argv[])
{
    while (--argc)
        printf("%s\n", *++argv);
    return 0;
}
 
// echo3.cpp
int main(int argc, char* argv[])
{
    while (*++argv)
        printf("%s\n", *argv);
    return 0;
}

Všetky tri programy, samozrejme, treba doplniť direktívou #include . Prvý z programov (echo1.cpp) je dostatočne jasný a netreba ho hlbšie analyzovať. Druhý z nich (echo2.cpp) na počítanie zostávajúcich argumentov využíva premennú argc. Prefixová verzia dekrementačného operátora –– je použitá preto, aby sme preskočili prvý argument s menom súboru. Pri vypisovaní jednotlivých argumentov používame premennú argv, ktorá ukazuje na práve aktuálny argument (resp. na ukazovateľ na tento argument). Inkrementáciou argv sprístupňujeme ďalšie a ďalšie argumenty. Opäť z rovnakých dôvodov ako predtým použijeme prefixový operátor ++. Konečne tretí program (echo3.cpp) je podobný druhému, cyklus však ukončíme nie vtedy, keď premennú argc znížime na nulu, ale vtedy, keď obsahom *argv bude prázdny ukazovateľ. To je, ako vieme, signál konca poľa. Inkrementovať argv musíme pri jeho prvom použití, teda už v podmienke príkazu while.

Pokračujeme v deklaráciách

Konečne by sme sa však mali vrátiť k tomu, čo sme minule nedokončili. Priznám sa, že predchádzajúci výklad bol úplne neplánovaný, pôvodne som si chcel len trošku zafilozofovať o blížiacom sa konci storočia – a hľa, ako sa mi to vymklo spod kontroly. Ale to nič, ide o užitočnú tému, ktorá vám možno uľahčí písanie niektorých programov.

Naposledy sme skončili rozprávaním o špecifikátoroch, používaných pri deklaráciách. Prebrali sme špecifikátory ukladacej triedy a funkčné špecifikátory, okrem nich C++ definuje ešte špecifikátory typu a tzv. špecifikátor typedef.

Špecifikátory typu

Základnými špecifikátormi typu sú nám už dávno známe kľúčové slová char, short, int, long, signed, unsigned, float, double a void. S ich pomocou vyjadrujeme výsledný typ deklarovaného mena. Pre ich použitie, resp. ich vzájomnú kombináciu však platia určité obmedzenia.

Špecifikátor char môžeme použiť spolu so špecifikátormi signed alebo unsigned (alebo aj bez nich, ale nie s oboma naraz), deklarujeme tak mená s typmi char, signed char a unsigned char (vieme, že to sú z hľadiska prekladača tri rôzne typy!).

Špecifikátor int môžeme skombinovať so špecifikátormi signed alebo unsigned a navyše so špecifikátormi short alebo long (opäť nie s oboma naraz). Povolené sú teda nasledujúce kombinácie:

int
signed int
unsigned int
short int
signed short int
unsigned short int
long int
signed long int
unsigned long int

Typ int sa však používa natoľko intenzívne, že ho možno zo všetkých uvedených kombinácií vynechať, s výnimkou tej prvej, keď ho zrejme vynechať nemôžeme, lebo by sme tým stratili akúkoľvek informáciu o type. Okrem toho je špecifikátor signed prakticky zbytočný, lebo všetky celočíselné typy (okrem typu char) sa implicitne berú ako znamienkové. Množina možných kombinácií sa nám takto zredukuje na nasledujúci zoznam:

int
unsigned
short
unsigned short
long
unsigned long

Je úplne zbytočné používať inak zapísané modifikácie – tieto pokrývajú všetky možnosti a navyše majú minimálnu dĺžku.

Špecifikátor float nemožno kombinovať so žiadnym iným, špecifikátor double môžeme doplniť špecifikátorom long, čím deklarujeme meno s typom long double. Niektoré prekladače (ako napr. Borland C++ 3.1) povolia aj kombináciu long float, ktorú interpretujú ako typ double, ale to je ich implementačné špecifikum, ktoré norma oficiálne nepovoľuje. Špecifikátor void nepripúšťa nijaké modifikátory.

Špecifikátory const a volatile

Ľubovoľný špecifikátor typu môže byť doplnený jedným zo špecifikátorov const a volatile (aj oboma). Pomocou kľúčového slova const deklarujeme tzv. konštantné objekty. Takéto objekty majú hodnotu určenú pri inicializácii a ďalej v programe ich nemožno meniť. Na rozdiel od „konštánt“ jazyka C, definovaných direktívou #define, ktoré boli vlastne makrami a nahrádzali sa skutočnými hodnotami počas spracovania zdrojového textu preprocesorom, objekty deklarované ako const sa správajú ako bežné, hoci nemodifikovateľné objekty plne pod správou kompilátora. Ten ich môže v rámci optimalizácie nahradiť vo výslednom binárnom kóde priamo príslušnými literálmi, ale pri preklade doplní ich mená do tabuľky symbolov pre debugger, a teda k nim máme počas ladenia prístup (čo pri #definovaných konštantách neplatilo).

Konštantné objekty môžeme použiť vo výrazoch, ktorých výsledok musí byť známy už vo fáze prekladu – tzv. konštantných výrazoch, ako jednotlivé návestia príkazu switch, rozmery polí a pod. Konštantnými môžu byť aj polia, ktoré sa takto stávajú poľami konštantných prvkov. Samostatnou kapitolou sú ukazovatele. Pri použití špecifikátora const pri deklarácii ukazovateľa musíme rozlišovať dve diametrálne odlišné polohy. Môžeme deklarovať ukazovateľ na konštantný objekt, vtedy je const pred špecifikátorom doménového typu ukazovateľa:

const int ci = 123;
const int * pci = &ci;

Ukazovateľ pci ukazuje na konštantnú premennú ci, ktorej hodnotu nemôžeme meniť (ani s pomocou dereferencie tohto ukazovateľa), ale hodnotu samotného ukazovateľa meniť môžeme! Pci tak môže ukazovať na inú konštantnú premennú, dokonca mu môžeme priradiť aj adresu nekonštantnej premennej, ktorá sa však bude javiť pri prístupe cez dereferencovaný ukazovateľ ako konštantná. Je teda možný nasledujúci zápis:

int i = 456;
const int * pci = &i;

Okrem toho môžeme deklarovať ako konštantný samotný ukazovateľ. O takýchto ukazovateľoch si však povieme pri opise deklarátorov.

Druhým podobným špecifikátorom je kľúčové slovo volatile. Pomocou neho napovedáme kompilátoru, že objekt takto deklarovaný môže byť menený asynchrónne voči behu nášho programu, napríklad hardvérom, obslužnou rutinou prerušenia, iným procesom alebo iným threadom. Kompilátor by teda nemal vo vzťahu k objektu volatile vykonávať nijaké optimalizácie a pri každom prístupe k takémuto objektu by mal čítať jeho hodnotu vždy nanovo z pamäte.

Používanie špecifikátora volatile sa dosť podobá používaniu const, t. j. môžeme definovať ukazovatele na volatile objekty alebo volatile ukazovatele na (ľubovoľné) objekty (opíšeme si neskôr spolu s konštantnými ukazovateľmi). Samozrejme, nemusíme sa starať, či objekty, ktoré meníme, sú alebo nie sú volatile, jediným obmedzením je určitá kompatibilita objektov a/alebo ukazovateľov vzhľadom na priradenie. Podobné pravidlá platia aj pre špecifikátor const a dajú sa zhrnúť takto: konštantný objekt možno priradiť nekonštantnému, ale nie naopak, pre volatile objekty takéto obmedzenie nie je; ukazovateľu na konštantný/volatile objekt môžeme bez problémov priradiť ukazovateľ na nekonštantný/ne-volatile objekt, naopak je to síce povolené, ale neodporúča sa (môže to spôsobiť výnimku pri prístupe k objektom, lebo konštantné objekty môže prekladač umiestniť v read-only segmente či stránke pamäte). Tam, kde sa požaduje hodnota typu const T alebo volatile T, môžeme použiť objekt typu T. Konštantné/volatile môžu byť aj referencie, ktoré sa inak používajú ako bežné premenné príslušného typu (const T& ako const T, volatile T& ako volatile T, atď.)

Deklarácia enumerácií

Enumerácia je zvláštnym druhom typu, ktorý môžeme opísať aj ako vymenovaný typ. Vo všeobecnosti je typ údajov nielen v C++ definovaný jednak rozsahom možných hodnôt, jednak množinou operácií, ktoré sú nad daným typom definované. Typ enumerácie je špecifický tým, že jeho rozsah hodnôt explicitne určujeme my, a to vymenovaním všetkých hodnôt, ktoré premenná takéhoto typu môže nadobudnúť. Jednotlivé hodnoty musia byť celočíselné (aj enumeračný typ sa považuje za celočíselný) a deklaráciou enumerácie súčasne deklarujeme tieto hodnoty ako pomenované konštanty. Pozrime sa, ako taká deklarácia vyzerá:

enum identifikátor { zoznam }

Identifikátor v deklarácii predstavuje meno novo definovaného enumeračného typu a platia preň rovnaké pravidlá ako pre identifikátory bežných premenných. Nezdieľa však s nimi priestor mien. Zoznam je zoznamom vymenovaných konštánt. Tieto konštanty sú zapisované buď v tvare „identifikátor“, alebo v tvare „identifikátor = hodnota“. Pokiaľ neuvedieme pri identifikátore konštanty hodnotu, použijú sa implicitné hodnoty – prvá konštanta bude mať hodnotu 0 (nula), každá ďalšia hodnotu o jednotku vyššiu. Uvedenie hodnoty túto postupnosť narušuje tým, že danej konštante priraďuje explicitne zadanú hodnotu. Jednotlivé konštanty sú oddelené čiarkou.

Pozrime sa na príklad, ktorý situáciu ozrejmí:

enum rgb { red, green, blue };

Táto deklarácia opisuje nový typ rgb, ktorého hodnotami sú konštanty red (s hodnotou 0), green (s hodnotou 1) a blue (s hodnotou 2).

enum flag { r, h, s = 5, a };

Flag je typ, ktorý môže nadobúdať štyri rôzne hodnoty: r, h, s, a. Ich celočíselné reprezentácie sú v poradí 0, 1, 5 a 6.

Hodnoty definovaných konštánt nemusia byť nijako usporiadané, dokonca sa môžu aj zhodovať (hodnoty, nie názvy konštánt!). Premennej typu enumerácia môžu byť priradené iba hodnoty rovnakého typu, teda nemôžeme premennej uvedeného typu rgb priradiť celočíselnú hodnotu 1, hoci v obore jeho možných hodnôt je konštanta green, ktorej celočíselná reprezentácia je 1. Možné to je len explicitným pretypovaním, ktoré sa však neodporúča vzhľadom na skutočnosť, že priradením napr. výrazu (rgb)5 premennej typu rgb bude mať takáto premenná obsah, ktorý síce formálne je typu rgb, ale nie je zhodný so žiadnou z povolených a vymenovaných konštánt. Opačné priradenie, teda priradenie hodnoty typu rgb premennej typu int je možné a má zmysel, do premennej sa uloží celočíselná reprezentácia hodnoty. Teda napr. výraz i = blue priraďuje premennej i hodnotu 2.

V prípade, že vynecháme v deklarácii enumerácie jej identifikátor, dostávame tzv. anonymnú enumeráciu. Takýto typ nemožno ďalej v programe používať a jeho jediným zmyslom je deklarácia pomenovaných konštánt. Teda

enum { RUNNING, READY, BLOCKED };

deklaruje tri pomenované konštanty: RUNNING, READY a BLOCKED. Ich hodnoty obyčajne nie sú zaujímavé, dôležitý je fakt, že existujú a možno ich používať. Hoci typom týchto konštánt je enumerácia, každé ich použitie vedie k automatickej konverzii na int.

Deklaráciu enumeračného typu môžeme použiť ako špecifikátor pri deklarácii premenných tohto typu. V princípe sú možné dva spôsoby použitia. Pri prvom súčasne s deklaráciou premennej deklarujeme aj nový enumeračný typ. Použijeme bežnú syntax – najprv uvedieme špecifikátor (v tomto prípade deklaráciu enum) a za ním deklarátor (t. j. napr. identifikátor premennej či premenných):

enum foo { A, B } x, y;

V príklade deklarujeme nový typ foo s dvoma povolenými hodnotami A a B a dve premenné x a y tohto typu.

Pri druhom spôsobe najprv typ foo deklarujeme samostatne a až potom (v inom deklaračnom príkaze) deklarujeme aj premenné x a y:

enum foo { A, B };
foo x;
enum foo y;

Zaujímavý je dvojaký spôsob deklarácie oboch premenných – bez kľúčového slova enum a s ním. Oba spôsoby sú ekvivalentné, druhý použijeme vtedy, ak identifikátor foo je zakrytý nejakou inou deklaráciou, napríklad lokálnej premennej s názvom foo. Vtedy je enumeračný typ dostupný svojím kvalifikovaným menom enum foo.

Špecifikátor typedef

Posledný špecifikátor, o ktorom si povieme, je trochu zvláštny. Hoci syntakticky je jeho použitie podobné použitiu iných špecifikátorov, význam jeho použitia je úplne iný – pomocou kľúčového slova typedef deklarujeme nové meno, ktoré môžeme neskôr použiť ako pomenovanie typu. Inak povedané, typedef predstavuje mechanizmus používateľskej definície typov. Jeden spôsob explicitnej deklarácie typov sme tu už mali – ide o enumeračný typ. Pomocou typedef však deklarujeme jednoslovné pomenovanie inak vo všeobecnosti komplexného typu. Klasický príklad: chceme deklarovať pole desiatich ukazovateľov na typ char. Pokiaľ vieme, ako na to priamo, napíšeme:

char* array1[10];

Na tomto príklade to vyzerá veľmi jednoducho a priamočiaro, ale predstavte si pole desiatich ukazovateľov na funkcie s návratovým typom „ukazovateľ na int“ a s jedným argumentom, ktorý je ukazovateľom na funkciu bez argumentov, a s návratovým typom void (áno, aj také veci sa nájdu v C++ a dokonca sa aj používajú). Pokiaľ nie ste v deklaráciách príliš zbehlí, asi neprídete na to, že správna forma zápisu deklarácie takéhoto poľa je:

int* (*array2[10])(void (*)());

Toto už vyzerá zložitejšie, však? Veľmi jednoduchým a pre začiatočníkov odporúčaným spôsobom, ako s takýmito komplikovanými typmi „vybabrať“, je deklarácia pomocných typov práve pomocou mechanizmu typedef. Pole array1 potom deklarujeme asi takto:

typedef char* pchar;
pchar array1[10];

V tomto úseku kódu deklarujeme nový typ pchar, ktorý je okrem názvu úplne ekvivalentný s typom char*. Potom jednoducho deklarujeme pole array1 ako pole desiatich prvkov typu pchar, čo sú, ako vieme, ukazovatele na typ char. Z príkladu vidíme aj spôsob použitia kľúčového slova typedef. Deklarácia vyzerá úplne rovnako, ako keby sme deklarovali bežnú premennú nejakého typu, navyše však pred celú deklaráciu pripojíme typedef. Novo deklarované meno nebude potom predstavovať premennú nejakého typu, ale akési náhradné meno (alias) pre tento typ. Je jasné, že z hľadiska miesta použitia je typedef takým istým špecifikátorom ako napr. const, len význam má úplne iný.

Uveďme si ešte príklad zjednodušenej deklarácie uvedeného poľa array2. Opäť si najprv deklarujeme pomocné typy – ukazovateľ na funkciu typu void bez argumentov:

typedef void (*pvf)();

a na funkciu vracajúcu int* s argumentom čerstvo deklarovaného typu pvf:

typedef int* (*pipfpvf)(pvf);

Teraz už môžeme bez problémov zapísať deklaráciu poľa array2:

pipfpvf array2[10];

Názvy oboch typov vyjadrujú nie príliš zreteľnú snahu o zakódovanie „zloženia“ typu do jeho mena (pvf = pointer to void function, pipfpvf ani nebudem rozoberať).

V uvedenom príklade sa používajú trochu obskúrne veci, ako ukazovatele na funkcie, polia ukazovateľov, o ktorých sme si zatiaľ nehovorili (prídu na rad v časti venovanej deklarátorom, teda onedlho). Na nich však možno najlepšie demonštrovať význam špecifikátora typedef. Okrem toho používame vlastné typy napr. vtedy, ak vopred nevieme, aký typ bude použitý pre určité premenné vo výslednom programe. Zadefinujeme si teda pomocou kľúčového slova typedef nový typ, ekvivalentný nejakému prvému odhadu (alebo prvému pokusu) a všetky relevantné premenné a objekty budeme deklarovať pomocou tohto nového typu. Ak sa v budúcnosti rozhodneme pre prechod k inej reprezentácii (napríklad namiesto float použijeme double alebo namiesto int long), stačí prepísať jediný riadok, ten, na ktorom typedef typ deklarujeme. Alebo iný príklad: chceme deklarovať typ WORD ako šestnásťbitové číslo bez znamienka. Používame nejaký starší 16-bitový prekladač, a preto deklarácia bude vyzerať takto:

typedef unsigned int WORD;

(špecifikátor int nie je povinný, je tam len pre názornosť). Čo však v prípade, že zmeníme prekladač a prejdeme na 32-bitovú platformu, kde je typ int dvakrát dlhší? Potom nám stačí zmeniť túto jedinú deklaráciu, a to asi takto:

typedef unsigned short WORD;

(keby sme boli predvídaví, deklarovali by sme typ WORD už vopred ako unsigned short – na väčšine platforiem je typ short 16-bitový, aspoň do rozšírenia sa 64-bitových procesorov).

Nakoniec ešte zopár drobností. Pomocou typedef mechanizmu môžeme raz definované meno typu predefinovať tak, aby predstavovalo rovnaký typ:

typedef int I;
typedef int I;
typedef I I;

Okrem toho nemôžeme redefinovať meno použité v inej ako typedef deklarácii:

enum rgb { red, green, blue };
typedef int rgb;  // chyba!

Deklarátory

Máme za sebou približne polovicu preberanej témy. Pokračovať budeme opisom deklarátorov, ktoré spolu so špecifikátormi robia deklarácie mien kompletnými. Na rozdiel od špecifikátorov, ktoré opisujú typ, ukladaciu triedu a iné vlastnosti deklarovaných mien (objektov či funkcií), pomocou deklarátorov určujeme názvy týchto mien [je to trochu krkolomný opis, ale pre objasnenie – ak deklarujeme napr. meno buf, tento jeho identifikátor (buf) je súčasťou deklarátora], ďalej môžeme modifikovať typ deklarovaných mien a prípadne môžeme uviesť aj inicializačnú hodnotu.

Za zoznamom špecifikátorov v deklarácii nasleduje zoznam deklarátorov, oddelených čiarkou. Každý deklarátor môže obsahovať inicializáciu – o tých si však povieme až na záver. Špecifikátory uvedené v deklarácii sa vzťahujú na každý deklarátor. Výsledný typ deklarovaného mena je potom daný jednak spoločným zoznamom špecifikátorov, jednak vlastnými modifikáciami, ktoré sú privátne pre každý deklarátor. Celú deklaráciu môžeme rozložiť na postupnosť čiastkových deklarácií. Každá z nich sa dá zapísať v tvare

T D

kde T je typ (daný špecifikátormi) a D je deklarátor (pre jednoduchosť vynechávame možnú inicializáciu). V prípade, že D je obyčajný, ničím „neprikrášlený“ identifikátor, bude jeho deklarovaným typom typ T. Prípadné uzavretie deklarátora D do zátvoriek nijako nezmení význam deklarácie, môže však pomôcť pri určovaní výsledného typu.

Deklarácia ukazovateľov

Na záver tejto časti seriálu si ešte povieme, ako je to s deklaráciou ukazovateľov a referencií. Hoci sme si kedysi pri rozprávaní o nich povedali okrem iného aj to, ako ich deklarujeme, bol to vzhľadom na nedostatok potrebných vedomostí (vašich, nie mojich) len základný a neúplný opis. Teraz si uvedieme všetky možnosti, ktoré nám C++ poskytuje.

Deklarácia ukazovateľa má vo všeobecnosti nasledujúci tvar:

T * cv-kvalifikátory D1

Zápis je tak trochu rekurzívny, D1 je opäť deklarátor (ľubovoľný). Ak je na mieste D1 obyčajný identifikátor, deklarácia mu priradí typ „cv-kvalifikovaný ukazovateľ na T“. V prípade zložitejšieho deklarátora je výsledným typom nejaký komplexnejší typ, ktorého základným kameňom je „cv-kvalifikovaný ukazovateľ na T“. Už teda tušíme, ako deklarovať napríklad pole ukazovateľov na typ int – deklarátor D1 bude nejakým spôsobom deklarovať pole (uvidíme ďalej ako). Zostáva ešte ozrejmiť, čo sú to cv-kvalifikátory. Ide o (medzerami oddeľovaný) zoznam kľúčových slov const a volatile. Zoznam nie je povinný a prakticky nemá význam použiť v ňom každé zo slov viac ako raz. Význam týchto kvalifikátorov je vám pravdepodobne jasný. Okrem spomínaného ukazovateľa na konštantný objekt môžeme deklarovať aj konštantný ukazovateľ na (ľubovoľný) objekt:

int i = 789;
int * const cpi = &i;

Premennú, na ktorú konštantný ukazovateľ ukazuje, môžeme pomocou neho meniť, nemôžeme však takýto ukazovateľ „prinútiť“, aby ukazoval na inú premennú. Nasledujúci kód je teda chybou:

int i = 222;
int j = 333;
int * const cpi = &i;
cpi = &j;

Konštantné ukazovatele môžeme bez problémov priradiť nekonštantným ukazovateľom, naopak to, samozrejme, nejde. Ďalej môžeme deklarovať aj konštantný ukazovateľ na konštantný objekt:

const int ci = 1000;
const int * const cpci = &ci;

Ukazovateľ cpci je v tomto prípade úplne obmedzený – nemôžeme meniť ani jeho hodnotu, ani prostredníctvom neho hodnotu premennej, na ktorú ukazuje. Nič nám, pravda, nebráni pretypovať si ho na taký ukazovateľ, aký nám vyhovuje, a hodnotu potom veselo meniť. Takýto prístup však svedčí o nie príliš čistom programátorskom štýle a zvyčajne o chybách v návrhu softvéru.

Pre kvalifikátor volatile platia podobné pravidlá. Oba kvalifikátory môžeme rôzne kombinovať:

const int * volatile vpci;
volatile char * const volatile cvpcc;

V príklade deklarujeme vpci ako volatile ukazovateľ na konštantný objekt typu int, cvpcc ako konštantný a volatile (na pohľad paradox, ale konštantný je len vzhľadom na náš program) ukazovateľ na volatile objekt typu char.

Deklarácia referencií

Referencie sú v mnohom podobné ukazovateľom. Ich deklarácia vyzerá takto:

T & cv-kvalifikátory D1

O častiach tejto deklarácie platí to, čo je uvedené v predchádzajúcom odseku, s výnimkou toho, že nemôžeme deklarovať typ void& a ďalej neexistujú referencie na referencie, polia referencií ani ukazovatele na referencie. To vyplýva z faktu, že referencia ako taká nepredstavuje plnohodnotný typ – de facto neexistuje premenná, ku ktorej by sme mohli pristupovať ako k referencii, vzhľadom na to, že po inicializácii sa referenčná premenná správa ako tá premenná, na ktorú sa odkazuje, s rovnakým typom aj vlastnosťami (s jedinou výnimkou – pozri ďalej).

Aj pri referenciách môžeme použiť kvalifikátory const a volatile, ale ich praktický význam je nulový – prekladače obyčajne tieto kvalifikátory (za znakom &) ignorujú. Iné je referencia na konštantný objekt, tá má veľký význam spolu s ukazovateľom na konštantný objekt predovšetkým ako formálny argument funkcie. Predstavme si, že odovzdávame nejakej funkcii ukazovateľ na reťazec, ktorý by táto funkcia nemala meniť (reťazec, nie ukazovateľ). Normálne je ukazovateľ jediným prostriedkom, ako funkcii odovzdať argument odkazom (C++ pozná iba odovzdávanie hodnotou). Ak však chceme prípadnej zmene zabrániť, stačí deklarovať argument danej funkcie ako konštantný:

void fnc(const char* str)
{ ... }

Podobná situácia je pri referenciách. Referenciou odovzdávame jednak argument, ktorý chceme meniť (teda použitie ekvivalentné ukazovateľu), a jednak argument, ktorý je priveľký na to, aby sa odovzdával hodnotou, čo by znamenalo kopírovanie veľkého množstva údajov na zásobník. V tomto druhom prípade môžeme deklarovať referenciu na konštantný objekt, čím zabránime prípadnej (i nechcenej) modifikácii pôvodného objektu. Pri formálnom parametri typu referencia sa do funkcie naozaj odovzdáva obsah tejto referencie, t. j. adresa odkazovanej premennej – to je tá výnimka, o ktorej som už hovoril.

Referencie na volatile objekty majú podobný význam ako ukazovatele na tieto objekty. V praxi sa vôbec volatile premenné používajú dosť zriedka, pokiaľ totiž nejaký objekt môže byť menený asynchrónne, na pozadí, obyčajne sa prístup k nemu nejakým spôsobom synchronizuje (semafory, zámky a pod.)

Ešte stále nekončíme

Nadnes toho bolo až-až, takže si niečo necháme aj pre ďalšiu, svojím spôsobom jubilejnú časť seriálu

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á