Sunteți pe pagina 1din 10

POINTERI (III)

Alocarea dinamic a memoriei: operatorii new i delete


Ne punem problema utilizrii, ntr-un program, a unui numr foarte mare de date de tip double, de exemplu, care trebuie prelucrate rapid i care, n consecin, trebuie s se afle n memorie n momentul rulrii, chiar dac iniial se afl ntr-un fiier pe disc. Este evident c n acest caz vom folosi tablouri cu dimensiuni adecvate volumului de date. Utilizarea tablourilor este foarte simpl dar, dac volumul de date prelucrat este cu adevrat mare, ne vom lovi de un neajuns major: nu pot fi declarate tablouri de dimensiuni variabile: alocarea unui tablou are un caracter static, este fcut la compilarea programului, prin urmare numrul de elemente trebuie desemnat printr-o constant. Programatorul trebuie s estimeze o dimensiune maxim, acoperitoare pentru toate situaiile n care va fi folosit respectivul tablou, i s-l declare n consecin. Apare astfel o risip inerent de spaiu de memorie. De exemplu, dac declarm tabloul
double mat[100][100];

i la rulare l ncrcm cu o matrice de 10 x 10 elemente, folosim numai 1% din spaiul rezervat prin program! Risipa este evident, iar remediul const n alocarea dinamic a memoriei, adic alocarea n momentul rulrii programului. Vom renuna s declarm tablouri, n locul lor vom folosi variabile de tip pointer pentru a reine adresele locaiilor de memorie obinute de la sistemul de calcul prin operaii de alocare. In cazul alocrii tablourilor se garanteaz c elementele succesive au locaii succesive i, n consecin, parcurgerea tablourilor alocate dinamic poate fi facut cu operatorul de indexare, ca n cazul alocrii statice, utiliznd identitatea dintre expresia p[i] i *(p+i). S reamintim de la bun nceput diferena esenial dintre pointeri i tablouri: instruciunea
double* p

declar o variabil de tip pointer ctre double, n timp ce


double tab[10];

declar un tablou al crui nume, tab, este o constant de tip pointer ctre double, cu valoarea: adresa primului element al tabloului, atribuirea
p=tab;

este corect, n locaia de memorie numit p se scrie adresa primului element din tabloul tab, n timp ce atribuirea
tab=p;

nu este permis: nu exist o locaie de memorie numit tab care s conin o adres care s fie modificat. Este clar c, la compilare, apare undeva ntr-un tabel numele tab urmat de valoarea sa, adresa primului element al tabloului, i aceast informaie este utilizat la proiectarea codului, dar programatorul nu are acces la acel tabel. Putem spune, aadar, c tab este un pointer constant, un pointer utilizat numai de compilator, cu o valoare constant, n timp ce p este un pointer variabil, a crui valoare poate fi modificat de ctre programator i care, n conscin, trebuie iniializat tot de ctre acesta.

Dup cum tim deja, un pointer poate fi iniializat corect doar n urmtoarele trei moduri: cu adresa unei variabile alocate deja de compilator:
int i=7; int* p; p=&i; cout<<*p<<endl;

//7

sau printr-o atribuire cu o expresie corect din aritmetica pointerilor, de exemplu


int tab[10]={0,1,2,3,4,5,6,7,8,9}; int* p; p=tab+5; cout<<tab[5]<<"=="<<*p<<endl; //5==5

