Kihagyás

7. gyakorlat

Ismétlés

  • Öröklődés
  • Polimorfizmus
  • Virtualizáció
  • Override

Pure virtual

Az előző órán megismertük a virtualizáció lényegét, most a pure virtual kifejezést nézzük át. Ahogy láttuk a virtualizált metódusoknak a viselkedése polimorfikusan felülírható, azaz az ősosztály-beli metódus X lefutású a gyerek osztály beli felüldefiniált lefutás Y. Ezzel feltételeztük, hogy az ős osztályban biztosan meg tudtuk írni az ahhoz köthető lefutást, az ősosztály is egy teljes egészében használható osztály.

Azonban sokszor a modellezett entitást nem tudjuk teljes egészében meghatározni, egy adott viselkedést meghatározó metódust például nem tudunk implementálni, mert nincs értelme az adott entitáson belül. Vegyünk egy egyetemi polgárt: ez a modell rendelkezik a teendoket_vegez metódussal, azonban ez hallgató esetén a tanulás, PhD-hallgató esetén a tanulás és az oktatás, vezető beosztású egyénnél ez lehet például az adminisztráció. Ha az egyetemi polgárt szeretnénk megvalósítani, nem tudjuk, hogy a teendoket_vegez metódus mit csinál mivel nem tudunk egy általános viselkedést megfogalmazni, ezért az osztályunk nem teljes. Nem teljes osztályból nem célszerű példányosítani, hiszen hasztalan önmagában, csakis az öröklődésben, általánosításban van haszna.

Ezek az osztályok az abstract osztályok, amelyek ismerősek Javaból is. Az absztrakt osztályok nem példányosíthatóak. Javaban erre külön kulcsszó is létezett, azonban C++ esetében egy osztály akkor absztrakt, ha van pure virtual (tisztán virtuális) metódusa, ám ekkor nem példányosítható az adott osztály. A tisztán virtuális metódus egy olyan metódus, aminek nincs implementációja az adott osztályban (nem csak üres az implementációja, azonban ha nem tennénk implementációt, fordítási hibát kapnánk, hiszen egy metódus deklarációhoz nincs definíció), ennek jelölése, hogy a virtuális metódusunkat egyenlővé tesszük 0-val. Erre példa:

1
2
3
4
5
6
7
class Egyetemi_polgar {
    std::string nev;
    public:
    Egyetemi_polgar(const std::string& nev) : nev(nev) {}   // Csak ez az egy konstruktor van, oroklodes eseten meg kell hivni

    virtual void teendoket_vegez() const = 0; // pure virtual
};

Ahogy látszódik, a teendoket_vegez metódus virtuális, de hiányzik a megvalósítás, ezt a törzs helyére írott '=0' kifejezéssel jelezzük. Ekkor az öröklési láncban lejjebb lévő (leszármazott) minden osztálynak (közvetlen leszármazottnak) kötelező megvalósítani. Ha nem valósítja meg a leszármazott osztály, az is absztrakt osztállyá válik.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Hallgato : public Egyetemi_polgar {
    std::string neptun;
    public:
    // A hallgato egy Egyetemi_polgar, azon reszet is inicializalni kell
    Hallgato(const std::string& nev, const std::string& neptun) : Egyetemi_polgar(nev), neptun(neptun) {}

    // A hallgato peldanyosithato, igy meg kell valositania a pure virtual metodust!
    virtual void teendoket_vegez() const override {
        std::cout << "Hallgato orat latogat." << std::endl;
    }
};

class Alkalmazott : public Egyetemi_polgar {
    public:
    Alkalmazott(const std::string& nev) : Egyetemi_polgar(nev){}
    virtual void megbeszelesre_jar() const {
        std::cout << "Alkalmazott megbeszelesre ment." << std::endl;
    }
};

Ahogy látszódik, a hallgató egyetemi polgár, és egy olyan típus, amiből lehet példányt gyártani, mivel meg van valósítva a pure virtual metódus.

Ezzel szemben az alkalmazott egy még mindig általános entitás (nem példányosítható), hiszen lehet kutató munkatárs, esetleg gazdasági osztályon dolgozó és még sok egyéb. Mindegyik alkalmazott jár megbeszélésekre, azonban ott más-más módon szerepelhetnek (polimorfikus hívás -> virtual method). Mivel az Alkalmazott szintén absztrakt, kell legyen pure virtual metódusa. Ez az örökölt, és meg NEM valósított teendoket_vegez pure virtual metódus.

1
2
3
4
5
6
7
class Gazdasagis : public Alkalmazott {
    public:
    Gazdasagis(const std::string& nev) : Alkalmazott(nev) {}
    void teendoket_vegez() const override {
        std::cout << "A gazdasagis penzugyeket intez, ez a teendoje." << std::endl;
    }
};

