Image
16.6.2016 0 Comments

C++ / Dedičnosť a polymorfizmus / 19. časť

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

To leto sa ale pekne začalo. Aha, pardon, veď vy ho máte už takmer za sebou. Ale možno si spomeniete na ten hrozný lejak na sklonku júna … no, asi už nie. Koniec koncov, to je vlastne jedno. Mesiac sa s mesiacom zišiel a my sa stretávame pri devätnástej časti nášho seriálu. Povieme si o tom, ako triedy dokážu jedna od druhej dediť svoje vlastnosti a ako je možné, že inštancie tried dokážu byť polymorfné.

Keď triedy dedia

V reálnom živote akt dedenia obyčajne predpokladá ukončenie životnosti jedného objektu (bohatého strýka z Ameriky) a prevod jeho vlastníctva (ťažko zarobených miliónov) na iný objekt (najlepšie na nás). Nie tak v C++. Tu vzťah dedičnosti predstavuje statickú väzbu medzi dvoma triedami, pri ktorej je z existujúcej triedy špecifickým spôsobom odvodená nová trieda, ktorá automaticky získava všetky atribúty a metódy nadradenej triedy. Statická väzba preto, lebo vzťah dedičnosti je známy už v čase kompilácie programu (compile-time). V C++ nie je možné odvodiť novú triedu, resp. nový objekt dedením za behu programu (run-time), čo ostatne vyplýva z nutnosti pre každý objekt, ktorý chceme v programe použiť, povinne deklarovať triedu.

Trieda, ktorej atribúty sú dedené, sa nazýva aj základná trieda, rodičovská trieda, resp. trieda predka. Odvodená trieda je dcérska trieda či trieda potomka.

Prvoradým zmyslom existencie dedenia medzi triedami je vyjadrenie vzťahu špecializácie/generalizácie. Predstavme si, že máme definovanú triedu zivocich. Z nej môžeme odvodiť triedy cicavec, vtak, hmyz a iné, ktoré sú špecifickými druhmi živočíchov. Každú z týchto tried môžeme ďalej rozvíjať, čím dostávame stromovú hierarchiu tried, v ktorej každá odvodená trieda je špeciálnym prípadom základnej triedy a naopak – základná trieda je zovšeobecnením hociktorej odvodenej triedy. V praxi takto dosiahneme, že vlastnosti, ktoré sú spoločné viacerým odvodeným triedam, budú sústredené do základnej triedy, čím sa ušetrí často zbytočné opakovanie deklarácií atribútov či metód. Navyše, ako uvidíme, C++ nám umožní všade tam, kde sa bude vyžadovať inštancia základnej triedy, použiť inštanciu ľubovoľnej odvodenej triedy, a to nielen v priamom vzťahu dedičnosti, ale aj v nepriamom, cez viacero stupňov hierarchie. Toto je ostatne nevyhnutná podmienka realizácie polymorfizmu. Ale k tomu sa ešte dostaneme.

Odvodená trieda automaticky dedí všetky verejné (public) a chránené (protected) údajové členy a členské funkcie základnej triedy, ku ktorým môžeme pristupovať tak, akoby boli deklarované v odvodenej triede. Už sme si spomenuli, že chránené členy sú prístupné jednak členským a spriateleným funkciám danej triedy, jednak členským a spriateleným funkciám všetkých odvodených tried, ibaže doteraz sme nevedeli, čo sú to odvodené triedy. Súkromné (private) členy nie sú odvodeným triedam prístupné.

Základné triedy (môže ich byť aj viac, vtedy hovoríme o viacnásobnej dedičnosti – o dôsledkoch si povieme neskôr) špecifikujeme pri deklarácii odvodenej triedy – za jej názov doplníme dvojbodku a čiarkami oddelený zoznam základných tried:

class Base
{
public:
  int a, b;
  void f();
};
 
class Derived : public Base
{
public:
  double c, d;
  int g(int);
};

