Mit diesen Antworten geraten Sie in Hypothesen, daher werde ich versuchen, der Klarheit halber eine einfachere, bodenständigere Erklärung abzugeben.
Die grundlegenden Beziehungen des objektorientierten Designs sind zwei: IS-A und HAS-A. Ich habe die nicht erfunden. So heißen sie.
IS-A gibt an, dass ein bestimmtes Objekt in einer Klassenhierarchie zu der darüber liegenden Klasse gehört. Ein Bananenobjekt ist ein Fruchtobjekt, wenn es eine Unterklasse der Fruchtklasse ist. Dies bedeutet, dass überall dort, wo eine Obstklasse verwendet werden kann, eine Banane verwendet werden kann. Es ist jedoch nicht reflexiv. Sie können eine Basisklasse nicht durch eine Basisklasse ersetzen, wenn diese bestimmte Klasse benötigt wird.
Hat-a angegeben, dass ein Objekt Teil einer zusammengesetzten Klasse ist und dass eine Eigentumsbeziehung besteht. In C ++ bedeutet dies, dass es sich um ein Mitgliedsobjekt handelt und daher die Eigentümerklasse verpflichtet ist, es zu entsorgen oder das Eigentum abzugeben, bevor es sich selbst zerstört.
Diese beiden Konzepte sind in Einzelvererbungssprachen leichter zu realisieren als in einem Mehrfachvererbungsmodell wie c ++, aber die Regeln sind im Wesentlichen dieselben. Die Komplikation tritt auf, wenn die Klassenidentität nicht eindeutig ist, z. B. wenn ein Banana-Klassenzeiger an eine Funktion übergeben wird, die einen Fruit-Klassenzeiger verwendet.
Virtuelle Funktionen sind zum einen eine Laufzeitsache. Es ist Teil des Polymorphismus, dass damit entschieden wird, welche Funktion zum Zeitpunkt des Aufrufs im laufenden Programm ausgeführt werden soll.
Das virtuelle Schlüsselwort ist eine Compiler-Direktive zum Binden von Funktionen in einer bestimmten Reihenfolge, wenn Unklarheiten bezüglich der Klassenidentität bestehen. Virtuelle Funktionen befinden sich immer in übergeordneten Klassen (soweit ich weiß) und geben dem Compiler an, dass die Bindung von Elementfunktionen an ihre Namen mit der Unterklassenfunktion zuerst und der übergeordneten Klassenfunktion danach erfolgen soll.
Eine Fruit-Klasse kann eine virtuelle Funktion color () haben, die standardmäßig "NONE" zurückgibt. Die Funktion color () der Bananenklasse gibt "YELLOW" oder "BROWN" zurück.
Aber wenn die Funktion, die einen Fruchtzeiger verwendet, color () für die an sie gesendete Bananenklasse aufruft - welche color () -Funktion wird aufgerufen? Die Funktion ruft normalerweise Fruit :: color () für ein Fruit-Objekt auf.
Das wäre in 99% der Fälle nicht das, was beabsichtigt war. Wenn jedoch Fruit :: color () als virtuell deklariert wurde, wird Banana: color () für das Objekt aufgerufen, da die richtige color () -Funktion zum Zeitpunkt des Aufrufs an den Fruit-Zeiger gebunden wäre. Die Laufzeit überprüft, auf welches Objekt der Zeiger zeigt, da es in der Fruit-Klassendefinition als virtuell markiert wurde.
Dies unterscheidet sich vom Überschreiben einer Funktion in einer Unterklasse. In diesem Fall ruft der Fruit-Zeiger Fruit :: color () auf, wenn er nur weiß, dass es sich um einen IS-A-Zeiger auf Fruit handelt.
Nun kommt also die Idee einer "reinen virtuellen Funktion" auf. Es ist ein ziemlich unglücklicher Satz, da Reinheit nichts damit zu tun hat. Dies bedeutet, dass die Basisklassenmethode niemals aufgerufen werden soll. In der Tat kann eine reine virtuelle Funktion nicht aufgerufen werden. Es muss jedoch noch definiert werden. Eine Funktionssignatur muss vorhanden sein. Viele Codierer erstellen der Vollständigkeit halber eine leere Implementierung {}, aber der Compiler generiert intern eine, wenn nicht. In diesem Fall wird Banana :: color () aufgerufen, wenn die Funktion aufgerufen wird, auch wenn der Zeiger auf Fruit zeigt, da dies die einzige Implementierung von color () ist.
Nun das letzte Puzzleteil: Konstruktoren und Destruktoren.
Reine virtuelle Konstruktoren sind völlig illegal. Das ist einfach raus.
Reine virtuelle Destruktoren funktionieren jedoch in dem Fall, dass Sie die Erstellung einer Basisklasseninstanz verbieten möchten. Nur Unterklassen können instanziiert werden, wenn der Destruktor der Basisklasse rein virtuell ist. Die Konvention besteht darin, es 0 zuzuweisen.
virtual ~Fruit() = 0; // pure virtual
Fruit::~Fruit(){} // destructor implementation
In diesem Fall müssen Sie eine Implementierung erstellen. Der Compiler weiß, dass Sie dies tun, und stellt sicher, dass Sie es richtig machen, oder er beschwert sich mächtig, dass er nicht mit allen Funktionen verknüpfen kann, die er zum Kompilieren benötigt. Die Fehler können verwirrend sein, wenn Sie nicht auf dem richtigen Weg sind, wie Sie Ihre Klassenhierarchie modellieren.
In diesem Fall ist es Ihnen also verboten, Instanzen von Obst zu erstellen, Sie dürfen jedoch Instanzen von Bananen erstellen.
Ein Aufruf zum Löschen des Fruchtzeigers, der auf eine Bananeninstanz verweist, ruft zuerst Banana :: ~ Banana () und dann immer Fuit :: ~ Fruit () auf. Denn egal was passiert, wenn Sie einen Unterklassen-Destruktor aufrufen, muss der Basisklassen-Destruktor folgen.
Ist es ein schlechtes Modell? Es ist in der Entwurfsphase zwar komplizierter, kann jedoch sicherstellen, dass zur Laufzeit eine korrekte Verknüpfung durchgeführt wird und dass eine Unterklassenfunktion ausgeführt wird, bei der Unklarheiten darüber bestehen, auf welche Unterklasse genau zugegriffen wird.
Wenn Sie C ++ so schreiben, dass Sie nur exakte Klassenzeiger ohne generische oder mehrdeutige Zeiger weitergeben, werden virtuelle Funktionen nicht wirklich benötigt. Wenn Sie jedoch eine Laufzeitflexibilität der Typen benötigen (wie in Apple Banana Orange ==> Fruit), werden die Funktionen mit weniger redundantem Code einfacher und vielseitiger. Sie müssen nicht mehr für jede Fruchtart eine Funktion schreiben, und Sie wissen, dass jede Frucht auf color () mit ihrer eigenen korrekten Funktion reagiert.
Ich hoffe, diese langatmige Erklärung verfestigt das Konzept und verwirrt die Dinge nicht. Es gibt viele gute Beispiele, die man sich ansehen und genug ansehen und sie tatsächlich ausführen und mit ihnen herumspielen kann, und Sie werden es bekommen.