Memoria dinamica: new, delete e i loro pericoli
Impara come funziona l'allocazione dinamica con new e delete, comprendi i pericoli delle perdite di memoria, del doppio rilascio e dell'uso dopo il rilascio, e scopri perché il C++ moderno non dovrebbe contenere alcun new/delete grezzo.
Allocazione dinamica con new e delete
Lo stack è veloce ma limitato in dimensione e scope. Quando ti servono oggetti che sopravvivono allo scope che li ha creati, o la cui dimensione non è nota a tempo di compilazione, allochi sull'heap (free store).
new T(args) alloca memoria per un T, ne chiama il costruttore e restituisce un T*. delete p chiama il distruttore e libera la memoria. Per gli array: new T[n] e delete[] p. Abbinare new a delete[] o new[] a delete è comportamento indefinito — il compilatore non lo rileverà.
new, delete e le loro varianti per array
Ecco l'API di allocazione grezza. Studiala per riconoscerla nel codice legacy, ma non scriverla mai in codice nuovo — usa invece gli smart pointer.
#include <iostream>
#include <cstring>
struct Widget {
int id;
Widget(int i) : id(i) { std::cout << "Widget " << id << " created\n"; }
~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
};
int main() {
// Single object
Widget* w = new Widget(1);
std::cout << "Using widget " << w->id << '\n';
delete w; // destructor called, memory freed
// Array
Widget* arr = new Widget[3]{{10}, {20}, {30}};
for (int i = 0; i < 3; ++i)
std::cout << arr[i].id << ' ';
std::cout << '\n';
delete[] arr; // MUST use delete[] for new[]
// UNDEFINED BEHAVIOR — don't do these:
// delete arr; // wrong: used delete instead of delete[]
// delete[] w; // wrong: used delete[] instead of delete
}Placement new
Il placement new costruisce un oggetto a un indirizzo di memoria specifico senza allocare. È usato dagli allocatori, dai memory pool e internamente da contenitori come std::vector. Devi chiamare il distruttore esplicitamente — delete NON va usato su oggetti creati con placement new.
#include <iostream>
#include <new> // required for placement new
#include <cstddef> // std::byte
#include <memory> // std::align
struct Sensor {
int id;
double reading;
Sensor(int i, double r) : id(i), reading(r) {
std::cout << "Sensor " << id << " init\n";
}
~Sensor() { std::cout << "Sensor " << id << " shutdown\n"; }
};
int main() {
// Pre-allocate raw storage (aligned)
alignas(Sensor) std::byte buffer[sizeof(Sensor)];
// Construct in pre-allocated memory
Sensor* s = new (buffer) Sensor(42, 3.14);
std::cout << "Reading: " << s->reading << '\n';
// Must destroy explicitly — do NOT use delete
s->~Sensor(); // explicit destructor call
// delete s; // WRONG — memory was not allocated by new
}I tre bug di memoria letali
L'uso grezzo di new/delete ti espone a tre categorie di bug:
1. Perdita di memoria (memory leak) — allocare con new senza mai chiamare delete. La memoria non viene mai restituita al sistema operativo (fino all'uscita del processo). In un server che gira a lungo, questo finisce per consumare tutta la RAM disponibile.
2. Doppio rilascio (double free) — chiamare delete due volte sullo stesso puntatore. La seconda delete corrompe le strutture dati interne dell'allocatore dell'heap, provocando spesso crash molto più tardi in codice del tutto estraneo. È estremamente difficile da debuggare.
3. Uso dopo il rilascio (use-after-free) — accedere alla memoria dopo una delete. La memoria potrebbe essere stata riutilizzata per un'altra allocazione, quindi potresti leggere o scrivere i dati di un altro oggetto. È una vulnerabilità di sicurezza (un bug da CVE).
Considera: f(new A(), new B()). Il compilatore può valutare gli argomenti in qualsiasi ordine e può intercalarli:
1. Allocare memoria per A
2. Allocare memoria per B
3. Costruire A
4. Costruire B
Se il passo 4 lancia un'eccezione, A viene perso — nessuno chiama delete su di esso. Anche f(std::shared_ptr(new A()), std::shared_ptr(new B())) aveva questo problema prima del C++17 (che garantisce che ogni argomento sia valutato completamente prima del successivo). La soluzione: usa sempre std::make_unique o std::make_shared — eseguono allocazione e costruzione come un'unica operazione a prova di eccezioni.
Panoramica sugli allocatori personalizzati
Le new/delete predefinite usano l'allocatore globale (tipicamente malloc/free), che è generico ma non ottimale per tutti gli schemi d'uso. Gli allocatori personalizzati offrono:
- Allocatori ad arena o lineari — spostano in avanti un puntatore e liberano tutto in una volta. Ottimi per i dati per singolo frame nei videogiochi.
- Pool allocator — pre-allocano blocchi di dimensione fissa. Allocazione e rilascio in O(1), nessuna frammentazione.
- Allocatori a stack — allocazione LIFO, estremamente veloce.
I contenitori C++ accettano l'allocatore come parametro template: std::vector. Il namespace std::pmr (polymorphic memory resources) introdotto in C++17 fornisce un framework di allocatori polimorfici a runtime che evita il problema del parametro template.
Nel C++ moderno (C++14 e successivi), il codice applicativo ben scritto non dovrebbe contenere alcuna new né delete grezza:
- Proprietà singola: usa std::make_unique
- Proprietà condivisa: usa std::make_shared
- Array dinamici: usa std::vector
- Stringhe: usa std::string
- Oggetti opzionali: usa std::optional
Le new/delete grezze possono comparire nel codice di libreria (allocatori, contenitori personalizzati), ma il codice applicativo non dovrebbe mai averne bisogno. Se vedi una new in una code review, chiedi perché — esiste quasi sempre un'alternativa più sicura.
- Abbinare
new/delete[]oppurenew[]/deleteè comportamento indefinito - Perdite di memoria, doppio rilascio e uso dopo il rilascio sono i tre bug di memoria letali
f(new A(), new B())può perdere memoria se un costruttore lancia un'eccezione (prima del C++17 anche con i wrapper shared_ptr)- Il placement new costruisce a un indirizzo dato — distruggi manualmente, non usare mai
delete - Il codice applicativo C++ moderno dovrebbe avere zero
new/deletegrezze — usa smart pointer e contenitori
Quiz — Metti alla prova le tue conoscenze
(15 XP)1. Che cosa succede se usi `delete` su memoria allocata con `new[]`?
2. Perché `f(new A(), new B())` può causare una perdita di memoria?
3. Come dovresti distruggere un oggetto creato con placement new?