Virtuelle Funktionen & VTable
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy — Lektion 52 von 104.
Das Schlüsselwort virtual löst das Problem, das wir in der vorherigen Lektion gesehen haben. Wenn Sie eine Methode als virtual deklarieren, bestimmt C++, welche Version aufgerufen werden soll, basierend auf dem tatsächlichen Objekttyp zur Laufzeit, nicht auf dem Zeigertyp.
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // Ausgabe: Woof!Jetzt wird die richtige Methode aufgerufen! Das Schlüsselwort override ist optional, wird aber empfohlen. Es teilt dem Compiler mit, dass Sie beabsichtigen, eine virtuelle Funktion zu überschreiben, wodurch Fehler abgefangen werden, wenn die Signaturen nicht übereinstimmen.
Wie funktioniert das? Wenn eine Klasse virtuelle Funktionen hat, erstellt der Compiler eine virtuelle Tabelle (vtable). Dies ist eine versteckte Nachschlagetabelle, die Zeiger auf die virtuellen Funktionen der Klasse enthält. Jedes Objekt mit virtuellen Funktionen speichert einen versteckten Zeiger (vptr) auf die vtable seiner Klasse.
Wenn Sie eine virtuelle Funktion über einen Zeiger aufrufen, sucht das Programm zur Laufzeit die korrekte Funktionsadresse in der vtable. Dies wird als dynamischer Dispatch bezeichnet. Es verursacht einen geringen Overhead im Vergleich zu regulären Funktionsaufrufen, ermöglicht jedoch ein leistungsfähiges polymorphes Verhalten.
Eine wichtige Regel: Wenn eine Klasse virtuelle Funktionen hat und als Basisklasse verwendet wird, sollte ihr Destruktor ebenfalls virtuell sein. Dies stellt eine ordnungsgemäße Bereinigung sicher, wenn abgeleitete Objekte über Basis-Pointer gelöscht werden:
class Animal {
public:
virtual ~Animal() {} // Virtueller Destruktor
virtual void speak() {}
};Aufgabe
EinfachLassen Sie uns ein Media-Player-System bauen, das die Leistungsfähigkeit von virtuellen Funktionen und Laufzeitpolymorphismus demonstriert. Sie werden eine Hierarchie von Medientypen erstellen, von denen jeder anders abgespielt wird, und sehen, wie virtuelle Funktionen das korrekte Verhalten ermöglichen, selbst wenn über Basisklassen-Pointer auf sie zugegriffen wird.
Sie werden Ihren Code über drei Dateien organisieren:
Media.h: Definieren Sie eine BasisklasseMedia, die jedes abspielbare Medium repräsentiert:- Ein geschütztes (protected)
std::string titleMitglied - Ein Konstruktor, der einen Titel entgegennimmt und speichert
- Eine virtuelle
play()Methode, die ausgibt:Playing media: <title> - Ein virtueller Destruktor, der ausgibt:
Media [<title>] destroyed
- Ein geschütztes (protected)
AudioTrack.h: Definieren Sie eine KlasseAudioTrack, die öffentlich vonMediaerbt:- Ein privates
std::string artistMitglied - Ein Konstruktor, der einen Titel und einen Interpreten entgegennimmt, den Titel an die Basisklasse übergibt und den Interpreten speichert
- Überschreiben Sie
play()unter Verwendung desoverrideSchlüsselworts, um auszugeben:Playing audio: <title> by <artist> - Ein Destruktor, der ausgibt:
AudioTrack [<title>] destroyed
- Ein privates
main.cpp: Lesen Sie drei Eingaben ein (jede in einer separaten Zeile):- Titel des Audiotracks (String)
- Name des Interpreten (String)
- Videotitel (String)
Definieren Sie eine Klasse
VideoClipdirekt in main.cpp, die öffentlich vonMediaerbt:- Ein privates
int durationMitglied (in Sekunden) - Ein Konstruktor, der einen Titel und eine Dauer entgegennimmt (Standarddauer auf 120)
- Überschreiben Sie
play(), um auszugeben:Playing video: <title> (<duration>s) - Ein Destruktor, der ausgibt:
VideoClip [<title>] destroyed
Erstellen Sie ein Array aus drei
Media*Pointern innerhalb eines Block-Scopes. Allokieren Sie dynamisch:- Ein Basis-
Media-Objekt mit dem Titel "Generic Media" - Ein
AudioTrack-Objekt mit dem eingegebenen Titel und Interpreten - Ein
VideoClip-Objekt mit dem eingegebenen Videotitel
Iterieren Sie durch das Array und rufen Sie
play()für jeden Pointer auf. Löschen Sie dann alle Objekte in umgekehrter Reihenfolge. Geben Sie nach dem Block aus:Playback complete!
Zum Beispiel mit den Eingaben Bohemian Rhapsody, Queen und Nature Documentary:
Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!Beachten Sie, wie der Aufruf von play() über Media* Pointer dank virtueller Funktionen die korrekte Methode der abgeleiteten Klasse aufruft. Beobachten Sie auch, wie der virtuelle Destruktor eine ordnungsgemäße Bereinigung gewährleistet — sowohl der Destruktor der abgeleiteten Klasse als auch der der Basisklasse werden ausgeführt, wenn über einen Basis-Pointer gelöscht wird.
Spickzettel
Das Schlüsselwort virtual ermöglicht Laufzeitpolymorphismus, indem es bestimmt, welche Methode aufgerufen werden soll, basierend auf dem tatsächlichen Objekttyp und nicht auf dem Zeigertyp:
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // Ausgabe: Woof!Das Schlüsselwort override ist optional, wird aber empfohlen. Es teilt dem Compiler mit, dass Sie beabsichtigen, eine virtuelle Funktion zu überschreiben, wodurch Fehler abgefangen werden, wenn die Signaturen nicht übereinstimmen.
Wie virtuelle Funktionen funktionieren: Der Compiler erstellt eine virtuelle Tabelle (vtable), die Zeiger auf die virtuellen Funktionen der Klasse enthält. Jedes Objekt mit virtuellen Funktionen speichert einen versteckten Zeiger (vptr) auf die vtable seiner Klasse. Wenn eine virtuelle Funktion über einen Zeiger aufgerufen wird, schlägt das Programm zur Laufzeit die korrekte Funktionsadresse in der vtable nach. Dies wird als dynamischer Dispatch bezeichnet.
Virtuelle Destruktoren: Wenn eine Klasse virtuelle Funktionen hat und als Basisklasse verwendet wird, sollte auch ihr Destruktor virtuell sein. Dies stellt eine ordnungsgemäße Bereinigung sicher, wenn abgeleitete Objekte über Basiszeiger gelöscht werden:
class Animal {
public:
virtual ~Animal() {} // Virtueller Destruktor
virtual void speak() {}
};Probier es selbst
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: Definiere hier die Klasse VideoClip, die öffentlich von Media erbt
// - Privates int-Member duration (in Sekunden)
// - Konstruktor nimmt title und duration entgegen (Standardwert für duration ist 120)
// - Überschreibe play(), um Folgendes auszugeben: Playing video: <title> (<duration>s)
// - Destruktor gibt aus: VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO: Konstruktor implementieren
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO: Die duration speichern
}
// TODO: play()-Methode überschreiben
void play() override {
// TODO: Diese Methode implementieren
}
// TODO: Destruktor implementieren
~VideoClip() {
// TODO: Diesen Destruktor implementieren
}
};
int main() {
// Eingaben lesen
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: Einen Block-Scope mit geschweiften Klammern erstellen
// Innerhalb des Blocks:
// 1. Erstelle ein Array aus drei Media*-Pointern
// 2. Dynamisch allokieren:
// - Ein Basis-Media-Objekt mit dem Titel "Generic Media"
// - Ein AudioTrack-Objekt mit dem eingegebenen Titel und Künstler
// - Ein VideoClip-Objekt mit dem eingegebenen Videotitel
// 3. Iteriere durch das Array und rufe play() für jeden Pointer auf
// 4. Lösche alle Objekte in umgekehrter Reihenfolge
// TODO: Nach dem Block ausgeben: Playback complete!
return 0;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische Container