V našom príklade trieda Base obsahuje dva údajové členy a a b typu int a jednu členskú funkciu f typu void(). Trieda Derived všetky tieto zložky dedí a navyše pridáva svoje vlastné: ďalšie dva údajové členy c a d typu double a členskú funkciu g typu int(int). Nasledujúce výrazy sú teda správne:

Base b;
Derived d;
b.a = 10;
b.b = 14;
b.f();
d.a = 17;
d.b = 900;
d.c = 8.854;
d.d = 1e40;
d.f();
int i = d.g(83);

zatiaľ čo tieto sú chybné:

b.c = 24.9;
b.d = 0.001;
int j = b.g(0);

čo je zrejmé, pretože trieda Base členy c, d a g() neobsahuje.

Zmena prístupu

V deklarácii triedy Derived vidíme, že pred názvom jej základnej triedy Base je uvedený špecifikátor public, ktorý aj v tomto prípade determinuje prístupové práva, no tentoraz pre zdedené členy. Vo všeobecnosti pred názvom každej základnej triedy môžeme uviesť jeden zo špecifikátorov private, protected, public. Ako sa na ich základe mení prístup k dedeným členom, vidíme z tabuľky č. 1.

Tab. 1  Zmena prístupu k zdedeným členom

Deklarovaný prístup k členu základnej triedy

 

    Špecifikátor prístupu k základnej triede

 

    public                  protected            private

public

public

protected

private

protected

protected

protected

private

private

V ľavom stĺpci sú uvedené všetky možnosti prístupu k údajovým členom základnej triedy, v hornom riadku sú takisto uvedené všetky špecifikátory, ktoré môžeme použiť pri deklarácii odvodenej triedy (za dvojbodkou). V jednotlivých políčkach tabuľky potom nájdeme výsledný typ prístupu. Je zrejmé, že ak je trieda verejnou (public) základnou triedou, zachovajú si jej verejné a chránené členy v odvodenej triede svoje prístupové práva bez zmeny. Zdedené členy chránenej (protected) základnej triedy budú v odvodenej triede chránené a podobne zdedené členy privátnej (private) základnej triedy budú v odvodenej triede privátne. Pre privátne členy základnej triedy zmena prístupu nemá zmysel, pretože tieto členy v odvodenej triede nie sú prístupné.

Pokiaľ špecifikátory prístupu vynecháme, použijú sa implicitné hodnoty – ak je odvodená trieda štruktúrou (struct), doplní sa public, ak je triedou (class), doplní sa private.

V prípade, že nám nevyhovuje spôsob zmeny prístupových práv k zdedeným členom, môžeme ho v deklarácii odvodenej triedy upraviť uvedením kvalifikovaného mena príslušného člena v sekcii s vhodným špecifikátorom:

class B
{
  // ...
public:
  int a, b;
};
 
class D : private B
{
  // ...
public:
  B::a;
  int c;
};

Trieda D dedí od triedy B člen a, ktorý je v triede B verejný. Vďaka tomu, že B je privátnou základnou triedou D, bol by za normálnych okolností člen a v triede D privátny. Uvedením jeho kvalifikovaného mena B::a (bez špecifikátora typu!) v sekcii za špecifikátorom public mu „vraciame“ jeho verejnosť aj v triede D.

Úprava prístupu týmto spôsobom nie je povolená v prípade, že by sme chceli obmedziť prístup k údajovému členu prístupnému v základnej triede, a ani v prípade, že by sme chceli povoliť prístupu k členu neprístupnému v základnej triede:

class B
{
private:
  int a;
public:
  int b;
};
 
class D : private B
{
protected:
  B::b;   // chyba
public:
  B::a;   // chyba
};

V príklade chceme privátny člen a triedy B urobiť verejným v triede D, okrem toho chceme verejný člen b triedy B urobiť chráneným v triede D. Ani jedno, ani druhé nám, samozrejme, prekladač nedovolí.

Prístup k chráneným členom

