9. gyakorlat¶
Ismétlés¶
Hibakezelés¶
A programok futás 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.
Hibakezelésre láttunk már példát a stringek számmá konvertálásakor és a dinamikus memóriakezeléáskor. Ekkor a hibát egy Exception jelezte, hogy a program végrehajtása során hiba lépett fel. Ekkor nem fut le teljesen a függvény (megszakad a futás a hiba eldobásakor), nem adja vissza a megfelelő értéket, hanem jelzi, hogy adott egy objektum, az tartalmazza a hiba részleteit, onnantól azzal kell foglalkozni.
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 |
|
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 kel emelni:
- const: A catch ág egy objektumot kap el ami lehet konstans, hiszen nem akarjuk megváltoztatni a hiba jellemzését, ezért amikor elkapjuk const típust kell megadni
- referencia: A hiba objektumok gyakran osztályok (erről később) ezért lehet öröklődési viszony köztük. Ahhoz, hogy az ilyen öröklődés polimorfikusan tudjon viselkedni, referenciát kell használnunk.
- 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 |
|
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 |
|
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 lineárisan 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 |
|
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 exceptiont. Ekkor továbbhalad a hiba a következő catch ágra, ami már tudja kezelni, így onnan nem halad tovább.
Látható, hogy felkészültünk egyéb hibák kezelésére is, nem csak a két konverziós hibára. Ha ezt rossz helyre írjuk, 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 |
|
warning: by earlier handler for ‘std::exception’ catch(const std::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 az std::exception osztályból (vagy egy leszármazottjábó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 |
|
Az egyszeru_sajat_hiba 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 sajat_hiba 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 sajat_hiba 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 |
|
Nem osztály hibák¶
A hibakezelés elején említésre került, hogy általában osztálytípusúak a hibák, azonban ez C++ esetében nem feltétlenül kell így legyen (Java esetén csak így lehetett). Bármit el lehet dobni, bármilyen értéket; legyen az _const char, _std::string, int, stb. Ezeket is el lehet kapni, azonban ekkor már nem használható az exception elkapása, hiszen ezek nem leszármazottjai az std::exception* osztálynak (és nem is konkrétan exception típusúaka). Ekkor a típusukat kell használnunk. Természetesen ezeket is elkaphatjuk referencia szerint, azonban primitív típusok esetén ez nem szükséges.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Minden hiba kezelése¶
Látható, hogy egyes hibák nem tartoznak az std::exception alá, így általánosan nem kezelhetők. Azonban ha szeretnénk megoldani, hogy biztosan elkapjunk minden hibát, minden lehetséges hibatípusra kell catch ágat írnunk. Azonban ez nem biztos, hogy minden esetben lehetséges (vagy azért mert rengeteg féle-fajta hiba van, amiket azonosan akarunk kezelni, vagy pedig azért mert nem is tudjuk pontosan, hogy milyen hibákra kell felkészíteni a programunkat). Azonban ezt is megoldhatjuk, ha elkapáskor a típusok helyett egyszerűen csak ...
(három pontot) írunk. Ezzel bármit el tudunk kapni a catch ágban.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
RAII (Resource acquisition is initialization)¶
Ez a fogalom annyit jelent, hogy az erőforráso foglalás az inicializálás során történik meg. A név ha nem is a legegyértelműbb, széles körben elterjedt.
Használata a hibakezeléshez köthető, méghozzá erőforrás használatakor. Legyen erre példa a következő:
-
try ágban foglaljunk erőforrást
-
hajtsuk végre a try ágat
-
exception dobódik
-
catch ág kezeli a hibát
Ekkor azonban a catch ágra ugrik azonnal a vezérléás, több esetén csakis az egyikre. Ha a try blokk végén volt erőforrás felszabadítás, az nem futott le. Javaban erre a megoldás a finally ág volt, mely minden esetben, hibátlan esetleg hibás (bármilyen) lefutás esetén végrehajtásra került; ezzel biztosítva az erőforrás felszabadítását. Ilyen C++ esetében nincsen, azonban pont erre megoldás a RAII és a feltételezés, hogy az erőforrások objektumok, melyek konstruktorral kerülnek inicializálásra és fontosabb, hogy destruktor hívódik megszűnésükkor.
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 60 61 |
|
Látható, hogy a try blokkban erőforrást egy objektum létrehozásával foglalunk, majd használjuk, de a felszabadítás előtt hiba exception dobódik. Ennek ellenére nincsen szükség finally ágra, hiszen a destruktor láthatóan így is lefutott. Mivel a try blokkot elhagytuk, a catch ágba kerültünk, így az objektum megszűnt, ami a destruktor lefutását eredményezte.
Smart pointerek¶
A pointerek használatának egy jelentős hátránya, hogy a memóriafelszabadítást a fejlesztőnek kell megoldania és egy - egy felszabadítás hiánya jelentős problémát okozhat. A smart pointerek használata ezt a felelősséget könnyíti, hiszen a memória foglalás és felszabadítás automatikusan történik.
A smart pointerek úgynevezett template osztályok. Számunkra ez azt jelenti, hogy használatukkor meg kell adni, hogy milyen típussal akarjuk használni a smart pointert '<' és '>' jelek között. Használatukhoz a memory header-t kell include-olni.
Pointerek használatakor a következő folyamatosak a lényegesebbek: - memóriafoglalás - másolás - memóriafelszabadítás
Smart pointerek használatával magát a pointert egy wrapper osztályba zárjuk és az adott osztály példányosításával hozunk létre pointert. Tehát ezek az objektumok nem dinamikus memóriát használó típusoknak értelmezendők (mint pl. Kurzus ahol a Hallgatókat tároltuk), hanem egy konkrét pointer típus helyett használandók. Az osztályoknak meg vannak valósítva a *
és ->
operátorai, így a szokványos pointer szintaxissal használhatjuk.
Memóriafoglalás: A smart pointer objektum példányosításakor lehetőség van memóriafoglalásra. (A smart pointerhez egyéb módon is foglalható memória, most csupán a konstruktoron keresztüli foglalást tárgyaljuk.) A memóriafoglalás után a smart pointer folyamatosan kezeli a memóriát.
Memóriafelszabadítás: Smart pointer objektumoknak is van élettartama, akárcsak egy általános objektumnak. Mikor egy objektum megszűnik, azt többé nem hivatkozhatjuk, így egy pointer esetében megszűnése előtt fel kell szabadítanunk a memóriát, különben meglévő referencia nélkül nem tudjuk, hogy melyik memóriaterületet kellene felszabadítani. Az objektum megszűnésekor lefut a destruktor, így a smart pointer automatikusan felszabadítja a foglalt memóriát.
Másolás: A dinamikus memória résznél a legtöbb feladatot a másolás okozott, hiszen deep-copy-t szerettünk volna elérni. Pointer másoláskor alap esetben nem foglalunk memóriát, hiszen csak értékeket állítunk be. (Ezért nagyon fontos, hogy nem dinamikus memóriát használó objektumról van szó, hanem pointer helyettesítőről.) A másolás viselkedése assignemnt operátor és másoló konstruktor használatával adható meg.
Unique pointer¶
Gyakran arra van szükség, hogy egy memóriaterületet csak egy változón keresztül érhessünk el. Ekkor std::unique_ptr
típust használhatunk.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
A fenti példában is látható, hogy a másolás nem megengedett unique pointer esetében, ezzel is biztosítva, hogy egy memóriacím nem tartozik két objektumhoz (változó). A másolás azonban gyakran igény ekkor is, - legyen ez egy ideiglenes memória beállításakor -, így ezt is meg kell oldani. Ebben az esetben a feladatunk az eredeti foglalt memória felszabadítása és a memóriacím átállítása volt. Amire nem kellett figyelni, hogy a lemásolandó elemet többet ne tudjuk használni a memória elérésére.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Shared pointer¶
Shared pointer esetében azt várjuk, hogy a memóriacímet több objektum (változó) is hivatkozhassa. Ekkor a megoldandó feladat, hogy a memória csak akkor legyen felszabadítva, ha már egy objektum (változó) sem hivatkozza a memóriacímet. Shared pointer esetében ez megoldott, így ezzel nem kell foglalkoznunk.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|