Image
12.6.2016 0 Comments

C++ / Štruktúry a uniony / 16. časť

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

Na úvod tohto pokračovania si dovolím malú skrytú reklamu. Naďabil som nedávno v kníhkupectve na knihu, ktorá na našom trhu výrazne chýbala. Vydalo ju vydavateľstvo Grada Publishing, s. r. o., jej názov je Jazyky C a C++ podle normy ANSI/ISO a podtitul Kompletní kapesní průvodce. Túto knihu vrelo odporúčam každému, kto berie C++ aspoň trochu vážne, pretože ide o jedinú publikáciu tohto typu v češtine (o slovenčine taktne pomlčím), ktorá navyše obsahuje opis C++ podľa aktuálnej normy ANSI, resp. ISO. Od kúpy by vás možno mohla odradiť cena, ktorá je asi 500 Sk (a to ide o pomerne malý paperbackový formát…), ale myslím si, že takáto investícia sa vám veľmi rýchlo vráti, aj keď v trochu inej podobe.

Ale teraz poďme k nášmu seriálu. Pomyselným spojivom medzi oboma jeho polovicami sú štruktúrované údajové typy – štruktúry, uniony a triedy. Ako uvidíme neskôr, štruktúry sú de facto zvláštnym prípadom tried. Uniony na rozdiel od štruktúr či tried predstavujú špecifické typy s mierne odlišnou sémantikou.

Štruktúry

Kedysi dávno, v jednej z prvých častí seriálu, sme si hovorili, že údajové typy v C++ môžeme rozdeliť na základné a odvodené. Základné typy, ako napríklad int či double, predstavujú základné stavebné kamene na vytváranie zložitejších typových konštrukcií. Každý zo základných typov je deklarovaný pomocou svojho kľúčového slova, má svoj rozsah hodnôt a definovanú množinu operácií. Naproti tomu odvodené typy sú rôznymi spôsobmi skonštruované z viacerých základných typov, z viacerých prvkov jedného základného typu alebo sú určitými sémantickými modifikáciami základných typov. Zatiaľ z odvodených typov poznáme polia, t. j. vektory prvkov (jediného!) základného typu, a, samozrejme, ukazovatele a referencie. (Funkcie, ktoré sú vlastne tiež údajovými typmi, môžeme považovať za odvodené typy alebo ich z tohto prehľadu môžeme vylúčiť.)

Pri návrhu programu veľmi často prídeme k záveru, že by sme potrebovali údajový typ, ktorý by bol nehomogénnym združením viacerých prvkov rozdielnych základných typov. Takémuto typu sa často hovorí „agregovaný“ a v C++ je jeho najjednoduchšou realizáciou údajový typ štruktúra (struct). Každý objekt, ktorý bude deklarovaný ako objekt typu štruktúry, bude predstavovať spomínanú kolekciu združených objektov základných alebo aj odvodených typov. Tieto združené objekty budeme nazývať členmi, resp. členskými objektmi danej štruktúry.

Deklarácia údajového typu štruktúra je pomerne jednoduchá. Predovšetkým si treba spomenúť na rozdiel medzi deklaráciou a definíciou a na zloženie deklarácií. Špecifikátor, ktorý použijeme pri deklarácii štruktúry, pozostáva z kľúčového slova struct a zo zoznamu deklarácií jednotlivých členov štruktúry uzavretého v krútených zátvorkách. Ak uvedieme za špecifikátorom deklarátor(y), deklarujeme súčasne s novým údajovým typom aj jeden či niekoľko objektov tohto typu. V praxi sa však oddeľuje deklarácia typu štruktúry, od deklarácie objektov typu tejto štruktúry a to z dôvodov, ktoré si spomenieme o chvíľu. Toto rozdelenie je možné vďaka tomu, že z deklarácie štruktúry môžeme bez problémov vynechať deklarátor(y). Bežne potom v programe uvidíme nasledujúci zápis:

struct S
{
    int a;
    double b;
    char c;
};
 
S s1, s2;

V príklade deklarujeme nový agregovaný údajový typ S (všimnite si, že nepotrebujeme mechanizmus typedef!) a následne dve premenné s1 a s2 tohto typu. Samotný typ S je v programe dostupný buď pod svojím krátkym názvom S, alebo v prípade, že neskôr deklarujeme nejaký iný objekt (lokálnu premennú, funkciu a pod.) s názvom S, pod úplným názvom struct S. Oba objekty s1 a s2 by sme mohli preto deklarovať aj takto:

