Quand les destructeurs virtuels sont nécessaires en C++ : analyse en 2025

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Destructeurs virtuels en C++ : pourquoi c'est toujours d'actualité (et quand on peut oublier)

\n\n

Si 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.

\n\n

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\n

Réponse courte (pour ceux qui sont pressés)

\n\n

Un 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.

\n\n

Pourquoi est-ce un problème ? Anatomie d'une fuite

\n\n

Regardons 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\n

Sortie :

\n
Derived created\nBase destructor
\n\n

Le 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.

\n\n

Solution — ajouter virtual devant le destructeur de Base :

\n\n
class Base {\npublic:\n    virtual ~Base() = default;  // ou un corps vide\n};
\n\n

Maintenant, 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.

\n\n

Quand le destructeur virtuel est obligatoire (règle en béton armé)

\n\n

Il 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\n

En pratique, cela signifie :

\n
    \n
  • La classe a au moins une fonction virtuelle (pure ou non)
  • \n
  • Vous prévoyez d'utiliser Base* ou Base& pour travailler avec Derived
  • \n
  • Les objets sont créés sur le tas (new) et supprimés via delete
  • \n
\n\n

Exemple — une hiérarchie classique de formes :

\n\n
class 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\n

Notez 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 —

Blogs

Book Recommendations