11. fejezet - Öröklődés


A függvényeknél azt mondtam, hogy a programozás egyik alapelve az újrafelhasználhatóság, azaz amit egyszer már megírtunk azt ne kelljen többször. Az objektumorientált paradigma erre adott egy új megoldást az osztályok által, de még tovább segíti az öröklődés (inheritance) által.



Kezdésként az előző fejezetben írt alma osztály mellé írjunk egy banánt és egy narancsot!

CODE
class Banan { private: int szin; int meret; }; class Narancs { private: int szin; int meret; int girizdek; };

Rögtön észre lehet venni, hogy az almához hasonlóan a banánnak és a narancsnak is van színe és mérete, sőt ha tovább gondolkozunk akkor lehet majd akár Hamoz() metódusuk is, ami nem sokban különbözik az egyes gyümölcsfajtákra (például vanheja = false;). Ez most a gyümölcsökre egy átlátható dolog még, de ha hosszabb metódusokat/osztályokat írsz, amikben sok ugyanolyan rész van (redundancia), akkor egy apró változtatás esetén az összes többi helyen is el kell végezni a módosítást. És ha elfelejtetted valahol? Na akkor van baj...

Egy olyan programozó aki az objektumorientált paradigma szerint gondolkozik a következőt mondja: az alma, a banán és a narancs a gyümölcsök családjába tartozik, és egy gyümölcsnek van színe, mérete és lehet hámozni. A programozó ezzel a gondolatmenettel általánosította (generalization) a három osztályt egy közös Gyumolcs ősosztályba.

Az általánosítás megfordítása a specializáció: például az alma egy speciális esete a gyümölcsnek. A specializált osztályokat más néven származtatott osztályoknak hívják, a módszert pedig származtatásnak vagy öröklődésnek. Nézzük meg hogy néz ez ki C++ -ban:

CODE
class Gyumolcs { protected: int szin; int meret; };
CODE
class Alma : public Gyumolcs { }; class Banan : public Gyumolcs { }; class Narancs : public Gyumolcs { private: int girizdek; };

Az ősosztályt tehát az osztály neve mögött egy : után adjuk meg. Az öröklésnek is lehet láthatósági szintje, erről majd később. Ha nem adtál meg szintet, akkor osztályok esetében private az alapértelmezett, struktúrák esetében public.

Nagyon fontos: a származtatott osztályok az ősosztály minden tagváltozóját és metódusát öröklik (a privátokat is, de azok nem lesznek láthatóak).

A kódban megjelent egy új láthatósági szint, a protected. Ez azt jelenti, hogy az ilyen tagváltozók elérhetők a származtatott osztályokban is, de kívülről továbbra sem.

Származtatott osztályok esetén az ősosztály default konstruktora magától meghívódik a konstruktor lefutása előtt (ha nem létezik az fordítási hiba). Az ősosztály destruktora pedig a destruktor lefutása után hívódik meg. Nyilván ez csak akkor igaz, ha egyáltalán meg lett hívva a destruktor!


Képzeljük el, hogy van a gyümölcsnek egy void Nojelmeg(int nap); metódusa, ami a gyümi méretét változtatja attól függően, hogy az év hanyadik napján vagyunk. Nyilván minden gyüminek máskor van szezonja, tehát valahogy meg kéne változtatni minden származtatott osztályban ezt a metódust. A következő kódban felüldefiniáltam a Nojelmeg metódust az alma osztályban:

CODE
// gyumolcs.h class Gyumolcs { protected: int szin; int meret; public: Gyumolcs(); // vigyázat! ez a kód szándékosan "rossz"! void Nojelmeg(int nap); }; // gyumolcs.cpp Gyumolcs::Gyumolcs() : szin(0), meret(0) { } void Gyumolcs::Nojelmeg(int nap) { std::cout << "egy altalanos gyumi nem no\n"; }
CODE
// alma.h class Alma : public Gyumolcs { public: Alma(); void Nojelmeg(int nap); }; // alma.cpp Alma::Alma() : Gyumolcs() { szin = 1; } void Alma::Nojelmeg(int nap) { if( nap > 70 && nap < 150 ) { meret = std::min(meret + 1, 10); std::cout << "az alma merete: " << meret << "\n"; } else { std::cout << "az almanak most nincs szezonja\n"; meret = 0; } }

Ha most deklarálok egy almát, akkor természetesen az alma Nojelmeg metódusa fog meghívódni. Ez a fajta felüldefiniálási módszer viszont ritka és kerülendő, általában hibát jelent. Ennek okát a következő szekció magyarázza el.


