POO Cauta

Saptamana 04 / 14 Programare Orientata Obiect

Constructori, destructor si metode de acces

Cum primeste un obiect o stare valida din primul moment, cum face curat in urma lui, ce face constructorul de copiere cand clasa are memorie alocata dinamic si cum pastreaza get si set datele private.

Obiective
  • Sa scrii constructori fara parametri, cu toti parametrii si cu mai putini parametri
  • Sa initializezi membrii in lista de initializatori
  • Sa inlocuiesti mai multi constructori cu unul care are valori implicite
  • Sa scrii constructorul de copiere pentru o clasa cu un membru alocat dinamic
  • Sa enumeri situatiile in care se apeleaza constructorul de copiere
  • Sa explici cand se executa destructorul si ce trebuie sa elibereze
  • Sa scrii metode get si set care pastreaza incapsularea

De ce avem nevoie de constructori

Saptamana trecuta obiectul era creat intai si completat abia dupa aceea, cu o functie de initializare sau membru cu membru. Intre cele doua momente obiectul exista intr-o stare invalida. Si nimic nu te obliga sa apelezi functia de initializare.

Constructorul este o metoda cu acelasi nume ca si clasa, fara tip returnat. Se executa automat cand obiectul este creat. Rolul lui este sa faca obiectul valid din primul moment.

Daca nu scrii niciun constructor, compilatorul genereaza unul implicit, care nu face nimic pentru membrii int. Daca scrii macar un constructor, compilatorul nu il mai genereaza pe cel implicit.

Tipuri de constructori

O clasa poate avea mai multi constructori, daca listele lor de parametri difera:

Ce constructor se apeleaza pentru fiecare obiect?

#include <iostream>
using namespace std;
class Punct {
    int x, y;
public:
    Punct() : x(0), y(0) { cout << "A "; }
    Punct(int v) : x(v), y(v) { cout << "B "; }
    Punct(int x, int y) : x(x), y(y) { cout << "C "; }
};
int main() {
    Punct p1;
    Punct p2(5);
    Punct p3(1, 2);
    Punct p4 = 7;
}

Punct p4 = 7; apeleaza constructorul cu un parametru int. Nu este o atribuire.

Lista de initializatori

Lista de initializatori vine dupa : si inaintea corpului. Acolo membrii sunt initializati, inainte sa se execute corpul. In corp poti doar sa atribui valori unor membri care exista deja.

class Produs {
    const int id;      // const: poate fi doar initializat
    string nume;
    double pret;
public:
    Produs(int id, string nume, double pret)
        : id(id), nume(nume), pret(pret) {}
};

id(id) nu e ambiguu: in afara parantezelor id este membrul, in interior este parametrul.

Lista de initializatori este obligatorie pentru membrii const, pentru membrii referinta si pentru membrii de tip clasa care nu au constructor implicit.

Valori implicite in lista de parametri

In loc de trei constructori poti scrie unul singur, cu valori implicite:

class Punct {
    int x, y;
public:
    Punct(int x = 0, int y = 0) : x(x), y(y) {}
};

Punct a;        // (0,0)
Punct b(3);     // (3,0)
Punct c(3, 4);  // (3,4)

Valorile implicite se dau de la dreapta la stanga: daca un parametru are una, toti cei de dupa el trebuie sa aiba. Le scrii doar in declaratie, nu si in definitia din afara clasei.

Destructorul

Destructorul se numeste ~NumeClasa(), nu are parametri si nici tip returnat. Se executa automat cand se termina durata de viata a obiectului:

Obiectele locale sunt distruse in ordinea inversa crearii.

#include <iostream>
#include <string>
using namespace std;
class Martor {
    string nume;
public:
    Martor(string n) : nume(n) { cout << "+" << nume << " "; }
    ~Martor() { cout << "-" << nume << " "; }
};
int main() {
    Martor a("a");
    {
        Martor b("b");
        Martor c("c");
    }
    Martor d("d");
}

Scrii un destructor cand obiectul detine o resursa, de exemplu memorie luata cu new. Destructorul o elibereaza.

Constructorul de copiere si membrii dinamici

Constructorul de copiere construieste un obiect nou dintr-unul existent. Daca nu il scrii, compilatorul genereaza unul care copiaza fiecare membru asa cum este. Pentru un pointer, asta inseamna ca se copiaza adresa, nu datele de la ea.

class Student {
    char* nume;
public:
    Student(const char* n) {
        nume = new char[strlen(n) + 1];
        strcpy(nume, n);
    }
    ~Student() { delete[] nume; }
};

Clasa aceasta este gresita din clipa in care copiezi un obiect: dupa Student b = a; ambii pointeri nume arata spre acelasi vector. Primul destructor il elibereaza, al doilea il elibereaza inca o data. Este un double delete, adica comportament nedefinit.

