POO Cauta

Saptamana 02 / 14 Programare Orientata Obiect

Seminar 2 - Pointeri, memorie dinamica si ownership

Cautam defectele de memorie dintr-un program care elibereaza un bloc de doua ori si uita altele, apoi construim o mica aplicatie cu note pe heap si hotaram, pentru fiecare bloc, cine il elibereaza.

Obiective
  • Folosesti operatorii de adresa si dereferentiere si nullptr
  • Transmiti pointeri si referinte functiilor si spui ce poate modifica fiecare
  • Aloci vectori dinamici cu new[] si ii eliberezi cu delete[]
  • Gasesti memory leaks si dangling pointers in cod existent
  • Spui, pentru fiecare bloc de pe heap, cine il detine si cine il sterge
  • Desenezi stiva si heap-ul unui program mic

Plan (90 min)

TimpActivitate
0-5Recapitulare prin intrebari: ce este &a, ce este *p?
5-15Predictia rezultatului, cautarea defectelor in programul de pornire
15-30Demonstratie: new[] / delete[], stiva si heap-ul in debugger
30-60Lucru individual: aplicatia cu note
60-75Cerinta noua: o copie independenta
75-85Verificare individuala: desenezi memoria, raspunzi la intrebari
85-90Trei lucruri pe care le stii acum

Programul de pornire

Codul este in repository, in folderul S02. Citeste-l inainte sa il rulezi. Numara alocarile si eliberarile.

S02/S02.cpp
#include <iostream>
using namespace std;
int suma(int v[], int n) {
	int i, s = 0;
	for (i = 0;i < n;i++)
		s = s + v[i];
	return s;
}
int main()
{
	int v[10], i, s = 0, n;
	n = 10;
	for (i = 0; i < 10;i++)
	{
		v[i] = i + s;
	}
	for (i = 0;i < 10;i++)
		cout << v[i] << " ";
	cout << endl;
	cout << suma(v, n);

	//int* db = (int*)malloc(n * sizeof(int));
	int* db = new int[n];
	for (i = 0;i < 10;i++)
		db[i] = i * 2;
	for (i = 0;i < n;i++)
		cout << db[i] << " ";
	int* const pc = (int*)malloc(n * sizeof(int));
	//pc = (int*)malloc(n * sizeof(int));
	const int* cp = (int*)malloc(n * sizeof(int));
	cp = (int*)malloc(n * sizeof(int));
	//cp[0] = 5;
	const int* const cpc = (int*)malloc(n * sizeof(int));
	cout << endl;
	cout << *db << endl;
	cout << *(db + 1) << endl;
	cout << 1[db];
	//free(db);
	//free(pc);
	delete[] db;
	delete[] db;

	return 0;
}

Gaseste defectele

Un bloc eliberat de doua ori

La final, programul face delete[] db; de doua ori. Pe unele calculatoare se opreste cu eroare, pe altele pare sa mearga. De ce este gresit oricum? Repara-l, apoi spune cum te fereste db = nullptr; de aceeasi greseala.

IndiciuDupa primul delete[], blocul nu mai e al tau, dar db retine in continuare vechea adresa.
Indiciudelete[] pe nullptr nu face nimic, deci un al doilea apel dupa db = nullptr; nu mai strica nimic.
Solutie
delete[] db;
db = nullptr;

A doua eliberare a aceluiasi bloc este comportament nedefinit: faptul ca programul nu se opreste la tine nu inseamna ca e corect. Observa si liniile comentate free(db);: un bloc alocat cu new[] nu se elibereaza niciodata cu free.

Blocurile uitate

Programul aloca patru blocuri cu malloc (pc, cp de doua ori, cpc) si nu elibereaza niciunul. Gaseste-le pe toate si elibereaza-le. Care dintre ele nu mai poate fi eliberat deloc la sfarsitul lui main?

Indiciucp = (int*)malloc(...) suprascrie singura copie a adresei primului bloc al lui cp.
Indiciufree primeste void*, iar cp si cpc sunt const int*. Compilatorul refuza conversia, deci ai nevoie de un cast: free((void*)cp);. Adauga si #include <cstdlib> pentru malloc si free.
Solutie
const int* cp = (int*)malloc(n * sizeof(int));
free((void*)cp);                     // inainte sa pierdem adresa
cp = (int*)malloc(n * sizeof(int));
// ... restul lui main ...
free(pc);
free((void*)cp);
free((void*)cpc);

Primul bloc al lui cp se pierde definitiv la reatribuire, deci trebuie eliberat inainte de ea. In C++ preferam oricum new[] / delete[]. Bonus: dupa cout << suma(v, n); lipseste un endl, de aceea pe ecran apare 450 2 4 ....

Liniile cu const

Doua linii sunt comentate: pc = (int*)malloc(...); si cp[0] = 5;. Decomenteaza-le pe rand, citeste mesajul compilatorului si explica-l cu cele trei feluri de pointeri const.