Vráťme sa ešte k spôsobu prístupu ku chráneným členom. Vieme, že k nim majú prístup okrem členských a spriatelených funkcií základnej triedy aj členské a spriatelené funkcie všetkých odvodených tried. Ale pozor! Tie môžu k nestatickým chráneným členom základnej triedy pristupovať len prostredníctvom objektu odvodenej triedy, resp. ukazovateľa či referencie naň (statické sú, samozrejme, prístupné pomocou kvalifikovaného mena):

class B
{
protected:
  int a;
};
 
class D
{
  void f(B&, D&);
  friend void g(B*, D*);
};
 
void D::f(B& b, D& d)
{
  b.a = 1;       // chyba
  d.a = 2;       // OK
  ((B&)d).a = 3; // chyba
}
 
void g(B* pb, D* pd)
{
  pb->a = 4;       // chyba
  pd->a = 5;       // OK
  ((B*)pd)->a = 6; // chyba
}
 
void h(B& b, D& d)
{
  b.a = 7;    // chyba
  d.a = 8;    // chyba
}

Členská funkcia D::f(), ako aj funkcia g(), ktorá je „priateľom“ triedy D, má prístup k zdedenému chránenému členu a triedy B, ale len prostredníctvom referencie, resp. ukazovateľa na objekt triedy D. Pri prístupe cez objekt typu B je člen a, samozrejme, skrytý. Všimnite si aj to, že pri pretypovaní argumentov d, resp. pd na referenciu, resp. ukazovateľ na triedu B (čo je dovolené – vysvetlíme o chvíľu) strácame k členu a prístup, hoci očividne pracujeme stále s objektom triedy D. V príklade je ešte pre ilustráciu nečlenská a nespriatelená funkcia h(), ktorej nie je člen a prístupný nijakým spôsobom.

Prekrývanie členov

C++ dovoľuje v deklarácii odvodenej triedy prekryť ľubovoľný člen základnej triedy. Nemusí ísť v tomto prípade o priameho predka, takisto je možné prekryť ľubovoľný člen hociktorej triedy, ktorá je v hierarchii „vyššie“. Prekryté členy sú v odvodenej triede dostupné pod svojím kvalifikovaným menom:

class B
{
public:
  int a, b;
};
 
class D : public B
{
public:
  int b, c;
};
 
void f()
{
  D d;
  d.a = 10;
  d.B::b = 20;
  d.b = 30;
  d.c = 40;
}

Trieda D v našom príklade má štyri dátové členy: a a b zdedené od triedy B a vlastné, b a c. Vlastný člen b triedy D prekrýva zdedený člen b triedy B, ku ktorému máme prístup len pomocou jeho kvalifikovaného mena B::b. Použité meno triedy v kvalifikovanom mene člena však neznamená nevyhnutne triedu, v ktorej tento člen musí byť definovaný, ale skôr triedu, v ktorej sa začne „hľadať“ požadovaný člen smerom nahor v hierarchii dedičnosti:

class A { public: int x; };
class B : public A {};
class C : public B { public: int x; };
 
C c;
c.x = 10;
c.B::x = 20;
c.A::x = 30;

Posledné dva riadky priraďujú hodnotu tomu istému členu x zdedenému z triedy A, pretože v triede B nie je nijaký člen s menom x definovaný.

Ekvivalencia tried

Dôležitou vlastnosťou C++ je, že ukazovateľ, resp. referencia na odvodenú triedu môže byť vždy konvertovaný(á) na ukazovateľ, resp. referenciu na hociktorú prístupnú základnú triedu. Je to nanajvýš logické, pretože každý objekt triedy D odvodenej z triedy B je súčasne platným objektom triedy B – obsahuje všetky jej členy a členské funkcie. Mohli by ste namietnuť: veď privátne členy sa nededia. Áno, to je pravda, ale len v tom zmysle, že k nim odvodená trieda nemá prístup. V skutočnosti sa v inštancii odvodenej triedy nachádza kompletná inštancia základnej triedy, o čom sa môžete presvedčiť v nasledujúcom príklade:

class B
{
private:
    int a;
public:
    int b;
    int f() { return a + b; }
};
 
class D : public B {};
 
void g(B& b)
{
    int x = b.f();
}
 
void main()
{
    D d;
    g(d);
}

