Image
9.6.2016 0 Comments

C++ / Vzťah polí a ukazovateľov / 10. časť

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

V predchádzajúcej časti sme si urobili krátku prestávku vo výklade a zaoberali sme sa ukážkou trochu väčšieho a užitočnejšieho programu v C++. Iste budete so mnou súhlasiť, že takto koncipovaných programov by bolo v záujme čo najväčšieho počtu príkladov podopierajúcich výklad vhodné uviesť  čo najviac, ale to by sa nám seriál neúmerne predĺžil a nezostalo by miesto na mnoho ďalších, potrebnejších informácií. Aj preto by som vás rád znovu vyzval: vymýšľajte si zadania programov sami a pokúšajte sa ich aj úspešne realizovať. Pravdepodobne nie všetky problémy zvládnete hneď na prvýraz, preto sa k tým nevyriešeným vráťte neskôr, keď budete skúsenejší, vybavení väčšími znalosťami. Píšem to už síce po niekoľkýkrát, ale najväčšou devízou v programovaní (a teraz nejde len o C++, ale o programovanie v ľubovoľnom jazyku) je prax, získaná napísaním mnohých, často aj úplne zanedbateľných programov, na ktorých si trebárs objasníte nejakú spornú otázku. Čo sa týka tých spomínaných problémov, odporúčam vám jediné – študovať, študovať, študovať (ospravedlňujem sa za parafrázu) všetko, čo vám príde pod ruku, t. j. manuály, elektronické príručky, knihy, skriptá, články, diskusné skupiny a pod. (prípadne sa opýtať niekoho skúsenejšieho; tí, ktorí máte prístup k internetu, skúste napr. diskusnú skupinu Usenetu comp.lang.c++).

Ale poďme späť k jazyku C++. V tejto časti sa pozrieme na to, ako je to so vzťahom medzi poliami a ukazovateľmi. Táto téma je jednou z najdôležitejších a súčasne najzložitejších a jej dokonalé zvládnutie je nevyhnutné, ak nechcete zostať pri písaní svojich programov niekde „v plienkach“.

Čo už vieme

V prvom rade si ozrejmíme základné fakty, týkajúce sa polí a ukazovateľov. Oboje sme síce už viac-menej podrobne preberali v predchádzajúcich častiach, ale pokiaľ máte tak ako ja staršie čísla PC REVUE beznádejne zavalené inými, novšími časopismi, iste vám príde vhod stručné zhrnutie. (Čo to tu rozprávam, veď vy už iste všetko ovládate z hlavy, alebo sa mýlim?)

Pole je údajový typ jazyka C++, ktorý predstavuje postupnosť po sebe idúcich premenných rovnakého typu. Pri deklarácii poľa uvádzame jeho veľkosť, t. j. počet jeho prvkov. Jednotlivé prvky sú identifikovateľné pomocou svojich indexov, na ich sprístupnenie používame operátor indexovania []. Polia v C++ sa indexujú od nuly, to znamená, že pole a, deklarované takto:

 

int a[10];

 

obsahuje 10 premenných typu int, ktoré sú prístupné ako a[0] až a[9]. Jednotlivé premenné sa v pamäti nachádzajú tesne za sebou (obr. 1).

Jazyk C++ priamo nepozná viacrozmerné polia, umožňuje však deklarovať polia, ktorých prvkami sú opäť polia. Pomocou tejto konštrukcie dokážeme vytvoriť údajový typ, ktorý sa správa prakticky rovnako ako viacrozmerné pole. Napríklad zápisom

 

double d[3][4];

 

deklarujeme objekt, ktorý je jednorozmerným poľom troch prvkov, d[0], d[1] a d[2]. Každý z týchto prvkov je sám osebe poľom so štyrmi prvkami, premennými typu double. Tieto štyri prvky „druhej úrovne“ sú pri každom z uvedených troch polí prístupné pomocou ďalšieho indexu, čím dostávame spolu dvanásť rôznych premenných, prístupných ako d[0][0] až d[2][3]. Vo výrazoch môžeme použiť, samozrejme, aj polia d[i], ktoré sa budú správať ako bežné jednorozmerné polia.

Obr. 1

