Kihagyás

Öröklődés, polimorfizmus, hibakezelés

Öröklődés

Az objektumorientáltság egyik alap pillére az öröklődés, azaz egy adott osztály speciális esetekre bontása; a meglévő tulajdonságok, funkciók kibővítése, pontosítása. Ahhoz, hogy meg tudjuk nézni, hogyan zajlik az öröklődés és milyen lehetőségeket rejt a nyelv, kell egy alap osztály.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Hallgato {
  std::string neptun;
protected:
  std::string nev;
  unsigned felvettOraszam;
public:
  Hallgato(const std::string& nev = "John Doe", unsigned felvettOraszam = 0) :
    nev(nev), felvettOraszam(felvettOraszam) {
    neptun = nev + std::to_string(felvettOraszam);
  }

  const std::string & getNev() const { return nev; }
  const std::string & getNeptun() const { return neptun; }
  unsigned getFelvettOraszam() const { return felvettOraszam; }

  void orakatHallgat() const {
    std::cout << "Orat latogat a Hallgato: " << neptun << std::endl;
  }
};

Ahogy látszik, egy Hallgato osztályt hoztunk létre. A neptun adattag private, így csak ez az osztály érheti el, azonban a felvettOraszam és a nev már nem a megszokott private láthatóságúak, hanem protected jelzőt kaptak. Protected tagokat a leszármazott típusok is elérhetik, ahogy azt korábban is láthattuk már. A felvettOraszam, nev a leszármazott típusokban is közvetlenül változtatható, azonban az neptun (egyetemi azonosító) kód minden példány privát tulajdonsága, kívülről nem elérhető, nem módosítható.

Egy PhD hallgató ugyan olyan hallgató, mint bárki más, csupán az órák hallgatása mellet órát is tarthat, azaz bővíti az eredeti típust, annak egy speciális esete.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class PhD_hallgato : public Hallgato {
  unsigned oktatottOraszam;
public:
  PhD_hallgato(const std::string nev, unsigned felvettOraszam, unsigned oktatottOraszam) :
    Hallgato(nev, felvettOraszam),
    oktatottOraszam(oktatottOraszam) {}

  unsigned getOktatottOraszam() const { return oktatottOraszam; }

  void oratTart() const {
    std::cout << "Orat tart a PhD_hallgato" << std::endl;
  }
};

Ezzel létrehoztunk egy Hallgato osztályból leszármaztatott PhD_hallgato típust. Ezt úgy képzelhetjük el, mintha egy objektum gömb köré egy nagyobb burkot helyeznénk el:

leszarmazott

Öröklődés láthatósága