struct S s1, s2;

Deklaráciu typu S aj deklaráciu objektov s1 a s2 môžeme, samozrejme, spojiť:

struct S { ... } s1, s2;

Tento spôsob je však oveľa menej prehľadný a navyše neumožňuje vloženie deklarácie štruktúry do hlavičkového súboru (kde je jej najčastejšie miesto, aby sme ju mohli používať vo viacerých zdrojových súboroch).

Nakoniec je tu ešte jedna možnosť, keď z deklarácie vypustíme názov typu S. V takom prípade deklarujeme objekty s1 a s2 ako objekty neznámeho (nepomenovaného) typu, nekompatibilného so žiadnym iným typom, hoci by mal aj rovnaké zloženie. To znamená, že napríklad v nasledujúcej deklarácii:

struct { int x; } r1;
struct { int x; } r2;

sú r1 a r2 objekty rôznych (!) typov, hoci majú na pohľad úplne rovnaké zloženie.

Čo sa týka zoznamu deklarácií členov štruktúry, ide o deklarácie v klasickom zmysle slova s niekoľkými obmedzeniami. Členmi štruktúr môžu byť premenné, funkcie, iné štruktúry či triedy, enumerácie, typy a bitové polia. O bitových poliach si povieme o chvíľu, všetko ostatné už poznáme. Členy štruktúry nemôžu byť deklarované so špecifikátormi auto, register alebo extern. Naopak, pri deklarácii môžeme použiť špecifikátor static, pomocou ktorého odlišujeme tzv. statické členy (bude o nich reč neskôr).

Štruktúra nesmie obsahovať údajový člen typu samej seba, môže však obsahovať ukazovateľ, resp. referenciu na svoj typ (teda aj ukazovateľ na samu seba, ak nám to nejako pomôže). Údajové členy, ktoré sú poľami, musia mať deklarované všetky rozmery.

Deklarácie členov štruktúry nesmú obsahovať inicializátory, inicializácia sa vykonáva pomocou špeciálnej členskej funkcie. Keďže nie je priveľmi bežné, aby štruktúry (t. j. typy struct) obsahovali členské funkcie či vnorené typy, obmedzíme sa zatiaľ len na štruktúry obsahujúce klasické premenné (t. j. údajové členy) a o členských funkciách budeme hovoriť až v súvislosti s triedami.

Prístup k členom štruktúry

Jednotlivé členy štruktúry musia byť nejakým spôsobom prístupné zvonka, inak by celý koncept štruktúr stratil zmysel. Toto sprístupnenie sa vykonáva pomocou dvoch operátorov. Oba sú binárne a infixové a majú v podstate najvyššiu prioritu (rovnakú ako operátory () a []). Prvým z týchto dvoch operátorov je operátor . (bodka). Jeho ľavým operandom je objekt typu štruktúry, pravým operandom je identifikátor niektorého člena tejto štruktúry. Majme definovanú štruktúru S a objekty s1 a s2 ako predtým:

struct S
{
    int a;
    double b;
    char c;
};
 
S s1, s2;

Potom jednotlivé členy objektu s1 naplníme napríklad takto:

s1.a = 123;
s1.b = 4.567;
s1.c = 'D';

Vidíme, že takto sprístupnené členy sú l-hodnotami, keďže aj objekt s1 je sám osebe l-hodnotou. Okrem toho môžeme objekty typu štruktúry priraďovať aj medzi sebou:

s2 = s1;

Toto priradenie sa vzhľadom na neexistenciu určitých členských funkcií realizuje člen po člene (lepšie povedané, bajt po bajte) – ide o tzv. shallow copy (plytkú kópiu). Jeho primárnou nevýhodou je, že ak je členom typu štruktúry ukazovateľ, skopíruje sa jeho hodnota z jednej štruktúry do druhej tak, ako je, čo vedie k tomu, že obe štruktúry budú de facto zdieľať jedny údaje (tie, na ktoré ukazuje ukazovateľ). To nám nemusí vždy vyhovovať, väčšinou chceme, aby každý objekt typu štruktúry obsahoval ukazovateľ na vlastné, privátne údaje. V takom prípade situáciu vyriešia spomínané členské funkcie, menovite konštruktor a operátor priradenia. Ale o tom až neskôr.