Ako vyplýva z gramatiky jazyka C++, pri prechádzaní „viacrozmerným“ poľom sa index uvedený najviac vpravo mení najrýchlejšie, čo znamená, že ak si predstavíme uvedené pole d ako maticu s tromi riadkami a štyrmi stĺpcami, bude táto matica uložená v pamäti po riadkoch, počnúc prvkom d[0][0], po poslednom prvku prvého riadka, d[0][2] bude nasledovať prvý prvok druhého riadka, d[1][0] a tak ďalej až do konca. Prvky budú v pamäti uložené opäť súvisle, tesne za sebou (obr. 2).

Obr. 2

Typ ukazovateľ predstavuje premennú, v ktorej je uložená adresa inej premennej. Veľkosť ukazovateľa závisí od architektúry počítača a platformy, pre ktorú je program určený. S každým ukazovateľom je združený jeho tzv. doménový typ, čo je typ premennej, na ktorú tento ukazovateľ ukazuje. Ak ukazovateľ dereferencujeme (operátor *), dostaneme práve premennú doménového typu. Ako sme si hovorili, program nijakým spôsobom nekontroluje, či ukazovateľ vôbec obsahuje platnú adresu, a ak áno, či je na nej skutočne uložená premenná zodpovedajúceho doménového typu. Pri dereferencii ukazovateľa sa jednoducho oblasť pamäte, na ktorú ukazovateľ ukazuje a ktorej veľkosť je daná veľkosťou doménového typu, interpretuje ako premenná tohto typu. Ak máme smolu a obsah ukazovateľa je neplatný, v lepšom prípade sa nám program skončí výnimkou (platí výhradne pre operačné systémy s ochranou pamäte – Windows, UNIX), v tom horšom si v prípade zápisu do dereferencovanej oblasti prepíšeme napríklad program alebo rovno operačný systém (platí predovšetkým pre „operačný systém“ MS-DOS blahej pamäti).

Tajomstvo polí

Polia v C++ sú ako každá iná premenná prístupné pomocou svojho názvu, t. j. pomocou bežného identifikátora. Ak máme ľubovoľnú premennú nejakého neštruktúrovaného typu (nie pole!), použitie jej identifikátora vo výraze spôsobí použitie jej obsahu, teda hodnoty, ktorá je v premennej uložená. Napríklad nech premenná pi typu double obsahuje (známu) hodnotu 3.14159. Použitie tejto premennej vo výraze 2 * pi bude mať za následok vynásobenie hodnoty v nej uloženej konštantou 2.

V prípade polí je však celá situácia o niečo zložitejšia. Identifikátor poľa reprezentuje toto pole ako celok len v troch prípadoch:

  • ak použijeme identifikátor poľa ako argument operátora sizeof,
  • ak použijeme identifikátor poľa ako argument operátora &,
  • ak použijeme identifikátor poľa ako inicializátor v deklarácii referencie.

Vo všetkých ostatných prípadoch použitia sa pole automaticky konvertuje na ukazovateľ na jeho prvý prvok!

Vysvetlime si teraz bližšie spomínané tri prípady, keď pole vystupuje ako celok. Ak aplikujeme na pole operátor sizeof, dostaneme jeho veľkosť v bajtoch. O tomto fakte sa môžete veľmi ľahko presvedčiť nasledujúcim programom:

 

float array[50];

printf("sizeof(float) = %i B\n", sizeof(float));

printf("sizeof array = %i B\n", sizeof array);

 