Funkcia g() prijíma ako argument referenciu na objekt triedy B, nad ktorým zavolá jeho členskú funkciu f(). Funkcia f() vracia súčet jedného privátneho a jedného verejného člena triedy B. My však funkcii g() odovzdáme ako argument referenciu na objekt triedy D. To, samozrejme, podľa uvedeného pravidla môžeme, referencia sa automaticky konvertuje. Lenže nad týmto objektom sa takisto vyvolá funkcia f(), ktorá sa bude snažiť pristupovať k privátnemu členu a, mysliac si, že pracuje s objektom triedy B. Keďže uvedený príklad funguje, je zrejmé, že inštancia triedy D musí tento člen obsahovať.

Inicializácia odvodených tried

Vieme už, že inicializáciu inštancií objektových typov majú na starosti špeciálne členské funkcie, nazývané konštruktory. Povedali sme si, za akých okolností prekladač môže automaticky vygenerovať vlastné verzie konštruktorov, a vieme aj to, že každá trieda môže mať niekoľko konštruktorov. Zatiaľ však nevieme, ako je to s inicializáciou odvodených tried – tie obsahujú inštancie svojich predkov, ktoré treba takisto nejakým spôsobom inicializovať.

Začneme triedou bez konštruktorov. Pri vzniku novej inštancie takejto triedy sa vyvolá implicitný konštruktor, vygenerovaný prekladačom. Jeho jedinou úlohou je vyvolanie implicitných konštruktorov všetkých základných tried (priamych, lebo tie následne budú volať konštruktory svojich základných tried atď.). Pokiaľ niektorá základná trieda nemá implicitný konštruktor (a súčasne má definovaný aspoň jeden iný konštruktor), máme smolu, lebo v takom prípade sa preklad skončí chybou. Pokiaľ je trieda inicializovaná pomocou kopírovacieho konštruktora, vytvoreného prekladačom, ten skôr, než inicializuje vlastné dátové členy, zavolá kopírovacie konštruktory základných tried.

Ak trieda obsahuje jeden či viacero konštruktorov, máme možnosť explicitne ovplyvniť, aké konštruktory základných tried sa použijú a s akými parametrami budú volané. Princíp je jednoduchý – za deklarátor konštruktora, ale ešte pred krútenú zátvorku, ktorou sa začína jeho telo, uvedieme za dvojbodku čiarkami oddelený zoznam inicializátorov. Každý z týchto inicializátorov má tvar

meno-triedy ( zoznam-výrazov )

alebo

identifikátor ( zoznam-výrazov )

Syntax s menom triedy sa používa na inicializáciu zdedených členov. V zátvorke uvedený zoznam výrazov nie je nič iné ako zoznam parametrov, na základe ktorých sa vyberie a zavolá príslušný konštruktor základnej triedy. Meno triedy musí predstavovať niektorého z priamych predkov odvodenej triedy. Druhý tvar – s identifikátorom – umožňuje takto zjednodušene inicializovať členy odvodenej triedy. Identifikátor predstavuje meno člena, v zátvorke uvedený zoznam jeho inicializačné hodnoty. Zoznam z toho dôvodu, že môžeme takto inicializovať aj vnorené objekty iných tried. Mimochodom, tento druhý tvar nám poskytuje jedinú možnosť, ako inicializovať konštantné údajové členy a členy typu referencie.

Ale dosť teórie, ukážme si radšej príklad:

class B1
{
  // ...
public:
  B1(int);
};
 
class B2
{
  // ...
public:
  B2(double);
};
 
class D : public B1, public B2
{
  const double c;
  B1 d;
public:
  D(int a, double b) : B1(a + 5),
                       B2(b * 3.45),
                       c(9.999),
                       d(a << 3)
  { /* ... */ }
};
 
D d(4, 11.23);