Druhým operátorom prístupu k členom štruktúry je operátor -> (pomlčka a znak „je väčšie“). Jeho jediný rozdiel oproti operátoru . je ten, že jeho ľavým operandom nie je priamo objekt typu štruktúry, ale ukazovateľ na tento objekt. Ak teda máme deklarovaný takýto ukazovateľ:

S* ps = &s1;

môžeme uvedené priradenia realizovať aj takto:

ps->a = 123;
ps->b = 4.567;
ps->c = 'D';

Je očividné, že výraz ps->a je ekvivalentný výrazu (*ps).a, len je oveľa prehľadnejší.

Objekty typu takýchto jednoduchých štruktúr (bez konštruktorov, bez neverejných členov, bez nadradených štruktúr a bez virtuálnych funkcií) môžeme spolu s deklaráciou aj inicializovať. Za znamienko = uvedieme v krútených zátvorkách uzavretý zoznam inicializátorov jednotlivých členov štruktúry, oddelených čiarkami. Hore uvedená inicializácia priradením po zložkách sa teda dá zapísať aj takto:

S s1 = { 123, 4.567, 'D' };

V prípade vnorených štruktúr postupujeme podobne – ako príslušný inicializátor uvedieme opäť v zátvorkách uzavretý zoznam inicializátorov vnorenej štruktúry.

Inicializácia štruktúr, resp. tried takýmto spôsobom nie je veľmi používaná, je oveľa výhodnejšie inicializovať jednotlivé členy štruktúry pomocou samostatnej funkcie so špeciálnym postavením – tzv. konštruktora. Bližšie sa o konštruktoroch dozviete v ďalších častiach.

Bitové polia

Zvláštnym typom členských údajov štruktúry sú tzv. bitové polia. Ide o bežné celočíselné premenné, pri ktorých však môžeme navyše určiť ich bitovú šírku. Ak máme také členské premenné, ktorých obsah sa vojde do menšieho počtu bitov, ako je najmenší typ, ktorý C++ poskytuje (t. j. char), ušetríme pomocou bitových polí často drahocenné miesto v pamäti (a nielen v pamäti, ale aj napríklad v súboroch a pod.). Okrem toho sa bitové polia využívajú vtedy, ak pracujeme s údajmi, ktoré získavame externe, mimo nášho programu a vieme o nich, že majú príslušnú štruktúru – typicky napríklad pri práci s hardvérovými registrami, ktoré bývajú delené po bitoch.

Deklarácia bitového poľa obsahuje za deklarátorom príslušného člena dvojbodku, nasledovanú celočíselnou konštantou, vyjadrujúcou šírku tohto poľa. Typ takéhoto člena musí byť int alebo unsigned (podľa toho sa, samozrejme, s jeho obsahom pracuje ako so znamienkovou či neznamienkovou hodnotou). Príklad:

struct Reg
{
    unsigned in : 3;
    unsigned out : 3;
    unsigned : 1;
    unsigned ok : 1;
}

Štruktúra Reg predstavuje fiktívny register, ktorého tri bity slúžia na čítanie údajov (člen in), tri bity na zápis údajov (člen out) a jeden bit pre pomyselný príznak chyby (člen ok). Zaujímavosťou v deklarácii Reg je nepomenované bitové pole so šírkou jedného bitu. Takéto pole nie je nijako prístupné a slúži len ako „vypchávka“. Prístup k jednotlivým členom sa realizuje pomocou bežných operátorov (. a ->). Ak sa snažíme do bitového poľa vložiť väčšiu hodnotu, ako umožňuje jeho šírka, výsledok závisí od implementácie – obyčajne sa hodnota oreže na šírku bitového poľa.

Treba poznamenať, že aj presný spôsob reprezentácie bitových polí v pamäti je implementačne závislý – nevieme vopred povedať, či napr. pole in bude zaberať tri najnižšie alebo tri najvyššie bity z celého priestoru, ktorý obsadzuje štruktúra Reg, a nevieme dokonca ani povedať, či bude štruktúra Reg v pamäti zaberať ten jeden bajt, ktorý by jej bohato stačil, alebo či bude vzhľadom na požiadavky zarovnávania objektov zaberať viacej bajtov (napríklad štyri).

