Dedičnosť

Čo vieme o OOP

* trieda = štruktúra, ktorá obsahuje položky (stavové premenné) a metódy ... zapuzdrenie (enkapsulácia),
* objekty môžu byť statické alebo dynamické,
* konštruktory a deštruktory = metódy, ktoré sa automaticky zavolajú pri vytvorení alebo rušení objektu.


Jednoduché dedenie

Naučíme sa používať ďalšiu, veľmi dôležitú vlastnosť OOP - dedičnosť (inheritance) tried:

* dedičnosť umožňuje vytvorenie novej triedy z už existujúcej triedy,
* potom hovoríme, že sme odvodili novú objektovú triedu z nejakej základnej triedy,
* odvodená trieda (potomok, nasledovník) zdedí všetky položky aj metódy zo svojej základnej triedy (predka, rodiča),
* v odvodenej triede môže dodefinovať nové položky aj metódy,
* v odvodenej triede môže predefinovať správanie sa zdedených metód.

Ukážka:
class TA // základná trieda
{
public:
int X;
void Fnc();
};

class TB : public TA // trieda TB je odvodená od triedy TA
{
public:
int Y;
bool Test();
}

Trieda TB je odvodená od základnej triedy TA, a preto obsahuje všetky položky a metódy z triedy TA. Trieda TB má navyše aj jednu novú položku a metódu. Zhrňme si, čo trieda TB obsahuje:

* položka X ... je zdedená z triedy TA
* položka Y ... je nová položka triedy TB
* metóda Fnc() ... je zdedená z triedy TA
* metóda Test() ... je nová metóda TB.

Pri používaní objektu triedy TB môžeme:

TB b;
b.X=1;
b.Y=2;
b.Fnc();
b.Test();

Pozor, objekt triedy TA nepozná ani položku Y, ani funkciu Test():

TA a;
a.Y=2; ... chyba, položka neexistuje
a.Test(); ... chyba, takáto metóda neexistuje

Hlavné výhody mechanizmu dedenia tried:

* objektové triedy môžeme "vylepšovať" tým, že z nich odvodíme nové triedy - ak nejaký programátor pripraví kvalitnú objektovú triedu, môžeme z nej odvodiť vlastné triedy,
* pri programovaní zdedených tried nemusíme mať k dispozícii zdrojový kód základnej triedy - na odvodenie novej triedy stačí poznať iba položky a metódy základnej triedy,
* vďaka dedičnosti môžeme využívať aj ďalšiu veľmi užitočnú vlastnosť OOP - polymorfizmus (mnohotvárnosť - o nej však nebudeme hovoriť).


Projekt Akvárium

Dedičnosť ukážeme na príklade projektu Akvárium:

* v akváriu žije niekoľko druhov tvorov: rybka, hadíky a potrava pre rybku,
* všetky živočíchy sa v akváriu pohybujú (použijeme časovač) a odrážajú od jeho okrajov,
* ak nájde ryba vo svojom okolí potravu, rybka ju zožerie a trochu vyrastie,
* potravu do budeme akvária "sypať ťahaním myšou.

Pri návrhu základnej triedy si všímame iba spoločné (všeobecné) vlastnosti a správanie sa objektov. Živočíchy majú niekoľko spoločných znakov:

* polohu a smer pohybu
* pri pohybe sa odrážajú od okrajov akvária.

Definujeme základnú triedu TTvor:
class TTvor
{
public:
float X, Y, dX, dY;
void Nastav(float nX, float nY, float ndX, float ndY);
void Pohni();
};

void TTvor::Nastav(float nX, float nY, float ndX, float ndY)
{
X=nX;
Y=nY;
dX=ndX;
dY=ndY;
}

void TTvor::Pohni()
{
X=X+dX;
Y=Y+dY;
if (X<0 || X>400) dX=-dX;
if (Y<0 || Y>300) dY=-dY;
}


Hierarchia tried

Vzťahy medzi základnou triedou TTvor a odvodenými triedami vyjadruje diagram:


TTvor




X, Y
dX, dY
Nastav
Pohni





TPotrava THad TRyba
Kresli



Tm
Nastav
Pohni
Kresli



S
Nastav
Kresli
JeBlizko
Rast

Šípky v takomto diagrame sa kreslia od odvodenej triedy smerom k základnej lebo odvodená trieda "vidí" zdedené položky a metódy, ale naopak základná trieda "nevidí" do svojich potomkov - nemôže používať ich položky ani metódy. V reálnych projektoch často býva hierarchia tried oveľa komplikovanejšia a najmä, býva v nej viac "poschodí" (t.j. aj odvodené triedy môžu mať svojich potomkov).


Odvádzanie tried

Zo základnej triedy odvodíme tri nové triedy: TPotrava, THad a TRyba:
class TPotrava : public TTvor
{
public:
void Kresli();
};

void TPotrava::Kresli()
{
g->Pixels[X][Y]=clBlack;
}

* trieda TPotrava zdedila všetky položky a metódy triedy TTvor, t.j.: X, Y, dX, dY, Nastav() a Pohni(),
* trieda TPotrava má zadefinovanú novú metódu Kresli().

Trieda THad má novú položku Tm, ktorá slúži pri vykresľovaní hadíka ako vlniacej sa sínusoidy. Ďalej potrebujeme:

* zmeniť inicializačnú metódu Nastav ... v nej vynulujeme premennú Tm,
* zmeniť metódu Pohni ... zmení čas Tm,
* dodefinovať metódu Kresli ... nakreslí funkciu sínus.

