Sunteți pe pagina 1din 10

POINTERI (III)

Alocarea dinamică a memoriei:


operatorii new şi delete

Ne punem problema utilizării, într-un program, a unui număr 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 rulării, chiar dacă iniţial se află într-un fişier 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 adevă-
rat mare, ne vom lovi de un neajuns major: nu pot fi declarate tablouri de dimensiuni va-
riabile: alocarea unui tablou are un caracter static, este făcută la compilarea programului,
prin urmare numărul de elemente trebuie desemnat printr-o constantă. Programatorul
trebuie să estimeze o dimensiune maximă, acoperitoare pentru toate situaţiile în care va fi
folosit respectivul tablou, şi să-l declare în consecinţă. Apare astfel o risipă inerentă de
spaţiu de memorie. De exemplu, dacă declarăm tabloul
double mat[100][100];
şi la rulare îl încărcăm cu o matrice de 10 x 10 elemente, folosim numai 1% din spaţiul
rezervat prin program! Risipa este evidentă, iar remediul constă în alocarea dinamică a
memoriei, adică alocarea în momentul rulării programului. Vom renunţa să declarăm
tablouri, în locul lor vom folosi variabile de tip pointer pentru a reţine adresele locaţiilor
de memorie obţinute de la sistemul de calcul prin operaţii de alocare. In cazul alocării
tablourilor se garantează că elementele succesive au locaţii succesive şi, în consecinţă,
parcurgerea tablourilor alocate dinamic poate fi facută cu operatorul de indexare, ca în
cazul alocării statice, utilizând identitatea dintre expresia p[i] şi *(p+i). Să reamintim de
la bun început diferenţa esenţială dintre pointeri şi tablouri: instrucţiunea
double* p
declară o variabilă de tip pointer către double, în timp ce
double tab[10];
declară un tablou al cărui nume, tab, este o constantă de tip pointer către double, cu
valoarea: adresa primului element al tabloului, atribuirea
p=tab;
este corectă, în locaţia de memorie numită p se scrie adresa primului element din tabloul
tab, în timp ce atribuirea
tab=p;
nu este permisă: nu există o locaţie de memorie numită tab care să conţină 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ă informaţie este utilizată la
proiectarea codului, dar programatorul nu are acces la acel tabel. Putem spune, aşadar, 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 cărui valoare poate fi modificată de
către programator şi care, în conscinţă, trebuie iniţializată tot de către acesta.

1
După cum ştim deja, un pointer poate fi iniţializat corect doar în următoarele 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ă, aşa cum vom vedea în continuare.

In limbajul C alocarea dinamică a memoriei poate fi facută numai cu funcţii din


biblioteci specializate în gestionarea memoriei, cele mai utilizate fiind funcţiile malloc(),
calloc(), free(), realloc(), toate din <malloc.h>. In C++ au fost introduşi, 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 spaţiu de
memorie pentru un singur obiect, caz în care new întoarce adresa obiectului alocat, sau
pentru a rezerva spaţiu pentru un tablou cu un numar variabil de obiecte (dar bine
precizat, la rulare, în momentul evaluării expresiei), caz în care new întoarce adresa
primului element din tablou.
Prin “obiecte” înţelegem în acest context: date de tip scalar (tipuri aritmetice sau
pointeri), structuri sau tablouri. Nu putem aloca tablouri de funcţii (nu exista aşa ceva)
dar putem aloca tablouri de pointeri catre funcţii.
Sintaxa operatorului new este simplă: cuvântul cheie new urmat de declaratorul
abstract al tipului pe care vrem să-l alocăm. Rezultatul operaţiei este adresa variabilei
alocate sau, în cazul alocării unui tablou, adresa primului element al tabloului. Evident că
aceste adrese trebuie reţinute în nişte poineri adecvaţi. Dacă sistemul de operare nu mai
gaseşte spaţiul necesar alocărei rezultatul întors de operatorul new este pointerul nul.
Spaţiul alocat este în zona de memorie liberă (heap).
Spaţiul ocupat cu operatorul new (şi numai acesta) poate fi eliberat cu ajutorul
operatorului delete. Pentru a reuşi eliberarea spaţiului, operatorul delete trebuie să pri-
mească adresa primului octet al locaţiei, adică exact valoarea obţinută 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;

2
*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ă observăm că expresia delete q nu are ca efect ştergerea din memorie a vari-


abilei q, ci dealocarea zonei ţintite de q, ceea ce înseamnă că zona de memorie
respectivă este pusă la dispoziţia sistemului de operare care, în principiu, o poate aloca
unui alt program care rulează concomitent cu al nostru. Mai precis, după instrucţiunea
delete q;
pointerul q rămâne, de fapt, nemodificat (şi poate fi folosit pentru a reţine o altă adresă),
în schimb ţinta sa, *q, nu mai are o valoare bine definită - vezi gunoiul apărut î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;
sau, echivalent, cu
int *p;
p = new int;
nu poate fi numită altfel decât “ţinta lui p” , *p.

Exemplul 2. Alocarea şi dealocarea unei variabile de tip structură.