Trieda B1 má jediný konštruktor s jediným argumentom typu int, podobne trieda B2 (okrem typu double). Trieda D má dvoch predkov, B1 aj B2, jej konštruktor má dva argumenty typu int a double. Vyvolaním tohto konštruktora sa inicializuje zdedený podobjekt triedy B1 hodnotou a+5, podobjekt triedy B2 hodnotou b*3.45.  Okrem toho sa inicializujú dva vlastné údajové členy triedy D – člen c (ktorý je konštantný a ďalej nemenný) hodnotou 9.999 a člen d, ktorý je inštanciou triedy B1 (ale inou ako tá zdedená). Ten sa inicializuje hodnotou a<<3 typu int, preto sa vyvolá jeho konštruktor B1::B1(int).

Zoznam inicializátorov v konštruktore nie je povinný; ak chýba, volajú sa implicitné konštruktory predkov (ktoré musia existovať a byť prístupné) a údajové členy triedy sa inicializujú implicitne (teda vlastne nijako, jedine že by išlo o statické objekty, ktoré sa vynulujú).

Poradie, v ktorom sa uplatňujú jednotlivé inicializátory, je presne stanovené: najprv sa inicializujú zdedené podobjekty základných tried v poradí deklarácie (nezávisle od poradia inicializátorov v konštruktore), potom sa inicializujú vlastné údajové členy (samozrejme, len tie nestatické!), opäť v poradí ich deklarácie v triede a nakoniec sa vykoná samotné telo konštruktora. Inak povedané, najprv sa vyvolajú konštruktory predkov, potom sa nastaví východiskový stav objektu, ktorý sa nakoniec prípadne upraví podľa aplikačných špecifík, definovaných telom konštruktora. Jedinou výnimkou z toho pravidla sú virtuálne základné triedy, ale o tých si povieme až neskôr.

Pri zániku inštancií odvodených tried sa vyvoláva deštruktor, ktorý okrem iného automaticky zabezpečí zrušenie vnorených objektov a zdedených podobjektov. Poradie volania deštruktorov je presne opačné ako pri inicializácii: najprv sa vykoná samotné telo deštruktora, potom sa volajú deštruktory nestatických členských objektov (v opačnom poradí, ako boli deklarované) a nakoniec sa zavolajú deštruktory základných tried (takisto v opačnom poradí, ako boli deklarované).

Virtuálne funkcie

Ľubovoľná členská funkcia s výnimkou konštruktora a niektorých operátorových funkcií môže byť deklarovaná ako virtuálna pripojením špecifikátora virtual k jej deklarácii. Predstavme si, že máme triedu B, ktorá obsahuje virtuálnu funkciu f(), a triedu D, odvodenú od triedy B, ktorá obsahuje funkciu f() s rovnakou signatúrou (t. j. počtom a typmi argumentov a typom návratovej hodnoty). Potom je aj funkcia D::f() virtuálna (nezávisle od toho, či je deklarovaná ako virtual, alebo nie) a je zaručené, že každé volanie f() nad objektom, ktorý je inštanciou triedy D, vyvolá funkciu D::f() bez ohľadu na to, či k nemu pristupujeme pomocou ukazovateľa, resp. referencie na typ triedy B (t. j. jej základnej triedy), alebo nie. Majme teda tento kód:

class B
{
public:
  virtual void f() { printf("in B\n"); }
};
 
class D : public B
{
public:
  void f() { printf("in D\n"); }
};
 
void g1(B* pb) { pb->f(); }
void g2(D* pd) { pd->f(); }
 
B b;
D d;
g1(&b);  // vypíše 'in B'
g2(&d);  // vypíše 'in D'
g1(&d);  // vypíše 'in D' !

V príklade sme definovali dve pomocné funkcie g1() a g2(). Obe volajú členskú funkciu f() svojho argumentu, ibaže jedna z nich používa ukazovateľ na typ B a druhá na typ D. Ak si program vyskúšate, uvidíte, že hoci funkcia g1() pracuje so svojím argumentom ako s objektom triedy B, pokiaľ jej odovzdáme ukazovateľ na objekt triedy D, vyvolá jeho funkciu f(), a nie funkciu f() zdedenú z triedy B.

