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

Konstruktor a destruktor

K objektům neodmyslitelně patří dvě speciální metody. Nazývají se konstruktor a destruktor a v tomto článku se seznámíme s jejich vytvářením a funkcí.

Motivace

Uvažujme třídu Vozidlo z předchozího článku.

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

// ...

Vozidlo trabant;

Tento kus kódu má zřejmý nedostatek. Když totiž vytvoříme instanci trabant, nemáme vůbec zajištěno, že hodnota pocetKol bude nastavená na čtyři, přestože to je pro Trabant pevná hodnota. Jinými slovy potřebujeme někam umístit prvotní inicializaci hodnot. V jazyce C jsme byli zvyklí nastavit hodnotu hned při deklaraci zápisem

int pocetKol = 4;

Takový zápis ale ve třídě použít nemůžeme (s příchodem C++11 již ano), jediné deklarace, které smějí být inicializovány pomocí přiřazení jsou statické konstantní členy (static const). K inicializaci datových členů slouží speciální metoda zvaná konstruktor. Naproti tomu se mnohdy hodí, když můžeme provést nějaké operace těsně předtím, než daná instance zanikne. Typickým příkladem může být spojový seznam, který je nutné před zničením instance vyprázdnit. K tomuto účelu slouží destruktor.

Konstruktor

Konstruktor je zvláštní metodou, neboť má předepsané jméno. Tím je vždy jméno třídy, pro kterou konstruktor vytváříme. Konstruktor též nemá návratový typ, a to ani void, a může mít libovolné množství parametrů. Konstruktor bývá v drtivé většině případů deklarován jako věřejná metoda. Přidejme tedy do naší modelové třídy konstruktor.

class Vozidlo
{
  private:
  int pocetKol;
  float maxRychlost;
  public:
  // ...
  // konstruktor nemá návratový typ a vždy se jmenuje jako třída
  Vozidlo(int k, float r);
};

Vozidlo::Vozidlo (int k, float r)
: pocetKol(k), maxRychlost(r)
{}

Tento konstruktor přebírá dva parametry, které poslouží k inicializaci privátních členů. Stejně tak jsme mohli vytvořit konstruktor bez parametrů a výchozí hodnoty si vymyslet napevno. Záleží pouze na nás, jakou cestu zvolíme. Mnohdy se hodí oba přístupy, tedy by bylo dobré mít konstruktory dva. Jazyk C++ nám to umožňuje díky přetěžování funkcí. Tělo konstruktoru může být vloženo do deklarace třídy, nebo může být umístěno mimo ve zdrojovém souboru. Takový příklad je i ve výpisu výše. Všimněme si dvojího použití Vozidlo. První je kvalifikace, druhé je jméno konstruktoru. Proměnné můžeme inicializovat dvěma způsoby — buď prostým přiřazováním hodnot v těle konstruktoru, nebo pomocí syntaxe, kterou můžeme vidět výše, kdy se mezi koncovou kulatou závorku a začátek těla konstruktoru napíše za dvojtečku čárkou oddělený seznam inicializací. Inicializace v seznamu lze napsat jen do konstruktoru a uvádějí se tak, že napíšeme prvně inicializovaný datový člen a do závorky k němu jeho hodnotu, kterou může být i parametr konstruktoru.

Konstruktor musí být ve třídě vždy přítomný. V našem prvním příkladu jsme žádný konstruktor neuvedli, přesto taková třída byla správně deklarovaná. V případě chybějícího konstruktoru si ho překladač vyrobí sám. Tento konstruktor nebude mít žádné parametry a jeho tělo bude prázdné.

Konstruktor je volán automaticky, když vytvoříme novou instanci. Pokud chceme uplatnit konstruktor s parametrem, zadáme hodnoty parametrů přímo při definici instance.

Vozidlo trabant(4, 75.5);

Voláme-li konstruktor bez parametrů, závorky v definici instance zapsat nesmíme. Je to z toho důvodu, že zápis se závorkami by překladač chybně vyhodnotil jako deklaraci funkce a kód by se pak nechoval správně.

// když existuje konstruktor bez parametrů, volá se instance takto
Vozidlo trabant;
// toto je chybně, nesmíme psát závorky
Vozidlo trabant2();

Konstruktor a přetypování

V jazyce C lze použít explicitní přetypování. Syntaxe je jednoduchá, do závorky umístíme nový typ a za závorku dáme ovlivněnou hodnotu. V jazyce C++, jako v nadmnožině jazyka C, můžeme toto samozřejmě provést také. Máme k dispozici ale ještě jiný formát zápisu představující spíše vytváření instance.

double a = 3.1415;
// formát známý z jazyka C
int i = (int)a;
// možné v C++
int j = int(a);

To není náhoda. V C++ je totiž možné použít tzv. konverzní konstruktor pro přetypování. Takový konstruktor musí mít právě jeden parametr a jinak se neliší. Kdykoliv se pak objeví přiřazení instance do proměnné jiného typu, pokusí se překladač vyhledat vhodný konstruktor, který by použil pro konverzi. Vývojáři C++ upravili i základní číselné typy, aby se chovali více jako objekty. Proto lze použít výše uvedený zápis. Kdybychom tedy navrhovali hypotetickou třídu Double, určitě bychom uvedli následující konstruktor.