(Mimochodom, automaticky nazývam tento fragment kódu programom, pretože pokladám za samozrejmosť, že tú nevyhnutnú omáčku, ako direktívu(y) #include, obalenie kódu do funkcie main() a pod., zvládnete bez problémov sami. V budúcnosti už takéto pripomienky robiť nebudem.)

Takže po spustení programu dostaneme na PC nasledujúce výsledky: veľkosť typu float je 4 bajty, veľkosť 50-prvkového poľa array je 200 bajtov. A keďže 50 × 4 je ešte stále 200, je zrejmé, že operátor sizeof vrátil veľkosť celého poľa. A keď sme už pri zisťovaní veľkosti, vyskúšajte si ešte nasledujúci príklad, pomocou ktorého môžeme zistiť počet prvkov poľa (ak ho z nejakého dôvodu nepoznáme):

 

float array[50];

printf("|array| = %i\n",

        sizeof array / sizeof array[0]);

 

Po spustení program vypíše, že veľkosť poľa je očakávaných 50 prvkov.

Pole ako každý iný objekt C++ (mimochodom, pojem objekt v C++ predstavuje oblasť v pamäti [v angličtine „region of storage“]) má svoju adresu, ktorú môžeme zistiť aplikovaním operátora & na jeho identifikátor. Doménovým typom vráteného ukazovateľa je práve typ nášho poľa. Teda ak máme pole a obsahujúce desať prvkov typu int, hodnotou výrazu &a je ukazovateľ na toto pole s typom „ukazovateľ na pole desiatich prvkov typu int“. Ak chceme získanú hodnotu uložiť do premennej, musíme správne deklarovať jej typ. Bez nároku na vysvetlenie (zatiaľ), tu je príklad takej deklarácie:

 

int a[10];    // pole

int (*p)[10]; // ukazovateľ

p = &a;

 

Po vykonaní tohto úseku sú nasledujúce dva príkazy ekvivalentné:

 

a[3] = 8;

(*p)[3] = 8;

 

Tretím prípadom vystupovania poľa ako celku je jeho použitie v prípade, že inicializujeme referenciu. Referencie, ako si iste spomínate, sú premenné, ktoré vystupujú ako akési odkazy (aliasy) na iné premenné – každá operácia, ktorú vykonávame s referenciou, sa realizuje na príslušnej odkazovanej premennej. V podstate referencie sú automaticky dereferencovanými ukazovateľmi. Len jediný raz pracujeme priamo s obsahom referenčnej premennej, a to pri jej inicializácii, keď určujeme, na akú premennú sa bude referencia odkazovať.

Referenčná premenná sa môže odkazovať prakticky na ľubovoľný objekt C++ a niet dôvodu, prečo by sa nemohla odkazovať na pole. Jediné, na čo si  treba  dať pozor, je podobne ako v predchádzajúcom odseku správne deklarovanie typu referencie. S výnimkou deklarácie argumentov a návratovej hodnoty funkcie (a ešte zopár ďalších) musí byť povinne súčasťou deklarácie inicializácia. V ďalšom príklade, podobnom tomu predchádzajúcemu, ukážeme (opäť bez vysvetľovania), ako taká deklarácia spolu s inicializáciou vyzerá:

 

int a[10];        // pole

int (&r)[10] = a; // referencia

 

Opäť po vykonaní tohto úseku sú nasledujúce dva príkazy ekvivalentné:

 

a[3] = 8;

r[3] = 8;

 

Všetky zvyšné prípady použitia identifikátora poľa vedú k jeho automatickej konverzii na ukazovateľ na jeho prvý prvok. Tento ukazovateľ má doménový typ zhodný s typom prvku poľa, nie je však modifikovateľnou l-hodnotou, nemôžeme teda identifikátoru poľa nič priradiť priraďovacím operátorom ani ho inkrementovať či dekrementovať. Hodnotou ukazovateľa je adresa uloženia prvého prvku v pamäti a jeho dereferencovaním by sme tento prvý prvok mohli bez problémov sprístupniť (pretože sa jeho typ zhoduje s doménovým typom ukazovateľa).

Zaiste vás napadlo, ako je to so základnou operáciou realizovanou nad poľami – indexovaním. Aj pri aplikovaní operátora [] sa pole konvertuje na ukazovateľ, ale na pochopenie toho, čo sa následne deje, si musíme vysvetliť, čo je to tzv. adresová aritmetika.

Adresová aritmetika

Pri rozprávaní o operátoroch + a – jazyka C++ sme si nepovedali, že ich operandmi môžu byť za určitých okolností aj ukazovatele. Na prvý pohľad sa môže zdať, že nemá zmysel niečo k ukazovateľu pripočítavať či odpočítavať, no v skutočnosti to nielenže zmysel má, ale je to neoddeliteľná a nevyhnutná súčasť C++.

Zoberme si najprv operátor +. Jedným z jeho operandov (ale nie oboma!) môže byť ukazovateľ, potom musí byť druhý operand celočíselného typu. Sčítanie prebehne takto: vezme sa celočíselný operand, vynásobí sa veľkosťou doménového typu ukazovateľa a získaná hodnota sa pripočíta k adrese uloženej v ukazovateli. Teraz si predstavte, že nejaký ukazovateľ ukazuje na prvý prvok nejakého poľa. Ak k nemu pripočítame napríklad hodnotu 1, „posunie“ sa tento ukazovateľ o 1 × veľkosť prvku poľa a bude ukazovať na ďalší prvok poľa! Takto môžeme prechádzať po prvkoch poľa pomocou ukazovateľa a všetko, čo  treba urobiť pre posun ukazovateľa, je jeho inkrementácia (pre posun v opačnom smere zase dekrementácia). Jediné, na čo treba dávať pozor, je kontrola, či náhodou nepristupujeme k prvkom, ktoré sú už mimo poľa (a ktoré tam vlastne vôbec nie sú). Ukazovateľ na prvý prvok poľa získame jednoducho – použijeme priamo identifikátor poľa.

A teraz si vysvetlime, čo sa deje pri použití operátora []. Podľa definície, výraz a[i] znamená to isté (a tak sa aj vypočíta) ako výraz *(a + i). Opíšme si, ako prebehne vyhodnotenie výrazu. Identifikátor a reprezentuje pole. Keďže nenastal ani jeden zo spomínaných troch prípadov, konvertuje sa automaticky na ukazovateľ na prvý prvok celého poľa. K tomuto ukazovateľu sa teraz pripočíta hodnota premennej i. Ako už vieme, to má za následok, že ukazovateľ a + i bude ukazovať na prvok o i pozícií ďalej. Keďže prvý prvok poľa má index 0, prvok o i pozícií ďalej bude mať index i. No a teraz stačí získaný ukazovateľ dereferencovať a máme k dispozícii prvok a[i]. Situáciu osvetlí obr. 3. Krátky príklad:

 

int a[8];

int *p;

 

for (int i = 0; i < 8; i++)

    a[i] = i;

 

p = a;

p[2] = 222;

*(p + 5) = 555;

 

for (i = 0; p < &a[8]; p++, i++)

    printf("a[%i] = %i\n", i, *p);

 

V príklade deklarujeme pole a ôsmich celých čísel a ukazovateľ p s doménovým typom int. V cykle for priradíme i-temu prvku poľa hodnotu i. Potom nasleduje priradenie p = a.

Obr. 3

Už vieme, že v tomto výraze sa a konvertuje na ukazovateľ na a[0]; tento ukazovateľ priradíme do premennej p. Ďalší riadok vyzerá, akoby na ňom bola chyba, indexujeme totiž nie pole, ale ukazovateľ. To je však v úplnom poriadku, operátor [] vďaka svojmu spôsobu vyhodnocovania môžeme použiť aj na ukazovatele (de facto len na ukazovatele – polia sa na ne konvertujú). Výraz p[2] teda prekladač vyhodnotí ako *(p + 2), čo nie je nič iné ako prvok a[2]. Na overenie si vyskúšame na nasledujúcom riadku aj rozpísaný tvar indexovacieho operátora. V druhom cykle for chceme vypísať hodnoty prvkov poľa a, aby sme videli, že sa hodnoty druhého a piateho z nich naozaj zmenili. Pre prístup k jednotlivým prvkom však tentoraz nepoužijeme operátor [], ale pomocný ukazovateľ, ktorý sa bude posúvať pozdĺž celého poľa. V premennej p, ktorú na tento účel použijeme, už máme adresu prvého prvku poľa a, premenná i nám poslúži na zistenie, na ktorý prvok momentálne ukazovateľ ukazuje (ale len pre výpis jeho indexu!). V inicializačnej časti cyklu for vynulujeme index i. Všimnite si ukončovaciu podmienku p < &a[8]. Za normálnych okolností prvok a[8] nie je súčasťou poľa, najvyšší prvok má index 7, ale na takéto účely je povolené použiť jeho adresu a môžeme si byť istí, že nenastane nijaká chyba (výnimka a pod.). Adresa prvku a[8] je vlastne adresou prvého bajtu za koncom poľa. My pri posúvaní ukazovateľa p chceme cyklus skončiť v okamihu, keď jeho hodnota bude s touto adresou zhodná – preto takáto podmienka. V rámci každej iterácie vypíšeme jednak index aktuálneho prvku (i) a jednak jeho hodnotu (*p). Nesmieme zabudnúť zakaždým premennú p inkrementovať a tak sa posunúť na ďalší prvok. Inkrementáciu môžeme vykonať buď vo vyhradenej časti príkazu for (ako v našom príklade), alebo priamo v rámci volania funkcie printf (teda miesto *p tam dáme *p++).

Tí šikovnejší z vás si možno všimli malú kuriozitu týkajúcu sa operátora []. Keďže pri jeho vyhodnocovaní dochádza k sčítavaniu a to je v C++ (ako aj bežne v matematike) komutatívne, môžeme namiesto *(a + 7) napísať *(7 + a), a teda následne namiesto a[7] napísať 7[a]. Je to zvláštne, ale správne a väčšina prekladačov nebude mať  námietky.

Povedzme si teraz ešte, ako to vyzerá s indexovaním prvkov viacrozmerných polí. Takéto polia sú realizované pomocou  jednorozmerných polí, celá situácia by teda mala byť jasná, no pre istotu si ju objasníme na príklade. Vezmime si už spomínané pole d, deklarované ako:

 

double d[3][4];

 

Indexovanie si opíšeme na sprístupňovaní prvku d[2][3]. Operátor [] sa asociuje zľava doprava, a teda ako prvý príde na rad index [2]. Identifikátor poľa d sa konvertuje na ukazovateľ na jeho prvý prvok. Tým je v tomto prípade štvorprvkové pole premenných typu double, ktorého veľkosť je 4 × sizeof(double), teda väčšinou 32 bajtov. Výraz d[2] sa, samozrejme, vyhodnotí ako *(d + 2). Dvojka sa vynásobí veľkosťou prvku poľa d, čo je spomínaných 32 bajtov. Tento offset sa pripočíta k začiatku poľa, čím dostaneme ukazovateľ na tretí jeho prvok, teda tretí (ergo posledný) riadok celej matice. Jeho dereferencovaním získame tento tretí prvok ako objekt typu „pole štyroch premenných typu double“. Dôležitý je tu fakt, že pole d je z hľadiska prekladača poľom troch prvkov, z ktorých každý má veľkosť 32 bajtov. To, že sú to opäť polia, nie je v danej chvíli podstatné.

Na výsledok výrazu d[2] aplikujeme opäť operátor [], tentoraz s indexom 3. d[2] je poľom štyroch prvkov s veľkosťou 8 bajtov. Index 3 sa teda vynásobí touto veľkosťou a pripočíta k ukazovateľu na začiatok poľa d[2]. Získame tak ukazovateľ na štvrtý prvok tohto poľa, ktorého dereferenciou získavame požadovaný štvrtý prvok zľava tretieho riadka celej matice. Vyhodnocovanie druhého operátora [] môžeme prepísať ako *(d[2] + 3), celý výraz d[2][3] sa teda vyhodnotí ako *(*(d + 2) + 3). Situácia je znázornená aj na obr. 4. Ešte malá poznámka: Vďaka komutatívnosti operátora + môžeme výraz d[2][3] zapísať aj ako 3[2[d]].

Obr. 4

To, čo sme si dosiaľ povedali o sčítavaní ukazovateľov s celými číslami, platí v rovnakej miere aj o odčítavaní, teda celočíselný operand sa vynásobí veľkosťou doménového typu ukazovateľa a výsledok sa odčíta od adresy uloženej v premennej typu ukazovateľ. Vďaka tomuto faktu je možné používať na indexovanie polí aj záporné indexy. Hoci na pohľad záporný index nemá zmysel, veď N-prvkové pole obsahuje len prvky s indexmi 0 až N–1, používa sa záporné indexovanie v prípadoch, keď k poľu pristupujeme pomocou ukazovateľa, ktorý ukazuje na iný ako prvý prvok. Teda ak napríklad máme úsek programu:

 

int a[100];

int *p = &a[50];

 

potom p[0] predstavuje 51. prvok poľa a, p[5] 56. prvok a p[–10] zase 41. prvok poľa a. V tomto príklade sme okrem iného aj určovali adresu 51. prvku poľa. Okrem uvedeného spôsobu je možné použiť aj trochu menej prehľadný, ale kratší spôsob:

 

p = a + 50;

 

Výraz je správny, pretože a po konverzii ukazuje na prvý prvok a pričítaním konštanty 50 dostaneme práve ukazovateľ na 51. prvok. Výrazy a + i a &a[i] sú teda ekvivalentné, ako aj výrazy *(a + i) a a[i] (ale to už vieme).

Dosiaľ sme si nepovedali ešte jeden dôležitý detail,  že ukazovatele možno od seba odčítavať. Po tom, čo sme si doteraz povedali, vás už iste napadne, ako je to možné. Predpokladáme, že dva ukazovatele ukazujú na prvky toho istého poľa. Potom sa ich rozdiel vypočíta tak, že sa urobí normálne aritmetické odčítanie oboch adries, ktoré sa potom vydelí veľkosťou doménového typu týchto ukazovateľov (je jasné, že ho oba musia mať rovnaký). Ako výsledok dostaneme počet prvkov poľa medzi oboma ukazovateľmi. Teda ak napríklad p1 ukazuje na prvok a[2] a p2 na prvok a[7], hodnotou výrazu p2 – p1 bude číslo 5. V prípade, že nie je splnená podmienka, že oba ukazovatele ukazujú do toho istého poľa, výsledok je nedefinovaný (s jednou výnimkou a tou je odčítavanie od už spomínaného ukazovateľa za posledný prvok poľa). Typom výsledku odčítania dvoch ukazovateľov je špeciálny typ ptrdiff_t, definovaný v hlavičkovom súbore <stddef.h> (obyčajne ako niektorý z celočíselných typov).

Ukazovatele môžu tiež vystupovať ako operandy relačných operátorov. Treba však dodržať určité podmienky. V prvom rade možno porovnávať ukazovatele rovnakých doménových typov, „menší“ bude ten, ktorý ukazuje na objekt na nižšej adrese. Ďalej možno porovnávať ľubovoľný ukazovateľ s ukazovateľom typu void* (realizuje sa ako bežné porovnanie adries, uložených v oboch ukazovateľoch) a taktisto možno ľubovoľný ukazovateľ porovnávať s konštantným výrazom s hodnotou 0. Ukazovateľ s hodnotou 0 je často používaný na signalizáciu, že „neukazuje nikam“. V jazyku C bola na tento účel definovaná známa konštanta (skôr makro) NULL. V jazyku C++ sa odporúča používať priamo literál 0.

Konečne sme sa prelúskali celou problematikou známeho schizofrénneho vzťahu polí a ukazovateľov v C++. Teória okolo tejto záležitosti je na pohľad jednoduchá a exaktná, ale z praxe viem, že jej úplné pochopenie nie je otázkou jedného prečítania. Ak niečomu nerozumiete, prejdite si text ešte raz a ešte raz, vyskúšajte si všetko, aj najmenšie detaily na príkladoch, až kým nebudete mať pocit, že všetkému dokonale rozumiete. Trošku vás zatiaľ bude zrejme brzdiť fakt, že pravdepodobne neviete správne deklarovať rôzne komplikované ukazovatele na ešte komplikovanejšie štruktúry (čím sú inak jazyky C a C++ neslávne známe). Vydržte, dostaneme sa k tomu.

Programujeme dynamicky

Na záver dnešného rozprávania si povieme niečo o dynamických premenných. Všetky premenné, ktoré sme dosiaľ deklarovali a používali, mali spoločnú vlastnosť: boli známe už v procese prekladu a bolo im pridelené miesto v dátovom segmente alebo na zásobníku (prípadne priamo v registroch procesora). Pre naše krátke a jednoduché programy to úplne stačilo. Predstavme si však, že potrebujeme mať v programe nejaké veľké dátové štruktúry a tieto štruktúry nechceme vytvárať ako lokálne premenné na zásobníku. Nehovorili sme si zatiaľ ešte nič o ukladacích triedach premenných, preto len stručne – všetky naše doterajšie premenné boli lokálne (automatické, deklarované vnútri nejakého bloku). Takéto premenné sa vytvárajú na zásobníku, ktorého veľkosť je obmedzená. Iným typom sú statické premenné, ktoré existujú v dátovom segmente programu, a pokiaľ sú inicializované, sú súčasťou binárneho obrazu programu, ktorý sa naťahuje z disku (zo súboru s programom). Niektoré prekladače, predovšetkým tie 16-bitové, však ukladajú do vykonateľného súboru aj neinicializované statické premenné a potom dostávame obrovské, hoci z hľadiska kódu jednoduché programy.

Dynamické premenné sa naproti tomu vytvárajú (t. j. prideľuje sa im miesto) až za behu programu v oblasti pamäte, nazývanej hromada (po česky halda, po anglicky heap – vyberte si). Vytvorenie dynamickej premennej a pridelenie určitej veľkosti pamäte sa obyčajne nazýva alokácia (opakom je dealokácia). V jazyku C na tieto účely slúžila dvojica knižničných funkcií malloc() a free(). Ich generálnou nevýhodou bola absencia akejkoľvek typovej informácie, jediné, čo bolo treba zadať, bola veľkosť prideľovanej pamäte. Funkcia malloc() vrátila ukazovateľ na začiatok pridelenej pamäte, ktorý bolo treba pretypovať na požadovaný typ. Funkcia free() alokovanú oblasť opäť uvoľnila. Celá operácia vyzerala asi takto:

 

char* buf = (char*)malloc(512);

...

free(buf);

 

Premenná buf po zavolaní funkcie malloc() obsahovala adresu polkilobajtovej oblasti pamäte, s ktorou bolo možné pracovať (vzhľadom na typ ukazovateľa sa táto oblasť primárne brala ako pole znakov). Po skončení práce bolo treba volaním funkcie free() túto oblasť opäť vrátiť, aby nedochádzalo k plytvaniu pamäťou.

Jazyk C++ okrem iných vylepšení radikálne zmenil prácu s dynamickými premennými zavedením dvojice operátorov new a delete. Ich význam je čiastočne zhodný s dvojicou funkcií malloc() a free(), ich hlavnou výhodou je však fakt, že sú to operátory a ako také sú súčasťou jazyka (obe uvedené funkcie sú súčasťou štandardnej knižnice). Pri dynamickom alokovaní premenných je teda možné pracovať s typovou informáciou a vykonávať príslušné opatrenia súvisiace s vytváraním nových premenných. Tieto opatrenia sa však týkajú objektových premenných, a preto si o nich povieme až v druhej polovici nášho seriálu, keď si budeme hovoriť o objektových črtách jazyka C++.

Vytvorenie premennej

Najprv sa budeme zaoberať operátorom new. Syntax na jeho použitie (značne zjednodušená) je takéto:

 

new  typ( inicializátor )

 

Operátor new vracia ukazovateľ na uvedený typ, ktorý už netreba pretypovávať. Typom môže byť ľubovoľný z nám dosiaľ známych typov, ako aj ich rôzne povolené kombinácie. Otázka, ako správne uviesť takýto zložito kombinovaný typ, spadá do oblasti deklarácií, takže si ju zatiaľ odložíme. Inicializátor predstavuje počiatočnú hodnotu vytvorenej dynamickej premennej. Ukážeme si teraz niekoľko príkladov použitia operátora new:

 

int* pi = new int;

double* pd = new double(8.854E–12);

char* pc = new char[128];

long** ppi = new (long *)[10];

 

V prvom príklade sme vytvorili jednoduchú premennú typu int. Inicializátor sme neuviedli, preto jej hodnota bude nedefinovaná. Premenná bude prístupná pomocou ukazovateľa ako *pi. Druhý príklad sa líši od prvého iba prítomnosťou inicializačnej konštanty. V treťom príklade alokujeme pole 128 znakov. Všimnite si, že hoci alokovaným typom je pole, operátor new nevracia ukazovateľ na typ „pole“, ale ukazovateľ na typ prvku poľa. Aj alokácia jednoduchej premennej, aj alokácia poľa premenných vracajú ukazovateľ rovnakého typu. Je to trochu mätúce, ale treba si na to zvyknúť – jednoducho, keď alokujeme pole prvkov typu T, musíme použiť ukazovateľ na typ T (teda premennú typu T*). Prvky nášho poľa sú klasicky prístupné zápisom pc[i].

Posledný príklad je trochu zložitejší. V ňom alokujeme pole desiatich ukazovateľov na typ long (t. j. pole desiatich prvkov typu long*). Podľa pred chvíľou uvedeného pravidla musíme použiť typ ukazovateľ na ukazovateľ na long, teda typ long**. Jednotlivé prvky tohto poľa sú neinicializované ukazovatele na typ long (obr. 5). Čo s nimi, to už je na nás. Môžeme im napríklad priradiť adresy nových, dynamicky vytvorených polí, z ktorých každé môže byť aj inej dĺžky. Dostaneme tak akúsi maticu s nerovnako dlhými riadkami, možností jej využitia je naozaj veľa.

Obr. 5

Zrušenie premennej

Tak ako sme dynamickú premennú vytvorili, potrebujeme ju aj zrušiť. Na to slúži druhý z dvojice operátorov, operátor delete. Jeho použitie je veľmi jednoduché – jeho argumentom je ukazovateľ na dynamickú premennú, ktorej sa chceme zbaviť. V prípade rušenia iných premenných ako polia použijeme syntax:

 

delete ukazovateľ

 

v prípade rušenia polí zase syntax:

 

delete [] ukazovateľ

 

Dôvod odlišnej syntaxe pre dealokáciu polí spočíva v nutnosti volať pre každý objektový prvok poľa špeciálnu funkciu nazývanú deštruktor, ale o tom až v budúcnosti. Pre polia neobjektových premenných sú obe syntaxe ekvivalentné, ale je žiaduce  odlišovať ich.

Ako príklad si uvedieme zrušenie dynamických premenných, vytvorených v predchádzajúcom odseku:

 

delete pi;

delete pd;

delete [] pc;

delete [] ppi;

 

Pred zrušením poľa ppi  treba prípadne zrušiť dynamické premenné, na ktoré ukazujú jednotlivé prvky tohto poľa (výrazom delete ppi[i], resp. delete [] ppi[i]).

Ešte malý príklad

Použitie oboch operátorov v jednom programe si demonštrujeme na krátkom príklade, v ktorom vytvoríme štruktúru na uloženie symetrickej matice. Pre takúto maticu platí, že aij = aji (t. j. je symetrická podľa hlavnej diagonály). Uložiť teda postačí len jej polovicu. Použijeme spomínané pole ukazovateľov, ktorého každému prvku priradíme ukazovateľ na jeden riadok matice. Riadky budú mať dĺžku od 1 po N, kde N je rozmer matice.

 

const N = 5;

double** a = new (double*)[N];

for (int i = 0; i < N; i++)

    a[i] = new double[i + 1];

...

for (i = 0; i < N; i++)

    delete [] a[i];

delete a;

 

Najprv vytvoríme pole ukazovateľov a, potom v cykle jednotlivé subpolia (riadky matice) s rozmerom i + 1, kde i je aktuálny index do poľa a. Po vytvorení štruktúry môžeme pristupovať k jednotlivým prvkom matice pomocou bežného výrazu a[i][j], len musíme dávať pozor, aby index riadka i bol väčší ako index stĺpca j. Čo sa udeje pri vyhodnocovaní tohto výrazu, ponechávam ako domácu úlohu (nemajte obavy, nabudúce bude aj riešenie). Po skončení práce s maticou zrušíme najprv jednotlivé riadky a potom aj samotné pole ukazovateľov.

Na záver

Dúfam, že som týmto pokračovaním potešil tých, ktorí volali po väčšom rozsahu jednotlivých častí. Nabudúce sa začneme zaoberať širokou a dôležitou problematikou deklarácie mien v C++.

Zobrazit Galériu
C++

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

Mohlo by Vás zaujímať

Ako na to

Čo robiť keď firme chýba IT expert?

08.12.2016 10:36

IT projekty majú z hľadiska nárokov na kapacity špecialistov premenlivý charakter a v určitých fázach často treba posilniť kapacity IT oddelení externými odborníkmi. Riešením je IT ousourcing, ako fo ...

Ako na to

Ako funguje sandbox?

08.12.2016 15:36

Každá aplikácia môže pre operačný systém počítača či mobilného zariadenia predstavovať potenciálnu hrozbu, a to aj v prípade, ak neobsahuje žiadne bloky škodlivého kódu. Murphyho zákony neúprosne defi ...

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ž ...

Žiadne komentáre

Vyhľadávanie

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

Najnovšie videá