POO Cauta

Saptamana 04 / 14 Programare Orientata Obiect

Seminar 4 - Constructori, destructori si durata de viata a obiectelor

Transformam clasa cont_b din repository intr-o clasa care este valida din momentul crearii si face curat in urma ei. Apoi urmarim cand se nasc si cand mor obiectele.

Obiective
  • Scrii un constructor implicit si un constructor cu parametri
  • Validezi argumentele, ca obiectul sa nu ajunga niciodata intr-o stare invalida
  • Scrii un destructor care elibereaza memoria detinuta de obiect
  • Marchezi cu const metodele care nu modifica obiectul
  • Prezici ordinea apelurilor de constructori si destructori in blocuri imbricate
  • Explici costul transmiterii unui obiect prin valoare in loc de referinta constanta

Plan (90 min)

TimpActivitate
0-5Recapitulare prin intrebari: ce este un invariant?
5-15Citim programul de pornire si prezicem ce afiseaza
15-25Demonstratie: un constructor si destructorul, cu mesaje
25-55Lucru individual: constructori, validare, destructor, metode const
55-70Experimente: durata de viata in blocuri, prin valoare vs prin referinta
70-80Cerinta noua
80-90Verificare individuala si intrebari scurte

Programul de pornire

Codul este in repository, in folderul S04. cont_b este deja o clasa, dar toti membrii sunt publici, iar main il completeaza camp cu camp. Memoria alocata nu este eliberata nicaieri.

S04/S04.cpp
#include <iostream>
using namespace std;

class cont_b {
public:
	string iban = "default iban";
	char* nume = nullptr;
	float* tranzactii = nullptr;
	int nr_tranz = 0;

	void afisare_cont_bancar() {
		cout << this->nume << endl;
		cout << this->iban << endl;
		for (int i = 0; i < nr_tranz; i++) {
			cout << tranzactii[i] << " ";
		}
	}

	void schimbareIban() {
		iban = iban + "1";
	}

	void efectuareTranzactie(int suma) {
		for (int i = 0; i < nr_tranz; i++) {
			if (tranzactii[i] == 0) {
				tranzactii[i] = suma;
				break;
			}
		}
	}

};





int main() {

	cont_b b1;

	int nr_tranzactii = 10;

	float* tranz = new float[nr_tranzactii];

	for (int i = 0; i < nr_tranzactii; i++) {
		tranz[i] = 0;
	}

	string nume = "Marcel Ion";
	b1.iban = "dc24993f-0118-4d5f-94ab-bc340b0b6502";
	b1.nume = new char[nume.length() + 1];
	strcpy_s(b1.nume, nume.length() + 1, nume.c_str());

	b1.nr_tranz = nr_tranzactii;
	b1.tranzactii = new float[nr_tranzactii];
	for (int i = 0; i < nr_tranzactii; i++) {
		b1.tranzactii[i] = tranz[i];
	}
	/*
	cout << b1.nume << endl;
	cout << b1.iban << endl;
	for (int i = 0; i < b1.nr_tranz; i++) {
		cout << b1.tranzactii[i] << " ";
	}
	*/

	b1.afisare_cont_bancar();
	b1.schimbareIban();
	b1.afisare_cont_bancar();
	b1.efectuareTranzactie(50);
	b1.afisare_cont_bancar();


	return 0;
}

Inainte de experimentul cu transmiterea prin valoare, prezice ce afiseaza programul de mai jos. Copia primeste propriul n, dar ce se intampla cu pointerul?

#include <iostream>
using namespace std;
struct Portofel {
    float* tranzactii;
    int n;
};
void alimentare(Portofel p, float suma) {
    p.tranzactii[0] = suma;
    p.n = 99;
}
int main() {
    float t[3] = {0, 0, 0};
    Portofel p = {t, 3};
    alimentare(p, 100);
    cout << p.tranzactii[0] << " " << p.n;
}

Copia are acelasi pointer ca originalul, deci scrie in aceeasi memorie. Noteaza asta, iti trebuie la experimentul 2.

De la initializare manuala la constructori

Un cont_b mereu valid

Fa membrii private. Inlocuieste completarea manuala din main cu constructori:

  • un constructor implicit: nume gol, iban "default iban", fara tranzactii;
  • un constructor cu parametri cont_b(const char* nume, string iban, const float* t, int nr), care copiaza numele si tranzactiile in memorie alocata de el;
  • un destructor care elibereaza ambii vectori.

Un nr negativ sau un t nul dau un cont cu 0 tranzactii. afisare_cont_bancar si sold nu modifica contul, asa ca le faci const.

