web-dev-qa-db-fra.com

Pourquoi le destructeur de classe de base (virtuel) est-il appelé lorsqu'un objet 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 et les classes de base sont exécutées?

Il serait 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'objet de classe le plus dérivé est supprimé.

Merci d'avance!

30
KedarX

La norme dit

Après avoir exécuté le corps du destructeur et détruit tous les objets automatiques alloués dans le corps, un destructeur pour la classe X appelle les destructeurs pour les membres directs non variants de X, les destructeurs pour le direct .__ de X. classes de base et, si X est le type de la classe la plus dérivée (12.6.2), son destructeur appelle les destructeurs de Les 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 plusieurs classes 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 déclaration de retour (6.6.3) dans un le destructeur peut ne pas retourner directement à l'appelant; avant de transférer le contrôle à l'appelant, aux destructeurs pour les membres et les bases sont appelées. Les destructeurs des éléments d'un tableau sont appelés dans l'ordre inverse de leur construction (voir 12.6).

De même, conformément à { RAII , les ressources doivent être liées à la durée de vie d'objets 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).
 }
15
Prasoon Saurav

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 spec C++ :

Après avoir exécuté le corps du destructeur et détruisant tout objets automatiques alloués dans le body, un destructeur pour les appels de classe X les destructeurs pour X directement membres, les destructeurs de X. classes de base directes et, si X est le type de la classe la plus dérivée (12.6.2), son destructeur appelle le destructeurs pour la base virtuelle de X Des classes. Tous les destructeurs sont appelés en tant que si elles ont été référencées avec un nom qualifié, c’est-à-dire en ignorant tout dérogation virtuelle possible destructeurs dans des classes plus dérivées . Les bases et les membres sont détruits dans le 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.

11
Igor Zevaka

Constructeur et destructeur sont différents du reste des méthodes régulières.

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 paramètres. 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 compiler inserts call to A():
struct B : A { B() {} };

// however this does not compile:
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 via 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 reste 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 avec virtual, toutes les méthodes héritées sont également virtuelles, donc même si vous ne marquez pas les destructeurs dans D et E comme virtual, ils seront toujours virtual et ils seront toujours appelés dans le même ordre.

11
stefanB

Parce que c'est comme ça que travaille le dtor. Lorsque vous créez un objet, les acteurs sont appelés à partir de la base, puis jusqu'aux objets les plus dérivés. Lorsque vous détruisez des objets (correctement), l'inverse se produit. Lorsque vous détruisez un objet via un pointeur (ou une référence, bien que ce soit assez inhabituel) au type de base, le temps nécessaire pour faire un dtor virtuel faisant la différence est déterminant. 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 peut aussi prendre une forme totalement différente.

2
Jerry Coffin

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::string 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é.

2
Georg Fritzsche

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 paramètres ou qui a des valeurs par défaut pour tous ses paramètres), ce constructeur est automatiquement appelé lors de la construction d'une instance dérivée.

Si votre classe de base a un constructeur qui nécessite des paramètres, 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 paramètres.

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

2
Amardeep AC9MF

Lorsqu'un objet est détruit, les destructeurs s'exécutent pour tous les sous-objets. Cela inclut à la fois la réutilisation par confinement et la réutilisation par héritage.

0
Ben Voigt