Grundlagen der Vererbung in C++
Lernen Sie die Grundlagen der Vererbung in C++, einschließlich Basis- und abgeleiteter Klassen sowie Zugriffsmodifizierer.
Die Vererbung (inheritance) ermöglicht es einer Klasse, die Eigenschaften und das Verhalten einer anderen Klasse zu übernehmen. Dadurch wird redundanter Code reduziert, Klassen können hierarchisch organisiert werden und Polymorphismus wird möglich. In diesem Artikel betrachten wir eine Book-Basisklasse sowie die abgeleiteten Klassen EBook und PrintedBook, zusammen mit Themen wie Konstruktorverkettung, virtuelle Funktionen, override, virtuelle Destruktoren, Upcasting/Downcasting und abstrakte Basisklassen.
1. Basisklasse: Book
Gemeinsame Eigenschaften und Verhaltensweisen werden in der Klasse Book zusammengefasst. Abgeleitete Klassen erben diese und können sie bei Bedarf anpassen.
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
class Book {
protected:
string title;
string author;
double basePrice;
int pageCount;
public:
Book(string t, string a, double price, int pages)
: title(t), author(a), basePrice(price), pageCount(pages) {}
virtual ~Book() = default;
virtual double GetFinalPrice() const {
return basePrice;
}
virtual void PrintInfo() const {
cout << "[Book] " << title
<< " | Autor: " << author
<< " | Seiten: " << pageCount
<< " | Preis: " << GetFinalPrice() << " €" << endl;
}
};
protected-Member sind außerhalb der Klasse nicht sichtbar, aber für Unterklassen zugänglich.
Ein virtueller Destruktor ist erforderlich, um Objekte über Basisklassenzeiger korrekt zu löschen.
2. Abgeleitete Klasse: EBook
E-Books haben keine Druckkosten, aber es können Mehrwertsteuer, Rabatte oder Lizenzgebühren anfallen. Wir passen die Preisberechnung entsprechend an.
class EBook : public Book {
double vatRate;
bool drmProtected;
public:
EBook(string t, string a, double price, int pages,
double vat, bool drm)
: Book(t, a, price, pages), vatRate(vat), drmProtected(drm) {}
double GetFinalPrice() const override {
double priceWithVat = basePrice * (1.0 + vatRate);
if (pageCount >= 500) priceWithVat *= 0.95;
return priceWithVat;
}
void PrintInfo() const override {
cout << "[EBook] " << title
<< " | DRM: " << (drmProtected ? "Ja" : "Nein")
<< " | Endpreis: " << GetFinalPrice() << " €" << endl;
}
};
Das Schlüsselwort override sorgt dafür, dass abweichende Funktionssignaturen schon zur Kompilierzeit erkannt werden.
3. Abgeleitete Klasse: PrintedBook
Gedruckte Bücher haben zusätzliche Papier- und Versandkosten. Je nach Einbandart (Softcover/Hardcover) werden unterschiedliche Aufschläge berechnet.
enum class Cover { Soft, Hard };
class PrintedBook : public Book {
Cover coverType;
double shipping;
double printCost;
public:
PrintedBook(string t, string a, double price, int pages,
Cover cover, double shipCost, double costPerPage)
: Book(t, a, price, pages),
coverType(cover), shipping(shipCost), printCost(costPerPage) {}
double GetFinalPrice() const override {
double total = basePrice + shipping + (printCost * pageCount);
if (coverType == Cover::Hard) total *= 1.10;
return total;
}
void PrintInfo() const override {
cout << "[PrintedBook] " << title
<< " | Einband: " << (coverType == Cover::Hard ? "Hardcover" : "Softcover")
<< " | Endpreis: " << GetFinalPrice() << " €" << endl;
}
};
Die Konstruktorverkettung ruft zuerst den Konstruktor der Basisklasse auf und initialisiert danach die Member der Unterklasse.
4. Polymorphismus: Upcasting und virtuelle Aufrufe
Abgeleitete Objekte können über Zeiger oder Referenzen des Basistyps verwaltet werden. Virtuelle Methoden sorgen dafür, dass der richtige Funktionsaufruf zur Laufzeit erfolgt.
int main() {
vector<unique_ptr<Book>> library;
library.push_back(make_unique<EBook>(
"C++ 20 Guide", "J. Doe", 100.0, 520, 0.10, true));
library.push_back(make_unique<PrintedBook>(
"Clean Code", "R. Martin", 150.0, 464, Cover::Hard, 20.0, 0.05));
for (const auto &bk : library) {
bk->PrintInfo();
}
}
Mit unique_ptr<Book> wird die Speicherverwaltung automatisiert, ein delete ist nicht nötig.
5. Downcasting mit dynamic_cast
Manchmal ist es notwendig, vom Basistyp wieder auf den abgeleiteten Typ zuzugreifen.
Dafür kann dynamic_cast sicher verwendet werden.
void TryPrintDRM(const Book* b) {
if (auto eb = dynamic_cast<const EBook*>(b)) {
cout << "EBook-DRM wurde überprüft." << endl;
} else {
cout << "Kein EBook." << endl;
}
}
Wenn dynamic_cast fehlschlägt, wird nullptr zurückgegeben.
Dadurch bleibt die Typensicherheit gewährleistet. RTTI muss aktiviert sein.
6. Abstrakte Basisklasse
Eine Klasse mit mindestens einer reinen virtuellen Funktion ist abstrakt und kann nicht direkt instanziiert werden. Durch eine abstrakte Basisklasse wird ein verbindliches Interface definiert.
class AbstractBook {
protected:
string title, author;
int pageCount;
public:
AbstractBook(string t, string a, int p)
: title(t), author(a), pageCount(p) {}
virtual ~AbstractBook() = default;
virtual double GetFinalPrice() const = 0;
virtual void PrintInfo() const = 0;
};
class SimpleEBook : public AbstractBook {
double basePrice, vat;
public:
SimpleEBook(string t, string a, int p, double price, double v)
: AbstractBook(t,a,p), basePrice(price), vat(v) {}
double GetFinalPrice() const override { return basePrice*(1+vat); }
void PrintInfo() const override {
cout << "[SimpleEBook] " << title
<< " | Preis: " << GetFinalPrice() << endl;
}
};
Abstrakte Klassen ermöglichen ein vertragsorientiertes Design (ähnlich einem Interface).
7. Zugriffsspezifizierer und Vererbungstypen
Es gibt drei Vererbungstypen: public, protected und private.
- public: öffentliche Member der Basisklasse bleiben öffentlich.
- protected: öffentliche Member der Basisklasse werden geschützt.
- private: alle geerbten Member werden privat.
In der Praxis wird meist public inheritance verwendet (eine “ist-ein”-Beziehung).
8. final und override Schlüsselwörter
override signalisiert dem Compiler, dass eine virtuelle Funktion überschrieben wird.
final verhindert, dass eine Funktion oder Klasse weiter überschrieben wird.
class FinalEBook : public EBook {
public:
using EBook::EBook;
double GetFinalPrice() const override final {
return EBook::GetFinalPrice();
}
};
9. Vollständiges Beispiel
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
enum class Cover { Soft, Hard };
class Book {
protected:
string title, author;
double basePrice;
int pageCount;
public:
Book(string t, string a, double price, int pages)
: title(t), author(a), basePrice(price), pageCount(pages) {}
virtual ~Book() = default;
virtual double GetFinalPrice() const { return basePrice; }
virtual void PrintInfo() const {
cout << "[Book] " << title
<< " | " << author
<< " | " << pageCount << " Seiten"
<< " | " << GetFinalPrice() << " €" << endl;
}
};
class EBook : public Book {
double vatRate; bool drmProtected;
public:
EBook(string t, string a, double price, int pages, double vat, bool drm)
: Book(t,a,price,pages), vatRate(vat), drmProtected(drm) {}
double GetFinalPrice() const override {
double x = basePrice*(1+vatRate);
if (pageCount >= 500) x *= 0.95;
return x;
}
void PrintInfo() const override {
cout << "[EBook] " << title
<< " | DRM: " << (drmProtected? "Ja":"Nein")
<< " | Endpreis: " << GetFinalPrice() << " €" << endl;
}
};
class PrintedBook : public Book {
Cover coverType; double shipping; double printCost;
public:
PrintedBook(string t, string a, double price, int pages,
Cover cover, double ship, double perPage)
: Book(t,a,price,pages), coverType(cover),
shipping(ship), printCost(perPage) {}
double GetFinalPrice() const override {
double c = basePrice + shipping + printCost*pageCount;
if (coverType == Cover::Hard) c *= 1.10;
return c;
}
void PrintInfo() const override {
cout << "[PrintedBook] " << title
<< " | Einband: " << (coverType==Cover::Hard?"Hardcover":"Softcover")
<< " | Endpreis: " << GetFinalPrice() << " €" << endl;
}
};
int main() {
vector<unique_ptr<Book>> books;
books.push_back(make_unique<EBook>("Modern C++", "A. Dev", 120.0, 520, 0.10, true));
books.push_back(make_unique<PrintedBook>("Clean Code", "R. Martin", 150.0, 464, Cover::Hard, 25.0, 0.04));
for (const auto& b : books) b->PrintInfo(); // polymorpher Aufruf
// Sicherer Downcast-Versuch
if (auto pb = dynamic_cast<PrintedBook*>(books[1].get())) {
cout << "PrintedBook Preis: " << pb->GetFinalPrice() << endl;
}
}
Der Code lässt sich in Visual Studio 2022 und GCC 11+ kompilieren und ausführen.
10. TL;DR
- Mit Vererbung bündeln wir gemeinsames Verhalten in der Basisklasse und spezifizieren es in abgeleiteten Klassen.
virtual-Methoden und das Schlüsselwortoverrideermöglichen dynamischen Polymorphismus.- Ohne virtuellen Destruktor ist das Löschen über einen Basiszeiger unsicher.
- Konstruktorverkettung: zuerst wird der Basiskonstruktor ausgeführt, dann der abgeleitete Konstruktor.
dynamic_castermöglicht sicheres Downcasting (gibtnullptrbei Fehlschlag zurück).- Abstrakte Basisklassen (pure virtual) können nicht instanziiert werden und erzwingen ein Vertragsverhalten.
- Beispiele sind kompatibel mit Visual Studio 2022 und GCC 11+.