Virtuálna funkcia odvodenej triedy v podstate „maskuje“ rovnakú funkciu základnej triedy. Slovo „maskuje“ sa veľmi nehodí, ale to je problém s prekladom pôvodných anglických výrazov overload a override. Prvý z nich opisuje situáciu, keď máme viacero funkcií s rovnakým názvom, ale s rôznou signatúrou. V takom prípade sme hovorili, že sa funkcie prekrývajú. Druhý z nich naproti tomu vyjadruje vzťah medzi virtuálnymi funkciami (s rovnakým názvom aj signatúrou) z tried zúčastňujúcich sa vzťahu dedičnosti.

Funkcia, ktorá má v odvodenej triede rozdielny počet a/alebo typy argumentov ako virtuálna funkcia s rovnakým menom v základnej triede, nie je virtuálna. Líšiť sa môže len návratovým typom, aj to len vtedy, ak virtuálna funkcia základnej triedy B vracia typ B* alebo B& a virtuálna funkcia odvodenej triedy D vracia typ D* alebo D&. Toto pravidlo však niektoré staršie prekladače nepodporujú.

Prístup k virtuálnej funkcii sa riadi špecifikátorom uvedeným v tej triede, prostredníctvom ktorej (teda vlastne ukazovateľa či referencie, na ktorú) je volanie realizované:

class B
{
public:
  virtual void f();
};
 
class D : public B
{
private:
  void f();
};
 
D d;
B* pb = &d;
D* pd = &d;
pb->f();  // OK
pd->f();  // chyba

Volanie f() prostredníctvom ukazovateľa pb je v poriadku, funkcia f() je v triede B verejná (i keď v skutočnosti sa volá funkcia D::f()). Volanie f() prostredníctvom pd už je však chybné, pretože f() je v triede D deklarovaná ako privátna.

Mimoriadne vhodné je používanie virtuálnych deštruktorov, čo nám zabezpečí vyvolanie vždy toho správneho deštruktora pre každý rušený objekt. Musíme však dať pozor na skutočnosť, že v rámci tela deštruktora, ale aj konštruktora sa mechanizmus virtuálnych funkcií neuplatňuje a volajú sa vždy iba členské funkcie danej triedy. Je to koniec koncov logické – v okamihu volania konštruktora základnej triedy ešte nie je kompletne skonštruovaný objekt odvodenej triedy a volanie jeho virtuálnej funkcie by operovalo nad nekonzistentným stavom objektu. Podobne v okamihu volania deštruktora základnej triedy je objekt odvodenej triedy už zrušený a volanie virtuálnej funkcie by v takomto prípade operovalo nad de facto zrušeným objektom.

Zmyslom virtuálnych funkcií je umožniť rozlišovanie skutočného typu objektov za behu programu, na rozdiel od rozlišovania typu vo fáze prekladu na základe typu ľavého operandu operátorov . alebo ->, používaných pri prístupe k členským funkciám. Z toho vyplýva aj skutočnosť, že virtuálnymi nemôžu byť statické funkcie, pretože tie sa nevzťahujú na nijakú existujúcu inštanciu typu. Čo sa týka praktického použitia virtuálnych funkcií, predstavme si, že máme spomínanú triedu zivocich, ktorá obsahuje virtuálnu funkciu vydaj_zvuk() (ospravedlňujem sa za trochu krkolomné príklady :). Každý živočích vydáva iné zvuky, preto bude mať každá odvodená trieda túto funkciu implementovanú inak. Virtuálnosť funkcie nám však umožní vypočuť si hlasy všetkých živočíchov v našej súkromnej ZOO jednoduchým spôsobom:

zivocich* zoo[MAX_ZIV];
// ...
for (int i = 0; i < MAX_ZIV; i++)
  zoo[i]->vydaj_zvuk();

Ukazovatele na jednotlivých obyvateľov ZOO sú uložené v poli zoo[]. Rôzne ukazovatele budú zrejme ukazovať na živočíchy rôznych tried, ale vďaka dedičnosti a vďaka virtuálnosti funkcie vydaj_zvuk() dosiahneme, že každý živočích sa ozve tak, ako mu „zobák narástol“.