1
class PhD_hallgato : public Hallgato {

Az öröklődést a kettőspont után adjuk meg, azonban nagyon fontos, hogy milyen láthatósággal. Ahogy itt megadtuk, az öröklődés publikus. Ez azt jelenti, hogy bármit is kapott az ős osztálytól a gyerek osztály, annak láthatóságát nem módosítja. Ha az öröklődés típusa protected, akkor a gyerek osztályban az ős eredetileg publikus tagjai úgy viselkednek, mintha előttük a protected jelző szerepelne. Ha private-ot írunk vagy nem írunk semmit, akkor a gyerek osztályban az örökölt tagok private láthatósággal jelennek meg.

Egy kisebb példán szemléltessük ezt:

 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
#include <iostream>
class Base {
  int priv;
protected:
  int prot;
public:
  int pub;
};

class PublicChild : public Base {
public:
  void goo() { 
    std::cout << prot << " " << pub << std::endl; 
  }  // priv az osben privat, a gyerek osztaly sem lathatja
};

class ProtectedChild : protected Base {
  void goo() { 
    std::cout << prot << " " << pub << std::endl; 
  } 
};

class PrivateChild : Base { 
  // vagy class PrivateChild : private Base {
public:
  void goo() {
    std::cout << prot << " " << pub << std::endl;  
  }
};

int main() {
  Base b;
  std::cout << b.pub << std::endl;
  //std::cout << b.prot << std::endl; // forditasi hiba, inaccessible
  // std::cout << b.priv << std::endl; // forditasi hiba, inaccessible
  PublicChild publ_c;
  std::cout << publ_c.pub << std::endl;   // Mivel a public maradt public, ez elerheto
  // std::cout << publ_c.prot << std::endl; // Meg mindig nem elerheto, sem a priv
  ProtectedChild prot_c;
  // std::cout << prot_c.pub << std::endl;   // Mar ez sem erheto el, hiszen a pub adattag ebben az osztalyban mar protected.

  PrivateChild priv_c;
  priv_c.goo();   // Lathato, az osztaly azonban eleri, amit megorokolt!
}

Látható, hogy az öröklődés láthatósága nem a leszármazott osztály elől rejt el elemeket, hanem "abba bekerülve" az adattagnak a láthatóságát módosítja. Ha private is az öröklődés láthatósága, az osztály eléri az örökölt nem private tagokat, csak a többi osztály számára módosítja ezen tagok elérhetőségét ezen osztályon/típuson keresztül.

Ős osztály konstruktora

Ahogy a fenti ábrán is látható, a leszármazott osztály tartalmazza az ős osztályt, tehát ha a leszármazott osztályt teljes mértékben inicializálni akarjuk, akkor a benne lévő őst is inicializálni kell. Ha nincsen default inicializálás (default konstruktor), akkor meg kell adni, hogy milyen módon / melyik konstruktorral legyen az ős inicializálva. Ezt a következő módon tettük meg a PhD_hallgato esetében:

1
2
3
PhD_hallgato(const std::string nev, unsigned felvettOraszam, unsigned oktatottOraszam) :
  Hallgato(nev, felvettOraszam),
  oktatottOraszam(oktatottOraszam) {}

Látható, hogy az inicializáló lista első "inicializáló" eleme egy Hallgato konstruktor hívás. Ezzel tudjuk megadni, hogy milyen adatokat adunk át az ős résznek. Fontos, hogy mindig az ős konstruktorokat kell hívni először, azután lehet csak leszármazott saját adattagjait beállítani!

Ős osztály tagfüggvényei

Azt láttuk, hogyan kell az ősosztály-beli adatokat inicializálni. Láttuk, hogy az ős private elemeit nem érhetjük el. Azonban a konstruktorban átadott információkat - ha nem is érjük el közvetlenül - nem veszítjük el, hiszen ha létezik olyan ősosztály-beli metódus (pl.: getterek), ami a privát adatot használja, akkor azt a metódust használhatjuk leszármazott osztályban is (feltéve, hogy nem privát). Az alábbiakban bemutatjuk, hogy PhD_hallgato egyik tagfüggvényében az örökölt Hallgato-beli orakatHallgat metódust is meg tudjuk hívni (ez a metódus pedig a Hallgato osztály privát adatával dolgozik).

 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
class Hallgato {
  std::string neptun;
protected:
  std::string nev;
  unsigned felvettOraszam;
public:
  Hallgato(const std::string& nev = "John Doe", unsigned felvettOraszam = 0) :
    nev(nev), felvettOraszam(felvettOraszam) {
      neptun = nev + " " + std::to_string(felvettOraszam);
  }

  const std::string & getNev() const { return nev; }
  const std::string & getNeptun() const { return neptun; }
  unsigned getFelvettOraszam() const { return felvettOraszam; }

  void orakatHallgat() const {
    std::cout << "Orat latogat a Hallgato: " << neptun << std::endl;
  }
};

class PhD_hallgato : public Hallgato {
  unsigned oktatottOraszam;
public:
  PhD_hallgato(const std::string nev, unsigned felvettOraszam, unsigned oktatottOraszam) :
    Hallgato(nev, felvettOraszam),
    oktatottOraszam(oktatottOraszam) {}

  unsigned getOktatottOraszam() const { return oktatottOraszam; }

  void oratTart() const {
    std::cout << "Orat tart a PhD_hallgato" << std::endl;
  }

