Objektum-orientált C++ VI.
Beszéljünk a származtatásról.
Elérkezett hát az idő, hogy megtárgyaljuk az osztályok származtatását és az ezekből adódó problémákat. Első nekifutásra az az érzés fogott el, hogy túl nagy fába vágom a fejszémet, amikor egy bejegyzésben akarok mesélni ezekről a dolgokról. Túl sok, túl összetett és még egy normális példát sem lehet hozzá írni, ami nem túl bonyolult de nem is rugaszkodik el annyira a valóságtól.
Annak idején én egy közlekedési példával lettem bevezetve a származtatás világába. Tegyük fel, hogy van egy személygépkocsi, egy tehergépkocsi, egy kamion, stb. osztályunk. Mindegyik osztályban van valami közös, például, hogy van kormánya, meg van X darab kereke. Ezek miatt a közös részek miatt érdemes őket egy bázisosztályból származtatni. Tehát legyen egy járművek osztályunk, amiben megvan minden közös tulajdonság, ami a járműveket jellemzi és minden egyediséget a belőle származtatott osztályokban valósítunk meg.
Ezzel sikerült elrugaszkodnunk eléggé a valóság talajáról - már ha ez esetben egyáltalán lehet erről beszélni. Térjünk hát át egy kézzel foghatóbb és egyszerűbben kódolhatóbb (mondhatni hétköznapi) problémára, melyet az A C++ programozási nyelv c. könyvben találtam. Tegyük fel, hogy valamilyen cég számára írunk egyfajta alkalmazott-nyilvántartó programot, ahol használunk valami ilyesféle típusokat:
struct Alkalmazott
{
std::string vezeteknev;
std::string keresztnev;
int fizetes;
};
struct Osztalyvezeto
{
Alkalmazott alk;
std::set<Alkalmazott*> csoport;
};
Az Osztalyvezeto
kódjából remekül látszik már így is, hogy az osztályvezető is egy alkalmazott, tehát nála is meg lehet/kell adni az alkalmazottaknál bevezetett mezőket. Viszont ennek a megoldásnak van egy olyan hatalmas hátránya, hogy az Osztalyvezeto
típusokra nem használhatjuk azokat a függvényeket, amiket az Alkalmazott
típusra írtunk. Több okból sem. Ebből az egyik, hogy például nem létezik Osztalyvezeto.fizetes
tag, csak Osztalyvezeto.alk.fizetes
. Ez talán még megoldható. De ha egy függvény egy Alkalmazott
-ra mutató pointert vár, akkor nem adhatunk neki egy Osztalyvezeto
-re mutató pointert. Ebben az esetben legalábbis nem. De ha így írjuk a dolgokat, akkor már igen:
struct Osztalyvezeto : public Alkalmazott
{
std::set<Alkalmazott*> csoport;
};
Ez esetben az Osztalyvezeto
osztályt származtattuk az Alkalmazott
osztályból, így az Alkalmazott
lett az Osztalyvezeto
bázisosztálya, így rendelkezik minden tulajdonsággal, amivel az Alkalmazott
rendelkezik, valamint Alkalmazott*
típusú változónak értékül adhatunk Osztalyvezeto*
-ot. Természetesen egy származtatott osztály is lehet bázisosztály így az Osztalyvezeto
-ből származtathatjuk az Igazgato
-t. Sőt, egy osztályt akár több bázisosztályból is származtathatunk. Az így létrejött kapcsolatrendszert szokás osztályhierarchiának nevezni és irányított gráffal lehet szépen szemléltetni, ahol a nyíl mindig a származtatott osztály felől mutat a bázisosztály felé.
A fenti példának is megvan az a hátránya, hogy ritkán szokás így használni, hogy csak adatszerkezet van függvények nélkül. A függvények behozatala némiképp megzavarja a képet, bár sok változás nem történik. Nézzük ezt a felállást:
class Alkalmazott
{
private:
std::string vezeteknev;
std::string keresztnev;
int fizetes;
public:
Alkalmazott(const std::string& vnev, const std::string& knev, const int& fiz) : vezeteknev(vnev), keresztnev(knev), fizetes(fiz) {}
void print() const
{
std::cout << "Név: " << vezeteknev << " " << keresztnev << std::endl;
std::cout << "Fizetés: " << fizetes << " Ft" << std::endl;
}
};
class Osztalyvezeto : public Alkalmazott
{
std::set<Alkalmazott*> csoport;
public:
Osztalyvezeto(const std::string& vnev, const std::string& knev, const int& fiz) : Alkalmazott(vnev, knev, fiz) {}
void print() const
{
Alkalmazott::print();
std::cout << "Csoport:" << std::endl;
for (std::set<Alkalmazott*>::iterator current = csoport.begin(); current != csoport.end(); ++current) {
(*current)->print();
}
}
};
Ami először feltűnik, hogy felülírjuk a már létező print()
függvényt a származtatott osztályban (meg belecsempésztem mindkét osztályba egy-egy konstruktort, hogy tudjak működő példát adni pár sorral lejjebb). Ugyanakkor a felülírott függvényt is meghívjuk, ugyanis másként nem tudnánk kiírni a vezeteknev, keresztnev, fizetes mezőket, mivel ezek az Alkalmazott
osztály private
mezői, tehát csak az Alkalmazott
osztályon belül érhetőek el. Ha a bázisosztály private
mezője úgy viselkedne a származtatott osztályban, mintha a származtatott osztály sajátja lenne, akkor a private értelmét vesztené, ugyanis bármilyen private mező feloldható lenne egy egyszerű származtatással.
Viszont akármennyire is hihetetlennek tűnhet, de már ez az egyszerű print()
utasítás is hordoz magában hibákat. Gondoljuk csak meg, hogy mit csinálhat az alábbi kód:
int main()
{
Alkalmazott alk("Iksz", "Ipszilon", 150000);
Osztalyvezeto oszt("Adsf", "Jklé", 350000);
Osztalyvezeto* osztp;
osztp = &oszt;
Alkalmazott* alkp;
alkp = &oszt;
alkp->print();
}
Mire számítunk? Természetesen arra, hogy kétszer egymásután ugyanazt olvashatjuk a képernyőn. Ennek ellenére nem ez fog történni, mivel az Alkalmazott* nem tud arról, hogy az Osztalyvezeto
objektum felülírta az eredeti print()
-et. Vagyis igazából nem is tud az Osztalyvezeto
létezéséről, ezért az eredeti, Alkalmazott
-ban megadott print()
-et hívja meg. Ezt most csúnyán megszívtuk, gondolhatná a kedves olvasó, mivel mi értelme van, hogy Alkalmazott*
-nak értékül adhatunk Osztalyvezeto*
-ot is, ha nem tudjuk használni az Alkalmazott*
-on keresztül az Osztalyvezeto
által felüldefiniált függvényeket? De szerencsére van megoldás a virtuális függvények személyében.
class Alkalmazott
{
/* ... */
public:
/* ... */
virtual void print() const
{
std::cout << "Név: " << vezeteknev << " " << keresztnev << std::endl;
std::cout << "Fizetés: " << fizetes << " Ft" << std::endl;
}
};
Azzal, hogy a print megadása elé tettünk egy virtual szócskát, jeleztük, hogy az adott függvényt a származtatott osztályokban felül lehet bírálni, így egy Alkalmazott*
esetén a virtuális függvény megfelelő példánya fog meghívódni. Egy osztálynak lehetnek tisztán virtuális (pure virtual) függvényei is, vagyis a bázisosztályban nem adunk meg semmiféle megvalósítást és a függvényt (maradva a print példájánál) így deklaráljuk:
class Alkalmazott
{
/* ... */
public:
/* ... */
virtual void print() const = 0;
virtual ~Alkalmazott() {}
};
Így az osztályunkat nem lehet példányosítani (fordítási idejű hibát generál), mivel egy függvénye nincs megvalósítva. Az ilyen osztályokat hívjuk absztrakt osztályoknak. Az absztrakt osztályokat felületként (interface) vagy más osztályok bázisosztályaként tudjuk használni. Egy pure virtual függvényt ha a származtatott osztályban nem adunk meg, akkor pure virtual marad ebből következik, hogy a származtatott osztály is absztrakt osztály lesz. Emellett az absztrakt osztályhoz virtuális destruktor is jár, hogy biztosítsuk a származtatott osztályunk utáni rendrakást.
Ezzel az objektum-orientált C++ világába bevezető sorozat befejező epizódjának végére értünk. Remélem mindenkire ragadt valami a bejegyzések olvasása közben. Sok sikert a gyakorlati alkalmazáshoz.