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 Implementierung Base
oder die in gefundene Implementierung wirklich aufrufen wollten Derived
. Es gibt zwei Möglichkeiten, dies zu entscheiden:
- Die erste ist zu entscheiden, dass Sie
Base
die Implementierung in gemeint haben , weil Sie von einem Typ aufgerufen haben Base
.
- Die zweite
Base
Möglichkeit besteht darin Base
, zu entscheiden, dass der Laufzeit-Typ des in dem eingegebenen Wert gespeicherten Werts oder Derived
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ählen Derived2
? 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ählen Derived
? 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.
~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.