  void orakatHallgat() const {
    std::cout << "Most a PhD hallato ment be egy orara" << std::endl;
    Hallgato::orakatHallgat();
  }
};

int main() {
    PhD_hallgato phd("Meme Joe", 5, 3);
    phd.orakatHallgat();
}

Kimenet

Most a PhD hallato ment be egy orara
Orat latogat a Hallgato: Meme Joe 5

C++-ban elképzelhető, hogy az ős és a gyerek osztály tartalmaz ugyanolyan szignatúrájú metódusokat. A példában épp ilyen az orakatHallgat metódus. Ha PhD_Hallgato osztályból kimondottan a Hallgato oszályban levő metódust szeretnénk meghívni, akkor használnunk kell a :: (szkópe) opertátort. A main metódusban létrehozott PhD_hallgato objektumon keresztül először meghívjuk a PhD_hallgato osztályban definiáltorakatHallgat metódust, amely meghívja a szkópolás felhasználásában az ősben definiált, azonos szignatúrájú metódust.

Polimorfizmus

Objektumorientált nyelvek egyik meghatározó eleme a dinamikus polimorfizmus, amikor a program adott pontján az ős típus által hivatkozunk ugyan egy objektumra, de az képes a valódi típusának megfelelő módon reagálni a rajta megvalósuló hívásokra.

A Hallgato és PhD_hallgato esetében írhatunk függvényeket, melyek Hallgatot várnak, és PhD_hallgatot kapnak. Ezen függvényekben a paraméteren keresztül azokat a metódusokat tudjuk elérni, amelyek a Hallgato osztályban definiáltak. Ugyanakkor elképzelhető, hogy a gyerek objektumok esetében szeretnénk, ha nem az ős beli, hanem a gyerek osztályban felüldefiniált változat hívódna meg. Felüldefiniálás esetében meg kell egyezzen:

  • metódus neve
  • metódus paraméterezése
  • metódus qualifierek

A fenti példában az void orakatHallgat() const metódusra ezek megegyeznek a Hallgato és a PhD_hallgato osztályokban.

Nézzük meg, mi történik, ha írunk egy metódust, ami egy Hallgato, vagy egy Hallgato& paramétert kap, és meghívja ennek az orakatHallgat metódusát!

 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
47
48
49
50
51
52
53
54
#include <string>
#include <iostream>
class Hallgato {
  std::string neptun;
protected:
  std::string nev;
  unsigned felvettOraszam;
public:
  Hallgato(const std::string& nev = "John Doe", unsigned felvettOraszam = 0) :
    nev(nev), felvettOraszam(felvettOraszam) {
    neptun = nev + " " +std::to_string(felvettOraszam);
  }

  const std::string & getNev() const { return nev; }
  const std::string & getNeptun() const { return neptun; }
  unsigned getFelvettOraszam() const { return felvettOraszam; }

  void orakatHallgat() const {
    std::cout << "Orat latogat a Hallgato: " << neptun << std::endl;
  }
};

class PhD_hallgato : public Hallgato {
  unsigned oktatottOraszam;
public:
  PhD_hallgato(const std::string nev, unsigned felvettOraszam, unsigned oktatottOraszam) :
    Hallgato(nev, felvettOraszam),
    oktatottOraszam(oktatottOraszam) {}

  unsigned getOktatottOraszam() const { return oktatottOraszam; }

  void oratTart() const {
    std::cout << "Orat tart a PhD_hallgato" << std::endl;
  }

