Destructeurs virtuels en C++ : pourquoi c'est toujours d'actualité (et quand on peut oublier)
\n\nSi vous avez déjà écrit en C++ et rencontré l'héritage, vous avez sûrement vu cette question sur Stack Overflow avec des dizaines de milliers de vues : « When to use virtual destructors? ». En 2025, le sujet n'a rien perdu de son acuité — les fuites mémoire dues à un virtual oublié continuent de faire des ravages en production. Mais il y a aussi un revers : ajouter virtual sans réflexion partout où c'est possible n'est pas non plus une bonne pratique.
Prenons le temps de comprendre une fois pour toutes : quand le destructeur virtuel est obligatoire, quand il est simplement une bonne pratique, et quand sa présence est le signe d'une mauvaise architecture. C'est parti.
\n\nRéponse courte (pour ceux qui sont pressés)
\n\nUn destructeur virtuel est nécessaire si et seulement si vous prévoyez de supprimer un objet d'une classe dérivée via un pointeur sur la classe de base. Si ce scénario n'existe pas dans le code, virtual n'est pas requis. Mais il y a des nuances que nous allons détailler ci-dessous.
Pourquoi est-ce un problème ? Anatomie d'une fuite
\n\nRegardons l'anti-patron classique qui fait échouer le code des débutants (et pas seulement).
\n\n#include <iostream>\n\nclass Base {\npublic:\n ~Base() { \n std::cout << "Base destructor\\n"; \n }\n};\n\nclass Derived : public Base {\nprivate:\n int* big_data = new int[1000]; // ressources à libérer\npublic:\n Derived() { std::cout << "Derived created\\n"; }\n ~Derived() {\n delete[] big_data;\n std::cout << "Derived destructor\\n";\n }\n};\n\nint main() {\n Base* ptr = new Derived();\n delete ptr; // Oups ! Seul ~Base() sera appelé\n return 0;\n}\n\nSortie :
\nDerived created\nBase destructor\n\nLe tableau big_data a fui. Le destructeur de Derived n'a pas été appelé car le compilateur, voyant le type Base*, a appelé le destructeur de cette classe. Et il n'est pas virtuel.
Solution — ajouter virtual devant le destructeur de Base :
class Base {\npublic:\n virtual ~Base() = default; // ou un corps vide\n};\n\nMaintenant, avec delete ptr, ~Derived() sera exécuté en premier, puis ~Base(). Les ressources sont libérées, la mémoire ne fuit pas, l'analyseur statique est content.
Quand le destructeur virtuel est obligatoire (règle en béton armé)
\n\nIl n'y a qu'un seul cas inconditionnel : si la classe est destinée à être utilisée de manière polymorphe via un pointeur ou une référence sur la classe de base, et que les objets des classes dérivées seront supprimés via ce pointeur.
\n\nEn pratique, cela signifie :
\n- \n
- La classe a au moins une fonction virtuelle (pure ou non) \n
- Vous prévoyez d'utiliser
Base*ouBase&pour travailler avecDerived\n - Les objets sont créés sur le tas (
new) et supprimés viadelete\n
Exemple — une hiérarchie classique de formes :
\n\nclass Shape {\npublic:\n virtual double area() const = 0;\n virtual ~Shape() = default; // obligatoire pour une classe abstraite\n};\n\nclass Circle : public Shape {\n double radius;\npublic:\n explicit Circle(double r) : radius(r) {}\n double area() const override { return 3.14159 * radius * radius; }\n};\n\nint main() {\n std::vector<std::unique_ptr<Shape>> shapes;\n shapes.push_back(std::make_unique<Circle>(5.0));\n // unique_ptr se chargera du delete, mais le destructeur de Shape doit être virtual\n return 0;\n}\n\nNotez bien : même si vous utilisez std::unique_ptr (ce qui est l'approche recommandée en 2025), le destructeur virtuel est toujours nécessaire. unique_ptr appelle delete via un pointeur sur la classe de base —