Curs C++ | Sectiunea 35 – Constructor si Destructor

 

🧰 O functie cu un nume identic cu ce al clasei poarta numele de constructor. Acesta are rolul de a construi obiectul in momentul crearii, spre exemplu initializarea variabilelor, alocarea de memorie etc.

💡 Constructorul poate accesa toate componentele clasei, precum o metoda normala, insa nu poate fi invocat direct. Mai mult, nu trebuie declarat cu un tip de return, nici macar tipul void.

Spre exemplu, folosind clasa creata in sectiunea anterioara, adaugam un constructor, aceasta va arata astfel:

class A {
private:
int value;
public:
A() { this->value = 0; };
int getValue() { return value; };
void setValue(int value) {
this->value = value;
}
void setValue() {value = 10;}
};

Clasa este echipata cu un constructor numit A (precum numele clasei) si are rolul de a initializa membrul value. In momentul crearii unui obiect in felul urmator, constructorul va fi invocat implicit.

A myObj;
std::cout << myObj.getValue(); //0

Atentie, chiar daca repetam aceasta informatie, este interzisa apelarea directa a constructorului, fie prin intermediul unui obiect ori direct prin operatorul scope.

myObj.A();
A::A();

Apelarile de mai sus vor rezulta in erori precum invalid use of ‘A::A’ sau cannot call constructor ‘A::A’ directly.

 

Supraincarcarea Constructorului

Bineinteles, constructorii pot fi la randul lor supraincarcati, in functie de nevoile specifice.

class A {
private:
int value;
public:
A() { this->value = 0; };
A(int value) { this->value = value; }
int getValue() { return value; };
void setValue(int value) {
this->value = value;
}
void setValue() {value = 10;}
};

🏗️ Spre exemplu, clasa de mai sus are doi constructori diferiti. Primul este cel folosit anterior, fara parametrii iar al doilea, cu un parametru de tip int, folosit pentru a seta membrul value.

💡 Alegerea constructorului corespunzator este facuta automat in momentul crearii obiectului.

A myObj1, myObj2(10);
std::cout << myObj1.getValue() << std::endl;//0
std::cout << myObj2.getValue(); //10

❗ De asemenea, in cazul in care constructorul pe care il dorim nu exista, acest lucru va rezulta in erori. Spre exemplu, daca am dori sa cream un obiect de forma:

A myObj3(10,100);

Vom primi urmatoarea eroare: no matching function for call to ‘A::A(int, int)’, nefiind singura. Compilatorul va fi confuz, va incerca fiecare constructor si ne va spune ca nu exista unul corespunzator dorintelor noastre.

 

Constructor de copiere

💡 Este un contructor special ce are rolul de a copia continutul unui obiect in alt obiect de acelasi tip. Are in general un singur parametru, fiind o referinta la obiectul din care dorim sa facem copierea.

💾 Constructorul de copiere este invocat implicit in momentul in care declararea unui obiect este urmata de o initializare. Poate fi de asemenea invocat daca in declararea obiectului folosim acest constructor specific.

🗃️ Cuvantul “copiere” nu trebuie neaparat luat in mod literal. Datele obiectului nu trebuiesc neaparat copiate, pot fi doar manipulate si procesate

❗ Mai mult, in cazul in care constructorul de copiere nu este definit dar totusi folosim o initializare in momentul declararii, continutul acestuia va fi copiat complet, camp cu camp, devenind o clona a acestuia.

In exemplul urmator avem doua clase diferite, prima are definit un constructor de copiere iar a doua nu.

class Class1 {
public:
Class1(int val) { this -> value = val; }
Class1(Class1 const &source) { value = source.value + 10; }
int value;
};
class Class2 {
public:
Class2(int val) { this -> value = val; }
int value;
};

Keyword-ul const folosit in cadrul constructorului este o promisiune a faptului ca nu vom incerca sa modificam valorile stocate in referinta obiectului ce dorim a fi copiat.

Cream cate un obiect pentru fiecare clasa folosind constructorul normal si mai cream cate un obiect pe care il initializam, astfel apeland constructorul de copiere.

Class1 obj11(100), obj12 = obj11;
Class2 obj21(200), obj22 = obj21;
cout << obj12.value << endl; //110
cout << obj22.value << endl; //200

Vom observa ca in primul caz, constructorul de copiere va fi apelat, intrucat acesta exista, iar in al doilea, nefiind declarat, se va realiza o copie identica.

❗ Tineti cont de faptul ca acest constructor poate fi folosit de fiecare data cand avem nevoie de o copie a unui obiect specific. Spre exemplu, in cazul in care transferam prin valoare un obiect unei functii.

 

Scurgeri de memorie

Constructorii pe care i-am folosit pana acum isi indeplinesc rolul de a initializa obiecte. Nu ne-am pus problema insa, daca am avea nevoie sa curatam in urma lor, dupa ce obiectele isi termina rolul si se sterg.

💧 In general, obiectelor li se aloca memoria necesara functionalitatilor specifice iar la finalul activitatii, aceasta memorie este eliberata automat. In cazul in care eliberarea/curatarea memoriei esueaza, va fi provocat asa numitul fenomen de scurgere de memorie (memory leaking).

🔥 Fenomenul va avea un impact imediat in performantele sistemului. Memoria alocata va ramane blocata si cu fiecare repetitie, memoria utilizabila va fi tot mai putina.

Avem posibilitatea sa provocam intentionat o scurgere de memorie. Sa aruncam o privire pe urmatorul cod.

class Class {
public:
Class(int val) {
value = new int[val];
}
int *value;
};

void createLeak() {
Class obj(10000);
}

int main() {
createLeak();
}

Clasa de mai sus are un singur constructor, cu rolul de a aloca memorie corespunzator parametrului primit. Obiectul acestei clase este creat in functia createLeak(). Procesul de creare este compus din doua etape:

  • obiectul in sine este creat si o parte de memorie este alocata implicit
  • constructorul aloca alta parte de memorie in mod explicit

Variabila obj face parte din prima categorie, avand memorie alocata implicit ce va fi eliberata dupa terminarea executiei functiei createLeak().

😥 Din pacate, memoria alocata de constructor va ramane in continuare alocata. Mai mult, am si pierdut singurul pointer ce indica la adresa acelei memorii, deoarece odata cu distrugerea obiectului, pointer-ul a fost si el distrus. Astfel, o bucata substantiala de memorie a fost pierduta (leaked).

 

Destructor

🛡️ Ne putem proteja de aceste posibile scurgeri de memorie folosind o functie speciala numita destructor. Acesta este caracterizat de urmatoarele atribute:

  • o clasa numita X are un destructor numit ~X 
  • o clasa poate avea un singur destructor
  • destructorul trebuie sa fie o functie fara parametrii
  • nu poate fi invocat explicit
  • este invocat implicit in momentul stergerii obiectului

Inarmati cu noile cunostinte despre destructor, putem adauga la clasa precedenta noua functionalitate, astfel rezolvand problema scurgerii de memorie.

~Class() {
delete [] value;
}

You may also like...