  void orakatHallgat() const {
    std::cout << "Most a PhD hallato ment be egy orara" << std::endl;
    Hallgato::orakatHallgat();
  }
};

/*void oratHallgat(Hallgato hallgato) {
  hallgato.orakatHallgat();
}*/

void oratHallgat(Hallgato& hallgato) {
  hallgato.orakatHallgat();
}
int main() {
  PhD_hallgato phd("Meme Joe", 5, 3);
  Hallgato hallgato("szimpla", 6);
  oratHallgat(phd);
  oratHallgat(hallgato);
}

Kimenet

Orat latogat a Hallgato: Meme Joe 5
Orat latogat a Hallgato: szimpla 6

Bármelyik esetet is választjuk, azaz ha Hallgato-t, vagy Hallgato&-et adunk át a globális oratHallgat függvényünknek, az mindenképp a Hallgato osztályban definiált metódust fogja meghívni, azaz így nem sikerült a dinamikus polimorfizmust megvalósítani. Ennek magyarázatához nézzük meg, mi is történik az egyes esetekben!

Érték szerinti paraméter átadás

Ebben az esetben statikusan megadjuk az oratHallgat függvénynek, hogy egy Hallgato típusú paramétert kell várnia és amikor a PhD_hallgato-t adtuk át, a rendszer másolatot készít erről az objektumról, amely másolat csak a statikus típusnak megfelelően a Hallgato adatait másolja le.

polimorf_copy_nem_virtualis

Referencia szerinti átadás

Azt már sokszor láttuk, hogy a referencia átadásakor az eredeti objektumra hivatkozunk ugyan, de azt, mint ős objektumot látjuk. Azaz bár az objektumnak csak az ősbeli magját tudjuk megszólítani, körülötte van az eredeti objektum minden eleme.

polimorf_referencia_nem_virtualis

Ennek ellenére ha az oratHallgat metódusnak egy szimpla Hallgato referenciaként adjuk át a PhD_hallgato objektumot, akkor is a statikus típusnak, azaz a Hallgato-nak megfelelő orakatHallgat metódus hívódott meg. A magyarázat minden esetben az, hogy statikusan (tehát nem futásidőben) megadtuk, hogy milyen típust fog használni az adott függvény. Ahhoz, hogy ilyenkor a dinamikus típusnak megfelelő típus hívódjon meg, alkalmaznunk kell a virtualizáció eszközét. A virtualizáció során az úgynevezett virtuális tábla leírja, hogy az egyes objektumoknak miként kell viselkedniük a polimorfikus hívások esetében.

C++ esetében nem alapértelmezett ezen tábla használata, mivel használata erőforrás igényesebb, de megadható, hogy adott osztályban mely metódusok lesznek azok, amelyeknél szeretnénk, ha ilyen esetekben a polimorfizmus érévényesülne. Ahhoz, hogy egyes metódusok virtuálisan tudjanak működni meg kell azokat jelölni a virtual kulcsszóval (ezt elegendő az ősosztályban megtenni).

Virtualizáció

Ahogy láttuk a virtualizáció nem alapértelmezés! Nézzük meg az átalakított példát, ahol már virtualizáljuk az orakatHallgat metódust!

 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
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <string>
#include <iostream>

using namespace std; //hogy ne kelljen minden std-beli elem ele kiirni, hogy "std::"
class Hallgato {
  string neptun;
protected:
  string nev;
  unsigned felvettOraszam;
public:
  Hallgato(const string& nev = "John Doe", unsigned felvettOraszam = 0) :
    nev(nev), felvettOraszam(felvettOraszam) {
    neptun = nev + " " +to_string(felvettOraszam);
  }

  const string & getNev() const { return nev; }
  const string & getNeptun() const { return neptun; }
  unsigned getFelvettOraszam() const { return felvettOraszam; }

  virtual void orakatHallgat() const {
    cout << "Orat latogat a Hallgato: " << neptun << endl;
  }

};

class PhD_hallgato : public Hallgato {
  unsigned oktatottOraszam;
public:
  PhD_hallgato(const sstring nev, unsigned felvettOraszam, unsigned oktatottOraszam) :
    Hallgato(nev, felvettOraszam),
    oktatottOraszam(oktatottOraszam) {}

  unsigned getOktatottOraszam() const { return oktatottOraszam; }

  void oratTart() const {
    cout << "Orat tart a PhD_hallgato" << endl;
  }

