Wird geladen...

Polymorphismus in C++: Virtuelle Funktionen und Vtable

Lernen Sie Polymorphismus in C++ mit virtuellen Funktionen, Vtable und dynamischer Bindung anhand praktischer Beispiele.

Polymorphismus ermöglicht es Objekten, die dieselbe Schnittstelle (Basistyp) teilen, zur Laufzeit unterschiedliches Verhalten zu zeigen. In C++ wird dynamischer Polymorphismus durch virtuelle Funktionen erreicht, und der Compiler implementiert diesen Mechanismus typischerweise mit einer vtable (virtuellen Tabelle) und einem vptr (virtuellen Zeiger) in jedem Objekt. In diesem Artikel untersuchen wir, wie virtuelle Funktionen funktionieren, wie das vtable/vptr-System aufgebaut ist, welche Leistungsauswirkungen es gibt und welche bewährten Praktiken gelten — mit realistischen Beispielen.


1) Was ist dynamischer Polymorphismus?

Dynamischer Polymorphismus bedeutet, dass welche Funktion aufgerufen wird, zur Laufzeit bestimmt wird. Dadurch kann über einen Basis-Klassenzeiger oder eine Referenz das Verhalten einer abgeleiteten Klasse aufgerufen werden.


#include <iostream>
#include <memory>
using namespace std;

struct Book {
    virtual ~Book() = default;           // erforderlich für polymorphe Basisklasse
    virtual void PrintInfo() const {     // virtuelle Funktion
        cout << "[Book] Allgemeine Informationen" << endl;
    }
};

struct EBook : Book {
    void PrintInfo() const override {
        cout << "[EBook] Digitale Ausgabe" << endl;
    }
};

struct PrintedBook : Book {
    void PrintInfo() const override {
        cout << "[PrintedBook] Physische Ausgabe" << endl;
    }
};

int main() {
    unique_ptr<Book> a = make_unique<EBook>();
    unique_ptr<Book> b = make_unique<PrintedBook>();

    a->PrintInfo();  // [EBook] ...
    b->PrintInfo();  // [PrintedBook] ...
}

Obwohl a und b als Basistyp (Book) erscheinen, werden die Aufrufe zur Laufzeit an die tatsächlichen Typen (EBook/PrintedBook) weitergeleitet.


2) Wie funktionieren vtable und vptr?

Wenn eine Klasse mindestens eine virtual-Funktion enthält, erzeugt der Compiler eine vtable für diese Klasse. Objekte enthalten normalerweise ein verborgenes Feld vptr, das auf die vtable der entsprechenden Klasse zeigt.

Beim Aufruf: objekt.vptr → vtable → tatsächliche Funktionsadresse.


Book (vptr) ──► vtable(Book)       : &Book::PrintInfo
EBook (vptr) ─► vtable(EBook)       : &EBook::PrintInfo
Printed (vptr)► vtable(PrintedBook) : &PrintedBook::PrintInfo

Diese indirekte Weiterleitung fügt eine kleine Aufrufkosten hinzu; in den meisten Programmen ist sie jedoch vernachlässigbar.


3) override, final und virtueller Destruktor

Das Schlüsselwort override überprüft zur Kompilierzeit, dass eine virtuelle Funktion der Basisklasse tatsächlich überschrieben wird. final verhindert, dass eine Methode oder Klasse weiter überschrieben wird. In polymorphen Basisklassen ist ein virtueller Destruktor unerlässlich, andernfalls führt das Löschen über einen Basiszeiger zu undefiniertem Verhalten.


struct Book {
    virtual ~Book() = default;
    virtual double Price() const { return 0.0; }
};

struct EBook final : Book {
    double Price() const override { return 99.0; }
};

// struct SpecialEBook : EBook {}; // FEHLER: EBook ist final

4) Unterschied zum statischen Polymorphismus (Overload/Template)

Überladungen (gleicher Name, unterschiedliche Signatur) und Templates werden zur Kompilierzeit aufgelöst und verwenden keine vtable. Dynamischer Polymorphismus wird zur Laufzeit durch virtuelle Aufrufe gelöst.


5) Entwurf einer polymorphen Schnittstelle (Beispiel Book)

Gemeinsames Verhalten wird in der Basisklasse abstrahiert und in den abgeleiteten Klassen spezifiziert. Beispiel: Preisberechnung für digitale und gedruckte Bücher.


#include <iostream>
#include <memory>
#include <string>
using namespace std;

class Book {
protected:
    string title, author;
public:
    Book(string t, string a) : title(t), author(a) {}
    virtual ~Book() = default;

