Menu

Memoria dinamica in C++: new e delete spiegati

Come allocare memoria a runtime con new, liberarla con delete ed evitare i memory leak, i puntatori pendenti e i double free che arrivano quando gestisci l'heap a mano.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Perché chiedere memoria a runtime

Finora ogni variabile che hai creato viveva sullo stack: la sua dimensione è nota in fase di compilazione e viene distrutta automaticamente quando il suo scope termina. È veloce e sicuro, ma non gestisce il caso in cui non sai quanta memoria ti serve finché il programma non è in esecuzione: un buffer dimensionato in base all'input dell'utente, una struttura che deve sopravvivere alla funzione che l'ha creata o un grafo la cui forma non è nota in anticipo.

Per questi casi il C++ ti permette di allocare dall'heap (chiamato anche free store) con new, e di restituire la memoria con delete. L'espressione new riserva un blocco, esegue il costruttore e restituisce un puntatore a quel blocco, basandosi direttamente sui puntatori che hai visto nella pagina precedente.

La variabile p in sé vive sullo stack: è solo un puntatore. L'int a cui punta vive sull'heap e resta vivo finché non fai delete, indipendentemente da quanti scope si aprono e si chiudono.

Stack e heap

Questa distinzione è l'intero motivo per cui esiste new, quindi vale la pena renderla concreta.

void demo() {
    int a = 10;            // sullo stack, sparisce quando demo() termina
    int* b = new int(10);  // 'b' sullo stack, l'int a cui punta sull'heap
}                          // 'a' distrutto; l'int sull'heap è un LEAK, non viene mai liberato

Le differenze principali:

  • Stack: durata automatica, molto veloce, dimensione limitata (di solito pochi MB), liberato per te quando lo scope termina.
  • Heap: durata manuale, leggermente più lento, grande, e liberato solo quando chiami delete.

Lo scambio è flessibilità contro responsabilità: la memoria sull'heap vive esattamente quanto vuoi tu, ma diventi tu quello che deve ricordarsi di liberarla.

Allocare array con new[]

Quando ti serve un blocco la cui lunghezza viene decisa a runtime, usa la forma per array new T[n]. Restituisce un puntatore al primo elemento, e lo liberi con il corrispondente delete[].

La regola è rigida e facile da sbagliare: la memoria ottenuta con new si libera con delete, e quella ottenuta con new[] si libera con delete[]. Mescolarli, cioè fare delete arr su qualcosa allocato con new[], è comportamento indefinito, anche se sul tuo computer sembra funzionare.

I tre bug classici

La gestione manuale della memoria ha un piccolo insieme di errori che causano la maggior parte dei bug sull'heap. Impara a riconoscerli tutti e tre.

1. Memory leak: non chiami mai delete. Il blocco resta riservato per sempre. Innocuo una volta, fatale in un ciclo.

void leaky() {
    int* p = new int(5);
    // ... nessun delete ...
}   // p sparisce; l'int sull'heap ora è irraggiungibile E non liberato

2. Puntatore pendente: usi la memoria dopo averla liberata. Il puntatore contiene ancora il vecchio indirizzo, ma quella memoria non è più tua.

3. Double free: fai delete due volte sullo stesso blocco. Questo corrompe la contabilità interna dell'heap e di solito provoca un crash.

int* p = new int(1);
delete p;
delete p;   // double free, comportamento indefinito, spesso un crash

Impostare un puntatore a nullptr dopo averlo eliminato disinnesca sia l'uso di un puntatore pendente sia il double free: dereferenziare nullptr provoca subito un crash (facile da individuare), e delete nullptr è esplicitamente un'operazione sicura che non fa nulla.

Un ciclo realistico alloca, usa, libera

Mettendo tutto insieme, ecco la forma corretta della gestione manuale: alloca, usa, libera esattamente una volta e non toccare più il puntatore dopo.

Nota che per un tipo classe delete u fa due cose: prima esegue il distruttore dell'oggetto, poi rilascia la memoria grezza. Quest'ordine conta quando i tuoi oggetti possiedono a loro volta delle risorse.

Un'insidia sottile: se viene lanciata un'eccezione tra new e delete, il delete non viene mai eseguito e hai un leak. Avvolgere ogni allocazione in un try/catch per gestirlo è noioso e soggetto a errori, ed è esattamente il problema che risolve la prossima pagina.

Prossimo: Smart pointer

Hai visto il costo completo della gestione manuale della memoria: ogni new è una promessa di fare delete più tardi, e una sola liberazione mancata, doppia o anticipata è comportamento indefinito. Il C++ moderno non fa quasi mai questa promessa a mano. La prossima pagina introduce gli smart pointer, std::unique_ptr e std::shared_ptr, oggetti che possiedono un'allocazione sull'heap e chiamano delete per te automaticamente quando escono dallo scope, trasformando tutti e tre i bug classici in cose di cui si occupano il compilatore e RAII al posto tuo.

Domande frequenti

Qual è la differenza tra new e delete in C++?

new alloca memoria sull'heap a runtime e restituisce un puntatore a essa; delete libera la memoria allocata con new. Ogni new deve corrispondere esattamente a un delete, altrimenti hai un memory leak. Per gli array usa new[] con delete[].

Cosa succede se ti dimentichi di chiamare delete in C++?

Ottieni un memory leak: il blocco sull'heap resta riservato per tutta la vita del programma anche se nulla punta più a esso. Un singolo leak di solito è innocuo, ma i leak dentro un ciclo o in un servizio che gira a lungo crescono finché il programma esaurisce la memoria e va in crash.

Nel C++ moderno dovrei usare direttamente new e delete?

Raramente. Preferisci contenitori come std::vector o smart pointer (std::unique_ptr, std::shared_ptr) che liberano la memoria automaticamente. Vale la pena capire new/delete grezzi perché gli smart pointer li incapsulano, ma nel codice di tutti i giorni sono una fonte di memory leak e puntatori pendenti.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA