Virtuelle Vererbung
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy — Lektion 54 von 104.
Das Diamond-Problem tritt auf, wenn eine Klasse von zwei Klassen erbt, die eine gemeinsame Basisklasse teilen. Ohne spezielle Behandlung erhält die abgeleitete Klasse zwei Kopien der gemeinsamen Basis, was zu Mehrdeutigkeit und verschwendetem Speicher führt.
class Animal {
public:
int age;
};
class Mammal : public Animal {};
class Bird : public Animal {};
class Bat : public Mammal, public Bird {};
Bat b;
b.age = 5; // Fehler: mehrdeutig - welches 'age'?Die Klasse Bat enthält zwei separate Animal-Unterobjekte: eines über Mammal und eines über Bird. Dies erzeugt das rautenförmige Vererbungsdiagramm, das dem Problem seinen Namen gibt.
Virtuelle Vererbung löst dies, indem sichergestellt wird, dass nur eine Kopie der gemeinsamen Basis existiert. Fügen Sie das virtual-Schlüsselwort hinzu, wenn Sie von der gemeinsamen Basis erben:
class Animal {
public:
int age;
Animal(int a = 0) : age(a) {}
};
class Mammal : virtual public Animal {
public:
Mammal(int a = 0) : Animal(a) {}
};
class Bird : virtual public Animal {
public:
Bird(int a = 0) : Animal(a) {}
};
class Bat : public Mammal, public Bird {
public:
Bat(int a) : Animal(a), Mammal(a), Bird(a) {}
};
Bat b(5);
b.age = 10; // Funktioniert! Es existiert nur ein 'age'Beachten Sie, dass Bat Animal direkt in seinem Konstruktor initialisieren muss. Bei virtueller Vererbung ist die am weitesten abgeleitete Klasse für die Konstruktion der virtuellen Basis verantwortlich, unabhängig von den Zwischenklassen.
Aufgabe
EinfachLassen Sie uns ein Mitarbeiterverwaltungssystem erstellen, das demonstriert, wie virtuelle Vererbung das Diamond-Problem löst. Sie werden eine Hierarchie erstellen, in der ein TeamLead sowohl von Developer als auch von Manager erbt, die beide eine gemeinsame Employee-Basisklasse teilen.
Sie werden Ihren Code über vier Dateien organisieren:
Employee.h: Definieren Sie die gemeinsame BasisklasseEmployeemit:- Einem geschützten
std::string nameundint id - Einem Konstruktor, der beide Werte entgegennimmt und ausgibt:
Employee [<name>] hired with ID <id> - Einer öffentlichen
getInfo()-Methode, die ausgibt:Employee: <name> (ID: <id>) - Einem virtuellen Destruktor, der ausgibt:
Employee [<name>] record closed
- Einem geschützten
Developer.h: Definieren Sie eineDeveloper-Klasse, die virtual public inheritance vonEmployeeverwendet:- Einem geschützten
std::string language-Member - Einem Konstruktor, der Name, ID und Sprache entgegennimmt — Name und ID an
Employeeübergibt, die Sprache speichert und ausgibt:Developer [<name>] specializes in <language> - Einer öffentlichen
code()-Methode, die ausgibt:<name> is coding in <language> - Einem Destruktor, der ausgibt:
Developer [<name>] signed off
- Einem geschützten
Manager.h: Definieren Sie eineManager-Klasse, die virtual public inheritance vonEmployeeverwendet:- Einem geschützten
int teamSize-Member - Einem Konstruktor, der Name, ID und Teamgröße entgegennimmt — Name und ID an
Employeeübergibt, die Teamgröße speichert und ausgibt:Manager [<name>] leads a team of <teamSize> - Einer öffentlichen
manage()-Methode, die ausgibt:<name> is managing <teamSize> people - Einem Destruktor, der ausgibt:
Manager [<name>] stepped down
- Einem geschützten
main.cpp: Lesen Sie vier Eingaben ein (jede in einer separaten Zeile):- Name (String)
- Mitarbeiter-ID (Ganzzahl)
- Programmiersprache (String)
- Teamgröße (Ganzzahl)
Definieren Sie eine
TeamLead-Klasse, die öffentlich von sowohlDeveloperals auchManagererbt:- Einem Konstruktor, der alle vier Parameter entgegennimmt und
Employee(die virtuelle Basis) sowie anschließendDeveloperundManagerdirekt initialisieren muss - Der Konstruktor sollte ausgeben:
TeamLead [<name>] ready to lead and code! - Einer
showRole()-Methode, diegetInfo(),code()undmanage()in dieser Reihenfolge aufruft - Einem Destruktor, der ausgibt:
TeamLead [<name>] promoted out
Erstellen Sie ein
TeamLead-Objekt innerhalb eines Block-Scopes, rufen SieshowRole()auf und lassen Sie es dann den Scope verlassen. Geben Sie nach dem Block aus:Organization restructured!
Zum Beispiel mit den Eingaben Alice, 101, C++ und 5:
Employee [Alice] hired with ID 101
Developer [Alice] specializes in C++
Manager [Alice] leads a team of 5
TeamLead [Alice] ready to lead and code!
Employee: Alice (ID: 101)
Alice is coding in C++
Alice is managing 5 people
TeamLead [Alice] promoted out
Manager [Alice] stepped down
Developer [Alice] signed off
Employee [Alice] record closed
Organization restructured!Beachten Sie, dass es nur einen Aufruf des Employee-Konstruktors und einen Aufruf des Employee-Destruktors gibt — die virtuelle Vererbung stellt sicher, dass nur eine Kopie der gemeinsamen Basis existiert. Der TeamLead muss Employee direkt initialisieren, da bei virtueller Vererbung die am weitesten abgeleitete Klasse für die Konstruktion der virtuellen Basis verantwortlich ist.
Spickzettel
Das Diamond-Problem tritt auf, wenn eine Klasse von zwei Klassen erbt, die eine gemeinsame Basisklasse teilen, was zu zwei Kopien der Basisklasse führt und Mehrdeutigkeit verursacht.
class Animal {
public:
int age;
};
class Mammal : public Animal {};
class Bird : public Animal {};
class Bat : public Mammal, public Bird {};
Bat b;
b.age = 5; // Error: ambiguous - which 'age'?Virtuelle Vererbung löst das Diamond-Problem, indem sichergestellt wird, dass nur eine Kopie der gemeinsamen Basisklasse existiert. Verwenden Sie das Schlüsselwort virtual bei der Vererbung:
class Animal {
public:
int age;
Animal(int a = 0) : age(a) {}
};
class Mammal : virtual public Animal {
public:
Mammal(int a = 0) : Animal(a) {}
};
class Bird : virtual public Animal {
public:
Bird(int a = 0) : Animal(a) {}
};
class Bat : public Mammal, public Bird {
public:
Bat(int a) : Animal(a), Mammal(a), Bird(a) {}
};
Bat b(5);
b.age = 10; // Works! Only one 'age' existsBei virtueller Vererbung muss die am weitesten abgeleitete Klasse die virtuelle Basisklasse direkt in ihrem Konstruktor initialisieren, unabhängig von dazwischenliegenden Klassen.
Probier es selbst
#include <iostream>
#include <string>
#include "Developer.h"
#include "Manager.h"
using namespace std;
// TODO: Definiere die Klasse TeamLead, die öffentlich von Developer und Manager erbt
// Hinweis: Bei virtueller Vererbung muss TeamLead die Klasse Employee (die virtuelle Basisklasse) direkt initialisieren
class TeamLead : public Developer, public Manager {
public:
// TODO: Implementiere den Konstruktor, der name, id, language und teamSize entgegennimmt
// Muss initialisieren: Zuerst Employee (virtuelle Basis), dann Developer, dann Manager
// Sollte ausgeben: TeamLead [<name>] ready to lead and code!
TeamLead(const std::string& name, int id, const std::string& language, int teamSize)
: Employee(name, id),
Developer(name, id, language),
Manager(name, id, teamSize) {
// TODO: Konstruktor-Nachricht ausgeben
}
// TODO: Implementiere die Methode showRole()
// Sollte getInfo(), code() und manage() in dieser Reihenfolge aufrufen
void showRole() {
// TODO: Rufe die drei Methoden auf
}
// TODO: Implementiere den Destruktor
// Sollte ausgeben: TeamLead [<name>] promoted out
~TeamLead() {
// TODO: Destruktor-Nachricht ausgeben
}
};
int main() {
// Eingaben lesen
string name;
int id;
string language;
int teamSize;
getline(cin, name);
cin >> id;
cin.ignore();
getline(cin, language);
cin >> teamSize;
// TODO: Erstelle ein TeamLead-Objekt innerhalb eines Block-Scopes
// Rufe showRole() auf und lasse es dann den Gültigkeitsbereich verlassen
{
// TODO: Erstelle TeamLead und rufe showRole() auf
}
// Abschlussnachricht nach dem Block ausgeben
cout << "Organization restructured!" << endl;
return 0;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische Container