    virtual double FinalPrice() const = 0;   // rein virtuell
    virtual void PrintInfo() const {
        cout << title << " - " << author
             << " | " << FinalPrice() << " €" << endl;
    }
};

class EBook : public Book {
    double base, vat;
public:
    EBook(string t, string a, double b, double v)
        : Book(t,a), base(b), vat(v) {}

    double FinalPrice() const override {
        return base*(1.0+vat);
    }
};

class PrintedBook : public Book {
    double base, ship; int pages; bool hard;
public:
    PrintedBook(string t, string a, double b, double s, int p, bool h)
        : Book(t,a), base(b), ship(s), pages(p), hard(h) {}

    double FinalPrice() const override {
        double cost = base + ship + 0.04*pages;
        if (hard) cost *= 1.1;
        return cost;
    }
};

int main() {
    unique_ptr<Book> b1 = make_unique<EBook>("Modern C++", "A. Dev", 100, 0.10);
    unique_ptr<Book> b2 = make_unique<PrintedBook>("Clean Code", "R. Martin", 120, 20, 464, true);

    b1->PrintInfo(); // EBook Verhalten
    b2->PrintInfo(); // PrintedBook Verhalten
}

6) Object Slicing und Verwendung von Referenzen/Zeigern

Wenn ein abgeleitetes Objekt per Wert in den Basistyp kopiert wird, wird der abgeleitete Teil abgeschnitten. Verwenden Sie für Polymorphismus immer eine Basisreferenz oder einen Basiszeiger.


PrintedBook pb("X","Y",100,20,200,false);
Book base = pb;   // SLICING: Nur der Book-Teil wird kopiert
base.PrintInfo(); // Book-Verhalten (kein abgeleitetes)

7) dynamic_cast und Typsicherheit

Verwenden Sie dynamic_cast für sichere Typumwandlungen von Basis zu abgeleitet (benötigt RTTI). Bei Fehlschlag gibt es nullptr zurück (bei Zeigern).


void PrintIfPrinted(const Book* b) {
    if (auto pb = dynamic_cast<const PrintedBook*>(b)) {
        cout << "Printed Preis: " << pb->FinalPrice() << endl;
    } else {
        cout << "Kein PrintedBook." << endl;
    }
}

8) Leistungsnotizen


9) final-Klasse/-Methode, override-Fehler und Best Practices


10) Visualisierung des vtable-Effekts (Schritt für Schritt)

  1. Die Klasse enthält mindestens eine virtuelle Funktion → der Compiler erstellt eine vtable.
  2. Beim Erzeugen eines Objekts zeigt der vptr auf die vtable der Klasse.
  3. Beim Aufruf liest der Compiler die Zieladresse über vptr → vtable statt statisch.
  4. Ein override ersetzt den entsprechenden Eintrag in der vtable der abgeleiteten Klasse.

11) Häufige Fehler


12) Komplettes Beispiel: Polymorphe Sammlung


#include <iostream>
#include <vector>
#include <memory>
using namespace std;

class Book {
public:
    virtual ~Book() = default;
    virtual double FinalPrice() const = 0;
    virtual void Print() const = 0;
};

class EBook : public Book {
    double base, vat;
public:
    EBook(double b, double v) : base(b), vat(v) {}
    double FinalPrice() const override { return base*(1+vat); }
    void Print() const override {
        cout << "[EBook] " << FinalPrice() << " €" << endl;
    }
};

class PrintedBook : public Book {
    double base, ship; int pages;
public:
    PrintedBook(double b, double s, int p) : base(b), ship(s), pages(p) {}
    double FinalPrice() const override { return base + ship + 0.05*pages; }
    void Print() const override {
        cout << "[Printed] " << FinalPrice() << " €" << endl;
    }
};

int main() {
    vector<unique_ptr<Book>> items;
    items.push_back(make_unique<EBook>(100, 0.1));
    items.push_back(make_unique<PrintedBook>(120, 20, 400));

    for (const auto& it : items) it->Print(); // dynamischer Polymorphismus
}

13) TL;DR

  • Dynamischer Polymorphismus in C++ wird durch virtuelle Funktionen ermöglicht.
  • Der Compiler implementiert virtuelle Aufrufe mit einer vtable/vptr.
  • Ein virtueller Destruktor ist für polymorphe Basisklassen erforderlich.
  • override stellt Signaturkorrektheit sicher; final verhindert weitere Überschreibungen.
  • Verwenden Sie Referenzen/Zeiger statt Wertkopien, um Slicing zu vermeiden.
  • Der Overhead ist meist gering; für kritische Pfade Alternativen wie CRTP/Overloads in Betracht ziehen.
  • Alle Beispiele laufen unter Visual Studio 2022 und GCC 11+.

Ähnliche Artikel