Solutie

pc este int* const: nu il poti muta, deci pc = ... e eroare. cp este const int*: il poti muta, dar nu poti modifica valorile prin el, deci cp[0] = 5 e eroare. cpc nu permite niciuna dintre ele.

O functie stricata intentionat

Functia de mai jos compileaza (g++ doar avertizeaza), dar este gresita.

int* creeazaValoare() {
    int valoare = 10;
    return &valoare;
}

Intoarce o adresa. De ce este nesigura? Cand inceteaza valoare sa existe? Rescrie functia astfel incat apelantul sa primeasca un rezultat valid si spune cine trebuie sa elibereze memoria.

Indiciuvaloare este o variabila locala. Unde stau variabilele locale si ce se intampla cu ele la return?
Solutie

valoare sta pe stiva lui creeazaValoare si este distrusa la return, deci apelantul primeste un dangling pointer. Doua variante corecte:

int creeazaValoare() { return 10; }            // prin valoare, fara pointer
int* creeazaValoare() { return new int(10); }  // apelantul o detine si face delete

Intrebari obligatorii

  1. Este o adresa. De ce este nesigura?
  2. Cand inceteaza valoare sa existe?
  3. Ce este un dangling pointer?
  4. Cine raspunde de stergerea memoriei alocate dinamic?
  5. Deseneaza: ce variabile sunt pe stiva, ce blocuri sunt pe heap si unde arata fiecare sageata?

Prezice, apoi verifica:

#include <iostream>
using namespace std;
void seteaza(int* p, int& r) { *p = 1; r = 2; p = nullptr; }
int main() {
    int a = 0, b = 0;
    int* q = &a;
    seteaza(q, b);
    cout << a << " " << b << " " << (q == &a);
}

#include <iostream>
using namespace std;
int main() {
    int* a = new int[3]{1, 2, 3};
    int* b = a;
    b[1] = 20;
    cout << a[1] << " " << (a == b) << endl;
    delete[] a;
    a = nullptr;
}

Dupa ultima linie, a este in siguranta, dar b retine inca vechea adresa. Cine trebuia sa elibereze blocul, a sau b? Ai voie sa mai faci si delete[] b;?

Sarcina minima

Note pe heap

Scrie cele patru functii de mai jos. adaugaNota trebuie sa mareasca vectorul cu un element: aloca un bloc mai mare, copiaza, elibereaza blocul vechi, actualizeaza pointerul si numarul de note. Testeaza, apoi deseneaza stiva si heap-ul imediat dupa ce adaugaNota se termina.

STIVA                HEAP
note ------------->  [7][9][10][6]
n = 4
IndiciuadaugaNota schimba si pointerul, si numarul de note ale apelantului, deci le primeste pe amandoua prin referinta: int*& note, int& n.
IndiciuElibereaza blocul vechi abia dupa ce l-ai copiat si inainte sa suprascrii note.
IndiciuIn stergeNote, pune pointerul pe nullptr si numarul pe 0, ca apelantul sa nu foloseasca din greseala blocul vechi.
#include <iostream>
using namespace std;

// Apelantul detine blocul intors.
int* creeazaNote(int n) {
  // TODO: aloca n intregi, toti 0 (pana atunci, main se opreste la note[0])
  return nullptr;
}

void adaugaNota(int*& note, int& n, int nota) {
  // TODO: mareste vectorul cu un element si pune nota la final
}

double medie(const int* note, int n) {
  // TODO
  return 0;
}

void stergeNote(int*& note, int& n) {
  // TODO: elibereaza, apoi reseteaza note si n
}

int main() {
  int n = 3;
  int* note = creeazaNote(n);
  note[0] = 7; note[1] = 9; note[2] = 10;
  adaugaNota(note, n, 6);
  cout << n << " " << medie(note, n) << endl;   // asteptat: 4 8
  stergeNote(note, n);
}
Intrare (stdin)

Deschide in Godbolt

Solutie
int* creeazaNote(int n) { return new int[n](); }

void adaugaNota(int*& note, int& n, int nota) {
    int* nou = new int[n + 1];
    for (int i = 0; i < n; i++) nou[i] = note[i];
    nou[n] = nota;
    delete[] note;
    note = nou;
    n++;
}

double medie(const int* note, int n) {
    if (n == 0) return 0;
    double s = 0;
    for (int i = 0; i < n; i++) s += note[i];
    return s / n;
}

void stergeNote(int*& note, int& n) {
    delete[] note;
    note = nullptr;
    n = 0;
}

Cerinta noua

Scrie int* copiazaNote(const int* note, int n), care intoarce o copie independenta. Modifica copie[0] si arata ca note[0] nu s-a schimbat. Cate blocuri sunt acum pe heap si cine il elibereaza pe fiecare? Ce s-ar strica daca copiazaNote ar intoarce pur si simplu note?

Codul din repository: S02

Varianta in engleza