Déclarez virtuels les destructeurs de vos classes polymorphes
Si vous créez des classes polymorphes, c'est-à-dire, héritant d'autres classes, vous devez déclarer vos destructeurs virtuels si vous ne souhaitez pas avoir des comportements étranges ainsi que des fuites mémoire.
Problèmes posés par l'absence du mot-clé virtual devant le destructeur
Nous allons voir, à travers l'exemple suivant, les problèmes engendrés par un destructeur non virtuel dans une classe polymorphe.
// Fichier main.cpp #include <iostream> class Base { public: Base() { std::cout << "Constructeur Base" << std::endl; } ~Base() { std::cout << "Destructeur Base" << std::endl; } void faireQuelqueChose() { std::cout << "Base::faireQuelqueChose" << std::endl; } }; class Fille : public Base { public: Fille() : Base() { std::cout << "Constructeur Fille" << std::endl; } ~Fille() { std::cout << "Destructeur Fille" << std::endl; } void faireQuelqueChose() { std::cout << "Fille::faireQuelqueChose" << std::endl; } }; int main(int argc, char **argv) { { Base base; base.faireQuelqueChose(); } // base est détruite ici. std::cout << std::endl; { Fille fille; fille.faireQuelqueChose(); } // fille est détruite ici. std::cout << std::endl; Base *pbase = new Fille(); pbase->faireQuelqueChose(); delete pbase; // erreur de libération de mémoire, le destructeur de Fille n'est pas appelé. std::cout << std::endl; return EXIT_SUCCESS; }
Voici la sortie de ce programme.
Constructeur Base Base::faireQuelqueChose Destructeur Base Constructeur Base Constructeur Fille Fille::faireQuelqueChose Destructeur Fille Destructeur Base Constructeur Base Constructeur Fille Base::faireQuelqueChose Destructeur Base
On constate que les variables base et fille (allouées sur la pile) se comportent correctement.
Pour l'objet base :
- Il est bien construit/détruit grâce à son constructeur/destructeur (Base/~Base).
- La méthode
faireQuelqueChose()est bien celle de la classe Base.
Pour l'objet fille :
- Il est bien construit grâce au constructeur de sa classe mère (Base), puis par son propre constructeur (Fille).
- Il est bien détruit grâce à son propre destructeur (~Fille) puis par celui de sa classe mère (~Base).
- La méthode
faireQuelqueChose()est bien celle de la classe Fille.
En revanche, le comportement de la variable dynamique pbase (allouée sur le tas) n'est pas celui attendu.
- Les constructeurs sont bien appelés (Base puis Fille)
- La méthode
faireQuelqueChose()est celle de la classe mère (Base) et non celle de la classe dérivée (Fille). - Seule la classe de base (Base) est libérée, la mémoire occupée par la classe dérivée (Fille) est perdue (fuites mémoires).
Les résultats sont indéfinis et dépendent du compilateur utilisé, donc le fonctionnement erroné de cet exemple peut varier d'un compilateur à un autre. Ne faites surtout pas comme dans cet exemple, c'est mal. Mais sachez qu'en général, pour une allocation dynamique, seule la classe de base est libérée. On obtient donc des objets partiellement détruits... Les fuites mémoire doivent absolument être éradiquées. Elles doivent être votre priorité lors de la conception de vos programmes plutôt qu'en phase de débogage, beaucoup plus difficile. La libération des ressources acquises par un objet doit se faire de manière automatique et ne doit nécessiter aucune action spéciale de l'utilisateur, hormis l'appel à delete.
Utilisez virtual avec vos destructeurs de classes polymorphes
La recette pour éliminer ce problème est simple. Il suffit d'ajouter le mot-clé virtual devant le nom du destructeur de la classe de base ainsi qu'à toutes ses classes dérivées. Dans notre exemple, il suffit de l'ajouter devant les destructeurs, et tant qu'on y est, devant les méthodes faireQuelqueChose().
class Base { public: Base() { std::cout << "Constructeur Base" << std::endl; } virtual ~Base() { std::cout << "Destructeur Base" << std::endl; } virtual void faireQuelqueChose() { std::cout << "Base::faireQuelqueChose" << std::endl; } }; class Fille : public Base { public: Fille() : Base() { std::cout << "Constructeur Fille" << std::endl; } virtual ~Fille() { std::cout << "Destructeur Fille" << std::endl; } virtual void faireQuelqueChose() { std::cout << "Fille::faireQuelqueChose" << std::endl; } };
Règle 1 : Toute classe ayant au moins une fonction virtuelle doit déclarer son destructeur virtuel.
Certains penseront alors que mettre systématiquement virtual devant le destructeur sera une bonne idée, et pourtant non. Si une classe possède une méthode virtuelle (ceci inclut le destructeur), alors des données supplémentaires sont ajoutées dans la classe. Ces données permettent au programme exécuté de savoir à quelles méthodes il doit faire appel dans le cas d'un héritage. Il s'agit du vptr (virtual table pointer). C'est une table virtuelle qui contient des pointeurs vers les fonctions virtuelles de le classe. La taille de ce pointeur est de 32 bits pour un processeur 32 bits, etc. La taille de la classe sera alourdie de la taille de ce pointeur. Cette taille est minime, mais imaginez que votre classe ne contienne qu'un entier, vous doublez alors sa taille et diminuez les performances. Enfin, notez que cela réduit la compatibilité C/C++ car les vptr n'existent pas en C.
Règle 2 : Inutile de déclarer un destructeur virtuel s'il ne s'agit pas d'une classe de base ou d'une classe fille.
Ne dérivez pas une classe n'ayant pas de destructeur non virtual
Contrairement à Java, C++ n'offre pas de mécanisme permettant d'interdire l'héritage (mot-clé final en Java). Il vous faudra donc vérifier, avant d'en hériter, que la classe de base possède bien un destructeur virtuel. Ainsi, vous ne devez pas créer de classe dérivée de la STL : hériter de list, vector, map, ou string... est donc une erreur !
Rendre abstraite une classe sans méthode virtuelle
Voici une petite astuce pour déclarer une classe abstraite si celle-ci ne possède pas de méthode virtuelle. Rappel : une classe abstraite ne peut être instanciée. L'astuce consiste à rendre virtuel pur son destructeur et le tour est joué. Les classes dérivées devront redéfinir le destructeur...
class AbstractClass { public: AbstractClass(); virtual ~AbstractClass() = 0; };
Conclusion
Utilisez virtual à bon escient : il ne faut ni l'ignorer ni en abuser. Si on a une hiérarchie de classes devant hériter les unes des autres, leurs constructeurs (et certainement leurs méthodes) devront être déclarés virtual (de la classe mère à la classe fille). Enfin, interdisez-vous formellement d'hériter de classes qui n'ont pas de destructeur virtuel.
Vous pourrez approfondir le sujet avec la sélection des livres suivants : Pour mieux développer avec C++, Standards de programmation en C++ et Le langage C++.
Derniers Commentaires