Binárny strom

Pre názornejšiu ukážku práce so štruktúrami si uvedieme krátky príklad. V ňom budeme používať abstraktný údajový typ „binárny strom“, ktorý je definovaný veľmi jednoducho: 1. uzol bez potomkov je binárnym stromom, 2. uzol, ktorého ľavým a/alebo pravým potomkom je binárny strom, je aj sám
osebe binárnym stromom (kde uzol je nejaká pomyselná entita, predstavujúca „stavebnú jednotku“ stromov). Príklad binárneho stromu je na obr. 1.

Obrázok 1 Binárny strom

Táto pomerne neformálna definícia s rekurzívnym nádychom nás vedie k nasledujúcej predstave: Reprezentujme uzol stromu ako štruktúru, ktorá bude popri nejakom užitočnom informačnom obsahu zahŕňať dva ukazovatele na ľavého a pravého potomka. Oba ukazovatele budú, samozrejme, ukazovať opäť na ďalšie uzly, ktoré môžu prípadne ukazovať zase na ďalšie uzly, a tak stále ďalej a ďalej do hĺbky stromu. Dohodneme sa takisto, že ak daný potomok neexistuje, príslušný ukazovateľ bude mať hodnotu 0. Celý strom bude reprezentovaný jedným, tzv. koreňovým uzlom, z ktorého budú viesť ukazovatele na nižšie a nižšie úrovne - až k jednotlivým listom stromu.

Napíšeme teda deklaráciu štruktúry Bnode, predstavujúcej uzol stromu:

struct Bnode
{
  char* str;
  int count;
  Bnode* left, right;
};

Povedzme si ešte, na čo binárny strom využijeme. Jednou z pekných a názorných aplikácií je jeho použitie ako prostriedku na počítanie výskytu slov v texte. Ako informačný obsah budeme uchovávať v každom uzle jedno slovo (ako textový reťazec) a jeho počet výskytov v prehľadávanom texte. Algoritmus takéhoto počítania slov je v skratke nasledujúci:

  1. Načítaj slovo.
  2. Ak je koreňový strom prázdny, vytvor nový uzol, nastav počet výskytov slova na 1 a choď na krok 1.
  3. Ak je slovo zhodné so slovom z koreňa stromu, inkrementuj počet výskytov a choď na krok 1.
  4. Ak je slovo menšie ako to z koreňa stromu, pokračuj od kroku 2 s ľavým podstromom.
  5. Ak je slovo väčšie ako to z koreňa stromu, pokračuj od kroku 2 s pravým podstromom.

V krokoch 4 a 5 jednoducho zoberieme ľavý alebo pravý podstrom a ďalej s ním rekurzívne pracujeme ako s pôvodným stromom. S každým novým načítaným slovom teda traverzujeme stromom od koreňa smerom nadol, až kým ho nenájdeme v niektorom uzle alebo kým neurčíme miesto, kam slovo vložíme v prípade, že ho ešte v strome nemáme.  Algoritmus zaručí, že z pohľadu každého uzla budú v jeho ľavom podstrome všetky slová menšie a v pravom podstrome všetky slová väčšie ako slovo v tomto uzle. (Čo sa týka pojmov väčší a menší, ide o lexikografické porovnanie znak po znaku.) Pre každé slovo teda bude existovať jednoznačná cesta, ktorá nás dovedie buď k uzlu s týmto slovom, alebo k miestu, kam slovo patrí. Je jasné, že táto cesta bude závisieť od doterajšieho spektra načítavaných slov a pre rôzne texty bude strom vyzerať rôzne. V krajnom prípade dokonca môžeme dostať tzv. degenerovaný strom, v ktorom každý uzol má napríklad len pravých potomkov – táto situácia nastane vtedy, keď budú slová v načítavanom texte zoradené podľa abecedy.

Nasleduje niekoľko funkcií, ktoré realizujú algoritmus:

int get_word(char*);
Bnode* create_node(char*);
void add_word(Bnode*, char*);
 
