Memoria dinamică

În programele din capitolele anterioare, întreaga memorie trebuia determinată înainte de execuția programului, prin definirea de variabile. Dar sunt situații în care necesarul de memorie se poate determina abia în momentul execuției programului. De exemplu, atunci când cantitatea de memorie depinde de datele de intrare furnizate de către utilizator. În aceste situații, programele au nevoie să aloce memorie dinamic, motiv pentru care limbajul C++ integrează operatorii new și delete.

Operatorii new și new[]

Memoria dinamică este alocată folosind operatorul new. new este urmat de un tip de dată și, dacă este cazul uneia sau mai multor secvențe, de numărul de elemente al acestora cuprins între paranteze drepte []. Operatorul returnează un pointer către începutul blocului de memorie alocat. Sintaxa este:

pointer = new tip_data
pointer = new tip_data [numar_de_elemente]

Prima expresie este folosită pentru a aloca memorie care conține un singur element de tipul tip_data. A doua instrucțiune este folosită pentru a aloca un bloc (tablou) de elemente de tip tip_data, unde numar_de_elemente este o valoare întreagă reprezentând numărul acestora. De exemplu:

1
2
int * foo;
foo = new int [5];

În acest caz, sistemul alocă dinamic spațiu pentru cinci elemente de tip int și returnează un pointer la primul element al secvenței, pointer care i se atribuie lui foo (variabilă pointer). De aceea, foo pointează acum către un bloc valid de memorie care poate stoca până la cinci elemente de tip int.



Aici, foo este un pointer și, de aceea, primul element către care pointează foo poate fi accesat die cu expresia foo[0] fie cu expresia *foo (cele două sunt echivalente). Al doilea element poate fi accesat fie cu foo[1] fie cu *(foo+1), și așa mai departe...

Există o mare diferență între declararea normală a unui tablou și alocarea dinamică a unui bloc de memorie cu operatorul new. Cel mai important este faptul că dimensiunea unui tablou normal trebuie să fie o expresie constantă, ceea ce înseamnă că dimensiunea trebuie determintă la momentul proiectării programului, înainte de rularea lui, în timp ce alocarea dinamică realizată cu operatorul new permite gestionarea memoriei în timpul execuției, folosind ca dimensiune orice variabilă.

Memoria dinamică solicitată de programul nostru este alocată de către sistem din memoria heap. Totuși, memoria calculatorului este o resursă limitată și poate fi epuizată. De aceea, nu avem există garanție că toate solicitările de alocare dinamică folosind operatorul new vor fi onorate de către sistem.

C++ asigură două mecanisme pentru a verifica dacă alocarea a fost reușită:

Unul presupune gestionarea excepțiilor. Folosind această metodă, o excepție de tipul bad_alloc apare cănd alocarea eșuează. Excepțiile sunt un instrument puternic C++ pe care îl vom explica mai tărziu în aceste lecții. Deocamdată, ar trebui să știți că dacă apare o asemnea exceptie și nu este tratată de un handler specific, execuția programului se încheie.

Aceasta este metoda folosită implcit de către operatorul new și cea folosită într-o declarație ca următoarea:

1
foo = new int [5];  // dacă alocarea eșuează, apare o excepție  

Cealaltă metodă este cunoscută ca nothrow și când o alocare eșuează, în loc de apariția excepției bad_alloc sau terminarea programului, pointer-ul returnat de new este un pointer nul, iar programul își continuă execuția normală.

Această metodă poate fi precizată printr folosirea unui obiect special numit nothrow, declarat în header <new>, ca argument pentru new:

1
foo = new (nothrow) int [5];
În acest caz, dacă eșuează alocarea acestui bloc de memorie, eșecul poate fi detectat verificând dacă foo este pointerul nul:

1
2
3
4
5
int * foo;
foo = new (nothrow) int [5];
if (foo == nullptr) {
  // eroare la alocarea memoriei. Luăm măsuri.
}

Metoda nothrow pare să producă un cod mai puțin eficient decât excepțiile, căci presupune verificarea explicită a obținerii pointerului nul după fiecare alocare. De aceea, în general, este preferat mecanismul excepțiilor, cel puțin pentru alocările critice. Totuși, majoritatea exemplelor noastre vor folosi mecanismul nothrow pentru simplicitatea sa.

Operatorii delete și delete[]

În cele mai multe cazuri, memoria alocată dinamic este necesară doar pentru o anumită perioadă de timp în cadrul unui program; de îndată ce nu mai este necesară, poate fi eliberată astfel incăt memoria să redevină disponibilă pentru alte solicitări de memorie dinamică. Pentru aceasta avem operatorul delete, a cărui sintaxă este:

1
2
delete pointer;
delete[] pointer;

Prima instrucțiune eliberează memoria pentru un singur element folosind new, iar a doua eliberează memoria alocată pentru un tablou de elemente folosind new și o dimensiune între paranteze drepte ([]).

Valoarea transmisă ca parametru delete poate fi un pointer către un bloc de memorie alocat anterior cu new sau un pointer nul (în cazul unui pointer nul, delete nu are niciun efect).

// rememb-o-matic
#include <iostream>
#include <new>
using namespace std;

int main ()
{
  int i,n;
  int * p;
  cout << "Cate numere vrei sa tastezi? ";
  cin >> i;
  p= new (nothrow) int[i];
  if (p == nullptr)
    cout << "Eroare: memoria nu poate fi alocata!";
  else
  {
    for (n=0; n<i; n++)
    {
      cout << "Tasteaza numar: ";
      cin >> p[n];
    }
    cout << "Ai tastat: ";
    for (n=0; n<i; n++)
      cout << p[n] << ", ";
    delete[] p;
  }
  return 0;
}
Cate numere vrei sa tastezi? 5
Tasteaza numar: 75
Tasteaza numar: 436
Tasteaza numar: 1067
Tasteaza numar: 8
Tasteaza numar: 32
Ai tastat: 75, 436, 1067, 8, 32,

Să observăm că valoarea dintre parantezele drepte din expresia new este valoarea unei variabile furnizată de către utilizator (i), nu este o expresie constantă:

1
p= new (nothrow) int[i];

Există întotdeauna posibilitatea ca un utilizator să dea pentru i o valoare așa de mare încât sistemul să nu dispună de suficientă memorie pentru a o aloca. De exemplu, când am răspuns cu 1 billion la întrebarea "Cate numere vrei sa tastezi? ", sistemul meu nu a putut aloca atâta memorie pentru program și am primit pe ecran mesajul pregătit pentru asemenea situații (Eroare: memoria nu poate fi alocata!).

Se consideră o buna practică ca programul să fie scris astfel încât să fie capabil de a gestiona eșecurile în cazul alocării de memorie, fie prin verificarea valorii pointerului (pentru nothrow) fie prin prinderea excepției.

Memoria dinamică în C

C++ integrează operatorii new și delete pentru alocarea dinamică a memoriei. Dar acești operatori nu erau definiți în limbajul C; în schimb, se folosea o bibliotecă din care se apelau funcțiile malloc, calloc, realloc și free, definite în header-ul <cstdlib> (cunoscut ca <stdlib.h> în C). Funcțiile sunt încă disponibile și în C++ și pot fi folosite pentru alocarea și eliberarea memoriei dinamice.

Să observăm, totuși, că blocurile de memorie alocate cu aceste funcții nu sunt întotdeauna compatibile cu cele returnate de new, deci ele nu pot fi amestecate; fiecare ar trebui gestionat cu propriul set de funcții sau operatori.

Index
Index