Menu
Coddy logo textTech

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() {}
};
challenge icon

Aufgabe

Einfach

Lassen 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 Basisklasse Media, die jedes abspielbare Medium repräsentiert:
    • Ein geschütztes (protected) std::string title Mitglied
    • 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
  • AudioTrack.h: Definieren Sie eine Klasse AudioTrack, die öffentlich von Media erbt:
    • Ein privates std::string artist Mitglied
    • Ein Konstruktor, der einen Titel und einen Interpreten entgegennimmt, den Titel an die Basisklasse übergibt und den Interpreten speichert
    • Überschreiben Sie play() unter Verwendung des override Schlüsselworts, um auszugeben: Playing audio: <title> by <artist>
    • Ein Destruktor, der ausgibt: AudioTrack [<title>] destroyed
  • main.cpp: Lesen Sie drei Eingaben ein (jede in einer separaten Zeile):
    1. Titel des Audiotracks (String)
    2. Name des Interpreten (String)
    3. Videotitel (String)

    Definieren Sie eine Klasse VideoClip direkt in main.cpp, die öffentlich von Media erbt:

    • Ein privates int duration Mitglied (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;
}
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung