Jak jsem slíbil minule, budeme se v dalším pokračování věnovat konstruktorům a destruktorům odvozených tříd. Probereme vícenásobnou a opakovanou dědičnost a pak se seznámíme s tzv. virtuálními metodami a abstraktními třídami.
V minulých dílech jsme se dozvěděli, že odvozená třída obsahuje všechny prvky, ať již datové nebo metody, základní třídy. Toto je pravda, až na pár speciálních prvků, kterými jsou právě konstruktor, destruktor a ještě operátor přiřazení, operator=().
Je zřejmé, že zdědění konstruktoru a destruktoru základní třídy by nemělo velký smysl, neboť nově odvozená třída má jistě nějaké nové prvky, o kterých základní třída nemůže mít tušení. Aby nová třída mohla zinicializovat své proměnné, obsahuje opět svůj vlastní konstruktor a jeho jméno je opět stejné jako jméno třídy. Protože však nová třída obsahuje i prvky třídy základní, je nutné je zinicializovat. Ukažme si krátký příklad a pak si řekneme, co se při konstrukci vlastně děje:
class A {
private:
int a;
public:
A() { a = 0; }
};
class B : public A {
private:
int b;
public:
B() { b = 0; }
};
Po vytvoření proměnné typu B dojde k zavolání konstruktoru této třídy. Nejprve je však zavolán konstruktor třídy A, který se volá před inicializací datových složek odvozené třídy. Některé inicializace prvků třídy B by totiž mohly být závislé na prvcích z A. Vidíme, že v tomto případě došlo k zavolání jediného definovaného konstruktoru. Pokud třída má definován explicitní konstruktor a implicitní definován není, musíme zařídit jeho zavolání v inicializační části konstruktoru třídy B. Příklad s explicitním voláním konstruktoru:
class A {
private:
int a;
public:
A(int _a) : a(_a) { ; }
};
class B : public A {
private:
int b;
public:
B(int _b, int _a) : b(_b), A(_a) { ; }
};
Pokud tomu tak nebude, překladač ohlásí chybu při překladu. V tomto příkladu si také můžete všimnout, že konstruktor odvozené třídy většinou obsahuje i parametry, které se pak předají konstruktoru základní třídy.
Stejná pravidla platí i pro destruktor, ale s tím rozdílem, že nyní se volání destruktoru základní třídy provede až jako poslední příkaz destruktoru třídy odvozené. Pro toto volání nemusíme nic napsat, protože destruktor má každá třída pouze jeden. K volání konstruktorů se vrátíme ještě jednou, až se dovíme co je to vícenásobná dědičnost.
Jazyk C++ nám umožňuje uvést v seznamu rodičů více tříd. Pro tyto třídy, tedy i v případě jednoduché dědičnosti, platí, že musí být plně deklarovány. Tím se zabrání např. specifikování jména právě deklarované třídy jako předka. Ukážeme si příklad na vícenásobnou dědičnost:
class A {
protected:
int a;
public:
A() { a = 0; }
};
class B {
protected:
int b;
public:
B() { b = 0; }
};
class C : public A,
private B {
private:
int c;
public:
C() { c = 0; }
};
Vidíme, že pro různé třídy můžeme použít různé specifikátory přístupu. V tomto případě budou ve třídě C dostupné následující proměnné: c s přístupovým právem private, b také s přístupovým právem private a konečně a s přístupovým právem protected. Vše vychází z tabulky uvedené v minulém pokračování kurzu. Představme si ale situaci, kdy se v rodičovských třídách budou vyskytovat prvky se shodným jménem:
class A {
protected:
int a;
public:
A() { a = 0; }
};
class B {
protected:
int a;
public:
B() { a = 0; }
};
class C : public A,
public B {
private:
int c;
public:
C() { c = 0; }
};
Nyní jsme odvozovali pomocí public v obou případech, což znamená, že přístupová práva v odvozeném objektu se nemění. Třída C nyní obsahuje proměnné: c (private), a z B (protected) a konečně a z A (také protected). Přidáme do třídy C metodu Vypis(), která bude vypisovat prvek a:
Vypis() { cout << a << endl; }
Po překladu ale dostaneme varování, protože překladač neví, které a vlastně chceme. Tento problém se nazývá konfliktem jmen. Řešení je následující: pokud budeme chtít použít proměnnou a pocházející z třídy A, musíme použít čtyřtečku (::) a před ní uvést identifikátor třídy z které prvek pochází (např. A::a, B::a). Stejná pravidla platí i pro metody. Pro metody je však třeba ještě zmínit následující příklad:
class A {
protected:
int a;
public:
A() { a = 0; }
void fce() { ; }
};
class B : public A {
protected:
int b;
public:
B() { b = 0; }
int fce(int _b) { return b; }
};
void main(void)
{
B b;
b.fce();
}
V tomto případě překladač opět ohlásí chybu, protože nevidí bezparametrickou metodu A::fce(). Pro zavolání správné metody musíme opět uvést b.A::fce().
Ještě si povíme, v jakém pořadí se zavolají konstruktory rodičovských tříd. Platí následující jednoduché pravidlo: konstruktory se zavolají v pořadí, ve kterém jsou uvedeny v seznamu předků v deklaraci odvozené třídy. Je vhodné zmínit, že předkem (platí samozřejmě i pro jednoduchou dědičnost) může být opět odvozená třída, potom dojde vlastně k rekurzivnímu volání konstruktorů.
Nejprve si ukažme příklad:
class A {
protected:
int a;
public:
A() { a = 0; }
};
class B : public A {
protected:
int b;
public:
B() { b = 0; }
};
class C : public A {
private:
int c;
public:
C() { c = 0; }
};
class D : public B, public C {
private:
int d;
public:
D() { d = 0; }
};
Třídy B a C jsou potomky třídy A. Třída D pak dědí vlastnosti tříd B a C a tím tedy i všechny jejich prvky. Znamená to, že třída D obsahuje jak třídu A zděděnou po B , tak třídu A zděděnou po C. Ačkoliv tedy nemůžeme v seznamu předků uvést stejnou třídu dvakrát, tak v tomto případě je přesto A dvakrát zděděna. To, že třída C obsahuje dvě proměnné a si můžeme vyzkoušet přidáním následující metody, kterou přidáme do třídy D:
class D : public B, public C {
private:
int d;
public:
D() { d = 0; }
void Test()
{
// a = 0; // Nelze
B::a = 1;
C::a = 5;
cout << "B::a = " << B::a << endl;
cout << "C::a = " << C::a << endl;
}
};
Zdrojový kód naleznete v sekci Downloads (projekt Dedic1).
To se někdy sice může hodit, ale ve většině případů je to na obtíž. Kdybychom si řekli, že budeme všude používat jen jednu z proměnných, např. tu z B zápisem B::b, a druhou necháme být, pak bychom jen zbytečně plýtvali pamětí. Ještě si ukážeme následující třídu:
class E : public A, public B, public C {
private:
int e;
public:
E() { e = 0; }
};
Pokud budeme mít takto deklarovanou třídu, překladač nám ohlásí následující chybu: třída A není přístupná, protože je už základní třídou B a C. Důvodem je, že k proměnné a bychom mohli přistupovat pouze zápisem A::a, ale to by může znamenat i proměnnou a v B nebo C (A je rodičem B i C ). Abychom vícenásobnému zdědění společného předka předešli, existuje klíčové slovo virtual, které lze uvést mezi specifikátory přístupu, přičemž nezáleží jestli uvedeme například public virtual nebo virtual public. Pokusíme se tedy přidat do seznamu prvků třídy E klíčové slovo virtual před třídu A. Při překladu dostaneme ale stále stejnou chybu, je totiž nutné uvést virtual ještě do seznamu předků tříd B a C. Pro úplnost ještě upravená verze:
class A {
protected:
int a;
public:
A() { a = 0; }
};
class B : public virtual A {
protected:
int b;
public:
B() { b = 0; }
};
class C : public virtual A {
private:
int c;
public:
C() { c = 0; }
};
class D : public B, public C {
private:
int d;
public:
D() { d = 0; }
};
class E : public virtual A, public B, public C {
private:
int e;
public:
E() { e = 0; }
void Test()
{
B::a = 1;
C::a = 5;
a = 0; // Pokud
dedime virtualne, pak je to povoleno
cout << "a = " << a << endl;
cout << "B::a = " << B::a << endl;
cout << "C::a = " << C::a << endl;
}
};
Zdrojový kód naleznete v sekci Downloads (projekt Dedic2).
Na výstupu programu vidíme, že hodnoty všech tří proměnných jsou stejné a rovné hodnotě posledního přiřazení. Je to tedy jedna "pravá" proměnná a ostatní zápisy jsou pak referencemi na tuto proměnnou, čímž se ušetří místo v paměti.
Může také nastat situace, kdy je třída děděna několikrát virtuálně a několikrát nevirtuálně. V tom případě bude v odvozené třídě jednou za všechna virtuální dědění a jednou za každé dědění nevirtuální.
Konstruktory se volají nejprve pro třídy uvedené s klíčovým slovem virtual a to opět v pořadí v jakém jsou uvedeny v seznamu předků, po nich teprve přijdou na řadu konstruktory nevirtuálně děděných tříd.
Polymorfizmus je vlastnost, která činí objektově orientované programování použitelným. Polymorfizmus znamená, že potomek nějaké třídy může kdekoliv zastoupit tuto třídu. Pokud se vrátíme k našemu příkladu se zoologickou zahradou, tak nám právě polymorfizmus zajistí, že můžeme místo instance třídy CZivocich napsat například instanci třídy CZirafa nebo CLev. Možná se ptáte na co to může být dobré. Následující ukázka je sestavena z více souborů a vysvětlí, k čemu může být polymorfizmus dobrý:
CZivocich.h:
#ifndef _ZIVOCICH_H_
#define _ZIVOCICH_H_
class CZivocich {
protected:
int m_dwMaxVek;
int m_dwVek;
public:
CZivocich(int _dwMaxVek, int _dwVek) : m_dwMaxVek(_dwMaxVek),
m_dwVek(_dwVek) { ; }
virtual HledejPotravu() { ; }
virtual void Zij() { ; }
};
#endif
CLev.h:
#ifndef _LEV_H_
#define _LEV_H_
class CLev : public CZivocich {
protected:
public:
CLev() :
CZivocich(15, 0) { ; }
virtual HledejPotravu();
virtual void Zij();
};
#endif
CLev.cpp:
#include <iostream.h>
#include "Zivocich.h"
#include "Lev.h"
CLev::HledejPotravu()
{
cout << "Lev : hledam nejake maso" << endl;
}
void CLev::Zij()
{
if(-1 == m_dwVek)
{
cout << "Lev : jsem uz po smrti" << endl;
}
else
{
if(m_dwVek < m_dwMaxVek)
{
HledejPotravu(); // Najime se
m_dwVek++; // Posuneme o den
cout << "Lev : mam prave narozeniny " << m_dwVek << endl;
}
else
{
cout << "Lev : umiram ve veku (" << m_dwVek << ")" << endl;
m_dwVek = -1;
}
}
}
CZirafa.h:
#ifndef _ZIRAFA_H_
#define _ZIRAFA_H_
class CZirafa : public CZivocich {
protected:
public:
CZirafa() : CZivocich(20,
0) { ; }
virtual HledejPotravu();
virtual void Zij();
};
#endif
CZirafa.cpp:
#include <iostream.h>
#include "Zivocich.h"
#include "Zirafa.h"
CZirafa::HledejPotravu()
{
cout << "Zirafa : hledam nejakou peknou zelen" << endl;
}
void CZirafa::Zij()
{
if(-1 == m_dwVek)
{
cout << "Zirafa : jsem uz po smrti"
<< endl;
}
else
{
if(m_dwVek < m_dwMaxVek)
{
HledejPotravu(); // Najime se
m_dwVek++; //
Posuneme o den
cout <<
"Zirafa : mam prave narozeniny " << m_dwVek << endl;
}
else
{
cout <<
"Zirafa : umiram ve veku (" << m_dwVek << ")" << endl;
m_dwVek = -1;
}
}
}
Virt1.cpp (hlavní soubor):
#include <iostream.h>
#include <string.h>
#include "Zivocich.h"
#include "Lev.h"
#include "Zirafa.h"
int main(int argc, char* argv[])
{
CZivocich *zvirata[2];
zvirata[0] = new CZirafa();
zvirata[1] = new CLev();
// Zij pet dni
for(int i = 0; i < 5; i++)
{
zvirata[0]->Zij();
zvirata[1]->Zij();
}
char c;
cin >> c;
return 0;
}
Zdrojový kód naleznete v sekci Downloads (projekt Virt1).
Zaměříme se zatím jen na hlavní program: nejprve vytvoříme pole složené z ukazatelů na rodičovskou třídu (CZivocich). Pak pomocí dynamické alokace vytvoříme dvě zvířata, reprezentovaná třídami CLev a CZirafa. Konstruktory jednotlivých tříd pak nastaví maximální věk zvířete. Pak už jen ve smyčce zavoláme pro jednotlivé prvky pole metodu Zij(), která ve svém těle ověřuje věk a pokud zvíře stále žije, zavolá metodu HledejPotravu(). Všimněte si, že ačkoliv voláme metodu Zij() pro instanci typu CZivocich, která je ale prázdná, tak překladač zajistí zavolání metody Zij() pro správnou třídu (tedy CLev nebo CZirafa). Tento kód by šel také přepsat s využitím jen jedné virtuální funkce, protože v tomto případě se obě funkce shodují, tedy kromě vypisovaného jména zvířete. Ale protože se život různých zvířat může lišit, rozhodl jsem se pro řešení v podobě dvou virtuálních funkcí. Kód naleznete v sekci Downloads (projekt Virt2).
Nyní je čas povědět si, jak tohle překladač zajistí. Nevirtuální metody, kterými jsme se zabývali doposud, využívaly tzv. časné vazby (angl. early binding). Při časné vazbě překladač určí typ instance již v okamžiku překladu. Zavolání metody je pak jen otázkou adresace. Naopak třídy obsahující alespoň jednu virtuální metodu (vysvětlíme za chvíli) využívají tzv. pozdní vazby (angl. late binding). Pozdní vazba se použije při volání metod pomocí ukazatelů, referencí nebo při volání metody z těla jiné metody (to je vidět v projektu Virt2). Spočívá v tom, že překladač musí pro každé zavolání virtuální metody přidat úsek kódu, který zajistí za běhu programu výpočet adresy, kde se nachází správná metoda. Zajištěno je to tím, že překladač připojí k datovým prvkům třídy tabulku adres virtuálních metod, která je pro programátora skrytá. Z této tabulky se pak za běhu programu určuje, která metoda se má opravdu zavolat. Znamená to tedy jisté operace navíc, které samozřejmě ovlivní výkon aplikace.
Virtuální metodou se stane libovolná metoda (kromě konstruktoru), před kterou přidáme klíčové slovo virtual. Specifikaci virtual neopakujeme u implementace metody. Virtuální metody se stejně jako normální metody dědí, tuto zděděnou metodu můžeme ve třídě změnit nebo ponechat stejnou jako v rodičovské třídě. Při změně lze pak původní verzi vyvolat opět pomocí čtyřtečky (::). Pokud je v základní třídě metoda označena jako virtual, pak je virtuální i ve všech odvozených třídách.V nové třídě není již nutné u této metody klíčové slovo znovu opakovat (já ho ale raději uvádím). Čistě virtuální metoda je metoda s následující deklarací:
virtual jmeno(parametry) = 0;
Třídu, která obsahuje alespoň jednu čistě virtuální metodu, nazýváme abstraktní třídou. Ve výše uvedeném příkladu bychom klidně mohli z virtuálních metod HledejPotravu() a Zij() udělat přidáním = 0 čistě virtuální metody. Tyto metody mohou sice mít tělo, ale obvykle se to nepoužívá. Abstraktní třídy obsahují metody, které jsou společné všem dalším odvozeným třídám, ale mohou obsahovat i datové prvky. Platí pro ně navíc pravidlo, že je lze použít jen jako předky jiných tříd. Znamená to, že nemůžeme vytvořit instanci abstraktní třídy, použít ji jako parametr předávaný hodnotou nebo výsledek funkce vracený hodnotou. To, že nelze vytvořit instanci abstraktní třídy neznamená, že nemůže mít konstruktor. Konstruktor je vyvolán v rámci vytváření odvozeného objektu a využívá se jako obvykle k inicializaci případných datových složek této třídy.
Konstruktor nemůže být virtuální, neboť je při vytvoření objektu vždy znám typ vytvářené instance. Naopak destruktor ve většině případů virtuální být musí. Ukažme si následující příklad:
#include <iostream.h>
class A {
protected:
int *lp_dwa;
public:
A()
{
cout << "Alokuji lp_dwa" << endl;
lp_dwa = new int[10];
}
~A()
{
cout << "Mazu pole lp_dwa" << endl;
if(lp_dwa) delete lp_dwa;
}
};
class B : public A {
protected:
int *lp_dwb;
public:
B()
{
cout << "Alokuji lp_dwb" << endl;
lp_dwb = new int[20];
}
~B()
{
cout << "Mazu pole lp_dwb" << endl;
if(lp_dwb) delete lp_dwb;
}
};
int main(int argc, char* argv[])
{
A *a;
a = new B;
delete a;
char c;
cin >> c;
return 0;
}
Zdrojový kód naleznete v sekci Downloads (projekt Virt3).
Po spuštění této verze vidíme, že proběhly sice dvě alokace, ale paměť byla uvolněna jen pro objekt A. Jak jsme uvedli, tak při konstrukci je znám typ ukazatele. Při destrukci objektu tomu tak není a proto se volá destruktor třídy na který je typována proměnná (tedy A). To má za následek neuvolnění dynamicky alokované paměti pro třídu B. Problém vyřeší následující úprava:
class A {
protected:
int *lp_dwa;
public:
A()
{
cout << "Alokuji lp_dwa" << endl;
lp_dwa = new int[10];
}
virtual ~A()
{
cout << "Mazu pole lp_dwa" << endl;
if(lp_dwa) delete lp_dwa;
}
};
Nyní vidíme, že byla uvolněna paměť pro obě dvě pole. Také je vidět v jakém pořadí jsou volány konstruktory a destruktory rodičovské a odvozené třídy. Pokud bychom znali typ instance, což většinou nemůžeme určit, pak bychom mohli samozřejmě ve volání delete přetypovat A na B:
int main(int argc, char* argv[])
{
A *a;
a = new B;
delete (B*)a;
char c;
cin >> c;
return 0;
}
Pro dnešek je to tedy vše a příště se ještě jednou a také naposledy vrátíme k přetěžování operátorů, tentokrát v souvislosti s polymorfizmem. Podíváme se na šablony a pravděpodobně i výjimky.
Příště nashledanou.