J'ai une solide compréhension de la plupart des théories OO, mais les destructeurs virtuels sont une source de confusion pour moi.
Je pensais que le destructeur était toujours appelé, peu importe quoi et pour chaque objet de la chaîne.
Quand êtes-vous censé les rendre virtuels et pourquoi?
Les destructeurs virtuels sont utiles lorsque vous pouvez potentiellement supprimer une instance d'une classe dérivée via un pointeur sur la classe de base:
class Base
{
// some virtual methods
};
class Derived : public Base
{
~Derived()
{
// Do some important cleanup
}
};
Ici, vous remarquerez que je n’ai pas déclaré le destructeur de Base comme étant virtual
. Voyons maintenant l'extrait suivant:
Base *b = new Derived();
// use b
delete b; // Here's the problem!
Puisque le destructeur de Base n'est pas virtual
et b
est un Base*
pointant sur un objet Derived
, delete b
a comportement non défini :
[Dans
delete b
], si le type statique de l'objet à supprimer est différent de son type dynamique, le type statique doit être une classe de base du type dynamique de l'objet à supprimer et le type statique doit avoir un destructeur virtuel ou le comportement est indéfini .
Dans la plupart des implémentations, l'appel du destructeur sera résolu comme n'importe quel code non virtuel, ce qui signifie que le destructeur de la classe de base sera appelé, mais pas celui de la classe dérivée, ce qui entraînera une fuite des ressources.
Pour résumer, créez toujours les destructeurs de classes de base virtual
lorsqu'ils doivent être manipulés de manière polymorphe.
Si vous souhaitez empêcher la suppression d'une instance via un pointeur de classe de base, vous pouvez rendre le destructeur de classe de base protégé et non virtuel; ce faisant, le compilateur ne vous laissera pas appeler delete
sur un pointeur de classe de base.
Vous pouvez en apprendre plus sur la virtualité et le destructeur de classe de base virtuelle dans cet article de Herb Sutter .
Un constructeur virtuel n'est pas possible mais un destructeur virtuel est possible. Laissez-nous expérimenter ....
#include <iostream>
using namespace std;
class Base
{
public:
Base(){
cout << "Base Constructor Called\n";
}
~Base(){
cout << "Base Destructor called\n";
}
};
class Derived1: public Base
{
public:
Derived1(){
cout << "Derived constructor called\n";
}
~Derived1(){
cout << "Derived destructor called\n";
}
};
int main()
{
Base *b = new Derived1();
delete b;
}
Le code ci-dessus affiche les éléments suivants:
Base Constructor Called
Derived constructor called
Base Destructor called
La construction de l'objet dérivé suit la règle de construction, mais lorsque nous supprimons le pointeur "b" (pointeur de base), nous constatons que seul le destructeur de base est appelé. Mais cela ne doit pas arriver. Pour faire le bon choix, nous devons rendre le destructeur de base virtuel. Voyons maintenant ce qui se passe dans ce qui suit:
#include <iostream>
using namespace std;
class Base
{
public:
Base(){
cout << "Base Constructor Called\n";
}
virtual ~Base(){
cout << "Base Destructor called\n";
}
};
class Derived1: public Base
{
public:
Derived1(){
cout << "Derived constructor called\n";
}
~Derived1(){
cout << "Derived destructor called\n";
}
};
int main()
{
Base *b = new Derived1();
delete b;
}
La sortie a changé comme suit:
Base Constructor Called
Derived Constructor called
Derived destructor called
Base destructor called
Donc, la destruction du pointeur de base (qui prend une allocation sur un objet dérivé!) Suit la règle de destruction, c'est-à-dire d'abord le dérivé, puis la base. D'autre part, rien ne ressemble à un constructeur virtuel.
Déclarez les destructeurs virtuels dans les classes de base polymorphes. Il s’agit de l’article 7 de Effective C++ de Scott Meyers. Meyers poursuit en résumant que si une classe a une fonction virtuelle , elle devrait avoir un destructeur virtuel et que les classes non conçues pour être des classes de base ou non conçu pour être utilisé de manière polymorphe devrait pas déclarer des destructeurs virtuels.
Sachez également que la suppression d'un pointeur de classe de base en l'absence de destructeur virtuel entraînera un comportement indéfini (---) . Quelque chose que j'ai appris récemment:
Comment la suppression de C++ doit-elle se comporter?
J'utilise C++ depuis des années et j'arrive toujours à me pendre.
Rendre le destructeur virtuel chaque fois que votre classe est polymorphe.
struct Base {
virtual void f() {}
virtual ~Base() {}
};
struct Derived : Base {
void f() override {}
~Derived() override {}
};
Base* base = new Derived;
base->f(); // calls Derived::f
base->~Base(); // calls Derived::~Derived
L'appel de destructeur virtuel n'est pas différent de tout autre appel de fonction virtuelle.
Pour base->f()
, l'appel sera envoyé à Derived::f()
et il en sera de même pour base->~Base()
- sa fonction primordiale - la Derived::~Derived()
sera appelée.
La même chose se produit lorsque le destructeur est appelé indirectement, par ex. delete base;
. L'instruction delete
appellera base->~Base()
qui sera envoyée à Derived::~Derived()
.
Si vous ne supprimez pas d'objet via un pointeur sur sa classe de base, il n'est pas nécessaire de disposer d'un destructeur virtuel. Faites juste protected
pour qu'il ne soit pas appelé accidentellement:
// library.hpp
struct Base {
virtual void f() = 0;
protected:
~Base() = default;
};
void CallsF(Base& base);
// CallsF is not going to own "base" (i.e. call "delete &base;").
// It will only call Base::f() so it doesn't need to access Base::~Base.
//-------------------
// application.cpp
struct Derived : Base {
void f() override { ... }
};
int main() {
Derived derived;
CallsF(derived);
// No need for virtual destructor here as well.
}
J'aime penser aux interfaces et aux implémentations d'interfaces. En langage C++, l'interface est une classe virtuelle pure. Destructor fait partie de l'interface et devrait être implémenté. Par conséquent, destructor doit être purement virtuel. Qu'en est-il du constructeur? Le constructeur ne fait en réalité pas partie de l'interface car l'objet est toujours instancié explicitement.
Pour être simple, le destructeur virtuel consiste à détruire les ressources dans un ordre approprié lorsque vous supprimez un pointeur de classe de base pointant sur un objet de classe dérivée.
#include<iostream>
using namespace std;
class B{
public:
B(){
cout<<"B()\n";
}
virtual ~B(){
cout<<"~B()\n";
}
};
class D: public B{
public:
D(){
cout<<"D()\n";
}
~D(){
cout<<"~D()\n";
}
};
int main(){
B *b = new D();
delete b;
return 0;
}
OUTPUT:
B()
D()
~D()
~B()
==============
If you don't give ~B() as virtual. then output would be
B()
D()
~B()
where destruction of ~D() is not done which leads to leak
Un mot clé virtuel pour destructeur est nécessaire lorsque vous souhaitez que différents destructeurs suivent l'ordre approprié pendant la suppression d'objets par le pointeur de la classe de base. par exemple:
Base *myObj = new Derived();
// Some code which is using myObj object
myObj->fun();
//Now delete the object
delete myObj ;
Si votre destructeur de classe dérivée est virtuel, les objets seront détruits dans un ordre (premier objet dérivé puis base). Si votre destructeur de classe dérivée n'est PAS virtuel, seul l'objet de la classe de base sera supprimé (car le pointeur appartient à la classe de base "Base * myObj"). Donc, il y aura une fuite de mémoire pour l'objet dérivé.
Les destructeurs de classe de base virtuelle sont la "meilleure pratique" - vous devez toujours les utiliser pour éviter (difficile à détecter) les fuites de mémoire. En les utilisant, vous pouvez être sûr que tous les destructeurs de la chaîne d'héritage de vos classes sont appelés (dans le bon ordre). L'héritage d'une classe de base à l'aide de destructor virtuel rend également le destructeur de la classe héritante automatiquement virtuel (vous n'avez donc pas à retaper 'virtual' dans la déclaration du destructeur de la classe héritante).
Qu'est-ce qu'un destructeur virtuel ou comment utiliser un destructeur virtuel?
Un destructeur de classe est une fonction portant le même nom que la classe précédente avec ~ qui réaffectera la mémoire allouée par la classe. Pourquoi nous avons besoin d'un destructeur virtuel
Voir l'exemple suivant avec certaines fonctions virtuelles
L'exemple indique également comment convertir une lettre en lettres majuscules ou minuscules
#include "stdafx.h"
#include<iostream>
using namespace std;
// program to convert the lower to upper orlower
class convertch
{
public:
//void convertch(){};
virtual char* convertChar() = 0;
~convertch(){};
};
class MakeLower :public convertch
{
public:
MakeLower(char *passLetter)
{
tolower = true;
Letter = new char[30];
strcpy(Letter, passLetter);
}
virtual ~MakeLower()
{
cout<< "called ~MakeLower()"<<"\n";
delete[] Letter;
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
Letter[i] = Letter[i] + 32;
return Letter;
}
private:
char *Letter;
bool tolower;
};
class MakeUpper : public convertch
{
public:
MakeUpper(char *passLetter)
{
Letter = new char[30];
toupper = true;
strcpy(Letter, passLetter);
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
Letter[i] = Letter[i] - 32;
return Letter;
}
virtual ~MakeUpper()
{
cout<< "called ~MakeUpper()"<<"\n";
delete Letter;
}
private:
char *Letter;
bool toupper;
};
int _tmain(int argc, _TCHAR* argv[])
{
convertch *makeupper = new MakeUpper("hai");
cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" ";
delete makeupper;
convertch *makelower = new MakeLower("HAI");;
cout<<"Eneterd : HAI = " <<makelower->convertChar()<<" ";
delete makelower;
return 0;
}
Dans l'exemple ci-dessus, vous pouvez voir que le destructeur des classes MakeUpper et MakeLower n'est pas appelé.
Voir l'exemple suivant avec le destructeur virtuel
#include "stdafx.h"
#include<iostream>
using namespace std;
// program to convert the lower to upper orlower
class convertch
{
public:
//void convertch(){};
virtual char* convertChar() = 0;
virtual ~convertch(){}; // defined the virtual destructor
};
class MakeLower :public convertch
{
public:
MakeLower(char *passLetter)
{
tolower = true;
Letter = new char[30];
strcpy(Letter, passLetter);
}
virtual ~MakeLower()
{
cout<< "called ~MakeLower()"<<"\n";
delete[] Letter;
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
{
Letter[i] = Letter[i] + 32;
}
return Letter;
}
private:
char *Letter;
bool tolower;
};
class MakeUpper : public convertch
{
public:
MakeUpper(char *passLetter)
{
Letter = new char[30];
toupper = true;
strcpy(Letter, passLetter);
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
{
Letter[i] = Letter[i] - 32;
}
return Letter;
}
virtual ~MakeUpper()
{
cout<< "called ~MakeUpper()"<<"\n";
delete Letter;
}
private:
char *Letter;
bool toupper;
};
int _tmain(int argc, _TCHAR* argv[])
{
convertch *makeupper = new MakeUpper("hai");
cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" \n";
delete makeupper;
convertch *makelower = new MakeLower("HAI");;
cout<<"Eneterd : HAI = " <<makelower->convertChar()<<"\n ";
delete makelower;
return 0;
}
Le destructeur virtuel appellera explicitement le destructeur d'exécution le plus dérivé de la classe afin qu'il puisse effacer l'objet de manière appropriée.
Ou visitez le lien
J'ai pensé qu'il serait utile de discuter du comportement "non défini", ou du moins du comportement non défini "crash" susceptible de se produire lors de la suppression via une classe de base (/ struct) sans destructeur virtuel, ou plus précisément sans vtable. Le code ci-dessous énumère quelques structures simples (la même chose serait vraie pour les classes).
#include <iostream>
using namespace std;
struct a
{
~a() {}
unsigned long long i;
};
struct b : a
{
~b() {}
unsigned long long j;
};
struct c : b
{
~c() {}
virtual void m3() {}
unsigned long long k;
};
struct d : c
{
~d() {}
virtual void m4() {}
unsigned long long l;
};
int main()
{
cout << "sizeof(a): " << sizeof(a) << endl;
cout << "sizeof(b): " << sizeof(b) << endl;
cout << "sizeof(c): " << sizeof(c) << endl;
cout << "sizeof(d): " << sizeof(d) << endl;
// No issue.
a* a1 = new a();
cout << "a1: " << a1 << endl;
delete a1;
// No issue.
b* b1 = new b();
cout << "b1: " << b1 << endl;
cout << "(a*) b1: " << (a*) b1 << endl;
delete b1;
// No issue.
c* c1 = new c();
cout << "c1: " << c1 << endl;
cout << "(b*) c1: " << (b*) c1 << endl;
cout << "(a*) c1: " << (a*) c1 << endl;
delete c1;
// No issue.
d* d1 = new d();
cout << "d1: " << d1 << endl;
cout << "(c*) d1: " << (c*) d1 << endl;
cout << "(b*) d1: " << (b*) d1 << endl;
cout << "(a*) d1: " << (a*) d1 << endl;
delete d1;
// Doesn't crash, but may not produce the results you want.
c1 = (c*) new d();
delete c1;
// Crashes due to passing an invalid address to the method which
// frees the memory.
d1 = new d();
b1 = (b*) d1;
cout << "d1: " << d1 << endl;
cout << "b1: " << b1 << endl;
delete b1;
/*
// This is similar to what's happening above in the "crash" case.
char* buf = new char[32];
cout << "buf: " << (void*) buf << endl;
buf += 8;
cout << "buf after adding 8: " << (void*) buf << endl;
delete buf;
*/
}
Je ne suggère pas si vous avez besoin de destructeurs virtuels ou non, bien que je pense qu'en général, c'est une bonne pratique de les avoir. Je souligne simplement la raison pour laquelle vous pouvez vous retrouver avec un crash si votre classe de base (/ struct) n'a pas de table vtable et que votre classe dérivée (/ struct) en possède et que vous supprimez un objet via une classe de base (/ struct) aiguille. Dans ce cas, l'adresse que vous transmettez à la routine libre du segment de mémoire n'est pas valide et constitue donc la raison du blocage.
Si vous exécutez le code ci-dessus, vous verrez clairement quand le problème se produit. Lorsque le pointeur this de la classe de base (/ struct) est différent du pointeur this de la classe dérivée (/ struct), vous allez rencontrer ce problème. Dans l'exemple ci-dessus, struct a et b n'ont pas de vtables. Les structures c et d ont des vtables. Ainsi, un pointeur a ou b vers une instance d'objet c ou d sera corrigé pour prendre en compte la table virtuelle. Si vous passez ce pointeur a ou b à supprimer, il tombera en panne car l'adresse n'est pas valide pour la routine libre du segment de mémoire.
Si vous envisagez de supprimer des instances dérivées contenant vtables à partir de pointeurs de classe de base, vous devez vous assurer que la classe de base contient une vtable. Une façon de le faire consiste à ajouter un destructeur virtuel, que vous voudrez peut-être tout de même nettoyer correctement les ressources.
Je pense que le noyau de cette question concerne les méthodes virtuelles et le polymorphisme, pas le destructeur en particulier. Voici un exemple plus clair:
class A
{
public:
A() {}
virtual void foo()
{
cout << "This is A." << endl;
}
};
class B : public A
{
public:
B() {}
void foo()
{
cout << "This is B." << endl;
}
};
int main(int argc, char* argv[])
{
A *a = new B();
a->foo();
if(a != NULL)
delete a;
return 0;
}
Imprimera:
This is B.
Sans virtual
, il affichera:
This is A.
Et maintenant, vous devez comprendre quand utiliser des destructeurs virtuels.
Si vous utilisez shared_ptr
(uniquement shared_ptr, pas unique_ptr), il n'est pas nécessaire que la classe de base destructor virtual:
#include <iostream>
#include <memory>
using namespace std;
class Base
{
public:
Base(){
cout << "Base Constructor Called\n";
}
~Base(){ // not virtual
cout << "Base Destructor called\n";
}
};
class Derived: public Base
{
public:
Derived(){
cout << "Derived constructor called\n";
}
~Derived(){
cout << "Derived destructor called\n";
}
};
int main()
{
shared_ptr<Base> b(new Derived());
}
sortie:
Base Constructor Called
Derived constructor called
Derived destructor called
Base Destructor called
lorsque vous devez appeler un destructeur de classe dérivée à partir de la classe de base. vous devez déclarer le destructeur de la classe de base virtuelle dans la classe de base.