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.
#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.
#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).
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.
Aperçu des allocateurs personnalisés
Les new/delete par défaut utilisent l'allocateur global (généralement malloc/free), qui est généraliste mais pas optimal pour tous les schémas d'utilisation. Les allocateurs personnalisés offrent :
- Allocateurs d'arène/linéaires — on avance un pointeur, et on libère tout d'un coup. Idéal pour les données de jeu propres à chaque image.
- Allocateurs de pool — pré-allouent des blocs de taille fixe. Allocation/libération en O(1), sans fragmentation.
- Allocateurs de pile — allocation LIFO, extrêmement rapide.
Les conteneurs C++ acceptent des paramètres de template d'allocateur : std::vector. L'espace de noms std::pmr (polymorphic memory resources) de C++17 fournit un cadre d'allocateurs polymorphes à l'exécution qui évite le problème du paramètre de template.
En C++ moderne (C++14 et au-delà), un code applicatif bien écrit ne devrait contenir aucun new ni delete brut :
- Propriété unique : utilisez std::make_unique
- Propriété partagée : utilisez std::make_shared
- Tableaux dynamiques : utilisez std::vector
- Chaînes : utilisez std::string
- Objets optionnels : utilisez std::optional
Les new/delete bruts peuvent apparaître dans du code de bibliothèque (allocateurs, conteneurs personnalisés), mais le code applicatif ne devrait jamais en avoir besoin. Si vous voyez un new lors d'une revue de code, demandez pourquoi — il existe presque toujours une alternative plus sûre.
- Associer
new/delete[]ounew[]/deleteest un comportement indéfini - Les fuites de mémoire, les doubles libérations et les utilisations après libération sont les trois bogues mémoire mortels
f(new A(), new B())peut fuir si un constructeur lève une exception (avant C++17, même avec des enveloppes shared_ptr)- Le placement new construit à une adresse donnée — détruisez manuellement, n'utilisez jamais
delete - Le code applicatif en C++ moderne ne devrait contenir aucun
new/deletebrut — utilisez des pointeurs intelligents et des conteneurs
Quiz — Testez vos connaissances
(15 XP)1. Que se passe-t-il si vous utilisez `delete` sur de la mémoire allouée avec `new[]` ?
2. Pourquoi `f(new A(), new B())` peut-il provoquer une fuite de mémoire ?
3. Comment devez-vous détruire un objet créé avec placement new ?