V jednej z predchádzajúcich častí sme si hovorili o niektorých základných princípoch OOP. Jedným z nich je polymorfizmus, ktorý je v C++ realizovaný práve pomocou virtuálnych funkcií. Jedinou podmienkou jeho úspešného používania je pristupovať k rôznym objektom pomocou ukazovateľa či referencie na nejakú spoločnú triedu, ktorej exportované funkcie musia byť virtuálne a implementované v každej odvodenej triede „po svojom“. Takto dosiahneme, že rôzne objekty budú na jednu a tú istú správu (t. j. volanie virtuálnej funkcie) reagovať rôznym spôsobom.

Abstraktné triedy

Pri návrhu objektovej hierarchie často dochádza k situácii, keď jednotlivé uzly hierarchického stromu predstavujú čisto všeobecné triedy a konkrétne triedy sa nachádzajú v listoch celého stromu. Z tohto dôvodu by bolo vhodné, keby sa dalo zakázať inštancionalizovať uzlové triedy. Prirovnaním k nášmu príkladu – nemá zmysel vytvoriť inštanciu triedy zivocich; ani v praxi neexistuje konkrétny tvor, nazývaný živočích. Zmysel majú len konkrétne triedy predstavujúce konkrétnych živočíchov (napr. vrana, pavuk a pod.).

C++ obsahuje na tento účel koncept tzv. abstraktných tried. Z takýchto tried nie je povolené vytvárať inštancie, nemôžu byť odovzdávané do a z funkcie hodnotou (hoci môžu byť odovzdávané ukazovateľom alebo referenciou) a nemôžu byť ani cieľom explicitného či implicitného pretypovania. Možno ich, samozrejme, použiť ako základné triedy. Abstraktná je každá trieda, ktorá obsahuje aspoň jednu čisto virtuálnu funkciu (pure virtual function). Takáto funkcia musí za svojím deklarátorom obsahovať ešte špecifikátor = 0. Príklad s našou triedou zivocich a jej virtuálnou funkciou vydaj_zvuk():

class zivocich
{
  // ...
public:
  virtual void vydaj_zvuk() = 0;
};

Funkciu vydaj_zvuk() zrejme bude ťažké definovať pre všeobecného živočícha. Keď ju však spravíme čisto virtuálnou, môžeme jej konkrétnu definíciu ponechať na odvodené triedy.

Čisto virtuálne funkcie, samozrejme, nemusia byť v abstraktnej triede definované. Na druhej strane to však ani nie je zakázané, takže sa teoreticky môžeme stretnúť aj so zápisom:

virtual void vydaj_zvuk() = 0
{
  // ...
}

Takto definovanú čisto virtuálnu funkciu môžeme volať len použitím jej plne kvalifikovaného mena zivocich::vydaj_zvuk().

Čisto virtuálne funkcie sú dedené odvodenou triedou takisto ako čisto virtuálne. Pokiaľ odvodená trieda nedoplní definíciu všetkých zdedených čisto virtuálnych funkcií, sama sa stáva abstraktnou.

Viacnásobná dedičnosť

Ako sme už spomínali, trieda môže byť odvodená od viac ako jednej základnej triedy. Pre túto tzv. viacnásobnú dedičnosť v princípe platia rovnaké pravidlá ako pre jednoduchú. Spôsob prístupu k zdedeným členom je možné špecifikovať pre každú základnú triedu zvlášť. Odvodená trieda nemôže priamo dediť od jednej základnej triedy viackrát, môže tak však urobiť nepriamo, cez medzitriedy:

class B { /* ... */ };
class M1 : public B { /* ... */ };
class M2 : public B { /* ... */ };
class D : public M1, public M2 { /* ... */ };

V tomto príklade bude trieda obsahovať dva zdedené podobjekty typu B. Ak takémuto dôsledku chceme zabrániť, musíme deklarovať príslušné základné triedy pri dedení ako virtuálne (pozor! nemýliť si s virtuálnymi funkciami), pridaním špecifikátora virtual:

class V { /* ... */ };
class M1 : virtual public B { /* ... */ };
class M2 : virtual public B { /* ... */ };
class D : public M1, public M2 { /* ... */ };

Tentoraz bude trieda D obsahovať iba jedinú kópiu objektu triedy B.

Trieda môže mať virtuálnych aj nevirtuálnych predkov súčasne (a to dokonca aj rovnakého typu):

class A { /* ... */ };
class B : virtual public A { /* ... */ };
class C : virtual public A { /* ... */ };
class D : public A { /* ... */ };
class E : public B, public C, public D { /* ... */ };

Trieda E obsahuje jednu nevirtuálnu kópiu objektu triedy A, zdedenú cez triedu D, a jednu virtuálnu, zdedenú cez triedy B a C.

Pri používaní viacnásobnej dedičnosti môžeme naraziť na problém existencie viac ako jedného člena s rovnakým menom, ale z rôznych základných tried. V takom prípade jednotlivé členy musíme rozlišovať pomocou ich kvalifikovaných mien:

class B1 { public: int a; };
class B2 { public: int a; };
class D : public B1, public B2 { public: void f(); }
void D::f()
{
  a = 1;      // chyba: B1::a alebo B2::a ?
  B1::a = 2;  // OK
  B2::a = 3;  // OK
}

V prípade použitia virtuálnych základných tried môže byť jeden údajový člen dosiahnuteľný cez viacero tried (viacerými cestami v tzv. orientovanom acyklickom grafe, predstavujúcom hierarchiu tried), nejde tu však o dvojznačnosť ako v predchádzajúcom prípade, pretože ide o jeden a ten istý člen:

class V { public: int a; };
class B1 : virtual public V {};
class B2 : virtual public V {};
class D : public B1, public B2 { public: void f(); }
 
void D::f()
{
  a = 1;      // OK: B1::a = B2::a = V::a
}

Virtuálne základné triedy prinášajú so sebou ešte jeden problém: ich použitie môže viesť k tomu, že jedno meno môže byť v grafe tried nájdené viacerými spôsobmi (viacerými cestami). K dvojznačnosti nedôjde vtedy, keď jedno z nájdených mien dominuje nad ostatnými. Meno B::f dominuje nad menom A::f, ak A je predkom B. V takom prípade sa „nájde“ meno B::f a meno A::f sa ignoruje.

Dosiaľ opisovaná možná dvojznačnosť sa týka aj pretypovania ukazovateľa, resp. referencie na odvodený objekt na ukazovateľ, resp. referenciu na základný objekt:

class V {};
class N {};
class M1 : public N, virtual public V {};
class M2 : public N, virtual public V {};
class D : public M1, public M2 {};
 
D d;
M1* pm1 = &d;  // OK
M2* pm2 = &d;  // OK
N* pn = &d;    // chyba!
V* pv = &d;    // OK

Pri pretypovaní ukazovateľa na objekt d na typ N, čo je inak povolené, keďže N je predkom D, dochádza k dvojznačnosti, pretože nie je jasné, či má pretypovaný ukazovateľ ukazovať na objekt N zdedený cez triedu M1 alebo cez triedu M2.

Virtuálne triedy trochu menia poradie volania konštruktorov, resp. deštruktorov pri inicializácii objektov odvodených tried, pretože ich konštruktory sú volané pred konštruktormi nevirtuálnych predkov v poradí najlepšie charakterizovanom slovami „depth-first left-to-right traversal“ spomínaného orientovaného acyklického grafu (DAG). Myslím, že by bolo zbytočné vysvetľovať, čo znamená depth-first prechod grafu, nemáme tu na to priestor ani čas. V prípade, že by to niekoho vážne zaujímalo, isto si dokáže nájsť príslušnú literatúru. Deštruktory sú, samozrejme, volané v presne opačnom poradí.

Nadnes stačí

Prebrali sme toho veru až-až, máte o čom premýšľať celý zvyšok leta. V budúcom pokračovaní budeme hovoriť o predefinovaní štandardných operátorov pre objektové typy údajov.

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á