void do_count()
{
  char wrd_buf[80];
 
  get_word(wrd_buf);
  Bnode* tree = create_node(wrd_buf);
 
  while (get_word(wrd_buf))
    add_word(tree, wrd_buf);
 
  // spracovanie výsledkov
}
 
void add_word(Bnode* root, char* word)
{
  if (!strcmp(word, root->str))
    root->count++;
  else if (strcmp(word, root->str) < 0)
    if (root->left)
      add_word(root->left, word);
    else
      root->left = create_node(word);
  else /* (strcmp(word, root->str) > 0) */
    if (root->right)
      add_word(root->right, word);
    else
      root->right = create_node(word);
}
 
Bnode* create_node(char* word)
{
  Bnode* node = new Bnode();
  node->str = new char[strlen(word) + 1];
  strcpy(node->str, word);
  node->count = 1;
  node->left = node->right = 0;
  return node;
}

Funkcia do_count() je hlavnou funkciou celého algoritmu. Obsahuje premennú wrd_buf, predstavujúcu pomocný buffer pre načítavané slová. Jeho dĺžka je obmedzená na 80 znakov; v prípade, že by sme chceli algoritmus spraviť dostatočne robustným a schopným počítať aj dlhšie slová, museli by sme pre každé načítané slovo vytvoriť miesto v pamäti dynamicky, čím by sa celý príklad trochu skomplikoval. Prípadné úpravy týmto smerom ponechávam preto na vás. V podstate stačí zabezpečiť, aby sa alokované miesto vo vhodnom okamihu uvoľnilo pomocou operátora delete – dá sa, samozrejme, využiť fakt, že pri vytváraní nového uzla potrebujeme nové slovo nejako uchovať, na čo môžeme použiť preň alokované miesto, ktoré v takom prípade uvoľňovať nebudeme.

Samotné načítanie nového slova má na starosti funkcia get_word(). Jej implementáciu v príklade schválne nenájdete, pretože bude závisieť od toho, z akého zdroja slová čítame – zo súboru, z konzoly a pod. Takže opäť miesto pre vašu realizáciu. Uvedený máte len prototyp funkcie, ktorá ako argument dostane ukazovateľ na miesto v pamäti, kam sa má nové slovo skopírovať (predpokladá sa, že je tam dostatok voľného priestoru). Funkcia musí vracať nenulovú hodnotu, kým je čo čítať, a nulovú, keď dôjdeme na koniec spracúvaného textu. Jeden z možných spôsobov realizácie jednoducho načítava znaky a kopíruje ich do vyhradeného buffera, až kým nedospeje k znaku, ktorý nie je súčasťou slova – typicky biele znaky, rôzne interpunkčné znamienka a iné.

Skôr než funkcia do_count() začne v cykle načítavať jednotlivé slová, pripraví si celý strom vytvorením jeho koreňového uzla na základe prvého načítaného slova. Strom je reprezentovaný premennou tree typu ukazovateľ na štruktúru uzla Bnode. Vytvorenie nového uzla má na starosti funkcia create_node(), ktorá alokuje novú inštanciu štruktúry Bnode a inicializuje jej polia. Pole str, v ktorom má byť uložený ukazovateľ na príslušné slovo, sa inicializuje priradením ukazovateľa na novo alokované miesto v pamäti, získaného pomocou operátora new. Toto miesto musí byť, vzhľadom na ukončovaciu nulu o jeden znak dlhšie ako samotné slovo. V príklade sa pre jednoduchosť netestuje, či prvé volanie get_word() nevrátilo náhodou nulu ani či bolo vytvorenie uzla úspešné (to vlastne ani funkcia create_node() nijako neindikuje).

Jadrom funkcie do_count() je cyklus while, v ktorom sa v každej iterácii načíta jedno nové slovo a zaznačí sa jeho výskyt do stromu. Toto zaznačenie realizuje funkcia add_word() s dvoma argumentmi. Prvým z nich je ukazovateľ na koreň stromu, do ktorého slovo pridávame, druhým je ukazovateľ na pridávané slovo. Ak sa toto slovo zhoduje so slovom uloženým v koreňovom uzle, nerobíme nič iné, len inkrementujeme príslušnú premennú count. V opačnom prípade testujeme, ktorým podstromom sa budeme ďalej zaoberať, pomocou štandardnej funkcie strcmp(). Vieme, že jej návratová hodnota je v prípade nezhody porovnávaných reťazcov kladná alebo záporná podľa toho, ktorý z oboch reťazcov je väčší. Po zistení, ktorým smerom sa máme vydať, najprv testujeme, či príslušný podstrom vôbec existuje (porovnaním príslušného ukazovateľa s nulou). Ak nie, jednoducho vytvoríme nový uzol pomocou funkcie create_node(), v opačnom prípade zavoláme funkciu add_word() rekurzívne s príslušným podstromom, čím sa celá procedúra zopakuje odznova, tentoraz však na nižšej úrovni.

