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.
c++
inheritance
memory
allocation
Ignorance inertielle
la source
la source
~derived()
qui délègue au destructeur de vec. Alternativement, vous supposez queunique_ptr<base> pt
connaîtrait le destructeur dérivé. Sans méthode virtuelle, cela ne peut pas être le cas. Alors qu'un unique_ptr peut recevoir une fonction de suppression qui est un paramètre de modèle sans aucune représentation d'exécution, et cette fonctionnalité n'est d'aucune utilité pour ce code.Réponses:
Lorsque le compilateur va exécuter l'implicite à l'
delete _ptr;
intérieur duunique_ptr
destructeur de (où_ptr
est le pointeur stocké dans leunique_ptr
), il sait précisément deux choses:_ptr
est. Puisque le pointeur est dedansunique_ptr<base>
, cela signifie qu'il_ptr
est du typebase*
.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'
dervied
objet vers lequel il pointe réellement ? Parce que si le compilateur ne sait pas qu'il détruit underived
, alors il ne sait pas du tout qu'ilderived::vec
existe , 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*
être détruit est en fait underived*
; après tout, il pourrait y avoir n'importe quel nombre de classes dérivéesbase
. Comment pourrait-il savoir à quel type ce particulierbase*
pointe réellement?Ce que le compilateur doit faire est de trouver le destructeur correct à appeler (oui,
derived
a un destructeur. Sauf si vous êtes= 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 dansbase
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 lebase*
en un pointeur vers l'adresse de laderived
classe 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 vers une classe de base.Si vous souhaitez 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.la source
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 puisse être traitée de manière identique à toute autre instance de tout autre type dérivé.
En C ++, l'héritage 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 particuliers, 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 dansBase
ou l'implémentation trouvée dansDerived
. Il y a deux façons de décider:Base
type, vous vouliez dire l'implémentation dansBase
.Base
valeur typée pourrait êtreBase
, ouDerived
que la décision quant à l'appel à effectuer, doit être prise 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 quand
virtual
vient 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
etDerived2
? 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
etDerived
? Ce ne sont que des appels de fonction, donc le comportement de l'appel de fonction se produit. Sans destructeur virtuel déclaré, le compilateur décidera de se lier directement auBase
destructeur, 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 le
~Base()
sera approprié, 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 n'importe quel 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 cette instance correctement.
la source