STIVA
a.nume: ●
HEAP
Ana

Student a("Ana"); - constructorul aloca textul pe heap.

Pasul 1/4

Solutia este un constructor de copiere care aloca memorie noua si copiaza continutul:

Student(const Student& s) {
    nume = new char[strlen(s.nume) + 1];
    strcpy(nume, s.nume);
}

Parametrul trebuie sa fie referinta. La Student(Student s), transmiterea lui s prin valoare ar cere ea insasi o copie, asa ca acel cod nu compileaza.

Cand se apeleaza constructorul de copiere

  1. Cand initializezi un obiect din altul: Cutie b = a; sau Cutie c(a);.
  2. Cand transmiti un obiect prin valoare unei functii.
  3. Cand o functie intoarce un obiect prin valoare. Aici compilatorul are voie, iar in unele cazuri este chiar obligat, sa sara peste copiere, asa ca nu te baza ca o vezi.
#include <iostream>
using namespace std;
class Cutie {
public:
    Cutie() { cout << "implicit" << endl; }
    Cutie(const Cutie&) { cout << "copiere" << endl; }
};
void prinValoare(Cutie) {}
void prinReferinta(const Cutie&) {}
int main() {
    Cutie a;
    Cutie b = a;
    Cutie c(a);
    prinValoare(a);
    prinReferinta(a);
}

Metode de acces: get si set

Datele raman private. Clasa decide ce se poate citi si cum se poate modifica.

const char* getNume() const { return nume; }   // doar citire
void setNota(int nt) {
    if (nt >= 1 && nt <= 10) nota = nt;        // invariantul ramane adevarat
}

Intoarce const char*, nu char*. Altfel apelantul poate scrie prin pointer direct in vectorul tau privat.

Ruleaza programul de mai jos. Apoi adauga setNume(const char*): ce trebuie sa faca cu vectorul vechi inainte sa retina numele nou?

#include <iostream>
#include <cstring>
using namespace std;

class Student {
  char* nume;
  int nota;
public:
  Student(const char* n = "anonim", int nt = 0) : nota(nt) {
      nume = new char[strlen(n) + 1];
      strcpy(nume, n);
      cout << "constructor " << nume << endl;
  }
  Student(const Student& s) : nota(s.nota) {
      nume = new char[strlen(s.nume) + 1];   // copia primeste memoria ei
      strcpy(nume, s.nume);
      cout << "constructor de copiere " << nume << endl;
  }
  ~Student() {
      cout << "destructor " << nume << endl;
      delete[] nume;
  }
  const char* getNume() const { return nume; }
  int getNota() const { return nota; }
  void setNota(int nt) {
      if (nt >= 1 && nt <= 10) nota = nt;   // refuzam notele invalide
  }
};

void afiseaza(Student s) {
  cout << s.getNume() << ": " << s.getNota() << endl;
}

int main() {
  Student a("Ana", 9);
  Student b = a;
  b.setNota(15);   // ignorata
  b.setNota(7);
  afiseaza(b);
  cout << a.getNota() << " " << b.getNota() << endl;
}
Intrare (stdin)

Deschide in Godbolt

Verifica-te

1. Ce declara Punct p();, daca Punct are constructor implicit?

2. Care membri trebuie initializati in lista de initializatori (nu pot fi setati in corpul constructorului)?

3. Ce afiseaza programul?
#include <iostream>
using namespace std;
class Dreptunghi {
    int l, h;
public:
    Dreptunghi(int l = 1, int h = 1) : l(l), h(h) {}
    int arie() const { return l * h; }
};
int main() {
    Dreptunghi a, b(4), c(2, 3);
    cout << a.arie() << " " << b.arie() << " " << c.arie();
}

4. Ce afiseaza programul?
#include <iostream>
using namespace std;
class T {
    int id;
public:
    T(int i) : id(i) { cout << "C" << id << " "; }
    ~T() { cout << "D" << id << " "; }
};
void f() { T t(2); }
int main() {
    T a(1);
    f();
    T b(3);
}

5. De cate ori se apeleaza constructorul de copiere? Scrie exact ce afiseaza programul.
#include <iostream>
using namespace std;
class Cutie {
public:
    Cutie() {}
    Cutie(const Cutie&) { cout << "copie "; }
};
void primeste(Cutie) {}
void priveste(const Cutie&) {}
int main() {
    Cutie a;
    Cutie b = a;
    primeste(a);
    priveste(a);
    primeste(b);
}

6. O clasa are char* nume alocat cu new[] in constructor si eliberat in destructor, dar nu are constructor de copiere. Ce se strica la Student b = a;?

7. Care este semnatura corecta a constructorului de copiere pentru Student?

8. Care metode de acces pastreaza incapsularea?

Scor: 0/8

Codul din repository: S04

Varianta in engleza