In der objektorientierten Programmierung, wie sie häufig in C++ und Object Pascal verwendet wird, ist eine virtuelle Funktion oder Methode eine vererbbare und überschreibbare Funktion bzw. Methode , die dynamisch aufgerufen wird . Virtuelle Funktionen sind ein wichtiger Bestandteil des (Laufzeit-) Polymorphismus in der objektorientierten Programmierung (OOP). Sie ermöglichen die Ausführung von Zielfunktionen, die zur Kompilierzeit nicht genau definiert wurden.

Viele Programmiersprachen, wie beispielsweise Java , behandeln standardmäßig alle Methoden als virtuell. [ 1 ] Einige dieser Sprachen, wie etwa Python , erlauben keine Änderung dieses Verhaltens. [ 2 ] Andere hingegen bieten Modifikatoren, die verhindern, dass Methoden von abgeleiteten Klassen überschrieben werden (wie beispielsweise die Schlüsselwörter `final` und `private` in Java [ 3 ] und PHP [ 4 ] ).

Zweck

Das Konzept der virtuellen Funktion löst folgendes Problem:

In der objektorientierten Programmierung kann, wenn eine abgeleitete Klasse von einer Basisklasse erbt, ein Objekt der abgeleiteten Klasse über einen Zeiger oder eine Referenz des Basisklassentyps anstatt des Typs der abgeleiteten Klasse referenziert werden. Werden Methoden der Basisklasse von der abgeleiteten Klasse überschrieben, kann die durch eine solche Referenz oder einen solchen Zeiger tatsächlich aufgerufene Methode entweder „früh“ (vom Compiler) entsprechend dem deklarierten Typ des Zeigers oder der Referenz oder „spät“ (d. h. vom Laufzeitsystem der Sprache) entsprechend dem tatsächlichen Typ des referenzierten Objekts gebunden (verknüpft) werden.

Virtuelle Funktionen werden „spät“ aufgelöst. Ist die betreffende Funktion in der Basisklasse „virtuell“, wird die Implementierung der am weitesten abgeleiteten Klasse entsprechend dem tatsächlichen Typ des referenzierten Objekts aufgerufen, unabhängig vom deklarierten Typ des Zeigers oder der Referenz. Ist sie nicht „virtuell“, wird die Methode „früh“ aufgelöst und anhand des deklarierten Typs des Zeigers oder der Referenz ausgewählt.

Virtuelle Funktionen ermöglichen es einem Programm, Methoden aufzurufen, die zum Zeitpunkt der Kompilierung des Codes möglicherweise noch gar nicht existieren.

In C++ werden virtuelle Methoden deklariert, indem virtualdem Funktionsdeklarationscode in der Basisklasse das Schlüsselwort `virtual` vorangestellt wird. Dieser Modifikator wird an alle Implementierungen dieser Methode in abgeleiteten Klassen vererbt, was bedeutet, dass sie sich gegenseitig überschreiben und spät gebunden werden können. Selbst wenn Methoden der Basisklasse die virtuelle Methode aufrufen, rufen sie stattdessen die Methode der abgeleiteten Klasse auf. Überladen liegt vor, wenn zwei oder mehr Methoden einer Klasse denselben Namen, aber unterschiedliche Parameter haben. Überschreiben bedeutet, dass zwei Methoden denselben Namen und dieselben Parameter besitzen. Überladen wird auch als Funktionsabgleich und Überschreiben als dynamische Funktionszuordnung bezeichnet.

Beispiel

C++

Klassendiagramm der Tiere

Eine Basisklasse Animalkönnte beispielsweise eine virtuelle Funktion haben eat. Die Unterklasse Llamaimplementiert diese Funktion eatanders als die Basisklasse selbst , aber man kann sie für jede Instanz der Klasse, die als „Animal“ bezeichnet wird, Wolfaufrufen und das Verhalten der jeweiligen Unterklasse abrufen. eateat

