na obsah klávesové zkratky na hlavní stránku na menu

Základy dědičnosti

Dědičnost je jednou ze základních vlastností objektových jazyků. Jazyk C++ umožňuje využívat jednonásobnou i vícenásobnou dědičnost a tím zpřehlednit zdrojový kód velkých aplikací.

Základy dědičnosti

Dědičností se obecně rozumí svázání několika tříd vztahem předek–potomek. Takový vztah je zcela přirozený pro mnoho různých problémů a jeho využití vede ke kategorizaci objektů, čili k jejich přehlednější správě, a hlavně k implementaci překrývajících se vlastností objektů pouze na jediném místě. Základní myšlenkou dědičnosti je to, že potomek může zastoupit předka, tedy že potomek vždy obsahuje veškeré vlastnosti rodičovské třídy a pouze rozšiřuje její schopnosti specifickým způsobem. Syntaxe dědičnosti je v jazyce C++ velmi jednoduchá, a proto si rovnou ukážeme příklad, v němž ze třídy Vozidlo zmíněné v článku o třídách podědíme třídu NakladniVozidlo.

class Vozidlo
{
  private:
  int pocetKol;
  float maxRychlost;
  public:
  float getMaxRychlost();
  int getPocetKol();
  Vozidlo(int k, float r);
};

class NakladniVozidlo: public Vozidlo
{
  public:
  NakladniVozidlo(int k, float r);
};

// ...

int main (int argc, char **argv)
{
  Vozidlo voz(4, 156.5);
  NakladniVozidlo nak(8, 85.0);
  std::cout << voz.getPocetKol() << std::endl;
  std::cout << nak.getPocetKol() << std::endl;
  return 0;
}

Vztah dědičnosti je uveden za dvojtečkou u názvu nové třídy. Pokud mezi jménem a otevírací složenou závorkou není nic uvedeno, pak třída od žádné jiné třídy nedědí a je tím pádem na vrcholu dědické hierarchie. To je v našem případě třída Vozidlo. Všimněme si kódu ve funkci main(). Tam vytváříme instanci třídy Vozidlo a instanci třídy NakladniVozidlo. Poté voláme metodu getPocetKol(). Jak je ale možné, že překladač přeloží tento kód, když třída NakladniVozidlo nemá žádnou metodu getPocetKol()? Přestože tato metoda není explicitně uvedena v deklaraci třídy, třída tuto metodu zdědí od rodičovské třídy Vozidlo. Překladač tedy správně zavolá metodu rodičovské třídy, která vrátí obsah datového členu pocetKol.

Přenos oprávnění přístupu při dědění

Při dědění se určitým způsobem přenášejí oprávnění rodičovské třídy. Připomeňme, že při deklaraci třídy můžeme použít tří různých typů oprávnění — public, private a protected, což jsme si vysvětlili již v článku o třídách. Oprávnění protected, které jsme zatím nepoužívali, slouží k označení těch členů rodičovské třídy, které budou přístupné ve třídě potomka, ale mimo dědickou hierarchii budou nedostupné. Oprávnění přístupu se ale mohou vyskytnout ještě jidne, a to v seznamu předků třídy, kde ovlivňují způsob převodu práv na potomka. Jednotlivá klíčová slova mají následující význam.

public
Klíčové slovo public uvedené před jménem třídy v seznamu předků značí veřejné dědění. Jde o nejtypičtější způsob dědičnosti (jiný se prakticky nepoužívá), kdy potomek získá veškeré veřejné a chráněné členy rodičovské třídy se stejnými oprávněními, s jakými byly deklarovány v rodičovské třídě.
protected
Klíčové slovo protected uvedené před jménem třídy v seznamu předků značí chráněné dědění. Potomek získá veškeré veřejné a chráněné členy rodičovské třídy s novým oprávněním protected, tedy veškeré původně veřejné členy rodičovské třídy budou v potomkovi chráněné.
private
Klíčové slovo private uvedené před jménem třídy v seznamu předků značí soukromé dědění. Potomek získá veškeré veřejné a chráněné členy rodičovské třídy s novým oprávněním private, tedy veškeré původně veřejné a chráněné členy rodičovské třídy budou v potomkovi soukromé. Jde o nejrestriktivnější způsob přenosu přístupových práv.

Pokud neuvedeme žádné oprávnění, C++ si doplní public při dědění struktur a private při dědění tříd. Všimněme si, že soukromé složky třídy nejdou zdědit. Potomek nemá přímý přístup k takovým členům, může ale využít zděděné veřejné a chráněné metody pro přístup k soukromým datům. Tak je tomu i v příkladu výše, kdy třída NakladniVozidlo nemá přístup k datovému členu pocetKol, ale zděděná metoda getPocetKol() již ano.

