Objektumok élete
Objektumok kezelése¶
Java esetében láttuk, hogy a program futásakor a memória különböző területeit használjuk. Ismétléshez érdemes megnézni ezt a tananyag részt. Az eddigiekben, amikor létrehoztunk egy-egy objektumot, akkor azt lokálisan tettük meg. Azaz minden objektumunk a stacken keletkezett, és az objektum addig élt, amíg az adott blokkban/szkópban járt a program végrehajtása, ahol az objektumot létrehoztuk. A blokk megszűnésével a blokkban létrehozott objektumok felszabadultak, a destruktoraik meghívódtak, és a memória terület, amelyen korábban az objektum elhelyezkedett, felszabadult.
Ha nagyon fontos az, hogy egy objektum sokáig elérhető lenne, akkor persze definiálhatjuk azt a globális névtérben, globális változóként is. Ekkor élettartalma nyilván megegyezik a program élettartalmával. Ugyanakkor nem szerencsés valamennyi objektumunkat így tárolni, mert a feleslegesen életben tartott, de nem használt objektumok memória gondokhoz vezethetnek.
Természetesen C++-ban is van arra lehetőség, hogy olyan objektumokat hozzunk létre, amelyek "túlélik" az őket létrehozó blokkokat, de mégsem globálisak.
Ezeknél létrehozásukkor jelezni kell, hogy ezek speciálisan, dinamikusan létrehozott objektumok, illetve biztosítani kell azt is, hogyha már nincs ezekre az objektumokra szükség, azt jelezzük, és felszabadítsük ezeket.
Java esetében ezt megtette helyettünk a garbage collector, C++ esetében a memória felszabadítás viszont a programozó feladata.
Dinamikus memóriakezelés¶
Dinamikus memóriakezelésére a heap memórián van lehetőségünk. Ez a stackkel szemben sokkal nagyobb méretű, igényelni kell a memóriát, lassabb és nem automatikus kezelésű. Ahhoz, hogy ezt a memória területet elérjük, pointer(ek)re van szükségünk. Memória foglalás során ennek a pointernek adunk értéket, mely a kapott (számunkra lefoglalt) memória kezdőcímére mutat. Ez a visszaadott érték void*
típusú, mely minden pointer-típusra konvertálható.
C-ben ezt a malloc, calloc, realloc függvényekkel tehetjük meg ezen memória foglalásokat.
C++-ban ezek helyett használhatjuk a new
operátort.
Ez is void*
típussal tér vissza sikeres memóriafoglalás esetén.
Hiba esetén std::bad_alloc
kivétel dobódik. Használata: T *var = new T;
, ahol T
egy típus.
Példa C++ dinamikus memória foglalásra, egy primitív típus esetén.
1 2 3 4 5 6 7 8 |
|
A példában a használt new
operátor kivételt is dobhat, ha sikertelen a memória allokáció, pl. nincs elegendő memória. Ekkor a kivételt vagy elkapjuk, vagy hagyjuk tovább dobódni. Ha nem szeretnénk a kivételkezeléssel foglalkozni, akkor használhatjuk a new
exception-mentes változatát. Ekkor a hibát nem kivétel dobással jelzi felénk a new
, hanem a visszatérési érték egy üres pointer (null pointer) lesz (mint pl. a C-beli malloc). A new
kivétel mentes változata: T *var = new (nothrow) T
, ahol T
egy típus.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
A fenti példában nem kell foglalkoznunk az esetleges kivételek kezelésével, azonban ha nem vizsgáljuk meg a visszatérési értéket, null pointer dereferálásba futhatunk!
A new
operátort használhatjuk egy-egy elem foglalására a heap-en, azonban legtöbbször több elemet szeretnénk lefoglalni. Ezt is megtehetjük a new[]
operátorral. Használata: T *var = new T[N]
, ahol N
egy nemnegatív egész szám, T
egy típus. Példa new[]
operátorra:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
A példában megpróbálunk memóriát foglalni 1000000000 méretű int
tömbnek.
Ha ennyit nem tud biztosítani a rendszer, bad_alloc
kivételt kapunk.
Ha sikerül, akkor használhatjuk indexelve, mint egy szokásos tömböt.
Természetesen a try-catch
kiírása nem kötelező, de kivétel esetén nem tudjuk azt lekezelni.
Ha nem szeretnénk kivételt kapni, itt is alkalmazható a nothrow
változat.
A dinamikusan foglalt memória nem szabadul fel automatikusan, erről nekünk kell gondoskodnunk! Ha nem tesszük meg, memory-leak
kerül a programba (vagyis a program egyszerűen felzabálja a memóriát, amely rossz esetben a számítógép működésének összeomlásához vezethet, hiszen ilyenkor a program elkezd swappelni, azaz a program elkezdi a memória tartalmát a merevlemezre kiírni - mert már nem fér a memóriába a sok adat-, ami gyötrelmesen belassíthat mindent).
A lefoglalt memóriát a delete
és a delete[]
operátorral szabadíthatjuk fel. A delete
a new
által foglalt memóriát a delete[]
a new[]
által foglalt memóriát szabadítja fel.
Ha megpróbálnánk new[]
által foglalt tömböt a delete
operátorral felszabadítani, a viselkedés definiálatlan (de általában lefut a program).
Példa memória felszabadításra:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Dinamikus memóriakezelés összefoglaló¶
Lefoglalt cella | operátor | hibakezelés | felszabadítás |
---|---|---|---|
Egy cella | new | try-catch | delete |
Több cella | new[] | try-catch | delete[] |
Egy cella | new (nothrow) | visszatérési érték. if(pointer) | delete |
Több cella | new (nothrow) | visszatérési érték. if(pointer) | delete[] |
Osztályok dinamikus adattagjai¶
Sokszor a dinamikus memóriakezelés nem egy-egy függvényhez köthető, hanem az osztályok az általuk reprezentált entitások tárolásához is dinamikusan foglalnak memóriát. Ekkor a memória foglalást használhatjuk ugyan úgy, mintha csak lokális változónak adnánk értéket, akár inicializáló listában is.
Az előző anyagban a Kurzus
osztálynak volt egy Hallgato
tömbje, mely fix méretű volt. Ebben az esetben nem tudtuk volna valóban növelni a kurzus létszámát, erre megoldás a dinamikus memóriakezelés.
Kurzus
osztály dinamikus Hallgato
tömbbel:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Destruktor ismét, avagy hogyan kezeljük a lefoglalt memóriát¶
Mivel a dinamikus memória nem szabadul fel automatikusan, nekünk kell ezt megtennünk. Azonban ezt objektumok esetében nehéz megmondanunk, hogy mikor tegyük meg. Bármikor kellhet az objektumnak az adott memória terület. Írhatnánk egy metódust, melyet meghívva felszabadítjuk a memóriát, azonban ekkor nincsen garancia, hogy utána nem használják az objektumot vagy nem felejti el a programozó.
A most bevezetett destruktor azonban pont egy olyan metódus, mely lefutása csakis az objektum életének végén megy végbe és utána biztosan nem használhatjuk az objektumot. A destruktorban történő memória felszabadítás megfelelő megoldás. Ezt olyan szinten vehetjük megoldásnak, hogy ha dinamikusan foglalunk memóriát az objektumunkban, akkor azt legkésőbb a destruktorban fel kell szabadítani (kivétel, ha egy másik objektum "felel" azért). Minden new
-hoz köthető egy delete
.
Példa destruktorban történő felszabadításra:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Öröklődés vs. tárolók újragondolva¶
Térjünk vissza egy korábbi példánkhoz, ahol adottak voltak az egyetem alkalmazottai. Legyenek kutatók is, őket a Kutató osztály reprezentálja. Egy kutató alkalmazott is. A munkásságát egy dinamikusan létrehozott int
tömb jegyzi. Egy-egy int
az adott munkásságának fontosságát jelölje! ezek a sima alkalmazottakra jellemző adatok mellett rendelkeznek külön munkássággal is, amelyet most egyszerűség kedvéért csak egy int
tömbbel reprezentálunk, amelynek méretét (munkak
), és kezdőcímét (munkassag
) tárolja el a Kutato
osztály:
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 |
|
Az Egyetem
az alkalmazottait nyilvántartja, legyen az egyszerű alkalmazott, vagy kutató.
1 2 3 4 5 6 7 8 |
|
Korábban már több megoldást láttunk, amelyek bár egyes esetekben működtek, nem bizonyultak minden esetben jó megoldásnak. A legegyszerűbb eset az, ha az egyetem Alkalmazott
ak egy tárolóját tartja nyilván. Azaz:
1 2 3 4 5 6 7 8 9 |
|
Ennek a megoldásnak az előnye, hogy mivel a vektorba kerüléskor az objektum másolódik, így az eredeti objektum élettartalmától független is elérhető a lemásolt objektum. Ugyanakkor a megoldás hátránya, hogy minden esetben a másolás csak az objektum azon részére történik meg, amely az Alkalmazott
osztály része, azaz egy Kutato
esetében annak speciális része elveszik, a polimorfizmus lehetősége nem kerül elő, innentől a kutató is csak egyszerű alkalmazottként van számontartva.
Másik megoldási lehetőség volt a reference_wrapper használata:
1 2 3 4 5 6 7 8 9 |
|
Ez a megoldás már megtartja a poliformizmus lehetőségét, ugyanakkor használata körültekintést igényel, az eltárolt objektumok élettartalmát nem vizsgálja ez a megvalósítás, és elképzelhető, hogy az eltárolt objektum adott esetben már nem létezik (pl. mert stacken létrehozott lokális objektum volt), amelyre való hivatkozás ezek után invaliddá válik.
A legjobb megoldás, amit alkalmazhatunk az, ha az Alkalmazott
objektumokat minden esetben dinamikusan allokáljuk, és eltárolásukat a rájuk mutató pointerekkel oldjuk meg:
1 2 3 4 5 6 7 8 9 |
|
Ezáltal lehetőségünk van az alkalmazottakat polimorfikusan kezelni, ugyanakkor ha az alkalmazottak kezelését teljesen az Egyetem
osztályra bízzuk, akkor biztosak lehetünk benne, hogy a pointerek eltárolása biztonságos lesz, és nem kerülünk abba a helyzetbe, hogy esetlegesen amikor hivatkoznánk az adott alkalmazott objektumra, az már invalid hivatkozásnak minősül.
Virtuális destruktor¶
Azt láttuk, hogyha 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 ahhoz, hogy a polimorfizmus érvényesülni tudjon, a metódust virtuálissá kell tenni. Az alábbi példában amegbeszeles
metódus végig járja az alkalmazottakat és mindegyikre a nekik megfelelő megbeszelesreJar
metódust hívja meg.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Kimenet
A kutatoi munka igen sok eszmecseret igenyel mas kutatokkal.
Alkalmazott megbeszelesre ment.
Amikor az egyetem osztály objektuma megszűnik, akkor mivel ez az objektum kezeli az Alkalmazott
-akat, így neki kell megszüntetni az alkalmazottak sorát, egyesével meghívva valamennyi elemre azok destruktorát.
Mivel aKutato
osztály definiál dinamikusan létrehozott adattagot, így annak megszüntetéséről gondoskodni kell a destruktorban.
Az Egyetem
osztály destruktorában kell meghívni az eltárolt alkalmazottak destruktorait. Amennyiben azonban az Alkalmazott
osztály destruktora nem virtuális, úgy itt sem érvényesül a polimorfizmus, így a Kutato
objektumokhoz rendelt munkassag
adattag által hivatkozott elemek nem fognak törlődni, ezáltal memóriaszemetet hagyva.
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
Valósítsuk meg a példát teljesen, hogy az elvárt viselkedésnek megfelelően működjön:
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 |
|
Kimenet
A kutatoi munka igen sok eszmecseret igenyel mas kutatokkal.
Alkalmazott megbeszelesre ment.
A Kutatonak ennyi munkassaga volt: 10
Alkalmazott destruktor
Alkalmazott destruktor
Létrehozva: 2020-09-09