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

Aufgabe

Einfach

Erstellen 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 Basisklasse Media, 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
  • AudioTrack.h: Definiere eine Klasse AudioTrack, die öffentlich von Media erbt:
    • 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üsselwort override, sodass Folgendes ausgegeben wird: Playing audio: <title> by <artist>
    • Einen Destruktor, der Folgendes ausgibt: AudioTrack [<title>] destroyed
  • main.cpp: Lies drei Eingaben ein (jeweils in einer eigenen Zeile):
    1. Titel des Audiotitels (String)
    2. Name des Interpreten (String)
    3. Videotitel (String)

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

    • 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;
}
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

Übe selbstständig: Online-C++-Compiler