sau prin alocare dinamic, aa cum vom vedea n continuare. In limbajul C alocarea dinamic a memoriei poate fi facut numai cu funcii din biblioteci specializate n gestionarea memoriei, cele mai utilizate fiind funciile malloc(), calloc(), free(), realloc(), toate din <malloc.h>. In C++ au fost introdui, special pentru alocarea dinamic, operatorii new i delete, care fac parte integrant din limbaj (nu necesit apelarea vreunei biblioteci). Operatorul new poate fi utilizat n dou moduri: pentru a rezerva spaiu de memorie pentru un singur obiect, caz n care new ntoarce adresa obiectului alocat, sau pentru a rezerva spaiu pentru un tablou cu un numar variabil de obiecte (dar bine precizat, la rulare, n momentul evalurii expresiei), caz n care new ntoarce adresa primului element din tablou. Prin obiecte nelegem n acest context: date de tip scalar (tipuri aritmetice sau pointeri), structuri sau tablouri. Nu putem aloca tablouri de funcii (nu exista aa ceva) dar putem aloca tablouri de pointeri catre funcii. Sintaxa operatorului new este simpl: cuvntul cheie new urmat de declaratorul abstract al tipului pe care vrem s-l alocm. Rezultatul operaiei este adresa variabilei alocate sau, n cazul alocrii unui tablou, adresa primului element al tabloului. Evident c aceste adrese trebuie reinute n nite poineri adecvai. Dac sistemul de operare nu mai gasete spaiul necesar alocrei rezultatul ntors de operatorul new este pointerul nul. Spaiul alocat este n zona de memorie liber (heap). Spaiul ocupat cu operatorul new (i numai acesta) poate fi eliberat cu ajutorul operatorului delete. Pentru a reui eliberarea spaiului, operatorul delete trebuie s primeasc adresa primului octet al locaiei, adic exact valoarea obinut cu operatorul new la alocare. La dealocarea unui tablou se utilizeaz forma delete [ ]. Vezi exemplele care urmeaz. Exemplul 1. Alocarea i dealocarea unei variabile simple:
#include<iostream> using namespace std; int main(void) { int *q,*p; p=new int; q=new int; *p=10;

*q=*p+23; cout<<"p="<<p<<endl; cout<<"q="<<q<<endl; cout<<"*q="<<*q<<endl; delete q; delete p; cout<<"q="<<q<<endl; cout<<"*q="<<*q<<endl; q=new int; *q=12; cout<<"q="<<q<<endl; cout<<"*q="<<*q<<endl; return 0;

} /* REZULTAT p=00346218 q=00346248 *q=33 q=00346248 *q=-572662307 //gunoi q=00346218 *q=12 Press any key to continue . . .*/

S observm c expresia delete q nu are ca efect tergerea din memorie a variabilei q, ci dealocarea zonei intite de q, ceea ce nseamn c zona de memorie respectiv este pus la dispoziia sistemului de operare care, n principiu, o poate aloca unui alt program care ruleaz concomitent cu al nostru. Mai precis, dup instruciunea
delete q;

pointerul q rmne, de fapt, nemodificat (i poate fi folosit pentru a reine o alt adres), n schimb inta sa, *q, nu mai are o valoare bine definit - vezi gunoiul aprut n mai sus. Subliniem c variabilele alocate dinamic sunt anonime, nu au un identificator asociat, nu au un nume propriu-zis. Astfel, variabila alocat cu
int *p = new int; int *p; p = new int;

sau, echivalent, cu nu poate fi numit altfel dect inta lui p , *p. Exemplul 2. Alocarea i dealocarea unei variabile de tip structur. In programul urmtor alocm dinamic o list cu trei noduri. In general, un nod este o structur care, pe lng cmpurile obinuite ale unei structuri, mai conine i un pointer ctre un o structur de acelai tip, pointer care va conine adresa nodului urmtor n list. Plecnd de la un prim nod, definim, din aproape n aproape, succesiunea de noduri care compun lista. Exemplul nostru este foarte simplu i are numai un rolul ilustrrii utilizrii operatorilor new i delete n alocarea structurilor. Deoarece am vrut s construim o list cu un numr cunoscut de noduri, 3, am utilizat de trei ori operatorul new n trei instruciuni de alocare:

#include<iostream> using namespace std; struct Nod{ int x; Nod* next; }; int main(void) { Nod* prim; prim=new Nod; prim->x=10; prim->next=new Nod; prim->next->x=20; prim->next->next=new Nod; prim->next->next->x=30; prim->next->next->next=NULL; for(Nod* q=prim; q!=NULL; q=q->next) cout<<q->x<<endl; delete delete delete return prim->next->next; prim->next; prim; 0;

} /* REZULTAT 10 20 30 Press any key to continue*/

*/