A következő nagyon fontos alapelv a többalakúság. Ez azt jelenti, hogy egy származtatott objektum megjelenhet úgy is, mint egy általános. Induljunk ki a fenti "hibás" kódból:

CODE
Alma a; Gyumolcs* g = &a; a.Nojelmeg(80); g->Nojelmeg(82); // output: // az alma merete: 1 // egy altalanos gyumi nem no

Tehát egy származtatott típusú objektumra mutathat ősosztály típusú pointer/referencia. De mit veszünk észre? Azt, hogy az ősosztály típusú pointerre meghívott metódus az ősosztály metódusát hívta meg!. Az esetek többségében viszont nem ezt szeretnénk, hanem azt, hogy ekkor is a származtatott osztály metódusa hívódjon meg. Ez utóbbit hívják dinamikus kötésnek és az OOP leggyakrabban használt fogalma.

C++ -ban van egy érdekes megkötés ami elsőre nem is nyilvánvaló: egy osztály csak akkor polimorfikus, ha van legalább egy virtuális metódusa (attól még a fenti kód működik). Ha egy metódust a virtual kulcsszóval deklarálsz, akkor minden hívás arra a metódusra a dinamikus típus metódusát fogja meghívni. Azaz a metódus dinamikusan kötött lesz.

CODE
class Gyumolcs { // ... public: // ... virtual ~Gyumolcs() {} virtual void Nojelmeg(int nap); }; // ... int main() { Alma a; Gyumolcs* g = &a; g->Nojelmeg(82); return 0; } // output: // az alma merete: 1

Ha egy metódust virtuálisnak deklaráltál, akkor az onnantól minden származtatott osztályban az lesz. Egy lényeges konvenció, hogy ha származtatást használsz, akkor az ősosztály destruktorát virtuálisnak kell deklarálni. Ez azért nagyon fontos, mert ellenkező esetben a delete operátor hatására csak az ősosztály destruktora fut le.

Még egy fontos dolog: ősosztályra mutató pointert nem lehet explicit konvertálni származtatott típusúra (hiszen semmi nem garantálja, hogy tényleg az). Egy még fontosabb dolog, hogy ne próbáld meg kierőszakolni ezt C-style cast-al. Ugyanis ez semmiféle ellenőrzést nem végez és ha elrontod, akkor olyan hibákat tud okozni, amit sosem találsz meg. Erre való a dynamic_cast operátor. Ha nem sikerült kasztolni a pointert/referenciát, akkor ez 0-t ad vissza jelezve, hogy nem érvényes az adott konverzió.

CODE
Alma a; Gyumolcs* g = &a; // OK Alma* pa = dynamic_cast<Alma*>(g); // nem OK (nullpointer lesz) Banan* pb = dynamic_cast<Banan*>(g);

Ez az operátor csak polimorfikus típusokra működik, tehát ha a gyümölcs osztálynak nincs virtuális metódusa, akkor fordítási hibát kapsz.

Egy metódust tisztán virtuálisnak deklarálhatsz úgy, hogy = 0;-t írsz a végére. Ilyenkor ezt a metódust kötelező lesz felüldefiniálni a származtatott osztályokban, tehát az ősosztályban nem szükséges megadni implementációt (de meg lehet). Az olyan osztályt aminek van tisztán virtuális metódusa absztrakt osztálynak nevezik és nem lehet példányosítani. Ha még az is igaz, hogy csak tisztán virtuális metódusai vannak, akkor pedig interfésznek hívják.


A public-ot már láttuk, de vajon mit csinál a másik kettő? Private öröklődés esetében az ősosztály minden protected és public metódusa private lesz a származtatott osztályban. Ennek az az értelme, hogy egy alternatív eszközt ad a logikai/fizikai tartalmazás kifejezésére. A következő kódban bal oldalon a szokványos megoldást használom, a jobb oldalon pedig privát öröklődést.

CODE
class Mag { public: void Kiesik() {} }; class Alma { private: Mag m; public: void ErjelMeg() { m.Kiesik(); } };
CODE
class Mag { public: void Kiesik() {} }; class Alma : private Mag { public: void ErjelMeg() { Kiesik(); } };

Fontos, hogy ilyenkor nem lesz érvényes semmiféle polimorfizmus, tehát Mag* nem mutathat Alma objektumra (és természetesen a dynamic_cast se fog működni). Ezt a fajta öröklést például akkor lehet használni, ha az ősosztály a származtatott osztály egy metódusát kell, hogy meghívja:

CODE
class Mag { public: void Kiesik() { TorolMagok(); } // kötelező felüldefiniálni virtual void TorolMagok() = 0; };
CODE
class Alma : private Mag { private: bool vanmagja; void TorolMagok() { vanmagja = false; } public: void ErjelMeg() { Kiesik(); } };

A protected öröklés hasonló az előbbihez, de az ősosztály public és és protected metódusai protectedek lesznek a származtatott osztályban (ezáltal a további származtatások is tudják használni). Az öröklött metódusok láthatósági szintje felüldefiniálható a using kulcsszóval:

CODE
class Alma : private Mag { public: using Mag::Kiesik; // ... };

Fontos, hogy ez a kulcsszó a metódus összes túlterhelésének megváltoztatja a láthatósági szintjét! Apropó túlterhelés...a két szó angolul nagyon hasonló, ne tessék összekeverni (overload = túlterhelés, override = felüldefiniálás)!


C++-ban semmi akadálya annak, hogy egy típusnak több őse legyen, ilyenkor mind a két ősből örökli a tagváltozókat és metódusokat. Ha a tagváltozók vagy metódusok között névütközés van, attól még mind a kettő be fog kerülni a származtatott osztályba, de ha használni akarod, akkor vagy a using kulcsszóval választasz a class scope-ban, vagy a metóduson belül explicit minősited a változót. A polimorfizmus természetesen mindegyik bázisosztályra működni fog.

CODE
class A { protected: int i; }; class B { protected: int i; };
CODE
class C : public A, public B { protected: using A::i; public: void SetAi(int v) { i = v; } void SetBi(int v) { B::i = v; } };

Egy érdekes probléma a diamond problem, amikoris a névütközés abból adódik, hogy több ősnek ugyanaz az ősosztálya, és így ez az ősősosztály többször kerül be a származtatott osztályba. Ilyenkor is lehet használni a using kulcsszót, vagy metódushíváskor a minősitést, de ilyenkor már a pointer konverzióval is gondok vannak.

CODE
class Gyumolcs { private: bool vanheja; public: Gyumolcs() : vanheja(true) {} void Hamoz() { vanheja = false; } }; class EhetoGyumolcs : public Gyumolcs { public: void Megesz() {} }; class FoldbenEroGyumolcs : public Gyumolcs { public: void Kias() {} };
CODE
class Dinnye : public EhetoGyumolcs, public FoldbenEroGyumolcs { }; int main() { Dinnye d; // nem OK d.Hamoz(); // OK d.EhetoGyumolcs::Hamoz(); // OK FoldbenEroGyumolcs* g1 = &d; // nem OK Gyumolcs* g2 = &d; // OK Gyumolcs* g3 = (EhetoGyumolcs*)&d; return 0; }

A probléma megoldható, de nem túl szép, másrészt memóriapazarló. C++-ban ennek megoldására van kitalálva a virtuális öröklés. Azt kell csinálni, hogy a Gyumolcs osztályt virtuális bázisosztályként jelöljük meg a két speciális gyümölcsben, így az ezekből származtatott osztályokba csak egyszer fog bekerülni.

CODE
class EhetoGyumolcs : public virtual Gyumolcs { public: void Megesz() {} }; class FoldbenEroGyumolcs : public virtual Gyumolcs { public: void Kias() {} };

Azt gondoljuk most, hogy jajjdejó, a Dinnye osztály csak 1 bájt. Hát egy túróst; a sizeof operátorral megnézve 9-et kapunk vissza és ettől rögtön be is ájul mindenki. Hogy miért van ez ahhoz bele kell mélyedni kicsit ezeknek a dolgoknak az implementációjába.


Rögtön úgy vezetném ezt fel, hogy C struktúrákra implementálom a dinamikus kötést. Ez elsőre egy nehezen érthető kód a függvénypointerek miatt, de nagyon jól szemlélteti, hogy mi történik a háttérben. Ha nem érted nem baj.

CODE
/* gyumolcs.h */ typedef struct _Gyumolcs { void** vtable; int meret; } Gyumolcs; void Gyumolcs_dGyumolcs(Gyumolcs* _this); void Gyumolcs_sdd(Gyumolcs* _this); void Gyumolcs_NojelMeg(Gyumolcs* _this, int nap); /* gyumolcs.c */ void Gyumolcs_Gyumolcs(Gyumolcs* _this) { _this->vtable = malloc(2 * sizeof(void*)); _this->vtable[0] = (void*)&Gyumolcs_sdd; _this->vtable[1] = (void*)&Gyumolcs_NojelMeg; _this->meret = 0; } void Gyumolcs_dGyumolcs(Gyumolcs* _this) { printf("Gyumolcs::~Gyumolcs()\n"); } void Gyumolcs_sdd(Gyumolcs* _this) { Gyumolcs_dGyumolcs(_this); free(_this->vtable) } void Gyumolcs_NojelMeg(Gyumolcs* _this, int nap) { typedef void (*nmfunc)(Gyumolcs*, int); nmfunc nm = (nmfunc)_this->vtable[1]; if( nm == &Gyumolcs_NojelMeg ) printf("egy altalanos gyumi nem no\n"); else nm(_this, nap); }
CODE
/* alma.h */ typedef struct _Alma { /* öröklött dolgok */ void** vtable; int meret; } Alma; void Alma_dAlma(Alma* _this); void Alma_sdd(Alma* _this); void Alma_NojelMeg(Alma* _this, int nap); /* alma.c */ void Alma_Alma(Alma* _this) { Gyumolcs_Gyumolcs((Gyumolcs*)_this); _this->vtable = malloc(2 * sizeof(void*)); _this->vtable[0] = (void*)&Alma_sdd; _this->vtable[1] = (void*)&Alma_NojelMeg; } void Alma_dAlma(Alma* _this) { printf("Alma::~Alma()\n"); } void Alma_sdd(Alma* _this) { Alma_dAlma(_this); Gyumolcs_dGyumolcs((Gyumolcs*)_this); free(_this->vtable) } void Alma_NojelMeg(Alma* _this, int nap) { if( nap > 70 && nap < 150 ) { ++_this->meret; printf("az alma merete: %d\n", _this->meret); } else { printf("az almanak ...\n"); _this->meret = 0; } }

A lényeges dolog itt a vtable változó, ezt hívják virtuális táblának és egy függvénypointer tömb. A származtatott Alma osztály konstruktora (vagy egy a konstruktor mellé tartozó sdd-hez hasonló külön metódus) beállítja a táblájába azokat a metódusokat amiket ő felüldefiniál (amiket nem, azokra nyilván az ősosztályét kell beállítani). Vegyük észre, hogy semmiféle reláció nincs a két struktúra között (nem is lehet kifejezni), az egyetlen közös dolog, hogy mindkettőnek a kezdő offsetjén ott van egy void** tömb. A használat az alábbi módon történik:

CODE
int main() { Gyumolcs* g; /* scalar deleting destructor */ typedef void (*sddfunc)(Alma*); sddfunc sdd; /* ez lenne az Alma* a = new Alma(); */ Alma* a = (Alma*)malloc(sizeof(Alma)); Alma_Alma(a); /* kód */ g = (Gyumolcs*)a; Gyumolcs_NojelMeg(g, 80); /* g->~Gyumolcs(); vagy a->~Alma(); vagy delete a; */ sdd = (sddfunc)a->vtable[0]; sdd(a); /* delete a; esetén még ez is */ free(a); system("pause"); return 0; }

Ez a kód minden csak nem típusbiztos, hiszen C-ben csak C-style cast van, és ha valami teljesen más struktúrával hívon meg a metódusokat akkor fatális hibák történhetnek! Ez tehát csak szemléltető kód, ne használd.

Többszörös öröklődésnél nyilván mindegyik osztály virtuális táblája öröklődik, ha volt. Ha viszont az öröklődés virtuális akkor tökmindegy, hogy az adott ősosztálynak van-e virtuális metódusa, a gyerek osztály kap egy vpointert. Így már érthető az a 9 bájt (2 db vpointer + az 1 bool).

Ha a Gyumolcs osztálynak lenne virtuális metódusa, akkor a Dinnye 9 helyett már 16 bájt lenne (saját vpointer + 2 db vpointer + az 1 bool + 3 bájt padding). Ezeket könnyen lehet ellenőrizni a #pragma pack(1) előfordító direktívával illetve a visual studio memória nézegetőjével.


Megmutattam hogyan lehet az osztályokat újrafelhasználni származtatás segítségével. A gyakorlati része ennek a témának nagyon kiterjedt, egy külön fejezetben akár meg is említek majd egy rakás tervmintát ami ezt használja. A következő fejezetben egy még magasabb szintre fogom emelni az újrafelhasználást a sablonok segítségével.


Höfö:

  • A korábban írt 2D vektor osztályt felhasználva írj 3D és 4D vektor osztályokat is!
  • Általánosítsd őket egy absztrakt vektor osztályba!

    Megj.: vektorokkal sose szokták ezt csinálni, de gyakorlásnak tök jó.

Irodalomjegyzék

http://en.cppreference.com/w/cpp
http://www.cplusplus.com/
http://www.parashift.com/c++-faq-lite/ (ezt különösen ajánlott elolvasni)
http://aszt.inf.elte.hu/~gsd/halado_cpp/ (ezt is)

back to homepage

Valid HTML 4.01 Transitional Valid CSS!