MySQL Java JavaScript PHP Python HTML-CSS C-sharp C++ Go

Memorie dinamică și smart-pointeri

Obiecte dinamice

În C++ se pot folosi diverse tipuri de obiecte, care diferă în ceea ce privește utilizarea memoriei. Astfel, obiectele globale sunt create la lansarea programului și eliberate la încheierea acestuia. Obiectele locale automate sunt create într-un bloc de cod și sunt șterse atunci când acel bloc de cod își încheie execuția. Obiectele locale statice sunt create înainte de prima lor utilizare și sunt eliberate la încheierea programului.

Obiectele globale, precum și cele locale statice, sunt plasate în memoria statică, iar obiectele locale automate sunt plasate în stivă. Obiectele din memoria statică și din stivă sunt create și șterse de către compilator. Memoria statică este eliberată la terminarea programului, iar obiectele din stivă există cât timp se execută blocul în care sunt definite.

Când blocul își încheie execuția, memoria din stivă alocată variabilelor blocului este eliberată. Este de remarcat că memoria alocată stivei are o dimensiune fixă și limitată.

Pe lângă aceste tipuri, în C++ se pot crea obiecte dinamice. Durata vieții acestora nu depinde de locul în care sunt create. Obiectele dinamice există până când sunt șterse explicit. Obiectele dinamice sunt plasate în memoria dinamică (free store). Aceasta este o zonă de memorie care nu este ocupată de sistemul de operare sau de alte programe încărcate în acel moment.

Utilizarea obiectelor dinamice are o serie de avantaje. În primul rând, utilizarea mai eficientă a memoriei – se alocă exact cât este necesar, iar după utilizare se eliberează imediat. În al doilea rând, putem folosi un volum mult mai mare de memorie, care altfel nu ar fi accesibil.

Dar acest lucru presupune și anumite restricții: trebuie să ne asigurăm că toate obiectele dinamice sunt șterse.

Pentru gestionarea obiectelor dinamice se folosesc operatorii new și delete.

Operatorul new alocă spațiu în memoria dinamică pentru un obiect și returnează un pointer către acel obiect.

Operatorul delete primește un pointer către un obiect dinamic și îl șterge din memorie.

Alocarea memoriei

Crearea unui obiect dinamic:

int *ptr{new int};
// sau așa
int *ptr = new int;

Operatorul new creează un nou obiect de tip int în memoria dinamică și returnează un pointer către acesta. Astfel, pointerul ptr conține adresa memoriei alocate. Valoarea unui astfel de obiect este nedefinită.

De asemenea, se poate inițializa obiectul la alocarea memoriei:

int *ptr{new int()};    // valoare implicită - 0
// int *ptr = new int(); - sau așa
std::cout << *ptr << std::endl;     // 0

Aici, obiectul din memorie către care pointează pointerul ptr primește valoarea implicită – numărul 0.

Pentru inițializare se pot folosi acolade:

int *ptr{new int{}};    // valoare implicită - 0
// int *ptr = new int{}; - sau așa
std::cout << *ptr << std::endl;     // 0

Se poate inițializa obiectul și cu o valoare specifică, de exemplu:

int *ptr{new int{5}};   
// variante alternative
// int *ptr = new int{5};
// int *ptr {new int(5)};
// int *ptr = new int(5);
std::cout << *ptr << std::endl;     // 5

Aici valoarea obiectului din memoria dinamică este 5.

Apoi, folosind pointerul, se poate atribui obiectului dinamic o altă valoare:

int *ptr{new int{5}};
std::cout << "*ptr = " << *ptr << std::endl;  // *ptr = 5
*ptr = 22;
std::cout << "*ptr = " << *ptr << std::endl;  // *ptr = 22

Eliberarea memoriei

Obiectele dinamice vor exista până când vor fi șterse explicit. După încheierea utilizării obiectelor dinamice, memoria acestora trebuie eliberată folosind operatorul delete:

#include <iostream>
  
int main()
{
    int *ptr{new int{5}};   // alocăm memorie
    std::cout << "*ptr = " << *ptr << std::endl;  // *ptr = 5
    delete ptr;             // eliberăm memoria
}

Acest lucru trebuie luat în considerare mai ales dacă obiectul dinamic este creat într-o parte a codului și utilizat în alta. De exemplu:

#include <iostream>
  
int* createPtr(int value)
{
    int *ptr {new int{value}};
    return ptr;
}
void usePtr()
{
    int *obj = createPtr(10);
    std::cout << *obj << std::endl;  // 10
    delete obj;  // obiectul trebuie eliberat
}
int main()
{
    usePtr();
}

În funcția usePtr primim din funcția createPtr un pointer către un obiect dinamic. Însă, după execuția funcției usePtr, acest obiect nu este șters automat din memorie (cum se întâmplă în cazul obiectelor locale automate). De aceea, trebuie șters explicit folosind operatorul delete.

Dacă operatorul delete nu este apelat explicit, memoria dinamică alocată va fi eliberată după terminarea programului.

Totuși, este important de menționat că, chiar și după eliberarea memoriei, pointerul conține în continuare vechea adresă, deși memoria respectivă este considerată eliberată și disponibilă pentru viitoare obiecte dinamice. Un astfel de pointer se numește "dangling pointer" (pointer agățat).

Putem chiar încerca să accesăm acest pointer. Însă utilizarea obiectului prin pointer după ștergerea sa sau aplicarea repetată a operatorului delete asupra unui pointer pot duce la rezultate imprevizibile:

int *ptr {new int{12}};
std::cout << *ptr << std::endl;  // 12
delete ptr;
 
// scenarii eronate
std::cout << *ptr << std::endl;  // obiectul indicat de ptr este deja șters!
delete ptr; // obiectul indicat de ptr este deja șters!

Pentru a evita astfel de pointeri agățați, se recomandă ca după eliberarea memoriei să se seteze pointerul la zero:

int *ptr {new int{12}};
std::cout << *ptr << std::endl;  // 12
delete ptr;
ptr = nullptr;          // resetăm pointerul

În cazul unei încercări de accesare a obiectului printr-un pointer nul, programul se va închide. Aplicarea operatorului delete asupra unui pointer nul nu are niciun efect.

Este de asemenea frecvent cazul în care mai mulți pointeri indică același obiect dinamic. Dacă operatorul delete este aplicat unuia dintre pointeri, memoria obiectului este eliberată, iar prin al doilea pointer nu se mai poate utiliza acel obiect. Dacă, după aceea, se aplică operatorul delete și asupra celui de-al doilea pointer, memoria dinamică poate fi coruptă.

Totuși, faptul că pointerii devin invalizi după aplicarea operatorului delete nu înseamnă că nu mai pot fi folosiți deloc. Ei pot fi folosiți din nou dacă li se atribuie adresa unui alt obiect:

#include <iostream>
 
int main()
{
    int *p1 {new int{12}};
    int *p2 {p1};   // p1 și p2 indică același obiect
     
    std::cout << *p1 << std::endl;  // 12
    std::cout << *p2 << std::endl;  // 12
    delete p1;      // adresele din p1 și p2 devin invalide
    p1 = nullptr;
    p2 = nullptr;
  
    p1 = new int{11};   // p1 indică un nou obiect
    std::cout << *p1 << std::endl;  // 11
    delete p1;
}

Aici, după ștergerea obiectului către care indica p1, acestuia i se atribuie adresa unui alt obiect în memoria dinamică. În consecință, putem folosi din nou pointerul p1. În același timp, adresa din pointerul p2 va rămâne în continuare invalidă.