Pourquoi le destructeur de classe de base (virtuel) est-il appelé lorsqu’un object de classe dérivé est supprimé?

Une différence entre un destructeur (bien sûr également le constructeur) et d’autres fonctions membres est que, si une fonction membre régulière a un corps dans la classe dérivée, seule la version de la classe Derived est exécutée. Alors que dans le cas de destructeurs, les versions dérivées ainsi que les versions de classe de base sont exécutées?

Il sera bon de savoir ce qui se passe exactement en cas de destructeur (peut-être virtuel) et de constructeur, qu’ils soient appelés pour toutes ses classes de base même si l’object classe le plus dérivé est supprimé.

Merci d’avance!

La norme dit

Après avoir exécuté le corps du destructeur et détruit les objects automatiques alloués dans celui-ci, un destructeur de classe X appelle les destructeurs des membres directs non variants de X , les destructeurs des classes de base directes de X et, si X est le type de dérivée (12.6.2), son destructeur appelle les destructeurs des classes de base virtuelles de X. Tous les destructeurs sont appelés comme s’ils étaient référencés avec un nom quali fi é, c’est-à-dire en ignorant les éventuels destructeurs de substitution virtuels dans des classes plus dérivées. Les bases et les membres sont détruits dans l’ordre inverse de l’achèvement de leur constructeur (voir 12.6.2). Une instruction return (6.6.3) dans un destructeur peut ne pas retourner directement à l’appelant; avant de transférer le contrôle à l’appelant, les destructeurs des membres et des bases sont appelés. Les destructeurs des éléments d’un tableau sont appelés dans l’ordre inverse de leur construction (voir 12.6).

En outre, conformément à RAII, les ressources doivent être liées à la durée de vie d’objects appropriés et les destructeurs de classes respectives doivent être appelés à libérer les ressources.

Par exemple, le code suivant perd de la mémoire.

struct Base { int *p; Base():p(new int){} ~Base(){ delete p; } //has to be virtual }; struct Derived :Base { int *d; Derived():Base(),d(new int){} ~Derived(){delete d;} }; int main() { Base *base=new Derived(); //do something delete base; //Oops!! ~Base() gets called(=>Memory Leak). } 

C’est par conception. Le destructeur de la classe de base doit être appelé pour pouvoir libérer ses ressources. En règle générale, une classe dérivée doit uniquement nettoyer ses propres ressources et laisser la classe de base se nettoyer elle-même.

De la spécification C ++ :

Après avoir exécuté le corps du destructeur et détruit les objects automatiques alloués dans celui-ci, un destructeur de la classe X appelle les destructeurs des membres directs de X, les destructeurs des classes de base directes de X et, si X est le type de la classe la plus dérivée ( 12.6.2), son destructeur appelle les destructeurs des classes de base virtuelles de X. Tous les destructeurs sont appelés comme s’ils étaient référencés avec un nom qualifié, c’est-à-dire en ignorant les éventuels destructeurs de substitution virtuels dans des classes plus dérivées. Les bases et les membres sont détruits dans l’ordre inverse de l’achèvement de leur constructeur (voir 12.6.2).

De plus, comme il n’y a qu’un seul destructeur, il n’y a aucune ambiguïté quant au destructeur qu’une classe doit appeler. Ce n’est pas le cas pour les constructeurs, où un programmeur doit choisir quel constructeur de classe de base doit être appelé s’il n’y a pas de constructeur par défaut accessible.

Constructeur et destructeur sont différents du rest des méthodes habituelles.

Constructeur

  • ne peut pas être virtuel
  • en classe dérivée, vous appelez soit explicitement le constructeur de la classe de base
  • ou, dans le cas où vous n’appelez pas le constructeur de la classe de base, le compilateur insère l’appel. Il appellera le constructeur de base sans parameters. Si un tel constructeur n’existe pas, vous obtenez une erreur du compilateur.

 struct A {}; struct B : A { B() : A() {} }; // but this works as well because comstackr inserts call to A(): struct B : A { B() {} }; // however this does not comstack: struct A { A(int x) {} }; struct B : A { B() {} }; // you need: struct B : A { B() : A(4) {} }; 

Destructor :

  • Lorsque vous appelez destructor sur une classe dérivée sur un pointeur ou une référence, la classe de base ayant un destructeur virtuel, le destructeur le plus dérivé est appelé en premier, puis le rest des classes dérivées dans l’ordre de construction inversé. Cela permet de s’assurer que toute la mémoire a été correctement nettoyée. Cela ne fonctionnerait pas si la classe la plus dérivée était appelée en dernier car à ce moment-là, la classe de base n’existerait pas en mémoire et vous obtiendriez segfault.

 struct C { virtual ~C() { cout << __FUNCTION__ << endl; } }; struct D : C { virtual ~D() { cout << __FUNCTION__ << endl; } }; struct E : D { virtual ~E() { cout << __FUNCTION__ << endl; } }; int main() { C * o = new E(); delete o; } 

sortie:

 ~E ~D ~C 

Si la méthode de la classe de base est marquée comme virtual toutes les méthodes héritées le sont également. Par conséquent, même si vous ne marquez pas les destructeurs dans D et E comme virtual ils seront toujours virtual et seront toujours appelés dans le même ordre.

Parce que c’est comme ça que travaille le dtor. Lorsque vous créez un object, les acteurs sont appelés à partir de la base, puis jusqu’au plus dérivé. Lorsque vous détruisez des objects (correctement), l’inverse se produit. Lorsque vous détruisez un object via un pointeur (ou une référence, bien que ce soit assez inhabituel) au type de base, le temps nécessaire à la création d’un dénominateur virtuel fait toute la différence. Dans ce cas, l’alternative n’est pas vraiment que seul le nom dérivé soit appelé, mais l’alternative est simplement un comportement indéfini. Cela peut prendre la forme d’invoquer uniquement le nom dérivé, mais cela pourrait aussi prendre une forme totalement différente.

Comme Igor le dit, les constructeurs doivent être appelés pour les classes de base. Considérez ce qui se produirait si cela ne s’appelait pas:

 struct A { std::ssortingng s; virtual ~A() {} }; struct B : A {}; 

Si le destructeur de A ne serait pas appelé lors de la suppression d’une instance B , A ne serait jamais nettoyé.

Un destructeur de classe de base peut être responsable du nettoyage des ressources allouées par le constructeur de la classe de base.

Si votre classe de base a un constructeur par défaut (un constructeur qui ne prend pas de parameters ou qui a des valeurs par défaut pour tous ses parameters), ce constructeur est automatiquement appelé lors de la construction d’une instance dérivée.

Si votre classe de base a un constructeur nécessitant des parameters, vous devez l’appeler manuellement dans la liste des initialiseurs du constructeur de la classe dérivée.

Votre destructeur de classe de base sera toujours appelé automatiquement lors de la suppression de l’instance dérivée, car les destructeurs ne prennent pas de parameters.

Si vous utilisez un polymorphism et que votre instance dérivée est pointée par un pointeur de classe de base, le destructeur de classe dérivée n’est appelé que si le destructeur de base est virtuel.

Lorsqu’un object est détruit, les destructeurs s’exécutent pour tous les sous-objects. Cela inclut à la fois la réutilisation par confinement et la réutilisation par inheritance.