Salta al contenuto
Note per Studenti C++ - allocazione dinamica della memoria con new e delete

C++ - allocazione dinamica della memoria con new e delete

In questa pagina 8

Come si costruisce un contenitore come std::vector: la memoria con dimensione nota solo a tempo di esecuzione si prende dal free store con new, si rilascia con delete, e una classe garantisce che questo avvenga sempre tramite il suo costruttore e il suo distruttore. Si basa su C++ - puntatori, array e castun puntatore è una variabile che contiene un indirizzo di memoria e sa il tipo dell'oggetto puntato (T*); si ottiene l'indirizzo con & e si accede al dato con * (dereference, un lvalue), nullptr indica nessun oggetto e due puntatori a tipi diversi sono tipi diversi; void* è un puntatore a memoria grezza (accetta qualsiasi puntatore ma non si dereferenzia) e i cast (static_cast, reinterpret_cast, const_cast) servono per interfacciarsi con hardware e codice non modificabile; il puntatore, a differenza della reference, si può spostare, può essere nullptr e va dereferenziato, e assegnarlo copia l'indirizzo (shallow copy) mentre assegnare una reference copia l'oggetto; un array C è una sequenza contigua di elementi dello stesso tipo, con dimensione costante, senza controllo dei limiti, che passato a una funzione decade in un puntatore al primo elemento; l'aritmetica dei puntatori somma o sottrae interi spostando di n elementi (non di n byte) e la differenza fra puntatori è in elementi; gli errori classici sono nullptr, puntatori non inizializzati, accessi fuori dai limiti e puntatori a oggetti usciti dallo scope.C++ - puntatori, array e cast → e sul layout visto in C++ - chiamate a funzione e layout della memoriauna chiamata a funzione crea un record di attivazione (RA) con una copia dei parametri e le variabili locali, impilato nello stack (LIFO) e tolto all'uscita; la dimensione dipende dalle variabili locali e il tempo per inserirlo è costante; il processo ha cinque regioni di memoria: text (istruzioni, spesso read-only), initialized data (globali e statiche inizializzate), uninitialized data o BSS (globali e statiche non inizializzate, azzerate dal sistema), stack (RA) e heap (allocazione dinamica con new e delete); la durata (storage duration) di una variabile è diversa dallo scope: le globali e le static sono statiche (esistono per tutto il programma, inizializzate una volta), le locali sono automatiche (create all'entrata nel blocco, distrutte in ordine inverso all'uscita); le globali sono inizializzate prima del main ma l'ordine fra file diversi non è definito.C++ - chiamate a funzione e layout della memoria →. In C lo stesso tema è malloc/free (Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →, Puntatori, struct e memoria dinamica in CPuntatori e passaggio per riferimento in C, array e aritmetica dei puntatori, stringhe, struct e typedef con l'operatore ->, malloc e free, puntatore a puntatore per modificare una testa; compilazione con Makefile; errori tipici (puntatori pendenti, perdite di memoria, off-by-one).Puntatori, struct e memoria dinamica in C →); la struttura dati costruita è una lista o un array dinamico (Realizzare contenitori su array e listeCome si realizza un ADT contenitore partendo da un array: lunghezza logica e capacità con raddoppio (costo ammortizzato), dizionario su array ordinato con ricerca binaria, coda doppia su array circolare, coda con priorità a livelli, ADT costruiti sopra altri ADT (pila di code, pila reversibile); tabella dei costi.Realizzare contenitori su array e liste →). Gli smart pointer che automatizzano tutto questo sono in C++ - smart pointerGli smart pointer (classi template di ) sono oggetti che detengono un puntatore e lo deallocano automaticamente all'uscita dallo scope (RAII), eliminando memory leak e dangling pointer con un overhead minimo. unique_ptr ha un solo proprietario: non si copia (costruttore e assegnamento di copia cancellati) ma si sposta (std::move, ritorno da funzione), ha reset(), release(), get(), * e ->; due unique_ptr costruiti con lo stesso puntatore grezzo sono un errore (doppia delete) che il compilatore non vede, e si evita con make_unique. shared_ptr ha più proprietari: si copia, tiene un contatore di riferimenti (use_count) e libera l'oggetto quando l'ultimo shared_ptr viene distrutto, a un costo maggiore; weak_ptr osserva senza possedere, per rompere i cicli. auto_ptr è deprecato (rimosso in C++17).C++ - smart pointer →.