A gazdaságis már olyan típus, amiből példányosítani szeretnénk, ezért meg kell valósítani az összes pure virtual metódust, ami jelen esetben a teendoket_vegez metódus. Mivel csak a Gazdaságis és a Hallgató példányosítható típus, csak azokból lehet objektumot előállítani, de ős típusként, paraméterként szerepelhet az összes többi típus is.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void egyetemi_polgar(Egyetemi_polgar& ep) {
    ep.teendoket_vegez();
}
void alkalmazott(Alkalmazott& a) {
    a.teendoket_vegez();
    a.megbeszelesre_jar();
}

Gazdasagis& gazdasagist_gyart(Gazdasagis& ) {
    // Csunya mod referencia keszitesere, de ez barmilyen
    // lekero fgv. is lehet.
    return g;
}

int main() {

  Gazdasagis gazdasagis("Egy gazdasagis");

  // Alkalmazott gazdasagis("Gazdasagis neve"); Ilyet nem tehetunk, hiszen Alkalmazottat hoznank letre
  Alkalmazott* gazdasagis_alkalmazottkent = new Gazdasagis("Gazdasagis aki alkalmazott");
  // Ilyet tehetünk, hiszen a pointer egy gazdasagisra mutat, ami egy alkalmazott.

  Egyetemi_polgar *gazdasagis_egyetemi_polgarkent = new Gazdasagis("Gazdasagis aki egyetemi polgar");

  Hallgato hallgato("Egy hallgato", "ha57z1");
  egyetemi_polgar(gazdasagis);
  egyetemi_polgar(*gazdasagis_alkalmazottkent);
  egyetemi_polgar(*gazdasagis_egyetemi_polgarkent);
  egyetemi_polgar(hallgato);

  alkalmazott(gazdasagis);
  alkalmazott(*gazdasagis_alkalmazottkent);
  // alkalmazott(*gazdasagis_egyetemi_polgarkent); HIBA!

  // Alkalmazott nem lehet, de garantaljuk a referenciaval, hogy egy leszarmazott kerul oda.
  Alkalmazott& gazdasagis_alkalmazott_nem_pointer = gazdasagist_gyart(gazdasagis);
  alkalmazott(gazdasagis_alkalmazott_nem_pointer);
}

Ahogy az látható, a Gazdaságis és Hallgató létrehozható, a többi típus azonban paraméterként és változó típusaként használható. Polimorfikusan Alkalmazott típusú változót létrehozhatunk, de ekkor meg kell oldanunk, hogy egy pl. Gazdaságis objektum kerüljön oda. Ezt pointer vagy referencia használatával megtehetjük.

Nagyon fontos, hogy az Egyetemi polgárként létrehozott gazdaságist nem tudjuk átadni Alkalmazottként, hiszen a rendszer csak annyit tud, hogy ott egy egyetemi polgár típus van (vagy annak egy leszármazottja). A futásidejű típus, és annak viselkedése már nem ismert.

Virtuális destruktor

Azt láttuk, hogy ha az ős osztály típusával szeretnénk kezelni a leszármazott osztályt és mégis a leszármazottban megvalósított viselkedést szeretnénk, akkor virtual metódust kell használni. Ez nem különbség a destruktor esetében sem.

Feljebb láthattuk, hogy egy Alkalmazott* típusú változónak adhatunk egy Gazdaságisnak foglalt memóriát. Azonban ekkor ha ki akartuk volna törölni a pointereket az Alkalamazott típust töröltük volna, hiszen:

  • a desturktor egy metódus
  • nem volt virtual
  • a pointer típusa ős típus volt

Ez a fenti példában nem okoz nagyobb hibát, azonban ha az egyik leszármazott osztály erőforrásokat foglal, annak felszabadítása lényeges. Ha nem fut le a desturktora egy rossz polimorfikus hívás miatt, az adott erőforrást elveszítjük, de nem lesz felszabadítva.

Legyen egy Kutató osztály. Ez a kutató egy Alkalmazott. A munkásságát egy dinamikus int tömb jelzi. Egy-egy int a munkásságának fontosságát jelölje!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Kutato : public Alkalmazott {
    unsigned munkak;
    int* munkassag = nullptr;
    public:
    void teendoket_vegez() const override { // hiszen Alkalmazott
        std::cout << "Kutatio munka" << std::endl;
    }
    Kutato(const std::string & nev, unsigned munkak) : Alkalmazott(nev), munkak(munkak), munkassag(new int[munkak]) {}
    ~Kutato() {
        std::cout << "A Kutatonak ennyi munkassaga volt: " << munkak << std::endl;
        delete[] munkassag; // new[] -> delete[]
    }
};