class THad : public TTvor
{
private:
int Tm; // parameter pre kreslenie hadíka - sínusoidy
public:
void Nastav(float nX, float nY, float ndX, float ndY);
void Pohni();
void Kresli();
};

void THad::Nastav(float nX, float nY, float ndX, float ndY)
{
TTvor::Nastav(nX, nY, ndX, ndY);
Tm=0;
}

void THad::Pohni()
{
TTvor::Pohni();
Tm=Tm+dX;
}

void THad::Kresli()
{
g->Pen->Color=clRed;
g->MoveTo(X-10, Y+2*sin(Tm/8.0-10/2.0));
for (int i=-10; i<10; i++)
g->LineTo(X+i, Y+2*sin(Tm/8.0+i/2.0));
}

V metódach Nastav aj Posun využívame zdedené metódy zo základnej triedy TTvar. Aby kompilátor rozlíšil medzi volaním zdedenej metódy Nastav a rekurzívnym volaním metódy Nastav, uvádzame pri volaní zdedenej metódy aj názov základnej triedy: TTvor::Nastav(...).

Odvodená trieda TRyba bude mať:

* novú položku S, v ktorej sa uchováva informácia o veľkosti ryby,
* predefinovanú metódu Nastav - navyše umožní nastaviť aj veľkosť ryby,
* nové pomocné metódy: JeBlizko a Rast.

class TRyba : public TTvor
{
private:
int S;
public:
void Nastav(float nX, float nY, float ndX, float ndY, int nS);
void Kresli();
bool JeBlizko(TPotrava *P); // nachádza sa potrava v okolí ryby?
void Rast();
};

void TRyba::Nastav(float nX, float nY, float ndX, float ndY, int nS)
{
TTvor::Nastav(nX, nY, ndX, ndY);
S=nS;
}

void TRyba::Kresli()
{
float r=sqrt(S);
g->Pen->Color=clBlue;
g->Brush->Color=clBlue;
g->Ellipse(X-2*r, Y-r, X+2*r, Y+r);
}

bool TRyba::JeBlizko(TPotrava *P)
{
float dx, dy;
dx=P->X-X;
dy=P->Y-Y;
return dx*dx/(2*2)+dy*dy<S;
}

void TRyba::Rast()
{
S++;
}

Dokončíme projekt:

* do formulára vložíme grafickú plochu (Image1) veľkosti 400x300 pixelov,
* zadefinujeme globálnu premennú TCanvas *g, ktorá ukazuje grafickú plochu,
* do formulára vložíme časovač Timer1 a nastavíme mu Interval=50;
* budeme používať funkciu sin, preto použijeme matematickú knižnicu: #include <math.h>.

const MAX=1000; // maximálny počet potravy

TRyba r;
THad h[10];
TPotrava p[MAX];
int poc;

void __fastcall TForm1::FormCreate(TObject *Sender)
{
r.Nastav(1, 150, 1, 0, 5);
for (int i=0; i<10; i++)
h[i].Nastav(i*30, 250+i*4, 1+random(101)/100.0, 0);
poc=0;
}

void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
int i;
r.Pohni();
for (i=0; i<10; i++) h[i].Pohni();
i=0;
while (i<poc)
if (!r.JeBlizko(&p[i])) {
p[i].Pohni();
i++;
} else {
r.Rast();
p[i]=p[poc-1];
poc--;
}
g->Brush->Color=clAqua;
g->FillRect(Image1->ClientRect);
r.Kresli();
for (i=0; i<10; i++) h[i].Kresli();
for (i=0; i<poc; i++) p[i].Kresli();
}

void __fastcall TForm1::Image1MouseMove(TObject *Sender,
TShiftState Shift, int X, int Y)
{
if (
!Shift.Contains(ssLeft) ||
poc>=MAX ||
X<0 || X>400 || Y<0 || Y>300) return;
p[poc].Nastav(X, Y, random(201)/100.0-1, random(201)/100.0-1);
poc++;
}


Dedičnosť a konštruktory

Volanie zdedených konštruktorov a deštruktorov funguje tak, že:

* pri vytvorení objektu sa automaticky vykoná: najskôr konštruktor základnej triedy, a potom odvodenej triedy
* pri rušení objektu sa automaticky vykoná: najskôr deštruktor odvodenej triedy, a až potom deštruktor základnej triedy

Ukážka:
class TA
{
public:
int X;
TA(); // konštruktor
TA(int nX); // konštruktor s parametrom
~TA(); // deštruktor
};

class TB : public TA
{
public:
int Y;
TB(); // konštruktor
TB(int nX, int nY); // konštruktor s parametrami
~TB(); // deštruktor
}

TA::TA()
{
X=0;
}

TA::TA(int nX)
{
X=nX;
}

TA::~TA()
{
}

TB::TB()
{ // konštruktor TA() sa vykoná automaticky
X=0; // až potom sa vykoná telo konštruktora TB()
}

TB::TB(int nX, int nY)
: TA(nX) // najskôr sa má vykonať konštruktor z TA(...) s parametrom
{
Y=nY; // až potom sa vykoná telo konštruktora
}

TB::~TB()
{
// najskôr sa vykoná telo deštruktora ~TB()
} // až potom sa vykoná deštruktor ~TA()

Poznámka: Ak by konštruktor TB(...) s parametrami z vyzeral nasledovne:

TB::TB(int nX, int nY)
{ // nepovieme, aby sa vykonal konštruktor triedy TA(...) s parametrom
Y=nY;
}

* najskôr sa zavolá konštruktor TA() bez parametrov
* až potom sa vykoná telo konštruktora TB(...).