POO Cauta

Saptamana 05 / 14 Programare Orientata Obiect

Seminar 5 - Copierea obiectelor si Regula celor trei

Copiem un obiect care detine memorie pe heap, vedem cum copia generata de compilator imparte aceeasi memorie, apoi scriem destructorul, constructorul de copiere si operator= care fac copiile independente.

Obiective
  • Explici ce fac constructorul de copiere si operator= generate de compilator
  • Deosebesti copierea superficiala (shallow) de copierea in adancime (deep) si le desenezi
  • Scrii un constructor de copiere care face copiere in adancime
  • Scrii operator= cu verificarea auto-atribuirii, care intoarce *this
  • Spui cand ruleaza constructorul de copiere si cand ruleaza operator=
  • Enunti Regula celor trei si o aplici unei clase cu membri pointeri

Plan (90 min)

TimpActivitate
0-5Recapitulare prin intrebari: cine detine aceasta memorie?
5-15Predictie: ce se intampla dupa Student s2 = s1;?
15-30Demonstratie: experimentul, adresele in debugger, diagrama memoriei
30-60Lucru individual: destructor, constructor de copiere, operator=
60-75Cerinta noua: adaugaNota
75-85Verificare individuala si intrebarile obligatorii
85-90Trei lucruri pe care le stii acum

Programul de pornire

Clasa de mai jos detine doua blocuri pe heap: numele si notele. Are destructor, dar nu are constructor de copiere si nici operator=. Citeste codul, apoi ruleaza-l.

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

class Student {
    char* nume;
    int* note;
    int nrNote;
public:
    Student(const char* n, const int* v, int nr) {
        nume = new char[strlen(n) + 1];
        strcpy(nume, n);
        nrNote = nr;
        note = new int[nrNote];
        for (int i = 0; i < nrNote; i++) note[i] = v[i];
    }
    ~Student() {
        cout << "~Student elibereaza numele de la " << (void*)nume << endl;
        delete[] nume;
        delete[] note;
    }
    void setNota(int i, int valoare) { note[i] = valoare; }
    void afisare() const {
        cout << nume << ":";
        for (int i = 0; i < nrNote; i++) cout << " " << note[i];
        cout << "   nume la " << (void*)nume << ", note la " << note << endl;
    }
};

int main() {
    int v[] = {8, 9, 7};
    Student s1("Ana", v, 3);
    Student s2 = s1;   // inca nu avem constructor de copiere: il genereaza compilatorul
    s1.afisare();
    s2.afisare();
    s2.setNota(0, 4);
    s1.afisare();      // s-a schimbat s1?
}                      // aici se distrug s2, apoi s1

Experimentul

Unde arata copiile?

Ruleaza programul si compara adresele afisate pentru s1 si s2. Apoi pune un breakpoint pe s2.setNota(0, 4); si urmareste s1.note si s2.note in fereastra Locals.

  1. Adresele sunt aceleasi sau diferite?
  2. Dupa setNota, de ce apare nota 4 si la s1?
  3. De cate ori ruleaza ~Student si ce adresa elibereaza de fiecare data?
IndiciuConstructorul de copiere generat copiaza fiecare membru asa cum este. Ce valoare are un membru char*?
IndiciuValoarea unui pointer este o adresa. Daca o copiezi, obtii un al doilea pointer catre acelasi bloc, nu un al doilea bloc.
Solutie

Adresele sunt identice: s2.nume == s1.nume si s2.note == s1.note. Aceasta este o copiere superficiala (shallow copy). Cand scrii prin s2.note, scrii in singurul bloc care exista, asa ca s1 vede schimbarea. La finalul lui main, s2 se distruge primul si elibereaza ambele blocuri. Apoi destructorul lui s1 elibereaza din nou aceleasi adrese: dublu delete.

COPIERE SUPERFICIALA         COPIERE IN ADANCIME
s1 ----+                     s1 --------> bloc A
       +----> bloc           s2 --------> bloc B
s2 ----+

Prezice, apoi verifica. Varianta asta nu are destructor, deci are memory leak, dar nu sterge nimic de doua ori:

#include <iostream>
using namespace std;
class Student {
public:
    int* note;
    int nrNote;
    Student(int n) : note(new int[n]), nrNote(n) {
        for (int i = 0; i < n; i++) note[i] = 5;
    }
    // fara destructor: varianta asta are memory leak, dar nu sterge nimic de doua ori
};
int main() {
    Student s1(2);
    Student s2 = s1;
    s2.note[0] = 10;
    cout << s1.note[0] << " " << (s1.note == s2.note);
}

STIVA
s1.nume: ●
s1.note: ●
HEAP
Ana
8 9 7

Student s1(...); - s1 detine doua blocuri pe heap.

Pasul 1/3

Implementeaza Regula celor trei

Constructorul de copiere

