[c++] Wann sollten virtuelle Destruktoren verwendet werden?



Answers

Deklarieren Destruktoren virtuell in polymorphen Basisklassen. Dies ist Artikel 7 in Scott Meyers ' Effective C ++ . Meyers fährt fort, dass, wenn eine Klasse eine virtuelle Funktion hat, sie einen virtuellen Destruktor haben sollte, und dass Klassen, die nicht als Basisklassen ausgelegt sind oder nicht für eine polymorphe Verwendung entworfen wurden, keine virtuellen Destruktoren deklarieren sollten.

Question

Ich habe ein solides Verständnis der meisten OO-Theorie, aber die eine Sache, die mich sehr verwirrt, ist virtuelle Destruktoren.

Ich dachte, dass der Destruktor immer aufgerufen wird, egal was und für jedes Objekt in der Kette.

Wann soll man sie virtuell machen und warum?




Ich denke, der Kern dieser Frage betrifft virtuelle Methoden und Polymorphie, nicht den Destruktor. Hier ist ein klareres Beispiel:

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;
}

Wird ausgedruckt:

This is B.

Ohne virtual es ausgedruckt:

This is A.

Und jetzt sollten Sie verstehen, wann virtuelle Destruktoren verwendet werden.




Virtuelle Basisklassen-Destruktoren sind "best practice" - Sie sollten sie immer verwenden, um Speicherlecks zu vermeiden (schwer zu erkennen). Mit ihnen können Sie sicher sein, dass alle Destruktoren in der Vererbungskette Ihrer Klassen aufgerufen werden (in der richtigen Reihenfolge). Wenn Sie von einer Basisklasse mit virtuellem Destruktor erben, wird der Destruktor der erbenden Klasse automatisch ebenfalls virtuell (Sie müssen also in der Deklaration der erbenden Klasse destructor nicht "virtual" eingeben).




Um einfach zu sein, soll Virtual Destructor die Ressourcen in einer ordnungsgemäßen Reihenfolge zerstören, wenn Sie einen Basisklassenzeiger löschen, der auf das abgeleitete Klassenobjekt verweist.

 #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



Beachten Sie außerdem, dass das Löschen eines Basisklassenzeigers, wenn kein virtueller Destruktor vorhanden ist, zu einem nicht definierten Verhalten führt . Etwas, das ich erst kürzlich gelernt habe:

Wie sollte sich überschreiben in C ++ löschen?

Ich benutze C ++ seit Jahren und es gelingt mir immer noch, mich selbst aufzuhängen.




Ich dachte, es wäre vorteilhaft, das "undefinierte" Verhalten oder zumindest das "crash" undefinierte Verhalten zu diskutieren, das beim Löschen durch eine Basisklasse (/ struct) ohne virtuellen Destruktor, oder genauer gesagt, keine vtable auftreten kann. Der folgende Code listet einige einfache Strukturen auf (das gleiche gilt für Klassen).

#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;
*/
}

Ich schlage nicht vor, ob Sie virtuelle Destruktoren brauchen oder nicht, obwohl ich generell denke, dass es eine gute Übung ist, sie zu haben. Ich weise nur auf den Grund hin, der zu einem Absturz führen könnte, wenn Ihre Basisklasse (/ struct) keine vtable und Ihre abgeleitete Klasse (/ struct) hat und Sie ein Objekt über eine Basisklasse (/ struct) löschen. Zeiger. In diesem Fall ist die Adresse, die Sie an die freie Routine des Heaps übergeben, ungültig und daher der Grund für den Absturz.

Wenn Sie den obigen Code ausführen, werden Sie deutlich sehen, wenn das Problem auftritt. Wenn der this-Zeiger der Basisklasse (/ struct) sich vom this-Zeiger der abgeleiteten Klasse (/ struct) unterscheidet, werden Sie auf dieses Problem stoßen. Im obigen Beispiel haben Struktur a und b keine VTables. Strukturen c und d haben Vtables. Somit wird ein a oder b Zeiger auf eine ac oder d Objektinstanz festgelegt, um die vtable zu berücksichtigen. Wenn Sie diesen a- oder b-Zeiger zum Löschen übergeben, stürzt er ab, weil die Adresse für die freie Routine des Heapspeichers ungültig ist.

Wenn Sie beabsichtigen, abgeleitete Instanzen mit vtables aus Basisklassenzeigern zu löschen, müssen Sie sicherstellen, dass die Basisklasse über eine vtable verfügt. Eine Möglichkeit besteht darin, einen virtuellen Destruktor hinzuzufügen, der auf jeden Fall Ressourcen ordnungsgemäß bereinigen soll.




Destruktor über einen Zeiger auf eine Basisklasse aufrufen

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

Der Aufruf des virtuellen Destruktors unterscheidet sich nicht von anderen virtuellen Funktionsaufrufen.

Für base->f() wird der Aufruf an Derived::f() base->~Base() , und das Gleiche gilt für base->~Base() - seine übergeordnete Funktion - der Derived::~Derived() wird aufgerufen.

Das Gleiche passiert, wenn der Destruktor indirekt aufgerufen wird, zB delete base; . Die delete Anweisung ruft base->~Base() die an Derived::~Derived() .

Abstrakte Klasse mit nicht virtuellem Destruktor

Wenn Sie ein Objekt nicht über einen Zeiger auf seine Basisklasse löschen, ist kein virtueller Destruktor erforderlich. Mach es einfach protected damit es nicht versehentlich aufgerufen wird:

// 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.
}



Related