Vai al contenuto

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.

new_delete.cpp
#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.

placement_new.cpp
#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).

Insidia

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.