PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : C++ virtual Destruktor


Gast
2011-02-06, 20:52:45
Abend,

ich glaube mich erinnern zu können, dass wenn eine Subklasse Zeigervariablen enthält, dass dann der Destruktor der Basisklasse virtual sein muss, um ein Speicherloch zu verhindern. Doch weshalb ist dem so?

Wann muss/sollte ein Konstruktor virtual sein?

LG

Krishty
2011-02-06, 21:06:30
ich glaube mich erinnern zu können, dass wenn eine Subklasse Zeigervariablen enthält, dass dann der Destruktor der Basisklasse virtual sein muss, um ein Speicherloch zu verhindern.Um pingelig zu sein: Das soll so sein, wenn die Subklasse den Speicher auch besitzt, also für seine Freigabe zuständig ist.
Doch weshalb ist dem so?Der Programmtext, der die abgeleitete Klasse benutzt, sieht meist nur die Basisklasse und greift indirekt über diese Schnittstelle auf die eigentliche Implementierung zu. Erst virtual in der Basisklasse erzwingt dann, dass bei einem Aufruf einer Methode die überschriebene Version der abgeleiteten Klasse aufgerufen wird.
Das gilt auch für Destruktoren: Zerstört der Anwender die Instanz (z.B. durch ein delete), ist sie in dem Augenblick für ihn noch vom Typ der Basisklasse. Falls der D’tor der Basisklasse nicht virtual ist, wird also auch nur der D’tor der Basisklasse aufgerufen und was immer die abgeleitete Klasse verwaltet bleibt liegen.
Das gilt natürlich nur für den Zugriff über die Basisklasse. Liegt die Instanz vollständig typisiert vor, wird auch automatisch der richtige D’tor aufgerufen. Dann braucht man virtual überhaupt nicht.
Wann muss/sollte ein Konstruktor virtual sein?Muss eigentlich nie (wenn der D’tor einer Basisklasse virtual ist, ist der D’tor der abgeleiteten Klassen erzwungenermaßen ebenfalls virtual, aber auch implizit, also ohne Hinschreiben), sollte wie oben beschrieben.

Gruß, Ky

Gnafoo
2011-02-06, 21:08:18
Der Destruktor verhält sich wie jede andere Funktion auch. Durch virtual erhält man dynamische Bindung, d. h. der Aufruf des Destruktors wird beim delete zur Laufzeit aufgelöst und zwar abhängig vom Typ des Objektes. Ein kurzes Beispiel:


struct Foo { ~Foo() { cout << "~Foo" << endl; } };
struct Bar : Foo { ~Bar() { cout << "~Bar" << endl; } };


Ist der Destruktor nicht virtuell, so wird der aufgerufene Destruktor zur Kompilierzeit abhängig vom jeweils verwendeten Typ bestimmt.


Bar *bar = new Bar();
delete bar; // Gibt ~Bar und ~Foo aus

Foo *foo = new Bar();
delete foo; // Gibt ~Foo aus


D. h. im zweiten Fall wurde nur der Destruktor von Foo aufgerufen, weil ein Foo* zerstört wurde. Das kann dazu führen, dass Ressourcen nicht richtig freigegeben werden, je nachdem, was der Destruktor tut. Setzt man dagegen virtual vor die Destruktoren, so erhält man dynamische Bindung und in beiden Fällen wird Bar::~Bar aufgerufen (und danach ~Foo), weil beim delete der Destruktor zur Laufzeit aufgelöst wird (idR. über eine vtable).

Edit: der zweite Fall – d. h. das Löschen über Foo* – erzeugt sogar explizit undefiniertes Verhalten (wer es nachlesen will, 5.3.5/3 im Standard). D. h. sobald man delete auf dem Zeiger der Basisklasse braucht, muss der Destruktor auch virtuell sein (ist aber sowieso das was man üblicherweise will).

Gast
2011-02-06, 21:18:57
Danke für die schnelle Antwort, ist eigentlich sehr einleuchtend, hätte ich auch selbst drauf kommen können. :)

Nur eine Frage noch:
Sollte der Konstruktor auch irgendwann als virtual deklariert werden? Der Standardkonstruktor wird ja ohnehin automatisch aufgerufen und jeden anderen Konstruktor aus der Basisklasse muss man ohnehin selbst aufrufen...

Krishty
2011-02-06, 21:24:15
Du musst beim Erzeugen eines Objekts immer seinen Typ kennen. Instanzieren ohne Typ geht nicht. Darum ist es unmöglich, dass der K’tor bei der Erzeugung nachgeschlagen werden muss. Darum können K’toren nicht virtual sein. (Übrigens gehen Aufrufe virtueller Methoden von K’toren aus an die derzeitige Klasse, nicht an die endgültige Klasse – darauf fallen die Leute immer wieder rein.)

Das trifft auch auf Kopierk’toren zu – hat man eine Schnittstelle und braucht eine Kopie ihres Objekts, was auch immer das ist, muss man eine virtuelle clone()-Methode anlegen.

FlashBurn
2011-02-06, 21:48:58
Foo *foo = new Bar();

Wieso sollte man sowas tun? Ich meine das bringt doch nur Probleme mit sich und sollte vom Compiler zumindest mit einer Warnung versehen werden!

Was ist wenn ich nun auf eine Methode aus Foo zugreifen will, die wiederrum auf Daten zugreift, die nur in Foo, nicht aber in Bar sind?

Coda
2011-02-06, 22:07:14
@Flashburn :facepalm:

http://www.cplusplus.com/doc/tutorial/polymorphism/

Neomi
2011-02-06, 22:09:29
@FlashBurn:
Warum sollte sowas angewarnt werden? Das ist korrekter Code und kann durchaus so gewollt sein. Und da Bar von Foo abgeleitet ist, ist ALLES von Foo auch in Bar enthalten. Schau dir lieber nochmal genau die Prinzipien von Objektorientierung an.

Gast
2011-02-06, 22:09:41
Foo *foo = new Bar();

Wieso sollte man sowas tun? Ich meine das bringt doch nur Probleme mit sich und sollte vom Compiler zumindest mit einer Warnung versehen werden!

Was ist wenn ich nun auf eine Methode aus Foo zugreifen will, die wiederrum auf Daten zugreift, die nur in Foo, nicht aber in Bar sind?

Schon was von Polymorphismus und dynamischer Bindung gehört?
Wenn du auf eine Methode aus Foo zugreifen willst und diese in Bar nicht überschrieben ist, kannst du dies doch ohne weiteres einfach über

foo->method();

tun. foo enthält nun alles von Foo und Bar, verstehe also das Problem nicht...

patermatrix
2011-02-07, 01:04:31
Schon was von Polymorphismus und dynamischer Bindung gehört?
Wenn du auf eine Methode aus Foo zugreifen willst und diese in Bar nicht überschrieben ist, kannst du dies doch ohne weiteres einfach über

foo->method();

tun. foo enthält nun alles von Foo und Bar, verstehe also das Problem nicht...
Also gegen aussen enthält es nur Foo.

foo->BarOnlyMethod() ist so nicht möglich.

FlashBurn
2011-02-07, 07:55:46
Sorry, habe es genau falsch herum gelesen :rolleyes:

Ich hatte gelesen:

class Bar {
};

class Foo : public Bar {
};

Foo* foo= new Bar();

Das war eigentlich das was ich meinte, weil so könnte man ja versuchen auf Members von foo zuzugreifen, die aber gar nicht vorhanden sind, da nur ein Objekt vom Typ Bar erstellt wurde (sofern das der Compiler zulässt?).

patermatrix
2011-02-07, 13:12:31
(sofern das der Compiler zulässt?).
Tut er nicht ;)

Ein Bar ist schliesslich kein Foo, ein Foo hingegen immer auch ein Bar (in deinem Beispiel).

mekakic
2011-02-08, 08:01:03
Der Destruktor verhält sich wie jede andere Funktion auch. Durch virtual erhält man dynamische Bindung, d. h. der Aufruf des Destruktors wird beim delete zur Laufzeit aufgelöst und zwar abhängig vom Typ des Objektes.Wobei der Unterschied doch ist, dass beim Destruktor nicht nur eine Implementation sondern auch die von möglichen Basisklasse zusätzlich aufgerufen werden und so Stück für Stück aufgeräumt wird.

Gnafoo
2011-02-09, 14:36:45
Klar ich meinte eigentlich nur, dass er sich in Bezug auf virtual wie jede andere Funktion verhält, eben indem man dynamische Bindung erhält. Davon abgesehen verhält er sich natürlich schon anders (normale Funktionen werden ja auch nicht automatisch aufgerufen).

Der Aufruf der Destruktoren in den Basisklassen ist aber so weit ich weiß keine spezielle Eigenart des Aufrufes, sondern wird vom Compiler automatisch am Ende des Destruktors in der abgeleiteten Klasse eingefügt. Sprich: der Compiler erzeugt am Ende von ~Bar() den Code um ~Foo() aufzurufen. Etwas Ähnliches passiert bei den Member-Variablen, deren Destruktoren ja auch automatisch aufgerufen werden.

Gast
2011-02-09, 22:14:19
kann das jemand bestätigen, dass der destruktor der basisklasse nach dem destruktor der subklasse automatisch aufgerufen wird?
würd mich mal interessieren ob man nicht doch noch den teil der superklasse extra zerstören muss...

Krishty
2011-02-09, 22:26:11
§12.45 After executing the body of the destructor and destroying any automatic objects allocated within the body, a destructor for class X calls the destructors for X’s direct non-variant members, the destructors for X’s direct
base classes and, if X is the type of the most derived class (12.6.2), its destructor calls the destructors for X’s virtual base classes. […] Bases and members are destroyed in the reverse order of the completion of their constructor (see 12.6.2). A return statement (6.6.3) in a destructor might not directly return to the caller; before transferring control to the caller, the destructors for the members and bases are called. Destructors for elements of an array are called in reverse order of their construction (see 12.6).