IndiciuIntreaba-te intai: ce stari trebuie sa fie imposibile? Un nr_tranz negativ? Un nume egal cu nullptr?
IndiciuAloca numele si in constructorul implicit (new char[1] care contine '\0'). Atunci destructorul poate apela mereu delete[] nume, iar afisarea nu mai are nevoie de verificari.
Solutie
cont_b(const char* n, string iban, const float* t, int nr) : iban(iban) {
    if (n == nullptr) n = "";
    nume = new char[strlen(n) + 1];
    strcpy(nume, n);
    if (nr < 0 || t == nullptr) nr = 0;
    nr_tranz = nr;
    tranzactii = nr > 0 ? new float[nr] : nullptr;
    for (int i = 0; i < nr; i++) tranzactii[i] = t[i];
}

~cont_b() {
    delete[] nume;
    delete[] tranzactii;
}

Obiectul detine ambii vectori, deci destructorul ii elibereaza. delete[] pe nullptr nu face nimic, deci un cont gol poate fi distrus fara probleme.

Experimentul 1: durata de viata in blocuri imbricate

Adauga temporar un cout in constructor si in destructor. Inainte sa rulezi, scrie ordinea la care te astepti.

#include <iostream>
#include <string>
using namespace std;
class Cont {
    string titular;
public:
    Cont(string t) : titular(t) { cout << "Constructor " << titular << endl; }
    ~Cont() { cout << "Destructor " << titular << endl; }
};
int main() {
    Cont p1("p1");
    {
        Cont p2("p2");
        Cont p3("p3");
    }
    Cont p4("p4");
}

Experimentul 2: prin valoare sau prin referinta constanta

#include <iostream>
#include <string>
using namespace std;
class Cont {
    string titular;
public:
    Cont(string t) : titular(t) { cout << "Constructor " << titular << endl; }
    Cont(const Cont& c) : titular(c.titular) { cout << "Copiere " << titular << endl; }
    ~Cont() { cout << "Destructor " << titular << endl; }
};
void procesareValoare(Cont c) { cout << "procesare" << endl; }
void procesareReferinta(const Cont& c) { cout << "procesare" << endl; }
int main() {
    Cont x("x");
    procesareValoare(x);
    procesareReferinta(x);
}

Intoarce-te acum la cont_b, care are destructor, dar nu are constructor de copiere. Daca il transmiti prin valoare unei functii, copia foloseste aceiasi doi vectori (ca la Portofel), iar destructorul ei ii elibereaza la iesirea din functie. Originalul ramane cu pointeri dangling, iar destructorul lui ii elibereaza a doua oara. Este un double delete: gresit, chiar daca programul se intampla sa nu se opreasca. Foloseste const cont_b&.

Intrebari obligatorii

  1. Ce constructor se apeleaza aici?
  2. De ce a aparut inca un constructor/destructor?
  3. Care este costul transmiterii prin valoare?
  4. Cand este de preferat o referinta?
  5. Cine detine aceasta memorie si cine o elibereaza?

Sarcina minima

Constructori, validare, destructor, const

Completeaza clasa. efectuareTranzactie refuza sumele <= 0 si scrie in primul loc care este inca 0; intoarce false cand nu poate.

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

class cont_b {
  string iban;
  char* nume;
  float* tranzactii;
  int nr_tranz;

public:
  cont_b() : iban("default iban"), nume(nullptr), tranzactii(nullptr), nr_tranz(0) {
      // TODO: nume trebuie sa fie un sir gol, nu nullptr
  }

  cont_b(const char* n, string iban, const float* t, int nr)
      : iban(iban), nume(nullptr), tranzactii(nullptr), nr_tranz(0) {
      // TODO: copiaza n si t in memorie alocata de tine; refuza nr < 0
  }

  // TODO: destructor care elibereaza nume si tranzactii

  void afisare_cont_bancar() const {
      if (nume != nullptr) cout << nume;
      cout << endl << iban << endl;
      for (int i = 0; i < nr_tranz; i++) cout << tranzactii[i] << ' ';
      cout << endl;
  }

  float sold() const {
      // TODO
      return 0;
  }

  bool efectuareTranzactie(float suma) {
      // TODO: refuza suma <= 0, completeaza primul loc liber (0)
      return false;
  }
};

int main() {
  float t[] = {50, 0, 0};
  cont_b c1("Marcel Ion", "dc24", t, 3);
  cout << c1.efectuareTranzactie(100) << c1.efectuareTranzactie(-5) << endl;   // asteptat: 10
  c1.afisare_cont_bancar();                    // asteptat: Marcel Ion / dc24 / 50 100 0
  cout << c1.sold() << endl;                   // asteptat: 150
  cont_b gol;
  gol.afisare_cont_bancar();
}
Intrare (stdin)

Deschide in Godbolt

Cerinta noua

Banca vrea sa stie in orice moment cate conturi exista. Adauga un contor static int, creste-l in fiecare constructor si scade-l in destructor, si ofera-l prin static int getNrConturi(). Verifica-l cu un cont creat intr-un bloc interior: ce arata contorul in bloc si ce arata dupa el?

Codul din repository: S04

Varianta in engleza