Príklad nie je kompletným programom, chýba mu jednak spomenuté načítavanie slov, jednak nejaké spracovanie získaných výsledkov, t. j. výpis výskytu jednotlivých slov. Takýto výpis môžeme realizovať jednoduchým rekurzívnym prechádzaním celého stromu pomocou tzv. in-order stratégie, keď sa strom spracúva takto: 1. spracujeme ľavý podstrom, 2. spracujeme koreňový uzol (vypíšeme slovo a jeho počet výskytov) a 3. spracujeme pravý podstrom. Kroky 1 a 3 robíme, samozrejme, iba v prípade, že príslušné podstromy existujú. Spracovanie každého podstromu robíme rovnakým algoritmom (tu je práve rekurzia). Výhodou in-order stratégie je fakt, že jednotlivé vypísané slová budú zoradené podľa abecedy. Okrem nej existujú ešte dve používané stratégie spracovania binárneho stromu –pre-order stratégia (vymenené kroky 1 a 2) a post-order stratégia (vymenené kroky 2 a 3). Oblasti ich použitia sú však dosť špecifické.

V uvedenom programe je použitý z dôvodu jednoduchosti a názornosti štýl programovania, ktorý za žiadnych okolností nesmiete používať – totiž nikde sa neuvoľňujú objekty alokované pomocou new. Takéto upratovanie by pre celý strom znamenalo opätovne jeho rekurzívne traverzovanie a postupné uvoľňovanie jednotlivých alokovaných reťazcov a uzlov smerom zdola nahor. Za domácu úlohu si môžete vyskúšať napísať funkciu, ktorá bude túto dealokáciu realizovať, ale oveľa efektívnejšie je definovať pre štruktúru špeciálnu členskú funkciu, tzv. deštruktor (opak konštruktora), ktorý bude mať na starosti likvidáciu objektu štruktúry. Onedlho si o ňom povieme viac.

Uniony

Druhým agregovaným typom, podobným štruktúram, ale s mierne odlišnou sémantikou, sú uniony. Úmyselne používam originálny anglický termín union, hoci napríklad v českej literatúre sa bežne stretnete s prekladom únia. Myslím si však, že netreba za každú cenu prekladať výrazy, ktoré si to nevyžadujú. Ešte stále (a vlastne dnes možno viac ako kedysi) je programovanie veľmi úzkou špecializáciou, ktorá má nárok na vlastné odborné výrazy, ktorým programátori bez problémov rozumejú aj bez prekladu. A neraz som už spomínal, že angličtina by mala byť „the second native language for all software and/or hardware engineers”.

Deklarácia unionov je veľmi podobná deklarácii štruktúr, s tým rozdielom, že namiesto kľúčového slova struct použijeme kľúčové slovo union. Aj uniony môžu obsahovať deklaráciu členských funkcií, nesmú však obsahovať virtuálne funkcie ani sa nesmú podieľať na akomkoľvek vzťahu dedičnosti. Takisto uniony nesmú mať statické členy.

Čo sa týka sémantiky, union si môžeme predstaviť ako štruktúru, ktorá obsahuje všetky svoje členy akoby „nad sebou“ – to znamená, že všetky jeho členy sa začínajú na rovnakej adrese, a teda všetky zdieľajú tú istú pamäťovú oblasť. Ak zmeníme hodnotu niektorého z členov unionu, zmení sa automaticky aj hodnota všetkých ostatných členov. Ukážme si podobný príklad:

union U
{
  int a;
  double b;
  char c;
};
 
U u;

K jednotlivým členom unionu pristupujeme klasickým spôsobom. Nech sme do členskej premennej a objektu u uložili hodnotu 123 príkazom:

u.a = 123;

