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.

A sorozat további részei

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha kedveled a régi jó dolgokat.