Ö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 |
|
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 |
|
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:
Öröklődés láthatósága¶
1 |
|
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 |
|
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 |
|
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 |
|
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 Hallgato
t várnak, és PhD_hallgato
t 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 |
|
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.
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.
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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ár
ként létrehozott gazdaságist nem tudjuk átadni Alkalmazott
ké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 |
|
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 aconst
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 awhat
.
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
Kimenet
Valami hibas volt foo futasa kozben.
Létrehozva: 2024-07-23