Ak teraz zmeníme hodnotu člena b napr. takto:

u.b = 4.56;

zmení sa aj hodnota členov a a c, o čom sa môžeme ľahko presvedčiť výpisom obsahu premennej u.a.

Je zrejmé, že pri prístupe k ľubovoľnému z členov sa jednoducho vezme príslušná oblasť pamäte a interpretuje sa ako premenná zodpovedajúceho typu (bežná vlastnosť C++). To nám umožňuje používať nasledujúci trik (inak veľmi častý – v rôznych obmenách):

union WORD
{
  unsigned short w;
  struct {
    unsigned char l;
    unsigned char h;
  } x;
};

Vidíme, že deklarovaný union WORD obsahuje dva členy – w typu unsigned short, čo je obyčajne 16-bitové celé číslo bez znamienka, a x typu štruktúra, obsahujúce zase dva členy l a h typu unsigned char, čo sú 8-bitové celé čísla bez znamienka. Oba členy w a x sa prekrývajú, čo nám umožňuje pristupovať k ľubovoľnému 16-bitovému číslu jednak ako k celku, jednak k jeho vyššiemu a nižšiemu bajtu samostatne. S pomocou unionu WORD je rozloženie čísla na vyšší a nižší bajt veľmi jednoduché:

WORD wrd;
wrd.w = 0xABCD;
printf("lo = 0x%2X\n", wrd.x.l);
printf("hi = 0x%2X\n", wrd.x.h);

Samozrejme, musíme vedieť, ako sú 16-bitové čísla ukladané v pamäti. Na bežných intelovských procesoroch sa bez výnimky používa Little-Endian notácia, teda najprv nižší a potom vyšší bajt. Preto je vo vnorenej štruktúre najprv člen l a za ním člen h.

Zvláštnym typom unionov sú tzv. anonymné uniony. V ich deklarácii chýba jednak názov unionu, a jednak akékoľvek deklarátory, takže dostávame niečo ako:

union

{
  int a;
  double b;
  char c;
};

Takéto uniony nedefinujú nový typ, ale predstavujú samostatné, nepomenované objekty. Prístup k nim sa nerealizuje pomocou operátorov . či ->, namiesto toho sú jednotlivé členy anonymných unionov prístupné priamo ako bežné premenné, s tým rozdielom, že zdieľajú spoločnú pamäť. Je očividné, že členy globálnych anonymných unionov sa nachádzajú v rovnakom priestore mien, takže nemôžeme mať v dvoch unionoch dva členy s rovnakým názvom. Ďalej anonymné uniony nesmú obsahovať neverejné členy a nesmú mať členské funkcie. Deklaráciou, ktorá obsahuje aspoň jeden deklarátor, nedeklarujeme anonymný union, nasledujúci kód je teda chybou:

union { int a; char* p; } obj;
a = 7;

k členom unionu obj musíme pristupovať klasickým spôsobom:

obj.a = 7;

Objekt obj je inštanciou normálneho unionu, ktorý však nie je deklarovaný vopred ako samostatný typ, ale jeho je deklarácia spojená s deklaráciou jednej z jeho inštancií.

Úvod do OOP

Záver tejto časti seriálu by som chcel venovať jemnému a veľmi neformálnemu úvodu do princípov objektovo-orientovaného programovania. Nebude to vyčerpávajúci opis a už vôbec nie učebnica OOP, pretože náš seriál sa nezaoberá otázkou „ako programovať“, ale takmer výhradne otázkou „ako (efektívne) programovať v C++“. Tento úvod pomôže tým z vás, ktorí nemajú OO koncept programovania v malíčku. O problematike OOP existuje veľké množstvo kníh, mne osobne sa veľmi páčila kniha Základy OOP od I. Kravala, ktorú vydal Computer Press a ktorú odporúčam do vašej pozornosti. Autor síce vysvetľuje princípy OOP s použitím Visual Basicu, ale tak zrozumiteľne a všeobecne, že nie je problém ich úspešne aplikovať v iných jazykoch, napríklad v C++.