Az egyetemben legyen egy alkalmazottakat tároló dinamikus tömb! Ekkor minden alkalmazott ebbe kerül bele. Természetesen Gazdaságis és Kutató is kerülhet bele, hiszen Alkalmazott mind a kettő.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Egyetem {
    unsigned alkalmazott_szam = 0;
    const Alkalmazott** alkalmazottak = nullptr;
    public:
    Egyetem() = default;

    Egyetem& operator+=(const Alkalmazott* a) {
        if (nullptr == alkalmazottak) {
            ++alkalmazott_szam;
            alkalmazottak = new const Alkalmazott*[alkalmazott_szam];
            alkalmazottak[0] = a;
        }

        // Mar van alkalmazott, de uj tomb kell.
        // Legyen egy uj, masoljunk at mindenkit, majd szabaduljunk meg a regitol

        ++alkalmazott_szam;
        const Alkalmazott** tmp = new const Alkalmazott*[alkalmazott_szam];
        for ( unsigned i = 0; i < alkalmazott_szam - 1; ++i) {
            tmp[i] = alkalmazottak[i];
        }
        // Az uj embert is berakjuk
        tmp[alkalmazott_szam - 1] = a;

        // mindenki az uj helyen van
        // toroljuk a regi helyet, az alkalmazottakat nem!
        delete[] alkalmazottak; // new[] -> delete[]
        // az uj ideiglenes helyre allitjuk az alkalmazottakat, hiszen mar mindenki ott van az uj helyen
        alkalmazottak = tmp;

        return *this;
    }

    ~Egyetem() {
        // Ha az egyetemet toroljuk, akkor nem csak a munkahelyet kell, hanem az alkalmazottakat is!
        for ( unsigned i = 0; i < alkalmazott_szam; ++i) {
            delete alkalmazottak[i]; // Egy alkalmazott letrehozasa: new -> delete
            // Itt alkalmazottat kapunk, de igazabol lehet Gazdasagis vagy Kutato.
            // Ha kutato, akkor az O eroforrasait is torolni kell!

            alkalmazottak[i] = nullptr;
        }
        delete[] alkalmazottak; // miutan az embereket kitoroltuk, a munkahelyuk is torlesre kerul.
        alkalmazottak = nullptr;
    }
};

Látható, hogy az egyetembe Alkalmazottak kerülnek be. Mikor az egyetemet töröljük töröljük az alkalmazottak munkahelyét és az alkalmazottakat is. Egy alkalmazott törlésekor azonban tudnunk kell, hogy Gazdaságis vagy Kutató, hiszen kutató esetén az ő erőforrásait is ki kell törölni. Ez automatikusan a destruktor használatával ment, de ha nem polimorfikus nem kerül meghívásra, akkor nem kerül meghívásra csak az Alkalmazott destruktora, ami a gyerekosztályban foglalt memóriát nem fogja felszabadítani.

Ezt javíthatjuk, ha az Alkalmazott desturktora is virtuális lesz, azaz működhet polimorfikusan. Erre az esetleges problémára a fordító is felhívja a figyelmünket az alábbi figyelmeztetéssel:

warning: deleting object of abstract class type ‘Alkalmazott’ which has non-virtual destructor will cause undefined behavior [-Wdelete-non-virtual-dtor] delete alkalmazottak[i]; // Egy alkalmazott letrehozasa: new -> delete

Feladatok

  1. Gyakorló feladatsor (a 9. gyakorlat gyakorló feladatsorának bővítése)

    • Hozzunk létre egy eloleny osztályt!

      • Legyen egy kromoszomaszam adattagja (unsigned). Írj paraméteres konstruktort az osztályhoz, mely kiírja a standard outputra, hogy "eloleny letrehozva".
      • Írj destruktort, mely kiírja: "eloleny torolve"
      • Legyen egy void taplalkozik() const metódusa, amely pure virtual
    • Hozzunk létre egy noveny osztályt, ami az elolenybol öröklődik publikusan.

      • Legyen egy unsigned klorofill_hetkonysag adattagja, amit a konstruktor állít be a kromoszomaszammal együtt.
      • Próbáld példányosítani. Mi történik? Hogy lehetne javítani?
      • Legyen egy private void fotoszintetizal() metódusa, amit meghív a taplalkozik(). A fotoszintetizal kiirja a standard outputra, hogy "a noveny db energiat allitott elo"
    • Származzon publikusan a meglévő allat osztaly is az eloleny osztalyból.

      • Javítsd a felmerülő fordítási hibákat! (Tipp: a macskák kromoszómaszáma 38)
      • Melyik virtual kulcsszó vált fölöslegessé?
      • Próbáld ki, hogy egy dinamikusan foglalt macska objektumra eloleny pointeren keresztül hivatkozol és azon keresztül törlöd. Mi a tapasztalat? Hogy tudod javítani?
    Megoldás

Utolsó frissítés: 2022-09-15 09:40:31