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.
- 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)
| Timp | Activitate |
|---|---|
| 0-5 | Recapitulare prin intrebari: ce este &a, ce este *p? |
| 5-15 | Predictia rezultatului, cautarea defectelor in programul de pornire |
| 15-30 | Demonstratie: new[] / delete[], stiva si heap-ul in debugger |
| 30-60 | Lucru individual: aplicatia cu note |
| 60-75 | Cerinta noua: o copie independenta |
| 75-85 | Verificare individuala: desenezi memoria, raspunzi la intrebari |
| 85-90 | Trei 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.
#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.
Indiciu
Dupa primuldelete[], blocul nu mai e al tau, dar db retine in continuare vechea adresa.Indiciu
delete[] 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?
Indiciu
cp = (int*)malloc(...) suprascrie singura copie a adresei primului bloc al lui cp.Indiciu
free 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.
Indiciu
valoare 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 deleteIntrebari obligatorii
- Este o adresa. De ce este nesigura?
- Cand inceteaza
valoaresa existe? - Ce este un dangling pointer?
- Cine raspunde de stergerea memoriei alocate dinamic?
- 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 = 4Indiciu
adaugaNota schimba si pointerul, si numarul de note ale apelantului, deci le primeste pe amandoua prin referinta: int*& note, int& n.Indiciu
Elibereaza blocul vechi abia dupa ce l-ai copiat si inainte sa suprascriinote.Indiciu
InstergeNote, 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)
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?