Amintim c prim->x este echivalent cu (*prim).x. Demn de reinut: instruciunea for de mai sus reprezint modalitatea general de parcurgere a unei liste liniare simplu nlnuite. Macroul NULL este utilizat pentru a marca sfitul listei (poate fi nlocuit peste tot unde apare cu valoarea 0). Eliberararea, n cursul rulrii programului, a locaiilor de memorie alocate dinamic i care nu mai sunt folosite n procesul de calcul este n totalitate n sarcina programatorului. Ca regul general, spaiul alocat cu new n interiorul unei funcii i care este utilizat numai n corpul acelei funcii (adic adresa sa nu este returnat funciei apelante) trebuie dealocat cu delete naintea ncheierii funciei, altfel adresa sa se pierde definitiv la ncheirea apelului i astfel memoria rmne ocupat inutil pn la terminarea execuiei programului. In exemplul urmtor exemplificm cele de mai sus: dac rulm programul cu instruciunea
delete p;

pus n comentariu, observm cum sistemul de operarare gsete din ce n ce mai greu spaiu liber pentru a satisface cererile de alocare dinamic. La fiecare apel de forma ocupa(i) este alocat pe stiv o locaie de 4 octei pentru pointerul p, care este variabil local, apoi, cu operatorul new, este alocat n zona de memorie liber spaiul necesar unei structuri de tip linie, care este utilizat numai n corpul funciei

ocupa(). La ncheierea apelului stiva coboar i cei 4 octei alocai lui p sunt eliberai n mod automat, dar zona alocat structurii de tip linie rmne alocat programului n

continuare i nu mai poate fi nici folosit i nici eliberat, pentru c adresa ei s-a pierdut odat cu dealocarea lui p. Urmtorul apel al funciei ocupa() va aloca dinamic o alt zon obiectului de tip linie. Exemplul 3. Eliberarea memoriei:
#include<iostream> using namespace std; struct linie{ int nrcrt; char text[1000000]; }; void stringCopy(char *dest, char *sursa){ while (*dest++ = *sursa++); return; } void ocupa(int n){ linie* p; p=new linie; p->nrcrt=n; stringCopy(p->text,"SPATIU OCUPAT IN ZONA DE MEMORIE LIBERA"); cout<<p<<" "<<p->nrcrt<<" "<<p->text<<endl; //delete p; return; } int main(void){ int i; for(i=0;i<1000;i++) ocupa(i); return 0; }

Iat cum arat consola de ieire la sfritul rulrii:

Observm c la fiecare apel adresa spaiului alocat crete cu 100000(hex), adic cu 1MB. Dac scoatem din comentariu instruciunea de dealocare i rulm programul, observm cum sistemul de operare reutilizeaz zona dealocat:

Este clar c aceasta este varianta corect. Alocarea dinamic a fost introdus tocmai pentru utilizarea judicioas a memoriei. Alocarea dinamic a unui tablou se deosebete de alocarea static n mod esenial prin faptul c dimensiunea tabloului alocat dinamic poate fi dat de o variabil (a crei valoare este cunoscut la execuie, bineneles). Exemplul 4 Alocarea i dealocarea unui tablou de variabile simple:
#include<iostream> using namespace std; int* aloca1D(int n){ int* p; p = new int[n]; for(int i=0;i<n;i++){ p[i]=100*(i+1); } return p; } void dealoca1D(int* p){ delete [] p; } int main(void){ int i,dim=5; int* vector; vector=aloca1D(dim); for(i=0;i<dim;i++) cout<<vector[i]<<endl; dealoca1D(vector); for(i=0;i<dim;i++) cout<<vector[i]<<endl; return 0; }

/* REZULTAT 100 200 300 400 500 -572662307 -572662307 -572662307 -572662307 -572662307 Press any key to continue*/

Dac, spre exemplificare, introducem n main() secvena


vector++; dealoca1D(vector);

programul trece de compilare far probleme dar la rulare obinem urmtorul mesaj de eroare:

Explicaie: valoarea pointerului vector a fost modificat, el nu mai inteste ctre primul octet al unei zone de memorie alocate cu new i, n consecin, sistemul de operare este pus n ncurctur. Un stil corect de programare presupune declararea pointerului n care ncarcm adresa unui tablou alocat cu new ca pointer constant (cu modificatorul const ), astfel:
int* const vector=aloca1D(dim);

Acum vector este cu adevarat numele noului tablou: este o constant de tip pointer care are ca int primul element al tabloului, el nu mai poate fi modificat, se comport exact ca numele unui tablou obinuit (obinut la compilare), cu singura diferena: dimensiunea sa se stabilete n momentul execuiei programului i nu la compilare. Exemplul 5.. Alocarea unui tablou de tablouri. In programul urmtor declarm, cu instruciunea:
int (*p)[5];

