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 du eine Methode als virtuell deklarierst, bestimmt C++, welche Version zur Laufzeit aufgerufen wird, 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!Jetzt wird die richtige Methode aufgerufen! Das Schlüsselwort override ist optional, wird aber empfohlen. Es teilt dem Compiler mit, dass du beabsichtigst, eine virtuelle Funktion zu überschreiben, und erkennt Fehler, wenn die Signaturen nicht übereinstimmen.
Wie funktioniert das? Wenn eine Klasse virtuelle Funktionen hat, erstellt der Compiler eine virtuelle Tabelle (vtable). Dies ist eine verborgene Nachschlagetabelle, die Zeiger auf die virtuellen Funktionen der Klasse enthält. Jedes Objekt mit virtuellen Funktionen speichert einen verborgenen Zeiger (vptr) auf die vtable seiner Klasse.
Wenn du eine virtuelle Funktion über einen Zeiger aufrufst, sucht das Programm die Adresse der korrekten Funktion zur Laufzeit in der vtable nach. Dies wird als dynamisches Dispatching bezeichnet. Im Vergleich zu regulären Funktionsaufrufen entsteht dadurch ein geringer zusätzlicher Aufwand, aber es wird leistungsfähiges polymorphes Verhalten ermöglicht.
Eine wichtige Regel: Wenn eine Klasse virtuelle Funktionen hat und als Basisklasse verwendet wird, sollte auch ihr Destruktor virtuell sein. Dadurch wird eine ordnungsgemäße Bereinigung gewährleistet, wenn abgeleitete Objekte über Basispointer gelöscht werden:
class Animal {
public:
virtual ~Animal() {} // Virtueller Destruktor
virtual void speak() {}
};Aufgabe
EinfachErstellen wir ein Mediaplayer-System, das die Leistungsfähigkeit virtueller Funktionen und des Laufzeit-Polymorphismus demonstriert. Du erstellst eine Hierarchie von Medientypen, bei der jeder Typ anders abgespielt wird, und siehst, wie virtuelle Funktionen das korrekte Verhalten ermöglichen, selbst wenn der Zugriff über Zeiger auf die Basisklasse erfolgt.
Du organisierst deinen Code auf drei Dateien verteilt:
Media.h: Definiere eine BasisklasseMedia, die beliebige abspielbare Medien darstellt:- Ein geschütztes
std::string title-Member - Einen Konstruktor, der einen Titel übernimmt und ihn speichert
- Eine virtuelle
play()-Methode, die Folgendes ausgibt:Playing media: <title> - Einen virtuellen Destruktor, der Folgendes ausgibt:
Media [<title>] destroyed
- Ein geschütztes
AudioTrack.h: Definiere eine KlasseAudioTrack, die öffentlich vonMediaerbt:- Ein privates
std::string artist-Member - Einen Konstruktor, der einen Titel und einen Interpreten übernimmt, den Titel an die Basisklasse weitergibt und den Interpreten speichert
- Überschreibe
play()mit dem Schlüsselwortoverride, sodass Folgendes ausgegeben wird:Playing audio: <title> by <artist> - Einen Destruktor, der Folgendes ausgibt:
AudioTrack [<title>] destroyed
- Ein privates
main.cpp: Lies drei Eingaben ein (jeweils in einer eigenen Zeile):- Titel des Audiotitels (String)
- Name des Interpreten (String)
- Videotitel (String)
Definiere eine Klasse
VideoClipdirekt in main.cpp, die öffentlich vonMediaerbt:- Ein privates
int duration-Member (in Sekunden) - Einen Konstruktor, der einen Titel und eine Dauer übernimmt (die Dauer standardmäßig auf 120 setzen)
- Überschreibe
play(), sodass Folgendes ausgegeben wird:Playing video: <title> (<duration>s) - Einen Destruktor, der Folgendes ausgibt:
VideoClip [<title>] destroyed
Erstelle innerhalb eines Blockbereichs ein Array aus drei
Media*-Zeigern. Weise dynamisch Speicher zu für:- Ein
Media-Basisobjekt mit dem Titel "Generic Media" - Ein
AudioTrack-Objekt mit dem eingegebenen Titel und Interpreten - Ein
VideoClip-Objekt mit dem eingegebenen Videotitel
Durchlaufe das Array und rufe für jeden Zeiger
play()auf. Lösche anschließend alle Objekte in umgekehrter Reihenfolge. Gib nach dem Block Folgendes aus:Playback complete!
Bei den Eingaben Bohemian Rhapsody, Queen und Nature Documentary sieht die Ausgabe beispielsweise so aus:
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!Beachte, wie der Aufruf von play() über Media*-Zeiger dank virtueller Funktionen die Methode der korrekten abgeleiteten Klasse aufruft. Beobachte außerdem, wie der virtuelle Destruktor eine ordnungsgemäße Bereinigung sicherstellt: Beim Löschen über einen Zeiger auf die Basisklasse werden sowohl der Destruktor der abgeleiteten Klasse als auch der Destruktor der Basisklasse ausgeführt.
Probier es selbst
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: Definiere hier die VideoClip-Klasse, die öffentlich von Media erbt
// - Privates int-Member duration (in Sekunden)
// - Konstruktor nimmt title und duration entgegen (Standard-duration auf 120)
// - Override play() to print: Playing video: <title> (<duration>s)
// - Destructor prints: 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: Speichere die duration
}
// TODO: play()-Methode überschreiben
void play() override {
// TODO: Implementiere diese Methode
}
// TODO: Destruktor implementieren
~VideoClip() {
// TODO: Implementiere diesen Destruktor
}
};
int main() {
// Eingaben lesen
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: Erstelle einen Block-Scope mit geschweiften Klammern
// Innerhalb des Blocks:
// 1. Erstelle ein Array von drei Media*-Zeigern
// 2. Dynamisch allokieren:
// - A base Media object with title "Generic Media"
// - Ein AudioTrack-Objekt mit dem eingegebenen Titel und Künstler
// - Ein VideoClip-Objekt mit dem eingegebenen Videotitel
// 3. Durchlaufe das Array und rufe play() auf jedem Zeiger auf
// 4. Lösche alle Objekte in umgekehrter Reihenfolge
// TODO: After the block, print: 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 & QuelldateienNamespaces & GültigkeitsbereichEinführung in OOP mit C++Klassen vs. ObjekteDer „this“-ZeigerMethoden (Member-Funktionen)Attribute (Datenmember)Grundlagen zu Ctors & DtorsRückblick – Einfacher Taschenrechner4Klassenattribute
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen und -KlassenZusammenfassung – Bankkontoverwaltung7Vererbung
Grundlagen der VererbungZugriffsebenen bei der VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick – Mitarbeiterhierarchie2Speicherverwaltung
Stack- vs. Heap-SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Intelligente Zeiger in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsbezeichner in C++Zugriffsbezeichner im DetailInformationsverbergungStruct vs. KlasseVerschachtelte und innere KlassenWiederholung – Studierendenverwaltungssystem8Polymorphie
Polymorphie zur Compilezeit vs. LaufzeitFunktionsüberladungVirtuelle Funktionen erneut betrachtetRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIRückblick – Shape-Calculator3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorMove-KonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktor im DetailDrei-/Fünf-/Null-RegelZusammenfassung – String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladung der Operatoren [] und ()TypkonvertierungsoperatorenWiederholung – Matrix-Klasse9Templates
FunktionstemplatesKlassen-TemplatesTemplate-SpezialisierungVariadische TemplatesGrundlagen zu SFINAE und Type TraitsRückblick – Generischer ContainerÜbe selbstständig: Online-C++-Compiler