  virtual void orakatHallgat() const {
    cout << "Most a PhD hallato ment be egy orara" << endl;
    Hallgato::orakatHallgat();
  }

};

/*void oratHallgat(Hallgato hallgato) {
  hallgato.orakatHallgat();
}*/ //marad a statikus tipus szerinti hivas, hisz itt ertek szerinti parameter atadas volt

void oratHallgat(Hallgato& hallgato) {
  hallgato.orakatHallgat();
} // PhD_hallgato eseteben a PhD_hallgato osztalyban definialt orakatHallgat metodus hivodik

int main() {
  PhD_hallgato phd("Meme Joe", 5, 3);
  Hallgato hallgato("szimpla", 6);
  oratHallgat(phd);
  oratHallgat(hallgato);
}

Kimenet

Most a PhD hallato ment be egy orara
Orat latogat a Hallgato: Meme Joe 5
Orat latogat a Hallgato: szimpla 6

Referenciával történő paraméter átadás esetén a polimorfikus hívás megvalósul a virtual módosítóval ellátott metódusok esetében. Érték szerinti paraméterátadásnál ugyanakkor marad a statikus típusnak megfelelő hívás, hiszen itt a másolás miatt "elvesztette" az objektum azon plusz tulajdonságait, ami kellhet a felüldefiniált viselkedéshez.

Ha tehát polimorfikusan akarunk típusokat kezelni, az alábbiakat kell tennünk:

  • virtual kulcsszó a kívánt metódusokra
  • referencia (vagy pointer) használata

Override

A virtuális felüldefiniálás feltételei miatt kisebb elírások is ahhoz vezethetnek, hogy nem a megfelelő metódust írjuk felül. Ennek kivédésére meg tudjuk jelölni a felüldefiniáló metódust az override jelzővel, hogy a rendszer leellenőrizze, valóban a megfelelő metódus prototípust alkalmaztuk-e. Ha olyan metódust látunk el ezzel a jelzővel, melynek nincsen virtuális megfelelője valamelyik ős osztályban az öröklődési láncban, akkor fordítási hibát kapunk.

A példában a PhD_hallgato osztály orakatHallgat metódusát a legbiztonságosabban az alábbi módon írhatjuk meg (a virtual kulcsszó elmaradhat, az override viszont jelzi, hogy az ősben kell legyen ennek a metódusnak megfelelő virtuális metódus):

1
2
3
4
void orakatHallgat() const override {
  cout << "Most a PhD hallato ment be egy orara" << endl;
  Hallgato::orakatHallgat();
}

Pure virtual metódusok

Az előzőekben megismertük a virtualizáció lényegét, ezt egészítjük most ki a pure virtual fogalommal. Ahogy láttuk a virtualizált metódusoknak a viselkedése polimorfikusan felülírható. Az eddigi példáinkban minden esetben az ősben is meg volt valósítva az adott metódus, amelyet a gyerek osztály metódusa módosított.

Sokszor kerülhetünk azonban olyan helyzetbe, hogy az ős esetében az adott viselkedés nem definiálható egyértelműen, vagy éppen sehogy, ezért nincs is értelme ezen metódusokat definiálni. Ilyen esetekben annak sincs nagyon értelme persze, hogy magát az ős osztályt példányosítsuk, mert csakis a speciálisabb gyerek osztály fog a megfelelő adatokkal rendelkezni ahhoz, hogy az adott működést egyértelműen visszaadja. Ezzel az esettel találkoztunk a Java nyelvben is, ezek voltak az abstract metódusok és osztályok. 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, ami azt is eredményezi, hogy 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 EgyetemiPolgar {
    string nev;
    public:
    EgyetemiPolgar(const string& nev) : nev(nev) {}   // Csak ez az egy konstruktor van, oroklodes eseten meg kell hivni

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

Ahogy látszódik, a teendoketVegez 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 EgyetemiPolgar {
  string neptun;
  public:
    // A hallgato egy EgyetemiPolgar, azon reszet is inicializalni kell
    Hallgato(const string& nev, const string& neptun) : EgyetemiPolgar(nev), neptun(neptun) {}

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

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

Ahogy látszódik, a Hallgato 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 teendoketVegez pure virtual metódus.

1
2
3
4
5
6
7
class Gazdasagis : public Alkalmazott {
  public:
    Gazdasagis(const string& nev) : Alkalmazott(nev) {}
    void teendoketVegez() const override {
        cout << "A gazdasagis penzugyeket intez, ez a teendoje." << 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 teendoketVegez metódus. Mivel csak a Gazdaságis és a Hallgato 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 egyetemiPolgar(EgyetemiPolgar& ep) {
    ep.teendoketVegez();
}
void alkalmazott(Alkalmazott& a) {
    a.teendoketVegez();
    a.megbeszelesreJar();
}

Gazdasagis& gazdasagistGyart(Gazdasagis g) {
    // 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.

  EgyetemiPolgar *gazdasagis_egyetemiPolgarkent = new Gazdasagis("Gazdasagis aki egyetemi polgar");

  Hallgato hallgato("Egy hallgato", "ha57z1");
  egyetemiPolgar(gazdasagis);
  egyetemiPolgar(*gazdasagis_alkalmazottkent);
  egyetemiPolgar(*gazdasagis_egyetemiPolgarkent);
  egyetemiPolgar(hallgato);

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

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

Ahogy az látható, a Gazdaságis és Hallgato 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 EgyetemiPolgárként létrehozott gazdaságist nem tudjuk átadni Alkalmazottként, hiszen a rendszer csak annyit tud, hogy ott egy EgyetemiPolgár típus van (vagy annak egy leszármazottja). A futásidejű típus, és annak viselkedése már nem ismert.

Hibakezelés

A programok végrehajtása közben különféle hibákba futhatnak, amik egy részére felkészíthetjük programunkat. Lehetnek rendszerhez kapcsolódó hibák, például elfogy a memória, nincs megfelelő jogosultság egy erőforráshoz vagy akár felhasználói hiba, például amikor szám helyett szöveget adnak meg a konvertáló függvénynek. Ezeket a hibákat kezelni kell, különben definiálatlan viselkedéssel folytatódhat tovább a program futása.

Pédaként nézzünk egy egyszerű esetet hibakezelésre! Legyen adott egy sztringünk, amit szeretnénk számmá konvertálni. Amennyiben ez nem sikerül, a program végrehajtása megszakad, egy hiba érték dobódik, amely tetszőleges típusú adat lehet, amit a megfelelően el kell kapni és kezelni kell.

Az ilyen megjelenő objektumokat try-catch párossal tudjuk kezelni. Minden a try ágban lévő hibát a catch ágban/ágakban tudunk kezelni. Például:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>
#include <string>
using namespace std;
int main() {
  try {
    int a = stoi("nem integer");
    /*
      Kod a-val ami lefut ha minden jol ment, hiba eseten mar nem.
    */
  }
  catch(const invalid_argument& error) {
    cerr << "A hiba konvertalas soran: " << error.what() <<endl;
  }
}

Kimenet

A hiba konvertalas soran: stoi

Látható, hogy a try ágban megpróbálunk végrehajtani egy feladatot, de hibát kapunk. A catch ágban azonban több részletet is ki kell emelni:

  • const paraméter: A catch ág egy objektumot kap el, ami lehet konstans. Mivel nem jellemző, hogy mi magunk meg szeretnénk változtatni a hiba jellemzését, így biztosabban el tudjuk kapni, ha a paraméter elé mi is kitesszük a const kulcsszót.
  • Referencia a paraméter: A hibák típusát gyakran valamilyen osztály definiálja. Láttuk, hogy a polimorfikus feldolgozás miatt hasznos lehet a hibát is referenciaként megadni, így persze lekezelni is így kell.

  • what metódus: az előre beépített hibáknak van egy meghatározott metódusuk, amely jellemzi azokat. Ez a what.

std::exception

Legtöbbször a hibáknak külön osztályt hozunk létre. Ekkor ezeket az std::expection osztályból származtatjuk le. Ennek az osztálynak a lényege, hogy polimorfikusan általánosítsa a hibákat, itt található a virtuális what metódus. Mivel polimorfikusan használhatók a típusok, az előbbi példát std::exception típussal is elkaphattuk volna:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <string>

using namespace std;
int main() {
  try {
    int a = stoi("nem integer");
    /*
     Kod a-val ami lefut ha minden jol ment, hiba eseten mar nem.
    */
  }
  catch(const exception& error) {
    cerr << "A hiba konvertalas soran (exception tipussal): " << error.what() <<endl;
  }
}

Kimenet

A hiba konvertalas soran: stoi

Látható, hogy így is az 'stoi' üzenetet kapjuk, hiszen még mindig az std::invalid_argument hiba dobódott, csak polimorfikusan kezeltük.

Referencia használata nélkül azonban csak az 'std::exeption' üzenetet kapnánk, hiszen a polimorfizmus nem működik érték szerinti átadás során. ROSSZ HASZNÁLAT:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <string>
using namespace std;
int main() {
  try {
    int a = stoi("nem integer");
    /*
      Kod a-val ami lefut ha minden jol ment, hiba eseten mar nem.
    */
  }
  // FONTOS, HOGY ÉRTÉK SZERINT KAPTA EL A HIBÁT. HELYTELEN!
  catch(const exception error) {
    cerr << "A hiba konvertalas soran (exception tipussal): " << error.what() <<endl;
  }
}

Kimenet

A hiba konvertalas soran (exception tipussal): std::exception

Arra, hogy ez nem a legjobb módja a hiba elkapásának fordítási hibaüzenet is figyelmeztet, amit érdemes tekintetbe venni, hiszen adott esetben ez segítehet a hiba pontosabb beazonosításában, meghatározásában, illetve a hiba okának a megszüntetésében. Fordítótól függően ugyan, de valami ehhez hasonló üzenetet kaphatunk:

warning: catching polymorphic type ‘const class std::exception’ by value [-Wcatch-value=] catch(const std::exception error) {

Több catch ág

Több catch feltételt is írhatunk a try ághoz, hiszen több féle hiba is felléphet egy-egy művelet során. Ekkor ezek egymás után kerülnek tesztelésre, és fontos, hogy az eddig tanultakhoz hasonlóan az első illeszkedő kivételkezelő fogja kezeli a hibát (nem pedig a legjobban illeszkedő, tehát ha egyszer kapunk egy gyerek típusú hibát, de az őst elkapó kivételkezelő van a kódban előrébb, akkor minden esetben az ősé fog lefutni).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
using namespace std;
int main() {
  try {
    int a = stoi("999999999999999999999999999999999999999999999999999999999999999999999999");
    /*
      Kod a-val ami lefut ha minden jol ment, hiba eseten mar nem.
    */
  }
  catch(const invalid_argument& error) {
    cerr << "A hiba konvertalas soran (exception tipussal): " << error.what() <<endl;
  }
  catch(const out_of_range& error) {
    cerr << "A hiba kovertalas soran (masodik catch ag): " << error.what() <<endl;
  }
  catch(const exception& error) {
    cerr << "Altalanos exception kezeles: " << error.what() << endl;
  }
}

Kimenet

A hiba kovertalas soran (masodik catch ag): stoi

Látható, hogy a második catch ágban folytatódik a program futása, hiszen az std::invalid_argument exception polimorfikusan sem tudja kezelni az éppen eldobott std::out_of_range típusú kivételt. Ekkor továbbhalad a hiba a következő catch ágra, ami már tudja kezelni, így ott kezeljük le..

A példában felkészültünk egyéb hibák kezelésére is, nem csak a két konverziós hibára. Mivel az std::exception hibák nagyobb csoportjának ősosztálya, így ezzel tényleg sokféle hibát kezelhetünk. Pont ezért, ha ezt rossz helyre írjuk, azaz ha a catch ág, amiben ezt a hibát kezeljük, megelőzi az összes többit, akkor információt veszíthetünk. Ha előre írnánk, akkor minden hibát polimorfikusan elkapna az std::exception, így a specifikus catch ágakba sosem kerülne a vezérlés.

ROSSZ PÉLDA:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
using namespace std;
int main() {
  try {
    int a = stoi("999999999999999999999999999999999999999999999999999999999999999999999999");
    /*
     Kod a-val ami lefut ha minden jol ment, hiba eseten mar nem.
    */
  }
  catch(const exception& error) {    // ROSSZ! Minden hibat elkap polimorfikusan
    cerr << "Altalanos exception kezeles: " << error.what() << endl;
  }
  catch(const invalid_argument& error) {
    cerr << "A hiba konvertalas soran (exception tipussal): " << error.what() <<endl;
  }
  catch(const out_of_range& error) {
    cerr << "A hiba kovertalas soran (masodik catch ag): " << error.what() <<endl;
  }
}

Kimenet

Altalanos exception kezeles: stoi

Természetesen erre is figyelmeztet bennünket a fordító:

warning: by earlier handler for ‘std::exception’ catch(const exception& error) { // ROSSZ! Minden hibat elkap polimorfikusan

Saját hiba típus

Saját hibát is definiálhatunk és hasonlóan általánossá tehetjuk az eddig ismert hibákhoz, ha azt direkt, vagy indirekt módon az std::exception osztályból származtatjuk:

 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
47
48
49
50
#include <iostream>
#include <exception>
using namespace std;

class egyszeruSajatHiba : public exception {
  const char* what() const noexcept override {
    return "Ez egy egyszeru sajat hiba!";
  }
};

class sajatHiba : public std::exception {
  string message;
  int rossz_szam;
public:
  sajatHiba(const string& message, int bad_number = 0) : message(message), rossz_szam(bad_number) {}

  const char* what() const noexcept override {
    return message.c_str();
  }

  int getBadNumber() const {
    return rossz_szam;
  }
};

void hibaKiiratas(const exception& e) {
  cerr << e.what() << endl;
}

int oszt(int elso, int masodik) {
  if (masodik == 0) {
    throw sajatHiba("Nullaval valo osztas", masodik);
  }
  return elso / masodik;
}

int main() {
  egyszeruSajatHiba esh;
  hibaKiiratas(esh);

  sajatHiba sh("Ez a sajat hibam. Keszen all a hasznalatra");
  hibaKiiratas(sh);

  try {
    oszt(342, -34);
  } catch (sajatHiba& exception) {
    cerr << exception.what() << endl;
    cerr << "A hibat az alabbi szam okozta: " << exception.getBadNumber() << endl;
  }
}

Kimenet

Ez egy egyszeru sajat hiba!
Ez a sajat hibam. Keszen all a hasznalatra
Nullaval valo osztas
A hibat az alabbi szam okozta: -34

Az egyszeruSajatHiba típus csak a beépített what metódust írja felül, hogy a hiba kiíratásakor megjelenő szöveg az általunk megadott legyen (ez megoldható volna az exception példányosításával, azonban így lesz saját hibatípusunk, így saját hibakezelőt is írhatunk az általunk definiált típushoz). Mivel a sajatHiba már általános std::exception-ként is használható csak a használata marad hátra. Nem elegendő megkonstruálnunk, el is kell dobni. Az eddigieken kívül természetesen a hiba objektumok is ugyanolyan objektumok, mint bármelyik másik, lehetnek adattagjai, különböző konstruktorai, metódusai (ahogy látjuk a gettereket is).

noexcept kulcsszó

A what felüldefiniálása során meg kellett adni egy noexcept nevű tulajdonságot. Ez azt jelzi, hogy a metódus biztosan nem fog hibát eredményezni, nem kerül sor újabb exception eldobására annak futása során.

throw kulcsszó

Ha egy folyamat végrehajtása során hibába fut a program, a programozónak kell megoldania, hogy egy hiba eldobásra kerüljön. Ezt a throw kulcsszóval tehetjük meg. Meg kell adni, hogy mi az amit el szeretnénk dobi. Ez lehet pl. a sajatHiba egy példánya.

 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
#include <iostream>

class sajatHiba : public std::exception {
  std::string message;
public:
  sajatHiba(const std::string& message) : message(message) {}

  const char* what() const noexcept override {
    return message.c_str();
  }
};

void foo() {
  // sok-sok futas

  // hiba:
  throw sajatHiba("Valami hibas volt foo futasa kozben.");
}

int main() {
  try {
    foo();
  }
  catch(const sajatHiba& sh) {
    std::cerr << sh.what() << std::endl;
  }
}

Kimenet

Valami hibas volt foo futasa kozben.


Utolsó frissítés: 2024-08-01
Létrehozva: 2024-07-23