Konstruktor a destruktor při dědění

Dosud jsme si nevysvětlili, jak vlastně C++ dědění provádí. Jelikož platí, že třída NakladniVozidlo musí obsahovat celou funkčnost třídy Vozidlo, aby ji mohla kdykoli zastoupit, musí C++ vnořit v paměti celou rodičovskou třídu do potomka. Jak vypadá v paměti instance třídy NakladniVozidlo, vidíme na obrázku níže.

Instance třídy Vozidlo a NakladniVozidlo v paměti RAM.

Instance třídy Vozidlo a NakladniVozidlo v paměti RAM.

Aby bylo možné používat třídu NakladniVozidlo, je potřeba nejprve zavolat její konstruktor. To provádíme ve funkci main(). Kontruktor ovšem nemá přístup k soukromým členů vnořené rodičovské třídy a jejich nepřímé použití bez inicializace by vedlo k chybám. Proto je nutné zavolat konstruktor předka. To se v C++ dělá v inicializační částí konstruktoru potomka.

NakladniVozidlo::NakladniVozidlo (int k, float r)
: Vozidlo(k, r)
{}

Pokud neuvedeme konstruktor předka v konstruktoru potomka, pokusí se C++ doplnit volání konstruktoru bez parametrů, a pokud ten neexistuje, dojde k chybě při překladu. Explicitní volání konstruktoru musí být provedeno v inicializační části konstruktoru a musí to být první inicializace za dvojtečkou. Není možné toto volání umístit až do těla konstruktoru, neboť to už by mohly být inicializovány některé datové členy potomka, které by mohly spoléhat na data z předka.

Takto zapsaná inicializace způsobí nejprve zavolání konstruktoru třídy Vozidlo a až poté konstruktoru třídy NakladniVozidlo. Destrukce instance by naopak měla probíhat opačným způsobem, tedy předek v dědické hierarchie umístěný nejvýše by měl být zničen jako poslední. Jelikož destruktor je v každé třídě pouze jeden, není nutné do destruktoru potomka vkládat žádný speciální kód. C++ za nás zavolá destruktory ve správném pořadí samo. Zkusme nyní do konstruktorů a destruktorů vložit vypisování zpráv, abychom viděli pořadí volání.

#include <iostream>

class Vozidlo
{
  private:
  int pocetKol;
  float maxRychlost;
  public:
  float getMaxRychlost()
  {
    return maxRychlost;
  }
  int getPocetKol()
  {
    return pocetKol;
  }
  Vozidlo(int k, float r)
  :pocetKol(k), maxRychlost(r)
  {
    std::cout << "volam konstruktor vozidla" << std::endl;
  }
  ~Vozidlo()
  {
    std::cout << "volam destruktor vozidla" << std::endl;
  }
};

class NakladniVozidlo: public Vozidlo
{
  public:
  NakladniVozidlo(int k, float r)
  :Vozidlo(k, r)
  {
    std::cout << "volam konstruktor nakladniho vozidla" << std::endl;
  }
  ~NakladniVozidlo()
  {
    std::cout << "volam destruktor nakladniho vozidla" << std::endl;
  }
};

int main (int argc, char **argv)
{
  NakladniVozidlo nak(8, 85.0);
  std::cout << nak.getPocetKol() << std::endl;
  return 0;
}

Po zavolání funkce main() obdržíme následující výpis, který potvrzuje správné pořadí volání konstruktorů a destruktorů.

volam konstruktor vozidla
volam konstruktor nakladniho vozidla
8
volam destruktor nakladniho vozidla
volam destruktor vozidla

Vícenásobná dědičnost

Vícenásobná dědičnost nalézá uplatnění při implementaci rozhraní v jazyce C++. Nejde o nic komplikovaného, třída v C++ může mít více předků, a to i ze stejné dědické hierarchie. Obecně tak hierarchie v jazyce C++ nejsou stromy, ale, pomineme-li orientaci, cyklické grafy. Vícero předků třídy deklarujeme jednoduše tak, že v seznamu předků vyjmenujeme všechny předky třídy a oddělíme je čárkou. Před každého předka je možné zapsat modifikátor oprávnění. V konstruktoru potomka je pak nutné uvést všechny konstruktory předků v pořadí, v jakém byly předci deklarováni v seznamu předků. Takto poděděná třída obsahuje poté veškerou funkčnost všech tříd, od kterých dědí.