Der folgende Code verursacht einen Speicherverlust:
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class base
{
void virtual initialize_vector() = 0;
};
class derived : public base
{
private:
vector<int> vec;
public:
derived()
{
initialize_vector();
}
void initialize_vector()
{
for (int i = 0; i < 1000000; i++)
{
vec.push_back(i);
}
}
};
int main()
{
for (int i = 0; i < 100000; i++)
{
unique_ptr<base> pt = make_unique<derived>();
}
}
Es ergab für mich keinen großen Sinn, da die abgeleitete Klasse keinen dynamischen Rohspeicher zuweist und unique_ptr sich selbst freigibt. Ich verstehe, dass der implizite Destruktor dieser Klassenbasis anstelle des abgeleiteten aufgerufen wird, aber ich verstehe nicht, warum das hier ein Problem ist. Wenn ich einen expliziten Destruktor für derivative schreiben würde, würde ich nichts für vec schreiben.
c++
inheritance
memory
allocation
Inertial Ignorance
quelle
quelle
~derived()
, die an vecs destruktor delegiert. Alternativ nehmen Sie an, dass Sieunique_ptr<base> pt
den abgeleiteten Destruktor kennen. Ohne eine virtuelle Methode kann dies nicht der Fall sein. Einem unique_ptr kann zwar eine Löschfunktion zugewiesen werden, die ein Vorlagenparameter ohne Laufzeitdarstellung ist, und diese Funktion ist für diesen Code nicht von Nutzen.Antworten:
Wenn der Compiler das Implizite
delete _ptr;
innerhalb desunique_ptr
Destruktors des Compilers ausführt (wo_ptr
der Zeiger in gespeichert istunique_ptr
), weiß er genau zwei Dinge:_ptr
ist. Da der Zeiger in istunique_ptr<base>
, ist das Mittel_ptr
vom Typbase*
.Dies ist alles, was der Compiler weiß. Wenn also ein Objekt vom Typ gelöscht wird
base
, wird es aufgerufen~base()
.Also ... wo ist der Teil, in dem es das
dervied
Objekt zerstört, auf das es tatsächlich zeigt? Denn wenn der Compiler nicht weiß, dass er a zerstörtderived
, dann weiß er überhaupt nicht, dass es überhauptderived::vec
existiert , geschweige denn, dass es zerstört werden sollte. Sie haben das Objekt zerbrochen, indem Sie die Hälfte davon unzerstört gelassen haben.Der Compiler kann nicht davon ausgehen , dass es sich bei
base*
einer Zerstörung tatsächlich um eine handeltderived*
. es könnte doch eine beliebige Anzahl von Klassen geben, von denen abgeleitet istbase
. Wie würde es wissen, auf welchen Typ diese bestimmtebase*
tatsächlich verweist?Was der Compiler tun muss, ist herauszufinden, welcher Destruktor aufzurufen ist (ja, er
derived
hat einen Destruktor. Sofern Sie= delete
kein Destruktor sind, hat jede Klasse einen Destruktor, unabhängig davon, ob Sie einen schreiben oder nicht). Dazu müssen einige in gespeicherte Informationen verwendet werdenbase
, um die richtige Adresse des Destruktorcodes zum Aufrufen abzurufen. Diese Informationen werden vom Konstruktor der tatsächlichen Klasse festgelegt. Dann muss es diese Informationen verwenden, um denbase*
Zeiger in einen Zeiger auf die Adresse der entsprechendenderived
Klasse umzuwandeln (die sich möglicherweise an einer anderen Adresse befindet oder nicht. Ja, wirklich). Und dann kann es diesen Destruktor aufrufen.Welchen Mechanismus habe ich gerade beschrieben? Es wird im Allgemeinen als "virtueller Versand" bezeichnet. Dies ist das, was passiert, wenn Sie eine Funktion aufrufen, die markiert ist,
virtual
wenn Sie einen Zeiger / Verweis auf eine Basisklasse haben.Wenn Sie eine abgeleitete Klassenfunktion aufrufen möchten, obwohl Sie nur einen Basisklassenzeiger / eine Basisklassenreferenz haben, muss diese Funktion deklariert werden
virtual
. Destruktoren sind diesbezüglich grundsätzlich nicht anders.quelle
Erbe
Der Sinn der Vererbung besteht darin, eine gemeinsame Schnittstelle und ein gemeinsames Protokoll für viele verschiedene Implementierungen zu verwenden, sodass eine Instanz einer abgeleiteten Klasse mit jeder anderen Instanz eines anderen abgeleiteten Typs identisch behandelt werden kann.
In C ++ bringt die Vererbung auch Implementierungsdetails mit sich. Das Markieren (oder Nicht-Markieren) des Destruktors als virtuell ist ein solches Implementierungsdetail.
Funktionsbindung
Wenn nun eine Funktion oder einer ihrer Spezialfälle wie ein Konstruktor oder Destruktor aufgerufen wird, muss der Compiler auswählen, welche Funktionsimplementierung gemeint war. Dann muss Maschinencode generiert werden, der dieser Absicht folgt.
Dies funktioniert am einfachsten, wenn Sie die Funktion zur Kompilierungszeit auswählen und nur so viel Maschinencode ausgeben, dass bei der Ausführung dieses Codeteils unabhängig von den Werten immer der Code für die Funktion ausgeführt wird. Dies funktioniert hervorragend, außer für die Vererbung.
Wenn wir eine Basisklasse mit einer Funktion haben (kann jede Funktion sein, einschließlich des Konstruktors oder Destruktors) und Ihr Code eine Funktion darauf aufruft, was bedeutet das?
Wenn Sie
initialize_vector()
den Compiler aufgerufen haben, müssen Sie anhand Ihres Beispiels entscheiden, ob Sie die in gefundene ImplementierungBase
oder die in gefundene Implementierung wirklich aufrufen wolltenDerived
. Es gibt zwei Möglichkeiten, dies zu entscheiden:Base
die Implementierung in gemeint haben , weil Sie von einem Typ aufgerufen habenBase
.Base
Möglichkeit besteht darinBase
, zu entscheiden, dass der Laufzeit-Typ des in dem eingegebenen Wert gespeicherten Werts oderDerived
die Entscheidung, welcher Aufruf zu treffen ist, zur Laufzeit beim Aufruf (jedes Mal, wenn er aufgerufen wird) getroffen werden muss.Der Compiler ist an dieser Stelle verwirrt, beide Optionen sind gleichermaßen gültig. Dies ist, wenn
virtual
in die Mischung kommt. Wenn dieses Schlüsselwort vorhanden ist, wählt der Compiler Option 2 aus, um die Entscheidung zwischen allen möglichen Implementierungen zu verzögern, bis der Code mit einem realen Wert ausgeführt wird. Wenn dieses Schlüsselwort fehlt, wählt der Compiler Option 1 aus, da dies das ansonsten normale Verhalten ist.Der Compiler wählt im Falle eines virtuellen Funktionsaufrufs möglicherweise immer noch Option 1 aus. Aber nur wenn es beweisen kann, dass dies immer der Fall ist.
Konstruktoren und Destruktoren
Warum geben wir keinen virtuellen Konstruktor an?
Intuitiver, wie würde der Compiler zwischen identischen Implementierungen des Konstruktors für
Derived
und wählenDerived2
? Das ist ziemlich einfach, es kann nicht. Es gibt keinen bereits vorhandenen Wert, anhand dessen der Compiler lernen kann, was wirklich beabsichtigt war. Es gibt keinen vorhandenen Wert, da dies die Aufgabe des Konstruktors ist.Warum müssen wir also einen virtuellen Destruktor angeben?
Intuitiver, wie würde der Compiler zwischen Implementierungen für
Base
und wählenDerived
? Es handelt sich nur um Funktionsaufrufe, sodass das Funktionsaufrufverhalten auftritt. Ohne einen deklarierten virtuellen Destruktor entscheidet der Compiler,Base
unabhängig vom Laufzeittyp der Werte , direkt an den Destruktor zu binden .Wenn in vielen Compilern die abgeleiteten keine Datenelemente deklarieren oder von anderen Typen erben, ist das Verhalten in den
~Base()
geeignet, es wird jedoch nicht garantiert. Es würde rein zufällig funktionieren, ähnlich wie vor einem Flammenwerfer zu stehen, der noch nicht gezündet worden war. Dir geht es eine Weile gut.Die einzig richtige Möglichkeit, einen Basis- oder Schnittstellentyp in C ++ zu deklarieren, besteht darin, einen virtuellen Destruktor zu deklarieren, sodass der richtige Destruktor für eine bestimmte Instanz der Typhierarchie dieses Typs aufgerufen wird. Auf diese Weise kann die Funktion mit den meisten Kenntnissen der Instanz diese Instanz ordnungsgemäß bereinigen.
quelle