Scrie Student(const Student& s). Trebuie sa aloce blocuri noi pentru nume si note si sa copieze continutul, nu adresele. Ruleaza din nou programul de pornire: adresele lui s1 si s2 trebuie sa fie acum diferite.

IndiciuFa ce face constructorul obisnuit, dar citeste din s.nume, s.note si s.nrNote.
IndiciuDe ce trebuie ca parametrul sa fie referinta? Gandeste-te ce ar apela transmiterea unui Student prin valoare.
Solutie
Student(const Student& s) {
    nume = new char[strlen(s.nume) + 1];
    strcpy(nume, s.nume);
    nrNote = s.nrNote;
    note = new int[nrNote];
    for (int i = 0; i < nrNote; i++) note[i] = s.note[i];
}

Daca parametrul ar fi Student s, transmiterea argumentului ar avea ea insasi nevoie de constructorul de copiere, care s-ar apela la nesfarsit. Compilatorul refuza aceasta forma.

Operatorul de atribuire

Scrie Student& operator=(const Student& s). Obiectul are deja blocurile lui, asa ca le eliberezi inainte sa copiezi. Testeaza cu s3 = s1; si cu s3 = s3;.

IndiciuPatru pasi: verifici this == &s, eliberezi blocurile vechi, copiezi ca in constructorul de copiere, return *this;.
IndiciuFara verificare, s3 = s3; isi elibereaza propriul nume si apoi copiaza din memoria eliberata.
Solutie
Student& operator=(const Student& s) {
    if (this == &s) return *this;
    delete[] nume;
    delete[] note;
    nume = new char[strlen(s.nume) + 1];
    strcpy(nume, s.nume);
    nrNote = s.nrNote;
    note = new int[nrNote];
    for (int i = 0; i < nrNote; i++) note[i] = s.note[i];
    return *this;
}

Intoarcerea lui *this prin referinta permite atribuiri inlantuite, ca a = b = c;, la fel ca la int.

Prezice, apoi verifica. Ce linie afiseaza copiere si ce linie afiseaza atribuire?

#include <iostream>
using namespace std;
class Student {
public:
    Student() {}
    Student(const Student&) { cout << "copiere" << endl; }
    Student& operator=(const Student&) { cout << "atribuire" << endl; return *this; }
};
int main() {
    Student s1;
    Student s2 = s1;
    Student s3;
    s3 = s1;
    Student s4(s1);
}

Intrebari obligatorii

  1. Ce este copierea superficiala (shallow copy)?
  2. Ce este copierea in adancime (deep copy)?
  3. Cand se apeleaza constructorul de copiere?
  4. Ce face constructorul de copiere generat de compilator?
  5. Care este diferenta dintre Student s2 = s1; si s2 = s1;?
  6. De ce operator= intoarce de obicei *this?

Sarcina minima

Un Student care se copiaza corect

Completeaza clasa de mai jos: constructorul de copiere si operator=. Ruleaz-o inainte sa incepi, ca sa vezi defectul, si din nou la final. Cand ai terminat, s1 trebuie sa isi pastreze notele dupa ce se schimba s2 si s3.

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

class Student {
  char* nume;
  int* note;
  int nrNote;
public:
  Student(const char* n, const int* v, int nr) {
      nume = new char[strlen(n) + 1];
      strcpy(nume, n);
      nrNote = nr;
      note = new int[nrNote];
      for (int i = 0; i < nrNote; i++) note[i] = v[i];
  }
  // TODO: constructor de copiere (copiere in adancime)
  // TODO: operator= (verificare auto-atribuire, eliberare, copiere, return *this)
  ~Student() {
      delete[] nume;
      delete[] note;
  }
  void setNota(int i, int valoare) { note[i] = valoare; }
  void afisare() const {
      cout << nume << ":";
      for (int i = 0; i < nrNote; i++) cout << " " << note[i];
      cout << endl;
  }
};

int main() {
  int v[] = {8, 9, 7};
  Student s1("Ana", v, 3);
  Student s2 = s1;
  s2.setNota(0, 4);
  int w[] = {10};
  Student s3("Dan", w, 1);
  s3 = s1;
  s3.setNota(1, 5);
  s3 = s3;
  s1.afisare();   // asteptat: Ana: 8 9 7
  s2.afisare();   // asteptat: Ana: 4 9 7
  s3.afisare();   // asteptat: Ana: 8 5 7
}
Intrare (stdin)

Deschide in Godbolt

Provocare

Cerinta noua: adauga void adaugaNota(int valoare), care mareste vectorul de note cu un element. Copiaza apoi un student si adauga o nota copiei. Se schimba originalul? Ce s-ar strica daca adaugaNota ar uita delete[] pentru vectorul vechi, si ce daca ar uita sa actualizeze nrNote?

Codul din repository: S05

Varianta in engleza