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.
- vtable: Tabelle mit den Adressen der virtuellen Funktionen der Klasse.
- vptr: Verstecktes Feld im Objekt, das die Adresse der vtable speichert.
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.
- Wenn Laufzeitverhalten erforderlich ist → virtual.
- Wenn Kompilierzeit-Strategien/Optimierungen geeignet sind → template/CRTP oder overload.
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
- Jeder virtuelle Aufruf ist ein indirekter Aufruf über die vtable (mögliche Verzweigungsvorhersagefehler).
- Inlining virtueller Funktionen ist unwahrscheinlicher (Ziel wird zur Laufzeit bestimmt).
- Bei heißen Schleifen mit vielen virtuellen Aufrufen: Erwägen Sie das Strategie-Pattern + Templates (CRTP) oder Funktionsobjekte für statische Bindung.
9) final-Klasse/-Methode, override-Fehler und Best Practices
- Verwenden Sie in Basisklassen einen virtuellen Destruktor.
- Markieren Sie jede überschriebene virtuelle Funktion mit override, um Signaturfehler frühzeitig zu erkennen.
- Verwenden Sie final für Methoden/Klassen, die nicht weiter überschrieben werden sollen.
- Verwalten Sie polymorphe Hierarchien mit Heap + Smart Pointern (
unique_ptr,shared_ptr). - Fügen Sie keine virtuellen Funktionen hinzu, wenn kein Polymorphismus erforderlich ist (unnötige Abstraktion).
10) Visualisierung des vtable-Effekts (Schritt für Schritt)
- Die Klasse enthält mindestens eine virtuelle Funktion → der Compiler erstellt eine vtable.
- Beim Erzeugen eines Objekts zeigt der vptr auf die vtable der Klasse.
- Beim Aufruf liest der Compiler die Zieladresse über vptr → vtable statt statisch.
- Ein override ersetzt den entsprechenden Eintrag in der vtable der abgeleiteten Klasse.
11) Häufige Fehler
- Vergessener virtueller Destruktor → undefiniertes Verhalten beim Löschen über Basiszeiger.
- Kopieren per Wert verursacht Object Slicing.
overridevergessen → bei Signaturfehler wird stillschweigend eine neue Funktion erstellt.- Unnötige Virtualität → unnötige Indirektion und Overhead bei einfachen Strukturen.
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+.