In programul următor alocăm dinamic o listă cu trei noduri. In general, un nod
este o structură care, pe lângă câmpurile obişnuite ale unei structuri, mai conţine şi un
pointer către un o structură de acelaşi tip, pointer care va conţine adresa nodului următor
în listă. Plecând 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
ilustrării utilizării operatorilor new şi delete în alocarea structurilor.
Deoarece am vrut să construim o listă cu un număr cunoscut de noduri, 3, am
utilizat de trei ori operatorul new în trei instrucţiuni de alocare:

3
#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 prim->next->next;
delete prim->next;
delete prim;
return 0;
}
/* REZULTAT
10
20
30
Press any key to continue*/

*/
Amintim că prim->x este echivalent cu (*prim).x. Demn de reţinut: instrucţiunea for
de mai sus reprezintă modalitatea generală de parcurgere a unei liste liniare simplu înlăn-
ţuite. Macroul NULL este utilizat pentru a marca sfâşitul listei (poate fi înlocuit peste tot
unde apare cu valoarea 0).
Eliberararea, în cursul rulării programului, a locaţiilor de memorie alocate
dinamic şi care nu mai sunt folosite în procesul de calcul este în totalitate în sarcina
programatorului. Ca regulă generală, spaţiul alocat cu new în interiorul unei funcţii şi
care este utilizat numai în corpul acelei funcţii (adică adresa sa nu este returnată funcţiei
apelante) trebuie dealocat cu delete înaintea încheierii funcţiei, altfel adresa sa se pierde
definitiv la încheirea apelului şi astfel memoria rămâne ocupată inutil până la terminarea
execuţiei programului.
In exemplul următor exemplificăm cele de mai sus: dacă rulăm programul cu in-
strucţiunea
delete p;
pusă în comentariu, observăm cum sistemul de operarare găseşte din ce în ce mai greu
spaţiu liber pentru a satisface cererile de alocare dinamică. La fiecare apel de forma
ocupa(i) este alocată pe stivă o locaţie de 4 octeţi pentru pointerul p, care este
variabilă locală, apoi, cu operatorul new, este alocat în zona de memorie liberă spaţiul
necesar unei structuri de tip linie, care este utilizată numai în corpul funcţiei

4
ocupa(). La încheierea apelului stiva coboară şi cei 4 octeţi alocaţi lui p sunt eliberaţi
în mod automat, dar zona alocată structurii de tip linie rămâne 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. Următorul apel al funcţiei 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 ieşire la sfărşitul rulării:

5
Observăm că la fiecare apel adresa spaţiului alocat creşte cu 100000(hex), adică cu 1MB.
Dacă scoatem din comentariu instrucţiunea de dealocare şi rulăm programul, observăm
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 deosebeşte de alocarea statică în mod esenţial


prin faptul că dimensiunea tabloului alocat dinamic poate fi dată de o variabilă (a cărei
valoare este cunoscută la execuţie, bineînţeles).

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;
}

6
/* REZULTAT

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

Dacă, spre exemplificare, introducem în main() secvenţa


vector++;
dealoca1D(vector);
programul trece de compilare fară probleme dar la rulare obţinem următorul mesaj de
eroare:

Explicaţie: valoarea pointerului vector a fost modificată, el nu mai ţinteste către primul
octet al unei zone de memorie alocate cu new şi, în consecinţă, sistemul de operare este
pus în încurcătură. Un stil corect de programare presupune declararea pointerului în care
încarcăm 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 obişnuit (obţinut la compilare), cu singura diferenţa: dimensiunea sa
se stabileşte în momentul execuţiei programului şi nu la compilare.

Exemplul 5.. Alocarea unui tablou de tablouri.


In programul următor declarăm, cu instrucţiunea:
int (*p)[5];

7
un pointer catre tipul int[5], adică pointer către tablouri de 5 întregi, pe care îl încăr-
căm (utilizând operatorul new) cu adresa unui astfel de tablou, primul dintr-o serie de n
tablouri succesive, fiecare cu câte 5 elemente. Definim astfel un tablou 2D cu un număr
variabil de linii şi cu un număr constant, 5, de coloane. Declaraţia funcţiei aloca1D1D ()
este mai complicată: lista parametrilor formali este formată numai de variabila int n iar
rezultatul este de tip int(*)[5], adică pointer către 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 secvenţa următoare:
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;

8
Declarăm un tablou, tab, format din 5 pointeri către int şi încărcăm 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* double
//
// mat -> mat[0] -> mat[0][0]
// mat[0][1]
// mat[0][2]
//
// mat[1] -> mat[1][0]
// mat[1][1]
// mat[1][2]
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);

9
afiseaza(qmat,m,n);
dealoca2D(qmat,m,n);

return 0;
}
Observăm că, în funcţia aloca2D(), alocarea se efectuează în două etape: mai întâi
alocam un tablou de pointeri (fiecare va ţinti către o linie a matricei) şi apoi încărcăm fie-
care astfel de pointer cu adresa primului element al unei linii nou alocate. Mai precis,
dacă dorim să lucrăm cu o matrice de tip n x m de elemente de tip double, avem nevoie
de un pointer mat de tip double** care să reţină adresa primului pointer dintre cei n
pointeri de tip double* (anume pointerii mat[0], mat[1], ... , mat[n-1]), care la rândul lor
trebuie să reţină adresele primelor elemente de pe fiecare linie. Dealocarea se face în
ordine inversă: întâi dealocam liniile, rând pe rând, şi apoi dealocăm coloana de pointeri.

10

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