18
Dec
2009

Acest articol isi propune sa explice care este ordinea in care sunt executati constructorii si destructorii claselor in C++, si sa arate prin exemple de ce declararea destructorilor "virtuali" este o practica recomandata. De asemenea voi exemplifica executia atunci cand o clasa are un membru care este instantierea unei alte clase.

In C++ obiectele sunt construite impreuna cu parintii (clasele sau structurile din care deriva) si toate celelalte obiecte pe care le contin in urmatoarea ordine:

  1. se aloca memorie pentru obiectul ce urmeaza sa se construiasca
  2. se ruleaza constructorul pentru clasa de baza
  3. membrii clasei sunt construiti in ordinea in care au fost declarati
  4. se ruleaza constructorului clasei derivate

Pentru distrugerea unui obiect se efectueaza urmatoarele:

  1. se ruleaza destructorul clasei
  2. membrii sunt distrusi in ordinea inversa a construirii
  3. se ruleaza constructorul clasei de baza
  4. se elibereaza memoria alocata pentru obiect

Voi incerca sa exemplific cu urmatoarea ierarhie de clase:

class A
{
public:
   A(){std::cout << "A fost rulat Constructorul lui A" << std::endl;}
   ~A(){std::cout << "A fost rulat Destructorul lui A" << std::endl;}
};

class B : public A
{
public:
   B(){std::cout << "A fost rulat Constructorul lui B" << std::endl;}
   ~B(){std::cout << "A fost rulat Destructorul lui B" << std::endl;}
};

class C : public B
{
public:
   C(){std::cout << "A fost rulat Constructorul lui C" << std::endl;}
   ~C(){std::cout << "A fost rulat Destructorul lui C" << std::endl;}
};
int main()
{
    C obj;

    return 0;
}

Programul de mai sus va afisa urmatorul rezultat:

A fost rulat Constructorul lui A
A fost rulat Constructorul lui B
A fost rulat Constructorul lui C
A fost rulat Destructorul lui C
A fost rulat Destructorul lui B
A fost rulat Destructorul lui A

Rezultatul este conform celor enuntate mai sus: la creare, se apeleaza mai intai constructorul clasei de baza, apoi al celei derivate (adica A, apoi B, apoi C), iar la distrugere mai intai destructorul clasei derivate si apoi al clasei de baza (adica in cazul nostru mai intai C, apoi B, apoi A).

Daca modificam programul de mai sus pentru a arata astfel:

int main()
{
    A* obj = new C();
    delete obj;
    
    return 0;
}

atunci programul va afisa doar

A fost rulat Constructorul lui A
A fost rulat Constructorul lui B
A fost rulat Constructorul lui C
A fost rulat Destructorul lui A

Se observa ca la distrugere, destructorul pentru B respectiv C nu au mai fost apelati. Motivul este ca obj este un pointer care indica spre un obiect de tip A, desi la adresa respectiva se afla de fapt un obiect de tip C. La distrugerea lui obj, se distruge un obiect de tip A, nu C. Prin urmare singurul destructor care este apelat e A. Aici apare o problema, intrucat B si C nu se mai distrug. Prin urmare, din obiectul mare C, doar acea parte care este A se distruge. Daca B si C ar aloca memorie pe heap care se sterge in destructor, sau ar aloca resurse ce urmeaza sa fie eliberate in destructor, acestea ar ramane ne-eliberate. Aceasta problema este cunoscuta sub numele de "object slicing".

Pentru a rezolva aceasta situatie, destructorul unei clase de baza trebuie sa fie declarat intotdeauna virtual. De fapt una din modificarile care se doresc pentru noua versiune a standardului C++0x este considerarea in mod implicit a destructorului unei clase de baza ca fiind virtual, fara a fi necesar sa fie marcat explicit ca virtual.

Daca modificam pe A astfel incat sa arata asa:

class A
{
public:
   A(){std::cout << "A fost rulat Constructorul lui A" << std::endl;}
   virtual ~A(){std::cout << "A fost rulat Destructorul lui A" << std::endl;}
};

atunci programul de mai sus, cu obj alocat pe heap, va avea acelasi rezultat ca si primul (cand se construia un obiect C pe stiva).

Spuneam mai sus ca daca destructorul clasei de baza nu e virtual si destructorii claselor derivate nu se mai apeleaza, dar acele clase aloca memorie ce trebuie eliberata in destructor, atunci ajungem sa avem "scurgeri de memorie". In exemplul de mai jos, clasa Copil este derivata din Parinte, si are un membru Avere. Acest obiect se aloca dinamic in constructor si se elibereaza in destructor.

class Avere 
{
public:
   Avere (): cont(10000) { std::cout << "Avere " << std::endl;}
   ~Avere () { std::cout << "~Avere " << std::endl;}
protected:
   int cont;
};

class Parinte 
{
public:
   Parinte() { std::cout << "Parinte " << std::endl;}
   ~Parinte () { std::cout << "~Parinte " << std::endl;}
};

class Copil : public Parinte 
{
public:
   Copil (): cec(new Avere) { std::cout << "Copil " << std::endl;}
   ~Copil () { std::cout << "~Copil " << std::endl; delete cec;}
protected:
   Avere* cec;
};

Daca executam programul de mai jos, doar partea Parinte a obiectului creat se distruge. Partea Copil nu se mai distruge, iar obiectul Avere ramane nefolosit pe heap.

int main()
{
   Parinte *ptr = new Copil;
   std::cout << std::endl;

   delete ptr;
   std::cout << std::endl;

   return 0;
}
Parinte
Avere
Copil

~Parinte

Se observa ca obiectul Avere se construieste inaintea obiectului Copil, adica ceea ce spuneam la inceput, ca mai intai se construiesc membrii si apoi ruleaza codul pentru constructorul clasei curente.

Pentru a rezolva problema de mai sus, trebuie marcat desctructorul Parinte ca fiind virtual. In acest caz probramul va afisa:

Parinte
Avere
Copil

~Copil
~Avere
~Parinte

Este important de cunoscut asadar ordinea construirii si distrugerii obiectelor intr-o ierarhie, iar marcarea destructorului unei clase de baza ca fiind virtual este necesara pentru a asigura distrugerea corecta a intregului obiect in orice situatie.