Abstrakte Klassen und Schnittstellen in C++
Lernen Sie abstrakte Klassen und Schnittstellen in C++ mit Pure-Virtual-Funktionen und praktischen Beispielen.
In C++ sind abstrakte Klassen Klassen, die mindestens eine rein virtuelle Funktion enthalten und nicht direkt instanziiert werden können. Ein separates Schlüsselwort für „Interface“ gibt es in C++ nicht; stattdessen fungiert eine abstrakte Klasse, die ausschließlich aus rein virtuellen Funktionen besteht, als Schnittstelle (Interface). In diesem Artikel behandeln wir das Design abstrakter Klassen und Schnittstellen, die Implementierung mehrerer Interfaces, das Dependency Inversion Principle (DIP) und ein praktisches Book-Beispiel.
1) Reine virtuelle Funktion und abstrakte Klasse
Eine virtuelle Funktion, die mit = 0 markiert ist, ist rein virtuell.
Eine solche Klasse wird abstrakt und kann nicht direkt instanziiert werden.
struct IPrintable {
virtual ~IPrintable() = default; // Virtuelle Destruktoren sind auch bei Interfaces gute Praxis
virtual void Print() const = 0; // rein virtuell → Interface-Vertrag
};
struct IEntity {
virtual ~IEntity() = default;
virtual std::string Key() const = 0;
};
Diese beiden Klassen fungieren jetzt als „Interfaces“. Implementierende Klassen müssen Print() und Key() definieren.
2) Book-Basis: Abstrakte Klasse
Wir definieren den gemeinsamen Vertrag mit einer abstrakten BookBase-Klasse.
Die abgeleiteten Klassen bestimmen die Preisberechnung.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
struct BookBase : IPrintable, IEntity {
protected:
string title, author;
public:
BookBase(string t, string a) : title(move(t)), author(move(a)) {}
virtual ~BookBase() = default;
// Rein virtuell: Abgeleitete Klassen berechnen den Preis
virtual double FinalPrice() const = 0;
// IPrintable-Vertrag
void Print() const override {
cout << title << " - " << author
<< " | " << FinalPrice() << " TL" << endl;
}
// IEntity-Vertrag
string Key() const override { return title + ":" + author; }
};
BookBase kann nicht direkt mit new instanziiert werden, da FinalPrice() rein virtuell ist.
Dennoch können wir Standardimplementierungen für Print() und Key() bereitstellen.
3) Klassen, die das Interface implementieren (EBook / PrintedBook)
Nun erstellen wir zwei verschiedene Buchtypen und erfüllen den FinalPrice()-Vertrag.
struct EBook : BookBase {
double base, vat;
EBook(string t, string a, double b, double v)
: BookBase(move(t), move(a)), base(b), vat(v) {}
double FinalPrice() const override { return base * (1.0 + vat); }
};
enum class Cover { Soft, Hard };
struct PrintedBook : BookBase {
double base, shipping; int pages; Cover cover;
PrintedBook(string t, string a, double b, double s, int p, Cover c)
: BookBase(move(t), move(a)), base(b), shipping(s), pages(p), cover(c) {}
double FinalPrice() const override {
double cost = base + shipping + 0.04 * pages;
if (cover == Cover::Hard) cost *= 1.1;
return cost;
}
};
Beide Klassen erfüllen das BookBase-Interface und können somit polymorph verwendet werden.
4) Mehrfach-Interface-Implementierung (Interface Segregation)
Kleine, fokussierte Interfaces anstelle eines großen zu entwerfen, reduziert Wartungskosten.
Zum Beispiel kann man eine IDiscountable-Schnittstelle für Rabattlogik erstellen.
struct IDiscountable {
virtual ~IDiscountable() = default;
virtual double ApplyDiscount(double rate) const = 0; // 0.10 → 10 %
};
struct DiscountedEBook : EBook, IDiscountable {
using EBook::EBook; // Konstruktoren erben
double ApplyDiscount(double rate) const override {
return FinalPrice() * (1.0 - rate);
}
};
Mit diesem Design unterstützt DiscountedEBook sowohl den BookBase-Vertrag als auch das Rabatt-Interface.
5) Dependency Inversion (DIP) und Testbarkeit
Klassen sollten von Abstraktionen (Interfaces) abhängen, nicht von konkreten Typen.
Dadurch werden Module lose gekoppelt und Unit-Tests einfacher.
Unten sieht eine „Pricing Engine“ nur das BookBase-Interface.
struct PricingEngine {
double Total(const vector<shared_ptr<BookBase>>& items) const {
double sum = 0.0;
for (const auto& b : items) sum += b->FinalPrice();
return sum;
}
};
int main() {
vector<shared_ptr<BookBase>> items;
items.push_back(make_shared<EBook>("Modern C++", "A. Dev", 100, 0.1));
items.push_back(make_shared<PrintedBook>("Clean Code", "R. Martin", 120, 20, 464, Cover::Hard));
PricingEngine eng;
cout << "Gesamt: " << eng.Total(items) << " TL" << endl;
for (const auto& b : items) b->Print(); // IPrintable
}
PricingEngine hängt nicht von konkreten Typen ab, sondern arbeitet nur mit dem BookBase-Interface.
Dadurch können Sie für Tests eine MockBook-Klasse bereitstellen.
6) Interfaces mit Mehrfachvererbung
C++ unterstützt Mehrfachvererbung; für Interfaces ist das meist sicher, da sie keinen Zustand (state) enthalten. Bei zustandsbehafteten Basisklassen kann dies jedoch das Diamond Problem verursachen; verwenden Sie in solchen Fällen virtual inheritance oder besser Komposition.
struct IStorable { virtual ~IStorable()=default; virtual void Save() const =0; };
struct ISearchable{ virtual ~ISearchable()=default; virtual bool Matches(string) const =0; };
struct SearchableBook : BookBase, IStorable, ISearchable {
using BookBase::BookBase;
double FinalPrice() const override { return 0.0; } // Beispiel
void Save() const override { /* In Datei speichern... */ }
bool Matches(string q) const override {
return title.find(q)!=string::npos || author.find(q)!=string::npos;
}
};
7) Fabrikmuster mit Interface (Factory-Beispiel)
Um die Erstellungslogik von konkreten Klassen zu trennen, kann man ein Fabrik-Interface verwenden.
struct IBookFactory {
virtual ~IBookFactory() = default;
virtual shared_ptr<BookBase> MakeEBook(string t,string a,double b,double v)=0;
virtual shared_ptr<BookBase> MakePrinted(string t,string a,double b,double s,int p,Cover c)=0;
};
struct DefaultBookFactory : IBookFactory {
shared_ptr<BookBase> MakeEBook(string t,string a,double b,double v) override {
return make_shared<EBook>(move(t), move(a), b, v);
}
shared_ptr<BookBase> MakePrinted(string t,string a,double b,double s,int p,Cover c) override {
return make_shared<PrintedBook>(move(t), move(a), b, s, p, c);
}
};
Höherwertige Module hängen nur von IBookFactory ab; die konkrete Implementierung kann leicht ausgetauscht oder für Tests gemockt werden.
8) Design-Tipps
- Definieren Sie in Interfaces immer einen virtuellen Destruktor.
- Halten Sie Interfaces klein und fokussiert (Interface Segregation Principle).
- Hängen Sie von Abstraktionen ab, nicht von konkreten Klassen (DIP).
- Verwenden Sie für polymorphe Nutzung Pointer/Referenzen; Kopieren per Wert führt zu Slicing.
- Verwenden Sie Mehrfachvererbung nur für Verhalten (Interfaces); für geteilten Zustand ist Komposition sicherer.
9) Häufige Fehler
- Vergessen eines virtuellen Destruktors in einer abstrakten Klasse.
- Zu große Interfaces (zwingt Implementierungen zu leeren Methoden).
- Direkte Abhängigkeit von konkreten Klassen (verringert Testbarkeit und Flexibilität).
- Kombination zustandsbehafteter Basisklassen bei Mehrfachvererbung → Diamond Problem.
10) TL;DR
- In C++ bedeutet „Interface“ = abstrakte Klasse mit ausschließlich rein virtuellen Funktionen.
- Abstrakte Klassen können nicht instanziiert werden; abgeleitete Klassen implementieren (override) den Vertrag.
- Bevorzugen Sie kleine, fokussierte Interfaces (ISP) und Abhängigkeit von Abstraktionen (DIP).
- Verwenden Sie Pointer/Referenzen für Polymorphismus; vergessen Sie den virtuellen Destruktor nicht.
- Mehrfachvererbung nur für Interfaces; für Zustände ist Komposition sicherer.
- Alle Beispiele laufen unter Visual Studio 2022 und GCC 11+.