Základnou entitou v OOP je objekt. Môžeme si ho predstaviť ako malé abstraktné niečo, čo má svoj vlastný stav. Jednotlivé zložky stavu nazývame atribúty objektu. Dovnútra objektu nevidíme – nevieme, čo ho tvorí ani ako je jeho stav zakódovaný. Toto je jeden zo základných konceptov  OOP, hovorí sa mu zapuzdrenie (encapsulation). Každý objekt má mať svoj stav skrytý pred okolím, aby nemohlo dôjsť k takým jeho modifikáciám, ktoré by uviedli objekt do nekonzistentného stavu.

Interakcia medzi objektmi sa deje pomocou posielania správ. Každý objekt dáva k dispozícii akýsi zoznam správ, na ktoré dokáže reagovať. Napríklad objekt Zamestnanec by mohol reagovať na správy „Nastav meno“, „Vráť plat“ a podobne. Dôležité je, že objekt, ktorý správu posiela, sa nemusí starať o to, akým spôsobom sa jeho správa spracuje – to má na starosti objekt, ktorý správu dostane a len on jediný pozná svoj stav, môže ho modifikovať, prípadne na zabezpečenie nejakej funkčnosti môže poslať správu iným objektom.

Máme teda predstavu objektu ako entity zapuzdrujúcej nejaký informačný obsah a exportujúcej kolekciu správ, na ktoré je schopná reagovať. Druhým veľmi dôležitým konceptom OOP je polymorfizmus. Táto vlastnosť znamená, že môžeme jednu správu poslať viacerým (rôznym) objektom a každý na ňu zareaguje inak, svojím vlastným spôsobom. Klasickým prípadom môže byť správa „Serializuj sa“. Serializácia objektu je zakódovanie jeho stavu tak, aby sa dal uložiť napríklad do súboru, s tým, že niekedy v budúcnosti sa z tohto uloženého stavu objekt dokáže rekonštruovať. Je zrejmé, že každý objekt sa bude serializovať inak; nás to však nemusí zaujímať, jednoducho pošleme príslušnú správu všetkým objektom a je ďalej len na nich, akým spôsobom ju spracujú.

Zapuzdrenie a polymorfizmus sú naozaj základnými princípmi OOP. Bez nich by celá koncepcia strácala akýkoľvek význam. Obyčajne však máme k dispozícii ešte pojmy trieda a dedičnosť. Triedy si môžeme predstaviť ako akési šablóny pre objekty, resp. ako metaobjekty slúžiace na generovanie nových objektov. V C++ sa pojem triedy stavia výrazne do popredia, pretože neexistuje spôsob, ako deklarovať objekt bez toho, aby sme najprv deklarovali jeho triedu. OOP však triedy nevyžaduje a je možné mať jazyk, ktorý je objektovoorientovaný a predsa koncept tried neobsahuje. Čo sa týka dedičnosti  o tej si povieme neskôr spolu s konkrétnymi príkladmi. V skratke ide o možnosť usporiadať objekty, resp. triedy do hierarchie na základe vzťahu generalizácie/špecializácie.

Nabudúce

V budúcej časti sa budeme konečne venovať triedam jazyka C++. Povieme si o rozdieloch medzi nimi a štruktúrami, o modifikátoroch prístupu k členom tried, o členských funkciách a o mnohom ďalšom. Dovtedy vám prajem krásnu jar, čo najmenej starostí a teším sa na stretnutie opäť o mesiac.

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

Ako zbaviť fotky hmly

08.12.2016 11:59

Hmla alebo dym sú často veľmi kreatívne nástroje. No všetkého veľa škodí. Fotka potom stráca kontrast a v podstate na nej nič nevidieť. Hmlu môžete neraz následnými úpravami odstrániť alebo zredukovať ...

Ako na to

Užitočné SW nástroje

08.12.2016 11:53

AllDup v4.0.3 Určenie: program na vyhľadávanie a odstraňovanie duplicitných súborov Vlastnosti: duplicitné súbory sa vyhľadávajú len na zvolených diskových jednotkách alebo len v rámci vybraných ...

Ako na to

Fotografovanie s bleskom

08.12.2016 11:47

Ak máte moderný fotoaparát so vstavaným alebo externým bleskom, zdá sa vám téma článku triviálna. Jednoducho nastavíte vhodný režim, vyberiete najlepšiu kompozíciu záberu, exponujete a o zvyšok sa už ...

Žiadne komentáre

Vyhľadávanie

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

Najnovšie videá