Aller au contenu

Mémoire dynamique : new, delete et leurs dangers

Apprenez comment fonctionne l'allocation dynamique avec new et delete, comprenez les dangers des fuites de mémoire, des doubles libérations et des utilisations après libération, et découvrez pourquoi le C++ moderne ne devrait contenir aucun new/delete brut.

Allocation dynamique avec new et delete

La pile (stack) est rapide mais limitée en taille et en portée. Lorsque vous avez besoin d'objets qui survivent à la portée qui les a créés, ou dont la taille est inconnue à la compilation, vous les allouez sur le tas (heap, ou free store).

new T(args) alloue la mémoire pour un T, appelle son constructeur et retourne un T*. delete p appelle le destructeur et libère la mémoire. Pour les tableaux : new T[n] et delete[] p. Associer new à delete[] ou new[] à delete est un comportement indéfini — le compilateur ne le détectera pas.

new, delete et leurs variantes pour tableaux

Voici l'API d'allocation brute. Étudiez-la pour la reconnaître dans du code hérité, mais n'écrivez jamais cela dans du nouveau code — utilisez plutôt des pointeurs intelligents.

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

Le placement new construit un objet à une adresse mémoire précise, sans allouer. Il est utilisé en interne par les allocateurs, les pools de mémoire et des conteneurs comme std::vector. Vous devez appeler le destructeur explicitement — delete ne doit PAS être utilisé sur des objets créés par 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
}

Les trois bogues mémoire mortels

Les new/delete bruts vous exposent à trois catégories de bogues :

1. Fuite de mémoire — allouer avec new sans jamais appeler delete. La mémoire n'est jamais rendue au système d'exploitation (jusqu'à la fin du processus). Dans un serveur qui tourne longtemps, cela finit par consommer toute la RAM disponible.

2. Double libération — appeler delete deux fois sur le même pointeur. Le second delete corrompt les structures de données internes de l'allocateur du tas, provoquant souvent des plantages bien plus tard, dans du code sans rapport. C'est extrêmement difficile à déboguer.

3. Utilisation après libération (use-after-free) — accéder à la mémoire après un delete. La mémoire a pu être réutilisée pour une autre allocation, si bien que vous pourriez lire ou écrire les données d'un autre objet. C'est une faille de sécurité (un bogue de la classe des CVE).

Piège

Considérez : f(new A(), new B()). Le compilateur peut évaluer les arguments dans n'importe quel ordre et peut entrelacer leur évaluation :

1. Allouer la mémoire pour A
2. Allouer la mémoire pour B
3. Construire A
4. Construire B

Si l'étape 4 lève une exception, A fuit — personne n'appelle delete dessus. Même f(std::shared_ptr(new A()), std::shared_ptr(new B())) présentait ce problème avant C++17 (qui garantit que chaque argument est entièrement évalué avant le suivant). La solution : utilisez toujours std::make_unique ou std::make_shared — ils effectuent l'allocation et la construction en une seule opération sûre vis-à-vis des exceptions.