Perché serve: costruire un vector

std::vector ha un comportamento "intelligente": cambia dimensione per ospitare gli elementi, conosce la propria dimensione (size()), si legge con [], aggiunge con push_back(). Tutto ciò non è supportato dall'hardware: è una sovrastruttura scritta dalla libreria standard. Per capire come, bisogna costruirne uno.

Una classe come class vector { int size, age0, age1, age2, age3; }; è statica: i membri sono decisi a tempo di compilazione. Per una struttura dinamica servono vettori le cui dimensioni sono note a tempo di esecuzione: esiste una zona di memoria adatta, il free store, e uno strumento di basso livello per usarla, l'allocazione dinamica. Il vettore è allora composto da due membri:

cpp
class vector {
    int sz;           // numero di elementi
    double* elem;     // puntatore al buffer di sz double sul free store
public:
    vector(int s);    // crea un array di s double e fa puntare elem ad esso
    int size() const { return sz; }
};

Il puntatore gestisce un buffer. Il resto (copia, dimensione variabile) si costruirà sopra questo: C++ - costruttori, inizializzazione, copia e spostamentouna buona interfaccia è completa e minimale, ha i costruttori giusti, supporta o proibisce la copia, usa tipi adeguati, dichiara const ciò che non modifica e libera le risorse nel distruttore; il costruttore di default (senza argomenti) esiste quando c'è un valore iniziale sensato, si ottiene con valori di default nei membri (in-class initializer) o argomenti di default, e explicit vieta la conversione implicita da un argomento; initializer_list permette la lista {a, b, c} e con le graffe ha la precedenza sul costruttore a un intero (vector{3} è un elemento, vector(3) sono tre); la copia di default è membro a membro (shallow copy) e con un puntatore a un buffer duplica il puntatore invece del contenuto (due oggetti che condividono i dati e una doppia delete), quindi servono il costruttore di copia e l'assegnamento di copia che fanno la deep copy (l'assegnamento prima alloca e copia, poi libera i vecchi dati, e deve tollerare l'auto-assegnamento); this è il puntatore all'oggetto corrente, parametro implicito delle funzioni membro; il costruttore e l'assegnamento di spostamento (T&&, rvalue reference) rubano il buffer della sorgente lasciandola vuota ma valida, il compilatore li sceglie da solo (con la copy elision a volte non servono nemmeno); i cinque (distruttore, copia, spostamento) vanno definiti insieme.C++ - costruttori, inizializzazione, copia e spostamento → e Esercizio - MyVector con memoria dinamica (laboratorio 3).

new: allocare dal free store

cpp
double* p = new double[4];     // 4 double sul free store; p punta al primo

new restituisce sempre un puntatore, anche se si alloca un solo elemento: l'indirizzo dell'oggetto, o del primo elemento di un array. La memoria restituita non ha un nome: la si gestisce tramite il puntatore. Il puntatore non sa quanti oggetti ci sono nell'array.

espressione risultato
new int un int
new int[4] quattro int consecutivi
new double[n] n può essere una variabile (a differenza degli array C)

