web-dev-qa-db-fra.com

Pourquoi la classe de base doit-elle avoir un destructeur virtuel ici si la classe dérivée n'alloue aucune mémoire dynamique brute?

Le code suivant provoque une fuite de mémoire:

#include <iostream>
#include <memory>
#include <vector>

using namespace std;

class base
{
    void virtual initialize_vector() = 0;
};

class derived : public base
{
private:
    vector<int> vec;

public:
    derived()
    {
        initialize_vector();
    }

    void initialize_vector()
    {
        for (int i = 0; i < 1000000; i++)
        {
            vec.Push_back(i);
        }
    }
};

int main()
{
    for (int i = 0; i < 100000; i++)
    {
        unique_ptr<base> pt = make_unique<derived>();
    }
}

Cela n'avait pas beaucoup de sens pour moi, car la classe dérivée n'alloue aucune mémoire dynamique brute et unique_ptr se désalloue. J'obtiens que le destructeur implicite de cette base de classe est appelé au lieu de dérivé, mais je ne comprends pas pourquoi c'est un problème ici. Si je devais écrire un destructeur explicite pour dérivé, je n'écrirais rien pour vec.

12
Inertial Ignorance

Lorsque le compilateur va exécuter implicitement delete _ptr; À l'intérieur du destructeur de unique_ptr (Où _ptr Est le pointeur stocké dans unique_ptr), Il sait précisément deux choses:

  1. L'adresse de l'objet à supprimer.
  2. Le type de pointeur qui est _ptr. Comme le pointeur est dans unique_ptr<base>, Cela signifie que _ptr Est du type base*.

C'est tout ce que le compilateur sait. Donc, étant donné qu'il supprime un objet de type base, il invoquera ~base().

Alors ... où est la partie où il détruit l'objet dervied vers lequel en fait pointe? Parce que si le compilateur ne sait pas qu'il détruit un derived, alors il ne sait pas derived::vecexiste du tout, encore moins qu'il doit être détruit . Vous avez donc cassé l'objet en en laissant la moitié non détruite.

Le compilateur ne peut pas supposer que tout base* En cours de destruction est en fait un derived*; après tout, il pourrait y avoir n'importe quel nombre de classes dérivées de base. Comment pourrait-il savoir vers quel type ce base* Pointe réellement?

Ce que le compilateur doit faire est de trouver le destructeur correct à appeler (oui, derived a un destructeur. Sauf si vous = delete Un destructeur, chaque classe a un destructeur , que vous en écriviez un ou non). Pour ce faire, il devra utiliser certaines informations stockées dans base pour obtenir la bonne adresse du code destructeur à invoquer, informations définies par le constructeur de la classe réelle. Il doit ensuite utiliser ces informations pour convertir le base* En un pointeur vers l'adresse de la classe derived correspondante (qui peut ou non être à une adresse différente. Oui, vraiment). Et puis il peut invoquer ce destructeur.

Ce mécanisme que je viens de décrire? Il est communément appelé "envoi virtuel": c'est-à-dire cette chose qui se produit chaque fois que vous appelez une fonction marquée virtual lorsque vous avez un pointeur/une référence à une classe de base.

Si vous voulez appeler une fonction de classe dérivée alors que tout ce que vous avez est un pointeur/référence de classe de base, cette fonction doit être déclarée virtual. Les destructeurs ne sont fondamentalement pas différents à cet égard.

14
Nicol Bolas

Héritage

Le point d'héritage est de partager une interface et un protocole communs entre de nombreuses implémentations différentes, de sorte qu'une instance d'une classe dérivée peut être traitée de manière identique à toute autre instance de tout autre type dérivé.

Dans l'héritage C++ apporte également des détails d'implémentation, le marquage (ou non) du destructeur comme virtuel est l'un de ces détails d'implémentation.

Liaison de fonction

Désormais, lorsqu'une fonction, ou l'un de ses cas spéciaux, comme un constructeur ou un destructeur, est appelée, le compilateur doit choisir l'implémentation de la fonction voulue. Il doit ensuite générer un code machine conforme à cette intention.

La façon la plus simple de procéder consiste à sélectionner la fonction au moment de la compilation et à émettre juste assez de code machine pour que, quelles que soient les valeurs, lorsque ce morceau de code s'exécute, il exécute toujours le code de la fonction. Cela fonctionne très bien, sauf pour l'héritage.

Si nous avons une classe de base avec une fonction (pourrait être n'importe quelle fonction, y compris le constructeur ou le destructeur) et que votre code appelle une fonction dessus, qu'est-ce que cela signifie?

En prenant votre exemple, si vous avez appelé initialize_vector() le compilateur doit décider si vous vouliez vraiment appeler l'implémentation trouvée dans Base, ou l'implémentation trouvée dans Derived. Il y a deux façons de décider:

  1. La première consiste à décider que parce que vous avez appelé à partir d'un type Base, vous vouliez dire l'implémentation dans Base.
  2. La seconde consiste à décider que, car le type d'exécution de la valeur stockée dans la valeur typée Base pourrait être Base ou Derived que la décision concernant l'appel à effectuer, doit être effectuée lors de l'exécution lors de l'appel (à chaque appel).

À ce stade, le compilateur est confus, les deux options sont également valides. C'est à ce moment que virtual entre dans le mix. Lorsque ce mot clé est présent, le compilateur choisit l'option 2 retardant la décision entre toutes les implémentations possibles jusqu'à ce que le code s'exécute avec une valeur réelle. Lorsque ce mot clé est absent, le compilateur choisit l'option 1 car c'est le comportement par ailleurs normal.

Le compilateur peut toujours choisir l'option 1 dans le cas d'un appel de fonction virtuelle. Mais seulement si cela peut prouver que c'est toujours le cas.

Constructeurs et destructeurs

Alors pourquoi ne spécifions-nous pas un constructeur virtuel?

Plus intuitivement, comment le compilateur choisirait-il entre des implémentations identiques du constructeur pour Derived et Derived2? C'est assez simple, ça ne peut pas. Il n'y a aucune valeur préexistante à partir de laquelle le compilateur peut apprendre ce qui était réellement prévu. Il n'y a pas de valeur préexistante car c'est le travail du constructeur.

Alors pourquoi devons-nous spécifier un destructeur virtuel?

Plus intuitivement, comment le compilateur choisirait-il entre les implémentations pour Base et Derived? Ce ne sont que des appels de fonction, donc le comportement d'appel de fonction se produit. Sans destructeur virtuel déclaré, le compilateur décidera de se lier directement au destructeur Base quel que soit le type d'exécution des valeurs.

Dans de nombreux compilateurs, si le dérivé ne déclare aucun membre de données, ni hérite d'autres types, le comportement dans la ~Base() conviendra, mais il n'est pas garanti. Cela fonctionnerait purement par hasard, un peu comme se tenir devant un lance-flammes qui n'avait pas encore été allumé. Tu vas bien pendant un moment.

La seule façon correcte de déclarer un type de base ou d'interface en C++ est de déclarer un destructeur virtuel, de sorte que le destructeur correct soit appelé pour une instance donnée de la hiérarchie de types de ce type. Cela permet à la fonction ayant la plus grande connaissance de l'instance de nettoyer correctement cette instance.

0
Kain0_0