Question

J'ai une solide compréhension de la plupart des théories d'OO, mais les destructeurs virtuels me dérangent beaucoup.

Je pensais que le destructeur était toujours appelé, peu importe quoi et pour chaque objet de la chaîne.

Quand voulez-vous les rendre virtuels et pourquoi?

Était-ce utile?

La solution

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 virtuel . 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 virtuel et que b est un Base * pointant sur un objet Derived , < code> delete b a comportement non défini :

  

[Dans supprimer b ], si le type statique de la   objet à supprimer est différent de son type dynamique, le statique   type doit être une classe de base du type dynamique de l'objet à être   supprimé et le type statique doit avoir un destructeur virtuel ou le   le comportement n'est pas dé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 de ressources.

En résumé, définissez toujours les destructeurs virtuels des classes de base 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 la classe de base destructor protégée et non virtuelle; ce faisant, le compilateur ne vous laissera pas appeler delete sur un pointeur de classe de base.

Pour en savoir plus sur la virtualité et le destructeur de classe de base virtuelle, consultez cet article de Herb Sutter .

Autres conseils

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 génère les éléments suivants:

Base Constructor Called
Derived constructor called
Base Destructor called

La construction d'un objet dérivé respecte la règle de construction, mais vous devez supprimer le " b " pointeur (pointeur de base), nous avons constaté 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 été modifiée comme suit:

Base Constructor Called
Derived Constructor called
Derived destructor called
Base destructor called

Ainsi, 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 vaut un constructeur virtuel.

Déclarez les destructeurs virtuels dans les classes de base polymorphes. Il s’agit de l’article 7 de la 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çues pour être utilisées de manière polymorphe ne devraient 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îne un comportement non défini . Quelque chose que j'ai appris récemment:

Comment annuler la suppression en C ++ doit-il se comporter?

J'utilise le C ++ depuis des années et j'arrive toujours à me pendre.

Rendre le destructeur virtuel chaque fois que votre classe est polymorphe.

Appel du destructeur via un pointeur sur une classe de base

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 du 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 - le Derived :: ~ Derived () sera appelé.

La même chose se produit lorsque le destructeur est appelé indirectement, par ex. delete base; . L'instruction delete appelle base- > ~ Base () qui sera envoyé à Derived :: ~ Derived () .

Classe abstraite avec destructeur non virtuel

Si vous n'allez pas supprimer un objet via un pointeur sur sa classe de base, il n'est pas nécessaire de disposer d'un destructeur virtuel. Faites-le simplement 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 réfléchir aux interfaces et à leur implémentation. 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

Le mot clé virtuel pour destructeur est nécessaire lorsque vous souhaitez que différents destructeurs suivent l'ordre approprié pendant que les objets sont supprimés via 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"). Il y aura donc une fuite de mémoire pour un objet dérivé.

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 avons-nous 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 minces

#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 constater 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

https: // web.archive.org/web/20130822173509/http://www.programminggallery.com/article_details.php?article_id=138

Les destructeurs de classes de base virtuelles sont des "meilleures pratiques". - vous devriez 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 le destructeur de la classe héritante automatiquement aussi virtuel (vous n'avez donc pas à retaper 'virtual' dans la déclaration du destructeur de la classe héritante).

J’ai pensé qu’il serait utile de discuter du "non défini". comportement, ou au moins le "crash" comportement non défini qui peut 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 vous dis 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 virtuelle et si votre classe dérivée (/ struct) en a 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 tas.

Si vous envisagez de supprimer des instances dérivées contenant des 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 est d’ajouter un destructeur virtuel, que vous voudrez peut-être tout de même nettoyer correctement les ressources.

Je pense que le cœur 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 sera imprimé:

This is A.

Et maintenant, vous devez comprendre quand utiliser des destructeurs virtuels.

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.

Si vous utilisez shared_ptr (uniquement shared_ptr, pas unique_ptr), vous n'avez pas besoin du destructeur de classe de base 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

Toute classe héritée publiquement, polymorphe ou non, devrait avoir un destructeur virtuel. En d'autres termes, s'il peut être pointé par un pointeur de classe de base, sa classe de base doit avoir un destructeur virtuel.

Si virtuelle, le destructeur de classe dérivée est appelé, puis le constructeur de la classe de base. Si ce n’est pas virtuel, seul le destructeur de la classe de base est appelé.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top