Si accede come a un array (C++ - puntatori, array e castun puntatore è una variabile che contiene un indirizzo di memoria e sa il tipo dell'oggetto puntato (T*); si ottiene l'indirizzo con & e si accede al dato con * (dereference, un lvalue), nullptr indica nessun oggetto e due puntatori a tipi diversi sono tipi diversi; void* è un puntatore a memoria grezza (accetta qualsiasi puntatore ma non si dereferenzia) e i cast (static_cast, reinterpret_cast, const_cast) servono per interfacciarsi con hardware e codice non modificabile; il puntatore, a differenza della reference, si può spostare, può essere nullptr e va dereferenziato, e assegnarlo copia l'indirizzo (shallow copy) mentre assegnare una reference copia l'oggetto; un array C è una sequenza contigua di elementi dello stesso tipo, con dimensione costante, senza controllo dei limiti, che passato a una funzione decade in un puntatore al primo elemento; l'aritmetica dei puntatori somma o sottrae interi spostando di n elementi (non di n byte) e la differenza fra puntatori è in elementi; gli errori classici sono nullptr, puntatori non inizializzati, accessi fuori dai limiti e puntatori a oggetti usciti dallo scope.C++ - puntatori, array e cast →): *p e p[0] sono equivalenti, p[2] è il terzo elemento. Non esiste controllo dei limiti: pd[4] = 4.4 oltre un array di 4 elementi, o pd[-3], compilano; il programma "termina misteriosamente" in modo diverso a ogni esecuzione. Sono bug difficilissimi.

Inizializzazione

scrittura effetto
double* p0; grosso problema: puntatore non inizializzato
double* p1 = new double; double non inizializzato (il tipo built-in non viene azzerato)
double* p2 = new double{5.5}; double inizializzato a 5.5
double* p3 = new double[5]; array non inizializzato
double* p4 = new double[5]{0,1,2,3,4}; array inizializzato con una lista
double* p5 = new double[]{0,1,2,3,4}; dimensione dedotta (5)

Per un tipo definito dall'utente new X e new X[17] chiamano il costruttore di default. Se la classe non ce l'ha (per esempio Y ha solo un costruttore che prende un int), new Y e new Y[17] sono errori; si scrive new Y{13} o new Y[17]{0, 2, 3, ..., 16}.

new_base.cppcpp
#include <iostream>
using namespace std;

struct X {                       // ha un costruttore di default
    X()  { cout << "  X()\n"; }
    ~X() { cout << "  ~X()\n"; }
};

int main()
{
    // new restituisce SEMPRE un puntatore: all'oggetto, o al primo elemento di un array
    int* pi = new int;                       // 1 int NON inizializzato
    int* qi = new int[4];                    // 4 int NON inizializzati
    double* p2 = new double{5.5};            // 1 double inizializzato a 5.5
    double* p4 = new double[5]{0, 1, 2, 3, 4};   // array inizializzato
    double* p5 = new double[]{0, 1, 2, 3};       // dimensione dedotta: 4

    *pi = 10;                                // la memoria non ha nome: la raggiungo col puntatore
    for (int i = 0; i < 4; ++i) qi[i] = i * i;
    cout << *pi << ' ' << qi[3] << ' ' << *p2 << ' ' << p4[4] << ' ' << p5[3] << '\n';

    // *p e p[0] sono equivalenti; p[2] e' il terzo elemento
    cout << (*p4 == p4[0]) << ' ' << p4[2] << '\n';

    // senza controllo dei limiti: p4[5] o p4[-1] compilano, ma sono errori (comportamento indefinito)

    cout << "costruzione di un oggetto:\n";
    X* px = new X;                           // new: alloca E chiama il costruttore
    cout << "costruzione di un array di 3:\n";
    X* parr = new X[3];
    cout << "delete:\n";
    delete px;                               // delete: chiama il distruttore E libera
    cout << "delete[]:\n";
    delete[] parr;                           // delete[] per ogni new[]

    delete pi;  delete[] qi;  delete p2;  delete[] p4;  delete[] p5;
    return 0;
}
10 9 5.5 4 3
1 2
costruzione di un oggetto:
  X()
costruzione di un array di 3:
  X()
  X()
  X()
delete:
  ~X()
delete[]:
  ~X()
  ~X()
  ~X()

Letto con il codice: new X costruisce un solo X; new X[3] chiama tre volte il costruttore; delete px chiama il distruttore e libera; delete[] parr chiama il distruttore su tutti gli elementi (in ordine inverso) e libera il blocco.

Un puntatore non è inizializzato automaticamente a nullptr, e dopo la liberazione resta col vecchio valore; per controllare: if (p0 != nullptr) oppure if (p0).

Garbage e memory leak

Un puntatore si può spostare da un oggetto a un altro (a differenza delle reference), e questo genera problemi:

cpp
double* p = new double;
double* q = new double[1000];
q[700] = 7.7;          // ok
q = p;                 // attenzione: q ora punta al singolo double
double d = q[700];     // errore: fuori dall'oggetto puntato

Dopo q = p, l'array di 1000 double non ha più nessun puntatore: è garbage (spazzatura), memoria allocata ma irraggiungibile e inservibile. Questo fenomeno è il memory leak, e può portare all'esaurimento della memoria.

Liberare la memoria

In C++ nessuno libera la memoria al posto nostro: non c'è un garbage collector, a differenza di Java e di altri linguaggi. Produrre garbage è un errore tecnico grave. Perché lasciarlo al programmatore? Per due ragioni:

  • efficienza: il garbage collector impiega molte risorse (deve capire quale memoria è ancora raggiungibile);
  • controllo: decidiamo noi quando la memoria è rilasciata, e quindi di nuovo disponibile.

Si usano istruzioni dedicate, applicate a puntatori: delete per la memoria allocata con new, delete[] per quella allocata con new[] (usare quella sbagliata è un errore).

Caccia al memory leak

cpp
double* calc(int res_size, int max) {
    double* p = new double[max];            // buffer di lavoro
    double* res = new double[res_size];     // risultato
    return res;
}
double* r = calc(100, 1000);                // ... e nessun delete di r

I leak sono due: p (il puntatore locale sparisce all'uscita ma la memoria no) e r (il chiamante non libera). La correzione: liberare p prima del return e liberare r nel chiamante.

memory_leak.cppcpp
#include <cstdlib>
#include <iostream>
#include <new>
using namespace std;

// Sostituiamo gli operatori globali per contare le allocazioni
static int n_new = 0, n_delete = 0;
void* operator new(size_t s)  { ++n_new; return malloc(s); }
void* operator new[](size_t s) { ++n_new; return malloc(s); }
void operator delete(void* p) noexcept { if (p) ++n_delete; free(p); }
void operator delete[](void* p) noexcept { if (p) ++n_delete; free(p); }
void operator delete(void* p, size_t) noexcept { if (p) ++n_delete; free(p); }
void operator delete[](void* p, size_t) noexcept { if (p) ++n_delete; free(p); }

double* calc_con_leak(int res_size, int max)
{
    double* p = new double[max];             // buffer di lavoro: MAI liberato -> leak
    double* res = new double[res_size];
    p[0] = 0;
    return res;                              // solo il puntatore e' copiato: la memoria resta la stessa
}

double* calc_corretta(int res_size, int max)
{
    double* p = new double[max];
    double* res = new double[res_size];
    p[0] = 0;
    delete[] p;                              // libero ciò che non esce dalla funzione
    return res;                              // res passa al chiamante, che dovra' liberarlo
}

int main()
{
    int a0 = n_new, d0 = n_delete;           // l'ambiente puo' avere allocato per conto suo
    double* r = calc_con_leak(100, 1000);
    delete[] r;                              // il chiamante libera il risultato
    cout << "versione con leak:  new = " << n_new - a0 << ", delete = " << n_delete - d0 << '\n';

    a0 = n_new; d0 = n_delete;
    double* s = calc_corretta(100, 1000);
    delete[] s;
    cout << "versione corretta: new = " << n_new - a0 << ", delete = " << n_delete - d0 << '\n';
    return 0;
}
versione con leak:  new = 2, delete = 1
versione corretta: new = 2, delete = 2

Il programma sostituisce gli operatori globali new/delete per contare le allocazioni: nella prima versione ci sono 2 new e 1 delete (l'altro blocco è perso), nella seconda 2 e 2.

Passaggio di memoria tra funzioni

L'allocazione dinamica permette di creare un oggetto in uno scope e passarlo a un altro (il chiamante). Non esiste un meccanismo analogo a quello che libera una variabile locale automatica (per questo si chiama "automatica"). Dopo return res; viene copiato solo il puntatore: la memoria allocata rimane la stessa e il chiamante ne diventa responsabile.

Stack                              Free store
calc:  res = 0xac00  ----+
main:  r   = 0xac00  ----+----->  [ 1000 double ]  (da liberare con delete[] r in main)

Anche per un oggetto: vector* p = f(4); ... delete q; nel chiamante.

Errori frequenti

errore codice perché è grave
doppia cancellazione int* p = new int{5}; delete p; delete p; dopo il primo delete, p non è più a nostra disposizione: il proprietario è il gestore del free store e ci potrebbe già essere un altro oggetto
dangling pointer (puntatore pendente) int* p = new int{5}; delete p; *p = 1; dopo delete, p ha ancora lo stesso valore, ma non è più valido: usarlo è un errore
rimedio delete p; p = nullptr; un delete su nullptr non fa nulla e if (p) funziona

Il distruttore

Torniamo a vector: a fine vita bisogna deallocare. Una funzione clean_up() richiede che l'utente se ne ricordi. Come garantire che avvenga in modo automatico?

Definizione. Il distruttore ~T() è una funzione membro che il compilatore chiama implicitamente quando un oggetto deve essere distrutto: esce dallo scope, oppure è deallocato con delete.

cpp
class vector {
    int sz;
    double* elem;
public:
    vector(int s) : sz{s}, elem{new double[s]} { for (int i = 0; i < s; ++i) elem[i] = 0; }
    ~vector() { delete[] elem; }
    // ...
};

Così vector fa automaticamente quello che con p si fa a mano:

cpp
void f3(int n) {
    double* p = new double[n];
    vector v(n);               // v è liberato automaticamente
    // ... uso p e v ...
    delete[] p;                // p va liberato a mano
}

I distruttori sono utili per ogni risorsa da rilasciare: memoria, file, thread, lock, stream. Un oggetto acquisisce le risorse nel costruttore, durante la vita può acquisirne altre o rilasciarne, a fine vita le rilascia tutte. Questa è l'idea di RAII, sviluppata in C++ - eccezioni e gestione delle risorse (RAII)Un errore che non si può gestire dove avviene si segnala con throw e si gestisce con try/catch più in alto; la libreria standard ha la gerarchia std::exception (logic_error e runtime_error con le loro derivate) e le eccezioni proprie si derivano pubblicamente da una di esse; si cattura per riferimento. Le eccezioni mettono in crisi il codice che acquisisce risorse (memoria con new, file, lock, socket): se una chiamata lancia prima della delete, la risorsa si perde. La soluzione è RAII (Resource Acquisition Is Initialization): acquisire la risorsa nel costruttore di un oggetto locale e rilasciarla nel distruttore, perché all'uscita da uno scope, anche per eccezione (stack unwinding), i distruttori degli oggetti locali vengono chiamati. Garanzie di una funzione: basic (nessuna perdita, stato valido), strong (commit or rollback: se fallisce lo stato osservabile è quello di prima) e no-throw (non lancia).C++ - eccezioni e gestione delle risorse (RAII) →.

Azioni di new e delete. new alloca la memoria e invoca il costruttore. delete invoca il distruttore e libera la memoria.

Se il distruttore non è scritto, ne esiste uno generato dal compilatore: non è intelligente (non libera al posto nostro la memoria puntata da un puntatore nudo) ma chiama i distruttori degli oggetti membro. Per questo struct Customer { std::string name; std::vector<std::string> address; }; non perde memoria: alla distruzione di Fred si chiamano i distruttori di string e vector<string>.

vector_v0.cppcpp
#include <iostream>
using namespace std;

class Vector {
public:
    explicit Vector(int s)
        : sz{s}, elem{new double[s]}                 // alloca il buffer sul free store
    {
        for (int i = 0; i < s; ++i) elem[i] = 0;     // new non inizializza i double
        cout << "  Vector(" << sz << "): buffer allocato\n";
    }
    ~Vector()                                        // chiamato da solo all'uscita dallo scope
    {
        delete[] elem;                               // libera il buffer
        cout << "  ~Vector(" << sz << "): buffer liberato\n";
    }
    int size() const { return sz; }
    double& operator[](int n) { return elem[n]; }              // lettura e scrittura
    double operator[](int n) const { return elem[n]; }         // solo lettura, su oggetti const

private:
    int sz;                    // numero di elementi
    double* elem;              // puntatore al buffer
};

void f3(int n)
{
    double* p = new double[n];       // gestione MANUALE: serve il delete[]
    Vector v(n);                     // gestione AUTOMATICA: ci pensa il distruttore
    v[0] = 3.5;
    v[n - 1] = 7;
    p[0] = v[0] + v[n - 1];
    cout << "  p[0] = " << p[0] << ", v.size() = " << v.size() << '\n';
    delete[] p;
}   // qui v viene distrutto

int main()
{
    cout << "entro in f3\n";
    f3(4);
    cout << "esco da f3\n";

    const Vector cv(2);              // oggetto const
    cout << "cv[1] = " << cv[1] << '\n';   // usa la versione const di operator[]
    return 0;
}
entro in f3
  Vector(4): buffer allocato
  p[0] = 10.5, v.size() = 4
  ~Vector(4): buffer liberato
esco da f3
  Vector(2): buffer allocato
cv[1] = 0
  ~Vector(2): buffer liberato

Il distruttore di v scatta alla } di f3, mentre il buffer p richiede il delete[] esplicito. cv è const: chiama operator[] nella versione const.

Accedere al buffer: operator[]

Il vettore si legge e scrive con [], più naturale di get()/set(). Un primo tentativo double operator[](int n) { return elem[n]; } ritorna una copia: v[3] = x; non compila perché v[3] non è un lvalue. Un double* come risultato costringerebbe a scrivere *v[3] = x;. La soluzione è la reference:

cpp
double& operator[](int n) { return elem[n]; }          // lettura e scrittura

Resta un problema: non funziona su oggetti const. Si fa l'overloading su const: due versioni della funzione, una const e una no. Quella const può restituire una copia o una const reference.

cpp
double& operator[](int n);              // non const: v[1] = 2.0;
double  operator[](int n) const;        // const: double d = cv[1];
cpp
void ff(const vector& cv, vector& v) {
    double d = cv[1];     // ok: versione const
    cv[1] = 2.0;          // errore: versione const non restituisce un lvalue
    d = v[1];             // ok: versione non const
    v[1] = 2.0;           // ok
}

Restituire una reference a un dato privato (elem[n]) non viola l'incapsulamento? L'accesso è deciso dalla classe: l'utente non ottiene il puntatore elem, ma un alias a un elemento, e operator[] è comunque l'interfaccia pubblica scelta dal progettista. L'attenzione è su due punti: non restituire mai una reference a una variabile locale e ricordare che la reference resta valida solo finché esiste il buffer (se il vettore viene riallocato, vecchie reference diventano dangling).

Pro e contro dell'allocazione dinamica

vantaggi svantaggi
accedere a grandi quantità di memoria gestione esplicita della deallocazione
blocchi di dimensione nota solo a run-time gestione tramite puntatori: sorgente di molti errori subdoli
controllare quando la memoria è allocata e deallocata maggior carico computazionale: allocazioni e deallocazioni passano per chiamate al sistema operativo

L'allocazione dinamica va giustificata: bisogna sapere perché è necessaria invece di altri strumenti. Usarla a sproposito è un errore di design.

Domande d'esame

1. Descrivere l'allocazione dinamica in C++ e gli strumenti usati, con l'effetto sul layout della memoria e un esempio dentro una classe. Traccia. Il programma ha text, data, BSS, stack e heap (free store); new riserva memoria sull'heap a run-time, il puntatore (sullo stack o nei dati) contiene l'indirizzo; si libera con delete/delete[]; classe con new nel costruttore e delete[] nel distruttore (vector). Soluzione completa: Esercizio - Allocazione dinamica e layout della memoria (primo appello 2022-23, teoria).

2. Che cos'è un memory leak, e che cos'è un dangling pointer? Come si evitano? Traccia. Leak: memoria allocata e non più raggiungibile né liberata (garbage). Dangling pointer: puntatore con il vecchio valore dopo delete, che non va più usato. Si evitano liberando con delete/delete[] e ponendo il puntatore a nullptr; meglio con classi con distruttore o smart pointer.

3. Che cosa succede con student_list = new students[size]; Come si dealloca? Può esserci stack overflow? Traccia. Alloca size elementi dal free store, con chiamata al sistema operativo; si dealloca con delete[] student_list;. L'overflow dello stack no: la memoria è nell'heap; semmai new fallisce con eccezione std::bad_alloc. student_list è un puntatore, non uno std::list.

Versione ripasso

scrittura effetto
new double; / new double[5]; non inizializzati
new double{5.5}; inizializzato
new double[5]{0,1,2,3,4}; / new double[]{0,1,2,3}; array inizializzato, dimensione dedotta
new X; / new X[17]; chiamano il costruttore di default (errore se non c'è)
new Y{13}; / new Y[17]{...}; con costruttore a un argomento
  • Il puntatore non è inizializzato a nullptr; controllo if (p0 != nullptr) o if (p0).
  • Garbage / memory leak: area allocata senza più riferimenti (q = p con q che puntava a 1000 double); può esaurire la memoria.
  • Niente garbage collector (a differenza di Java): per efficienza e controllo. delete per new, delete[] per new[].
  • Caccia al leak:
cpp
double* calc(int res_size, int max) {
    double* p = new double[max];          // leak se non liberato
    double* res = new double[res_size];
    delete[] p;                           // libero il buffer di lavoro
    return res;                           // passa al chiamante (copia solo il puntatore)
}
double* r = calc(100, 1000);  /* ... */  delete[] r;   // il chiamante libera

Contatori sostituendo operator new/delete: versione con leak new = 2, delete = 1, corretta 2, 2.

Esercizi su questo argomento

Teoria collegata