import std ;class Animal { public : // Absichtlich nicht virtuell: void move () { std :: println ( "Dieses Tier bewegt sich auf irgendeine Weise" ); }virtual void eat () = 0 ; };// Die Klasse "Animal" kann optional eine Definition für "Eat" enthalten. class Llama : public Animal { public : // Die nicht-virtuelle Funktion "Move" wird geerbt, aber nicht überschrieben. void eat () override { std :: println ( "Lamas fressen Gras!" ); } };

Dies ermöglicht es einem Programmierer, eine Liste von Objekten der Klasse zu verarbeiten Animalund jedem Objekt nacheinander mitzuteilen, dass es fressen soll (durch Aufruf von eat), ohne wissen zu müssen, welche Art von Tier sich in der Liste befindet, wie jedes Tier frisst oder wie die vollständige Menge der möglichen Tierarten aussieht.

C

In C könnte der Mechanismus hinter virtuellen Funktionen folgendermaßen bereitgestellt werden:

#include <stdio.h>// Ein Objekt verweist auf seine Klasse... typedef struct Animal { const struct AnimalVTable * vtable ; } Animal ;// ...die die virtuelle Funktion Animal.eat enthält typedef struct AnimalVTable { void ( * eat )( Animal * self ); // 'virtuelle' Funktion } AnimalVTable ;/*  Da Animal.move keine virtuelle Funktion ist,  ist sie nicht in der obigen Struktur enthalten. */ void move ( const Animal * self ) { printf ( "<Tier an Position %p> hat sich bewegt \n " , ( void * )( self )); }/*  Im Gegensatz zu `move`, das `Animal.move` direkt ausführt,  kann die Funktion `eat` zur Kompilierzeit nicht wissen, welche Funktion (falls überhaupt eine) aufgerufen werden soll.  `Animal.eat` kann erst zur Laufzeit aufgelöst werden, wenn `eat` aufgerufen wird. */ void eat ( Animal * self ) { const AnimalVTable * vtable = self -> vtable ; if ( vtable -> eat ) { ( * vtable -> eat )( self ); // `Animal.eat` ausführen } else { fprintf ( stderr , "'eat' virtuelle Methode nicht implementiert \n " ); } }/*  Implementierung von Llama.eat. Dies ist die Zielfunktion,  die von 'void eat(Animal* self)' aufgerufen wird. */ static void Llama_eat ( Animal * self ) { printf ( "<Llama at %p> Lamas eat grass! \n " , ( void * )( self )); }// Initialisierung der Klasse const AnimalVTable Animal_vt = { NULL }; // Die Basisklasse implementiert Animal.Eat nicht const AnimalVTable Llama_vt = { Llama_eat }; // Die abgeleitete Klasse jedoch schonint main ( void ) { // Objekte als Instanzen ihrer Klasse initialisieren Animal animal = { & Animal_vt }; Animal llama = { & Llama_vt }; move ( & animal ); // Animal.move move ( & llama ); // Llama.move eat ( & animal ); // Animal.eat kann nicht aufgelöst werden, daher "Nicht implementiert" auf stderr ausgeben eat ( & llama ); // Llama.eat auflösen und ausführen }

Abstrakte Klassen und rein virtuelle Funktionen

Eine rein virtuelle Funktion oder Methode ist eine virtuelle Funktion, die von einer abgeleiteten Klasse implementiert werden muss, sofern diese nicht abstrakt ist . Klassen mit rein virtuellen Methoden werden als „abstrakt“ bezeichnet und können nicht direkt instanziiert werden. Eine Unterklasse einer abstrakten Klasse kann nur dann direkt instanziiert werden, wenn alle geerbten rein virtuellen Methoden von dieser Klasse oder einer Oberklasse implementiert wurden. Rein virtuelle Methoden besitzen typischerweise eine Deklaration ( Signatur ), aber keine Definition ( Implementierung ).

Beispielsweise MathSymbolkann eine abstrakte Basisklasse eine rein virtuelle Funktion bereitstellen doOperation(), und abgeleitete Klassen implementieren diese Plus, um konkrete Implementierungen zu liefern. Die Implementierung von wäre in der Klasse nicht sinnvoll , da es sich um ein abstraktes Konzept handelt, dessen Verhalten ausschließlich für jede gegebene Unterklasse von definiert ist . Ebenso wäre eine gegebene Unterklasse von ohne eine Implementierung von nicht vollständig . MinusdoOperation()doOperation()MathSymbolMathSymbolMathSymbolMathSymboldoOperation()

Obwohl rein virtuelle Methoden typischerweise keine Implementierung in der Klasse besitzen, die sie deklariert, ist es in einigen Sprachen (z. B. C++ und Python) zulässig, dass rein virtuelle Methoden eine Implementierung in der deklarierenden Klasse enthalten. Dies ermöglicht die Bereitstellung eines Fallback- oder Standardverhaltens, an das eine abgeleitete Klasse gegebenenfalls delegieren kann. [ 5 ] [ 6 ]

Rein virtuelle Funktionen können auch dort verwendet werden, wo Methodendeklarationen zur Definition einer Schnittstelle dienen – ähnlich wie es das Schlüsselwort `interface` in Java explizit spezifiziert. In diesem Fall stellen abgeleitete Klassen alle Implementierungen bereit. Bei diesem Entwurfsmuster enthält die abstrakte Klasse, die als Schnittstelle dient, ausschließlich rein virtuelle Funktionen, jedoch keine Datenmember oder gewöhnlichen Methoden. In C++ funktioniert die Verwendung solcher rein abstrakter Klassen als Schnittstellen, da C++ Mehrfachvererbung unterstützt . Da viele objektorientierte Programmiersprachen (OOP) jedoch keine Mehrfachvererbung unterstützen, bieten sie häufig einen separaten Schnittstellenmechanismus. Ein Beispiel hierfür ist die Programmiersprache Java .

Verhalten während des Baus und der Zerstörung

Programmiersprachen unterscheiden sich in ihrem Verhalten während der Ausführung des Konstruktors oder Destruktors eines Objekts. Aus diesem Grund wird vom Aufruf virtueller Funktionen in Konstruktoren im Allgemeinen abgeraten.

In C++ wird die Basisfunktion aufgerufen. Genauer gesagt wird die am weitesten abgeleitete Funktion aufgerufen, die nicht weiter von der Klasse des aktuellen Konstruktors oder Destruktors abgeleitet ist. [ 7 ] : §15.7.3 [ 8 ] [ 9 ] Handelt es sich bei dieser Funktion um eine rein virtuelle Funktion, tritt undefiniertes Verhalten auf. [ 7 ] : §13.4.6 [ 8 ] Dies gilt selbst dann, wenn die Klasse eine Implementierung für diese rein virtuelle Funktion enthält, da ein Aufruf einer rein virtuellen Funktion explizit qualifiziert werden muss. [ 10 ] Eine konforme C++-Implementierung ist nicht verpflichtet (und in der Regel auch nicht in der Lage), indirekte Aufrufe rein virtueller Funktionen zur Kompilier- oder Linkzeit zu erkennen . Einige Laufzeitsysteme geben einen Fehler wegen eines Aufrufs einer rein virtuellen Funktion aus, wenn sie zur Laufzeit auf einen solchen Aufruf stoßen .

In Java und C# wird die abgeleitete Implementierung aufgerufen, jedoch werden einige Felder vom abgeleiteten Konstruktor noch nicht initialisiert (obwohl sie mit ihren Standardwerten Null initialisiert werden). [ 11 ] Bestimmte Entwurfsmuster , wie beispielsweise das Abstract Factory Pattern , fördern diese Verwendung aktiv in Sprachen, die diese Möglichkeit unterstützen.

Virtuelle Destruktoren

Objektorientierte Programmiersprachen verwalten die Speicherbelegung und -freigabe beim Erstellen und Zerstören von Objekten in der Regel automatisch. Einige objektorientierte Sprachen erlauben jedoch die Implementierung einer benutzerdefinierten Destruktormethode . Nutzt die jeweilige Sprache die automatische Speicherverwaltung, wird der benutzerdefinierte Destruktor (in diesem Kontext üblicherweise Finalisierer genannt) mit Sicherheit für das betreffende Objekt aufgerufen. Wird beispielsweise ein Objekt vom Typ Wolf erstellt, das von Animal erbt, und besitzen beide benutzerdefinierte Destruktoren, wird der in Wolf deklarierte Destruktor aufgerufen.

Bei manueller Speicherverwaltung kann die Situation komplexer sein, insbesondere im Zusammenhang mit statischer Dispatch . Wird ein Objekt vom Typ Wolf erzeugt, auf das ein Animal-Zeiger verweist, und wird dieser Animal-Zeiger gelöscht, kann der aufgerufene Destruktor derjenige von Animal und nicht der von Wolf sein, es sei denn, der Destruktor ist virtuell. Dies ist insbesondere in C++ der Fall, wo dieses Verhalten häufig zu Programmierfehlern führt, wenn Destruktoren nicht virtuell sind.

Siehe auch

Referenzen