181 Stimmen

Warum sollte ich einen virtuellen Destruktor für eine abstrakte Klasse in C++ deklarieren?

Ich weiß, dass es eine gute Praxis ist, virtuelle Destruktoren für Basisklassen in C++ zu deklarieren, aber ist es immer wichtig, die virtual Destruktoren auch für abstrakte Klassen, die als Schnittstellen fungieren? Bitte geben Sie einige Gründe und Beispiele an, warum.

220voto

Airsource Ltd Punkte 31802

Bei einer Schnittstelle ist das sogar noch wichtiger. Jeder Benutzer Ihrer Klasse wird wahrscheinlich einen Zeiger auf die Schnittstelle halten, nicht einen Zeiger auf die konkrete Implementierung. Wenn er sie löschen will, wird er, wenn der Destruktor nicht virtuell ist, den Destruktor der Schnittstelle aufrufen (oder den vom Compiler bereitgestellten Standard, wenn Sie keinen angegeben haben), nicht den Destruktor der abgeleiteten Klasse. Sofortiges Speicherleck.

Zum Beispiel

class Interface
{
   virtual void doSomething() = 0;
};

class Derived : public Interface
{
   Derived();
   ~Derived() 
   {
      // Do some important cleanup...
   }
};

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}

5 Stimmen

delete p ruft ein undefiniertes Verhalten hervor. Es ist nicht garantiert, dass der Aufruf Interface::~Interface .

0 Stimmen

@Mankarse: Kannst du erklären, was dazu führt, dass es undefiniert ist? Wenn Derived keinen eigenen Destruktor implementiert hätte, wäre es dann immer noch ein undefiniertes Verhalten?

18 Stimmen

@Wallacoloo: Es ist undefiniert wegen [expr.delete]/ : ... if the static type of the object to be deleted is different from its dynamic type, ... the static type shall have a virtual destructor or the behavior is undefined. ... . Es wäre immer noch undefiniert, wenn Derived einen implizit generierten Destruktor verwenden würde.

37voto

Die Antwort auf Ihre Frage lautet: oft, aber nicht immer. Wenn Ihre abstrakte Klasse den Klienten verbietet, delete auf einen Zeiger auf sie aufzurufen (oder wenn sie dies in ihrer Dokumentation sagt), können Sie keinen virtuellen Destruktor deklarieren.

Sie können Clients verbieten, delete auf einen Zeiger darauf aufzurufen, indem Sie den Destruktor schützen. Auf diese Weise ist es absolut sicher und vernünftig, einen virtuellen Destruktor wegzulassen.

Sie werden am Ende keine virtuelle Methodentabelle haben und Ihren Klienten Ihre Absicht signalisieren, sie durch einen Zeiger darauf nicht löschbar zu machen, also haben Sie in der Tat einen Grund, sie in diesen Fällen nicht als virtuell zu deklarieren.

[Siehe Punkt 4 in diesem Artikel: http://www.gotw.ca/publications/mill18.htm ]

0 Stimmen

Der Schlüssel zu Ihrer Antwort ist "auf dem delete nicht aufgerufen wird". Wenn Sie eine abstrakte Basisklasse haben, die als Schnittstelle konzipiert ist, wird delete normalerweise in der Schnittstellenklasse aufgerufen.

0 Stimmen

Wie John bereits sagte, ist das, was Sie vorschlagen, ziemlich gefährlich. Du verlässt dich auf die Annahme, dass Clients deiner Schnittstelle niemals ein Objekt zerstören werden, das nur den Basistyp kennt. Die einzige Möglichkeit, das zu garantieren, wenn es nicht-virtuell ist, ist, den Dtor der abstrakten Klasse zu schützen.

0 Stimmen

Michel, ich habe es gesagt :) "Wenn du das tust, machst du deinen Destruktor geschützt. Wenn du das tust, können Clients nicht mit einem Zeiger auf diese Schnittstelle löschen." und in der Tat verlässt es sich nicht auf die Clients, aber es muss es erzwingen, indem es den Clients sagt "Du kannst nicht...". Ich sehe keine Gefahr

26voto

davidag Punkte 2577