un pointer catre tipul int[5], adic pointer ctre tablouri de 5 ntregi, pe care l ncrcm (utiliznd operatorul new) cu adresa unui astfel de tablou, primul dintr-o serie de n tablouri succesive, fiecare cu cte 5 elemente. Definim astfel un tablou 2D cu un numr variabil de linii i cu un numr constant, 5, de coloane. Declaraia funciei aloca1D1D () este mai complicat: lista parametrilor formali este format numai de variabila int n iar rezultatul este de tip int(*)[5], adic pointer ctre int[5].
#include<iostream> #include<iomanip> using namespace std; int (*aloca1D1D(int n))[5]{ int (*p)[5]; p = new int[n][5]; for(int i=0;i<n;i++){ for(int j=0;j<5;j++){ p[i][j]=10*i +j; } } return p; } void dealoca1D1D(int (*p)[5]){ delete [] p; return; } int main(void){ int dim=3; int (*p)[5]; p=aloca1D1D(dim); for(int i=0;i<3;i++){ for(int j=0;j<5;j++) cout<<setw(5)<<p[i][j]; cout<<endl; } dealoca1D1D(p); return 0; } /*REZULTAT: 0 1 2 3 4 10 11 12 13 14 20 21 22 23 24 Press any key to continue*/

Avem deci un exemplu de alocare a unui tablou 2D de tipul n x 5. Alocarea unui tablou de tipul 5 x n este mult mai simpl, vezi secvena urmtoare:
int i,j, n=7; int* tab[5]; for(i=0;i<5;i++) tab[i]= new int[n]; for(i=0;i<5;i++) for(j=0;j<n;j++) tab[i][j]=i*j;

Declarm un tablou, tab, format din 5 pointeri ctre int i ncrcm fiecare pointer cu adresa unui vector de n elemente ntregi, alocat dinamic. Exemplul 6. Alocarea unui tablou 2D cu ambele dimensiuni variabile.
#include<iostream> using namespace std; double** aloca2D(int nrLin, int nrCol){ // double** double* // // mat -> mat[0] -> // // // // mat[1] -> // // int i; double** mat; mat= new double*[nrLin]; for(i=0;i<nrLin;i++){ mat[i]=new double[nrCol]; } return mat; } void dealoca2D(double** mat,int nrLin, int nrCol){ for(int i=0;i<nrLin;i++) delete mat[i]; delete mat; } void afiseaza(double** mat,int nrLin, int nrCol){ int i,j; for(i=0;i<nrLin;i++){ for(j=0;j<nrCol;j++) cout<<mat[i][j]<<" cout<<endl; } } void citeste(double** mat,int nrLin, int nrCol){ int i,j; for(i=0;i<nrLin;i++){ for(j=0;j<nrCol;j++) { cout<<"m["<<i<<"]["<<j<<"]="; cin>>mat[i][j]; } cout<<endl; } } int main(void){ int m,n; double **qmat; cout<<"dati nr. de linii, m=";cin>>m; cout<<"dati nr. de coloane, n=";cin>>n; qmat=aloca2D(m,n); citeste(qmat,m,n);

double mat[0][0] mat[0][1] mat[0][2] mat[1][0] mat[1][1] mat[1][2]

";

afiseaza(qmat,m,n); dealoca2D(qmat,m,n); return 0; }

Observm c, n funcia aloca2D(), alocarea se efectueaz n dou etape: mai nti alocam un tablou de pointeri (fiecare va inti ctre o linie a matricei) i apoi ncrcm fiecare astfel de pointer cu adresa primului element al unei linii nou alocate. Mai precis, dac dorim s lucrm cu o matrice de tip n x m de elemente de tip double, avem nevoie de un pointer mat de tip double** care s rein adresa primului pointer dintre cei n pointeri de tip double* (anume pointerii mat[0], mat[1], ... , mat[n-1]), care la rndul lor trebuie s rein adresele primelor elemente de pe fiecare linie. Dealocarea se face n ordine invers: nti dealocam liniile, rnd pe rnd, i apoi dealocm coloana de pointeri.

10

S-ar putea să vă placă și