class Double
{
  // ...
  public:
  Double (int a);
  Double (long a);
  // ...
};

// ...

// zde se zavolá konstruktor s parametrem int
Double a = 3;

Někdy se nám takové chování nicméně vůbec nemusí hodit a mohlo by dokonce být zdrojem chyb. Předpokládejme, že konstruktor přebírá jeden ukazatel. Pak je ovšem možné, že se provede implicitní konverze na literál nula, který bude chápán jako NULL. Abychom předešli možným problémům, můžeme některé jednoparametrické konstruktory označit klíčovým slovem explicit. Takové konstruktory se pak nesmějí účastnit implicitních konverzí. Přetypování z long bychom tedy ve třídě Double zakázali takto.

class Double
{
  // ...
  public:
  Double (int a);
  explicit Double (long a);
  // ...
};

Kopírovací konstruktor

Představme si, že máme funkci, která má za návratový typ naši třídu Vozidlo.

Vozidlo proved ()
{
  Vozidlo res;
  // práce s proměnnou res vynechána
  return res;
}

// ...
Vozidlo vysledek = proved();

Co se přesně stane na poslední řádce těla funkce proved()? Zde vracíme instanci res a předáváme ji hodnotou. To tedy znamená, že při zavolání funkce vyrobíme lokální instanci, tu pozměníme a při návratu vyrobíme její kopii (kopie by se nevytvářela, kdybychom jako návratový typ dali Vozidlo&, to by ovšem v tomto případě bylo chybně, neboť bychom předávali referenci na instanci, která ihned poté zanikne). Kopii přiřadíme do proměnné vysledek a původní instance zanikne, neboť je to lokální automaticky vytvořená proměnná.

Jak se taková kopie instance vytváří, když třídy obecně mohou obsahovat libovolná data? Slouží k tomu metoda zvaná kopírovací konstruktor. Ten má pouze jediný parametr a tím je konstantní reference na stejnou třídu. Pokud žádný kopírovací konstruktor nevytvoříme, doplní za nás překladač výchozí podobu konstruktoru, která vytváří mělkou kopii. To v případě naší třídy Vozidlo postačuje, pokud bychom ale ve třídě spravovali dynamicky alokovaná data, pak by vytvoření mělké kopie a následné zničení proměnné res vedlo k porušení paměti, neboť instance vysledek by pracovala nad uvolněnými daty. Abychom se takovému pochybení vyhnuli, musíme vytvořit vlastní kopírovací konstruktor, který zajistí vytvoření hluboké kopie. Zkusme cvičně vytvořit kopírovací konstruktor ve třídě Vozidlo.

class Vozidlo
{
  private:
  int pocetKol;
  // ...
  public:
  // ...
  Vozidlo (const Vozidlo &v);
};

Vozidlo::Vozidlo (const Vozidlo &v)
{
  // můžeme přistupovat k soukromým členům v
  // je to jiná instance, ale stejná třída
  pocetKol = v.pocetKol;
  // zde bychom zkopírovali naše dynamická data
}

Kdybychom nyní zavolali funkci proved(), zavolal by se po provedení return kopírovací konstruktor a do parametru v by byla předána instance res. V kopírovacím konstruktoru můžeme libovolně přistupovat ke všem členům druhé instance, neboť přístupová práva se aplikují na třídu jako na celek a nikoliv na jednotlivé instance.

Destruktor

Destruktor je na rozdíl od konstruktoru metoda, která je volána těsně předtím, než instance zanikne. Zánik instance je běžná věc, která se stává např. tehdy, když opustíme blok kódu, kde byla vytvořena staticky alokovaná instance (jako trabant v příkladu výše). V těle destruktoru se typicky nachází kód, který uvolňuje zabrané prostředky třídou. I jméno destruktoru je odvozeno od jména třídy, abychom se však odlišili od konstruktoru, musíme přidat znak tildy (~). Destruktor smí být ve třídě právě jeden, nemá opět žádný návratový typ a nemá žádné parametry. Stejně jako u konstruktoru i destruktor je v případě absence uživatelské deklarace vytvořen automaticky a je prakticky vždy deklarován jako veřejný. Přidejme pro ukázku do třídy Vozidlo destruktor.

class Vozidlo
{
  private:
  // ...
  public:
  // ...
  Vozidlo(int k, float r)
  :pocetKol(k), maxRychlost(r)
  {}
  
  ~Vozidlo()
  {}
};

V předchozím příkladu je vidět, že destruktor této třídy je nanejvýš zbytečný. Skutečně, destruktory se hojně používají u tříd pracujících s dynamicky alokovanou pamětí, neboť je to ideální místo pro úklid po alokacích. V takto jednoduché třídě, jakou je Vozidlo, ale není co uklízet (datové členy jsou staticky alokované, a proto budou zničeny automaticky stejně jako lokální proměnné v jazyce C).