Dziedziczenie wirtualne
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 54 z 104.
Problem diamentu występuje, gdy klasa dziedziczy po dwóch klasach, które mają wspólną klasę bazową. Bez specjalnej obsługi klasa pochodna ma dwie kopie wspólnej klasy bazowej, co powoduje niejednoznaczność i marnowanie pamięci.
class Animal {
public:
int age;
};
class Mammal : public Animal {};
class Bird : public Animal {};
class Bat : public Mammal, public Bird {};
Bat b;
b.age = 5; // Błąd: niejednoznaczność – które 'age'?Klasa Bat zawiera dwa oddzielne podobiekty Animal: jeden za pośrednictwem Mammal, a drugi za pośrednictwem Bird. Tworzy to diagram dziedziczenia w kształcie rombu, od którego problem wziął swoją nazwę.
Dziedziczenie wirtualne rozwiązuje ten problem, zapewniając, że istnieje tylko jedna kopia wspólnej klasy bazowej. Dodaj słowo kluczowe virtual podczas dziedziczenia po wspólnej klasie bazowej:
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; // Działa! Istnieje tylko jedno 'age'Zauważ, że Bat musi bezpośrednio zainicjalizować Animal w swoim konstruktorze. W przypadku dziedziczenia wirtualnego za konstruowanie wirtualnej klasy bazowej odpowiada klasa najbardziej pochodna, niezależnie od klas pośrednich.
Wyzwanie
ŁatwyZbudujmy system zarządzania pracownikami, który pokazuje, jak dziedziczenie wirtualne rozwiązuje problem diamentu. Utworzysz hierarchię, w której TeamLead dziedziczy zarówno po Developer, jak i po Manager, które mają wspólną klasę bazową Employee.
Rozmieścisz kod w czterech plikach:
Employee.h: Zdefiniuj wspólną klasę bazowąEmployee, która zawiera:- Chronione składowe
std::string nameorazint id - Konstruktor przyjmujący obie wartości i wypisujący:
Employee [<name>] hired with ID <id> - Publiczną metodę
getInfo(), która wypisuje:Employee: <name> (ID: <id>) - Wirtualny destruktor, który wypisuje:
Employee [<name>] record closed
- Chronione składowe
Developer.h: Zdefiniuj klasęDeveloper, która korzysta z wirtualnego dziedziczenia publicznego poEmployee:- Chronione składowe
std::string language - Konstruktor przyjmujący imię i nazwisko, identyfikator oraz język: przekazuje imię i nazwisko oraz identyfikator do
Employee, zapisuje język i wypisuje:Developer [<name>] specializes in <language> - Publiczną metodę
code(), która wypisuje:<name> is coding in <language> - Destruktor, który wypisuje:
Developer [<name>] signed off
- Chronione składowe
Manager.h: Zdefiniuj klasęManager, która korzysta z wirtualnego dziedziczenia publicznego poEmployee:- Chronione składowe
int teamSize - Konstruktor przyjmujący imię i nazwisko, identyfikator oraz rozmiar zespołu: przekazuje imię i nazwisko oraz identyfikator do
Employee, zapisuje rozmiar zespołu i wypisuje:Manager [<name>] leads a team of <teamSize> - Publiczną metodę
manage(), która wypisuje:<name> is managing <teamSize> people - Destruktor, który wypisuje:
Manager [<name>] stepped down
- Chronione składowe
main.cpp: Odczytaj cztery dane wejściowe (każdą w osobnym wierszu):- Imię i nazwisko (ciąg znaków)
- Identyfikator pracownika (liczba całkowita)
- Język programowania (ciąg znaków)
- Rozmiar zespołu (liczba całkowita)
Zdefiniuj klasę
TeamLead, która publicznie dziedziczy zarówno poDeveloper, jak i poManager:- Konstruktor przyjmujący wszystkie cztery parametry, który musi bezpośrednio zainicjalizować
Employee(bazę wirtualną), a następnieDeveloperiManager - Konstruktor powinien wypisać:
TeamLead [<name>] ready to lead and code! - Metodę
showRole(), która wywołuje kolejnogetInfo(),code()imanage() - Destruktor, który wypisuje:
TeamLead [<name>] promoted out
Utwórz obiekt
TeamLeadw zakresie bloku, wywołajshowRole(), a następnie pozwól, by obiekt wyszedł poza zakres. Po bloku wypisz:Organization restructured!
Na przykład dla danych wejściowych Alice, 101, C++ i 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!Zwróć uwagę, że wywołanie konstruktora Employee i wywołanie destruktora Employee występują tylko raz: dziedziczenie wirtualne gwarantuje, że istnieje tylko jedna kopia wspólnej klasy bazowej. Klasa TeamLead musi bezpośrednio inicjalizować Employee, ponieważ przy dziedziczeniu wirtualnym za konstruowanie bazy wirtualnej odpowiada klasa najbardziej pochodna.
Spróbuj swoich sił
#include <iostream>
#include <string>
#include "Developer.h"
#include "Manager.h"
using namespace std;
// TODO: Zdefiniuj klasę TeamLead, która publicznie dziedziczy po Developer i Manager
// Pamiętaj: Przy dziedziczeniu wirtualnym TeamLead musi bezpośrednio inicjalizować Employee (wirtualną klasę bazową)
class TeamLead : public Developer, public Manager {
public:
// TODO: Zaimplementuj konstruktor przyjmujący name, id, language i teamSize
// Należy zainicjalizować: najpierw Employee (wirtualną klasę bazową), potem Developer, a następnie Manager
// Powinno wypisać: 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: Wypisz komunikat konstruktora
}
// TODO: Zaimplementuj metodę showRole()
// Metoda powinna wywołać getInfo(), code() i manage() w tej kolejności
void showRole() {
// TODO: Wywołaj te trzy metody
}
// TODO: Zaimplementuj destruktor
// Powinno wypisać: TeamLead [<name>] promoted out
~TeamLead() {
// TODO: Wypisz komunikat destruktora
}
};
int main() {
// Wczytaj dane wejściowe
string name;
int id;
string language;
int teamSize;
getline(cin, name);
cin >> id;
cin.ignore();
getline(cin, language);
cin >> teamSize;
// TODO: Utwórz obiekt TeamLead w zakresie bloku
// Wywołaj showRole(), a następnie pozwól obiektowi wyjść poza zakres
{
// TODO: Utwórz obiekt TeamLead i wywołaj showRole()
}
// Wypisz końcowy komunikat po zakończeniu bloku
cout << "Organization restructured!" << endl;
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generycznyPoćwicz samodzielnie: Kompilator C++ online