1935 Stimmen

Wann sollten virtuelle Destruktoren verwendet werden?

Ich habe ein solides Verständnis der meisten OOP Theorie, aber die eine Sache, die mich sehr verwirrt, sind virtuelle Zerstörer.

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

Wann sollen Sie sie virtuell machen und warum?

5voto

gonjay Punkte 697

Ich denke, der Kern dieser Frage ist über virtuelle Methoden und Polymorphismus, nicht der Destruktor speziell. Hier ist ein deutlicheres 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 wird er ausgedruckt:

This is A.

Und jetzt sollten Sie wissen, wann Sie virtuelle Destruktoren verwenden sollten.

5voto

Trantor Punkte 762

Virtuelle Basisklassen-Destruktoren sind "best practice" - Sie sollten sie immer verwenden, um (schwer zu erkennende) Speicherlecks zu vermeiden. Mit ihnen können Sie sicher sein, dass alle Destruktoren in der Vererbungskette Ihrer Klassen (in der richtigen Reihenfolge) aufgerufen werden. Wenn Sie von einer Basisklasse erben, die einen virtuellen Destruktor verwendet, wird der Destruktor der ererbenden Klasse automatisch auch virtuell (so dass Sie in der Deklaration des Destruktors der ererbenden Klasse nicht erneut "virtuell" eingeben müssen).

4voto

user2578542 Punkte 41

Was ist ein virtueller Destruktor oder wie verwendet man einen virtuellen Destruktor?

Ein Klassendestruktor ist eine Funktion mit dem gleichen Namen der Klasse, der eine ~ vorangestellt ist, die den Speicher, der von der Klasse zugewiesen wurde, wieder freigeben wird. Warum wir einen virtuellen Destruktor brauchen

Siehe das folgende Beispiel mit einigen virtuellen Funktionen

Das Beispiel zeigt auch, wie Sie einen Buchstaben in Groß- oder Kleinbuchstaben umwandeln können

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

Anhand des obigen Beispiels können Sie sehen, dass der Destruktor für die beiden Klassen MakeUpper und MakeLower nicht aufgerufen wird.

Siehe das nächste Beispiel mit dem virtuellen Destruktor

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

Der virtuelle Destruktor ruft explizit den am meisten abgeleiteten Laufzeit-Destruktor der Klasse auf, so dass er in der Lage ist, das Objekt auf angemessene Weise zu löschen.

Oder besuchen Sie den Link

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

3voto

nickdu Punkte 617

Ich dachte, es wäre von Vorteil, das "undefinierte" Verhalten zu diskutieren, oder zumindest den "Absturz" undefiniertes Verhalten, das beim Löschen durch eine Basisklasse (/struct) ohne einen virtuellen Destruktor, oder genauer gesagt keine vtable auftreten kann. Der folgende Code listet einige einfache Structs auf (dasselbe 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 denke, dass es im Allgemeinen eine gute Praxis ist, sie zu haben. Ich weise nur auf den Grund hin, warum es zu einem Absturz kommen kann, wenn Ihre Basisklasse(/struct) keine vtable hat und Ihre abgeleitete Klasse(/struct) schon und Sie ein Objekt über einen Zeiger der Basisklasse(/struct) löschen. In diesem Fall ist die Adresse, die Sie an die Free-Routine des Heaps übergeben, ungültig und somit der Grund für den Absturz.

Wenn Sie den obigen Code ausführen, werden Sie deutlich sehen, wann das Problem auftritt. Wenn der this-Zeiger der Basisklasse (/struct) nicht mit dem this-Zeiger der abgeleiteten Klasse (/struct) übereinstimmt, wird dieses Problem auftreten. In dem obigen Beispiel haben struct a und b keine vtables. structs c und d haben vtables. Daher wird ein a- oder b-Zeiger auf eine c- oder d-Objektinstanz so angepasst, dass die vtable berücksichtigt wird. Wenn Sie diesen a- oder b-Zeiger zum Löschen übergeben, kommt es zum Absturz, da die Adresse für die Free-Routine des Heaps ungültig ist.

Wenn Sie vorhaben, abgeleitete Instanzen, die vtables haben, aus Zeigern der Basisklasse zu löschen, müssen Sie sicherstellen, dass die Basisklasse eine vtable hat. Eine Möglichkeit, dies zu tun, besteht darin, einen virtuellen Destruktor hinzuzufügen, den Sie möglicherweise ohnehin benötigen, um Ressourcen ordnungsgemäß aufzuräumen.

2voto

Ulrich Eckhardt Punkte 16066

Ich denke, dass die meisten Antworten hier am Thema vorbeigehen, mit Ausnahme der akzeptierten, was auch gut so ist. Lassen Sie mich jedoch noch eine weitere mit einer anderen Sichtweise auf das Thema hinzufügen: Sie brauchen einen virtuellen Destruktor, wenn Sie Instanzen dieser Klasse polymorph löschen wollen.

Das geht irgendwie an der Frage vorbei, also lassen Sie mich das näher ausführen: Wie bereits erwähnt, kommt es zu unerwünschtem Verhalten, wenn Sie die delete base_ptr und der Destruktor ist nicht virtuell. Allerdings gibt es hier mehrere Annahmen, die explizit gemacht werden müssen:

  • Wenn Ihre Klasse keine Basisklasse ist, werden Sie hoffentlich keinen Code wie diesen schreiben. In diesem Fall meine ich nicht die manuelle Speicherverwaltung, die an und für sich schlecht ist, sondern die öffentliche Ableitung von dieser Klasse. Eine Klasse, die nicht als Basisklasse konzipiert ist, sollte nicht geerbt werden, wie z.B. std::string . C++ erlaubt es Ihnen, sich selbst in den Fuß zu schießen. Das ist allerdings Ihr Fehler, nicht der der Basisklasse, die keinen virtuellen Destruktor hat.
  • Wenn der Destruktor nicht zugänglich ist (geschützt oder privat), wird dieser Code nicht kompiliert, so dass das unerwünschte Verhalten nicht auftreten kann. Ein geschützter Destruktor ist nützlich, insbesondere für Mixins, aber auch (in geringerem Maße) für Schnittstellen. Man möchte den Overhead virtueller Funktionen nicht in Kauf nehmen, wenn man sie nicht tatsächlich nutzt. Den Destruktor stattdessen geschützt zu machen, verhindert unerwünschtes Verhalten, schränkt Sie aber ansonsten nicht ein.
  • Wenn Sie tatsächlich eine Klasse schreiben, von der abgeleitet werden soll, werden Sie in der Regel ohnehin virtuelle Funktionen haben. Als Benutzer dieser Funktionen werden Sie sie in der Regel nur über einen Zeiger auf die Basisklasse verwenden. Wenn diese Verwendung auch das Entsorgen der Funktionen beinhaltet, muss sie auch polymorph sein. Dies ist dann der Fall, wenn Sie den Destruktor virtuell machen sollten.

Für eine ähnlich andere Sichtweise auf das Thema, lesen Sie auch Wann sollten Sie keine virtuellen Destruktoren verwenden?

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X