Ich habe beschlossen, einige Nachforschungen anzustellen und zu versuchen, Ihre Antworten zusammenzufassen. Die folgenden Fragen werden Ihnen helfen zu entscheiden, welche Art von Zerstörer Sie benötigen:

  1. Soll Ihre Klasse als Basisklasse verwendet werden?
    • Nein: Öffentlichen nicht-virtuellen Destruktor deklarieren, um v-Zeiger auf jedem Objekt der Klasse zu vermeiden * .
    • Ja: Lesen Sie die nächste Frage.
  2. Ist Ihre Basisklasse abstrakt? (d. h. irgendwelche virtuellen reinen Methoden?)
    • Nein: Versuchen Sie, Ihre Basisklasse abstrakt zu machen, indem Sie Ihre Klassenhierarchie umgestalten
    • Ja: Lesen Sie die nächste Frage.
  3. Möchten Sie die polymorphe Löschung durch einen Basiszeiger zulassen?
    • Nein: Deklarieren Sie einen geschützten virtuellen Destruktor, um die unerwünschte Verwendung zu verhindern.
    • Ja: Öffentlichen virtuellen Destruktor deklarieren (in diesem Fall kein Overhead).

Ich hoffe, das hilft.

* Es ist wichtig zu beachten, dass es in C++ keine Möglichkeit gibt, eine Klasse als final (d.h. nicht unterklassifizierbar) zu kennzeichnen. Falls Sie also beschließen, Ihren Destruktor als nicht-virtuell und öffentlich zu deklarieren, denken Sie daran, Ihre Mitprogrammierer ausdrücklich davor zu warnen, von Ihrer Klasse abzuleiten.

Referenzen:

12 Stimmen

Diese Antwort ist teilweise veraltet, da es in C++ jetzt ein finales Schlüsselwort gibt.

11voto

OJ. Punkte 28189

Ja, das ist immer wichtig. Abgeleitete Klassen können Speicher zuweisen oder Verweise auf andere Ressourcen enthalten, die bei der Zerstörung des Objekts bereinigt werden müssen. Wenn Sie Ihre Schnittstellen/abstrakten Klassen nicht mit virtuellen Destruktoren ausstatten, wird jedes Mal, wenn Sie eine Instanz einer abgeleiteten Klasse über ein Handle der Basisklasse löschen, der Destruktor Ihrer abgeleiteten Klasse nicht aufgerufen.

Das bedeutet, dass Sie die Möglichkeit von Speicherlecks eröffnen.

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted

0 Stimmen

Richtig, in der Tat in diesem Beispiel kann es nicht nur Speicherleck, aber möglicherweise abstürzen :-/

7voto

Evan Teran Punkte 83711

Es ist nicht immer erforderlich, aber ich halte es für eine gute Übung. Es ermöglicht, dass ein abgeleitetes Objekt sicher durch einen Zeiger eines Basistyps gelöscht werden kann.

So zum Beispiel:

Base *p = new Derived;
// use p as you see fit
delete p;

ist missgebildet, wenn Base hat keinen virtuellen Destruktor, weil es versuchen wird, das Objekt zu löschen, als wäre es ein Base * .

0 Stimmen

Wollen Sie nicht boost::shared_pointer p(new Derived) so ändern, dass es aussieht wie boost::shared_pointer<Base> p(new Derived); ? vielleicht verstehen die Leute dann Ihre Antwort und stimmen ab

0 Stimmen

EDIT: Einige Teile "kodifiziert", um die spitzen Klammern sichtbar zu machen, wie von litb vorgeschlagen.

0 Stimmen

@EvanTeran: Ich bin nicht sicher, ob sich dies seit der ursprünglichen Antwort geändert hat (die Boost-Dokumentation unter boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm es hätte sein können), aber es ist heutzutage nicht wahr, dass shared_ptr versucht, das Objekt so zu löschen, als wäre es ein Base * - merkt es sich den Typ des Objekts, mit dem Sie es erstellt haben. Siehe den referenzierten Link, insbesondere den Teil, in dem es heißt: "Der Destruktor ruft delete mit demselben Zeiger auf, komplett mit seinem ursprünglichen Typ, auch wenn T keinen virtuellen Destruktor hat oder void ist."

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