Kihagyás

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
#include<iostream>
using namespace std;

int main() {
  int *p = new int;
  cin >> *p;
  cout << "A " << p << " a címen lévő érték: " << *p << endl;
}

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
#include<iostream>
using namespace std;

int main() {
  int *p = new (nothrow) int;
  if (p == nullptr) { // vagy lehetne if (!p), ami null pointer esetén igazra értékelődne ki
    cerr << "Nem sikerült memóriát foglalni." << endl;
    return 1;
  }
  cin >> *p;
  cout << "A " << p << " a címen lévő érték: " << *p << endl;
}

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
#include<iostream>
using namespace std;

int main() {
  int *array;
  try {
    array = new int[1000000000];
  } catch (bad_alloc& error) {
    cerr << "Nem sikerult a memoria foglalas" << endl;
    return 1;  
  }
  cin >> array[0] >> array[1000000000-1];
  cout << array[0] << " " << array[1000000000-1] << endl;
}

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

using namespace std;

int main() {
  int* array = new int[10];
  array[0] = 1;
  // Feltöltjük és használjuk a tömböt

  int* p = new int;
  // Beállítjuk a p-t és használjuk

  // Nincs szükségunk már a pointerekre, ezért a memóriát visszaszolgáltatjuk
  delete p; // new-val foglalt
  delete[] array; //new[]-bel foglalt (a méretet nem kell megadni)
}

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
class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  // Hallgato hallgatok[10]; Ez volt korábban, de ennek fix a mérete
  Hallgato* hallgatok;
public:
  Kurzus(const string& nev, const string& kod) :
    nev(nev),
    kod(kod),
    hallgatok(new Hallgato[10]) // dinamikus memória foglalás 10 Hallgato-nak -> hallgatok = new Hallgato[10];
  {
  }
};

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
class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  // Hallgato hallgatok[10]; Ez volt korábban, de ennek fix a mérete
  Hallgato* hallgatok;
public:
  Kurzus(const string& nev, const string& kod) :
    nev(nev),
    kod(kod),
    hallgatok(new Hallgato[10])  //dinamikus memória foglalás 10 Hallgato-nak -> hallgatok = new Hallgato[10];
  {
  }

  ~Kurzus() {
    delete[] hallgatok;  //a lefoglalt eroforras felszabaditasa. delete[] mivel new[] volt.
  }
};

Ö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
class Alkalmazott  {
  public:
    const string& nev;
    Alkalmazott(const std::string& nev) : nev(nev){}
    ~Alkalmazott() {
        cout << "Alkalmazott destruktor" <<endl;
    }
    virtual void megbeszelesreJar() const {
      cout << "Alkalmazott megbeszelesre ment." << endl;
    }
};

class Kutato : public Alkalmazott {
  unsigned munkak;
  int* munkassag = nullptr;
public:
  void megbeszelesreJar() const override { // hiszen Alkalmazott
    std::cout << "A kutatoi munka igen sok eszmecseret igenyel mas kutatokkal." << 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 Egyetem az alkalmazottait nyilvántartja, legyen az egyszerű alkalmazott, vagy kutató.

1
2
3
4
5
6
7
8
class Egyetem {
    ????? alkalmazottak;
public:
    Egyetem() = default;
    Egyetem& operator+(const Alkalmazott& alkalmazott) {
        //TODO alkalmazottak koze felvenni a paraméterben kapott alkalmazottat!
    }
};

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 Alkalmazottak egy tárolóját tartja nyilván. Azaz:

1
2
3
4
5
6
7
8
9
class Egyetem {
  vector<Alkalmazott> alkalmazottak;
public:
  Egyetem() = default;
  Egyetem& operator+(const Alkalmazott& alkalmazott) {
    alkalmazottak.push_back(alkalmazott);
    return *this;
  }
};

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
class Egyetem {
  vector<reference_wrapper <const Alkalmazott>> alkalmazottak;
public:
  Egyetem() = default;
  Egyetem& operator+(const Alkalmazott& alkalmazott) {
    alkalmazottak.push_back(alkalmazott);
    return *this;
  }
};

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
class Egyetem {
  vector<Alkalmazott*> alkalmazottak;
public:
  Egyetem() = default;
  Egyetem& operator+(Alkalmazott* alkalmazott) {
    alkalmazottak.push_back(alkalmazott);
    return *this;
  }
};

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
class Egyetem {
  vector<Alkalmazott*> alkalmazottak;
public:
  Egyetem() = default;
  Egyetem& operator+(Alkalmazott* alkalmazott) {
    alkalmazottak.push_back(alkalmazott);
    return *this;
  }

  void megbeszeles() const {
    for(const auto& alk : alkalmazottak)
      alk->megbeszelesreJar();
  }
};

int main() {
  Egyetem e;
  Alkalmazott *a = new Alkalmazott("Bela");
  Kutato *k = new Kutato("Tudos", 10);
  e+k+a;
  e.megbeszeles();
}

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
#include <iostream>
#include <vector>;
#include <algorithm>
using namespace std;
class Alkalmazott  {
public:
  const string& nev;
  Alkalmazott(const std::string& nev) : nev(nev){}
  virtual ~Alkalmazott() {
    cout << "Alkalmazott destruktor" <<endl;
  }
  virtual void megbeszelesreJar() const {
    cout << "Alkalmazott megbeszelesre ment." << endl;
  }
};

class Kutato : public Alkalmazott {
  unsigned munkak;
  int* munkassag = nullptr;
public:
  void megbeszelesreJar() const override { // hiszen Alkalmazott
    std::cout << "A kutatoi munka igen sok eszmecseret igenyel mas kutatokkal." << 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[]
  }
};

class Egyetem {
  vector<Alkalmazott*> alkalmazottak;
public:
  Egyetem() = default;
  ~Egyetem() {
    for(int i=0; i<alkalmazottak.size();i++)
      delete alkalmazottak[i];
  }
  Egyetem& operator+(Alkalmazott* alkalmazott) {
    alkalmazottak.push_back(alkalmazott);
    return *this;
  }

  void megbeszeles() const {
    for(const auto& alk : alkalmazottak)
      alk->megbeszelesreJar();
  }
};

int main() {
  Egyetem e;
  Alkalmazott *a = new Alkalmazott("Bela");
  Kutato *k = new Kutato("Tudos", 10);
  e+k+a;
  e.megbeszeles();
}

Kimenet

A kutatoi munka igen sok eszmecseret igenyel mas kutatokkal.
Alkalmazott megbeszelesre ment.
A Kutatonak ennyi munkassaga volt: 10
Alkalmazott destruktor
Alkalmazott destruktor


Utolsó frissítés: 2024-11-20
Létrehozva: 2020-09-09