Teraz, gdy rozmawialiśmy o tym, czym dziedziczenie jest w skrócie w sensie, porozmawiajmy o tym, jak jest ono używane w C++.
Dziedziczenie w C++ odbywa się pomiędzy klasami. W relacji dziedziczenia (is-a) klasa, z której dziedziczona jest, nazywana jest klasą nadrzędną, klasą bazową lub nadklasą, a klasa dokonująca dziedziczenia nazywana jest klasą podrzędną, pochodną klasa lub podklasa.

Na powyższym diagramie Fruit jest rodzicem, a Apple i Banana są dziećmi.

Na tym diagramie Triangle jest zarówno dzieckiem (do Shape), jak i rodzicem (do Right Triangle).
Klasa potomna dziedziczy zarówno zachowania (funkcje składowe), jak i właściwości (zmienne składowe) od rodzica (z zastrzeżeniem pewnych ograniczeń dostępu, które omówimy w przyszłej lekcji).
Te zmienne i funkcje stają się członkami klasy pochodnej.
Ponieważ klasy podrzędne są klasami pełnoprawnymi, mogą (oczywiście) mieć własne elementy specyficzne dla tej klasy. Za chwilę zobaczymy przykład.
Klasa Person
Oto prosta klasa reprezentująca ogólną osobę:
#include <string>
#include <string_view>
class Person
{
// In this example, we're making our members public for simplicity
public:
std::string m_name{};
int m_age{};
Person(std::string_view name = "", int age = 0)
: m_name{ name }, m_age{ age }
{
}
const std::string& getName() const { return m_name; }
int getAge() const { return m_age; }
};Ponieważ ta klasa Person została zaprojektowana do reprezentowania ogólnej osoby, zdefiniowaliśmy tylko elementy, które byłyby wspólne dla każdego typu osoby. Każda osoba (niezależnie od płci, zawodu itp.) ma imię i wiek, więc są one tutaj przedstawione.
Zauważ, że w tym przykładzie wszystkie nasze zmienne i funkcje zostały upublicznione. Ma to na celu wyłącznie uproszczenie tych przykładów. Zwykle ustawialibyśmy zmienne jako prywatne. O kontroli dostępu i jej interakcji z dziedziczeniem porozmawiamy w dalszej części tego rozdziału.
Klasa BaseballPlayer
Powiedzmy, że chcemy napisać program, który śledzi informacje o niektórych graczach w baseball. Gracze w baseball muszą zawierać informacje specyficzne dla graczy w baseball — na przykład możemy chcieć przechowywać średnią odbijań gracza i liczbę home runów, które udało mu się trafić.
Oto nasza niekompletna klasa baseballisty:
class BaseballPlayer
{
// In this example, we're making our members public for simplicity
public:
double m_battingAverage{};
int m_homeRuns{};
BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
: m_battingAverage{battingAverage}, m_homeRuns{homeRuns}
{
}
};Teraz chcemy także śledzić imię i wiek gracza baseballowego i mamy już te informacje w ramach naszej klasy Osoba.
Mamy trzy możliwości wyboru, w jaki sposób aby dodać imię i wiek do BaseballPlayer:
- Dodaj imię i wiek do klasy BaseballPlayer bezpośrednio jako członkowie. To prawdopodobnie najgorszy wybór, ponieważ duplikujemy kod, który już istnieje w naszej klasie Person. Wszelkie aktualizacje osoby będą musiały zostać dokonane także w BaseballPlayer.
- Dodaj osobę jako członka BaseballPlayer za pomocą kompozycji. Musimy jednak zadać sobie pytanie: „czy baseballista ma osobę”? Nie, tak nie jest. Nie jest to więc właściwy paradygmat.
- Pozwól BaseballPlayerowi odziedziczyć te atrybuty od Osoby. Pamiętaj, że dziedziczenie reprezentuje relację jest-jest. Czy baseballista jest osobą? Tak, to prawda. Zatem dziedziczenie jest tutaj dobrym wyborem.
Uczynienie BaseballPlayer klasą pochodną
Aby BaseballPlayer dziedziczył z naszej klasy Person, składnia jest dość prosta. Po class BaseballPlayer deklaracji stosujemy dwukropek, słowo „public” i nazwę klasy, którą chcemy odziedziczyć. Nazywa się to dziedziczeniem publicznym. O tym, co oznacza dziedziczenie publiczne, porozmawiamy więcej na przyszłej lekcji.
// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
public:
double m_battingAverage{};
int m_homeRuns{};
BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
: m_battingAverage{battingAverage}, m_homeRuns{homeRuns}
{
}
};Korzystając ze diagramu wyprowadzania, nasze dziedziczenie wygląda następująco:

Gdy BaseballPlayer dziedziczy po osobie, BaseballPlayer nabywa funkcje składowe i zmienne od osoby. Dodatkowo BaseballPlayer definiuje dwóch własnych członków: m_battingAverage i m_homeRuns. Ma to sens, ponieważ te właściwości są specyficzne dla BaseballPlayera, a nie jakiejkolwiek osoby.
Zatem obiekty BaseballPlayer będą miały 4 zmienne składowe: m_battingAverage i m_homeRuns z BaseballPlayer oraz m_name i m_age z Person.
Łatwo to udowodnić:
#include <iostream>
#include <string>
#include <string_view>
class Person
{
public:
std::string m_name{};
int m_age{};
Person(std::string_view name = "", int age = 0)
: m_name{name}, m_age{age}
{
}
const std::string& getName() const { return m_name; }
int getAge() const { return m_age; }
};
// BaseballPlayer publicly inheriting Person
class BaseballPlayer : public Person
{
public:
double m_battingAverage{};
int m_homeRuns{};
BaseballPlayer(double battingAverage = 0.0, int homeRuns = 0)
: m_battingAverage{battingAverage}, m_homeRuns{homeRuns}
{
}
};
int main()
{
// Create a new BaseballPlayer object
BaseballPlayer joe{};
// Assign it a name (we can do this directly because m_name is public)
joe.m_name = "Joe";
// Print out the name
std::cout << joe.getName() << '\n'; // use the getName() function we've acquired from the Person base class
return 0;
}Co wypisuje wartość:
Joe
To się kompiluje i działa, ponieważ joe jest graczem BaseballPlayer, a wszystkie obiekty BaseballPlayer mają zmienną składową m_name i funkcję członkowską getName() odziedziczoną z klasy Person.
Klasa pochodna pracownika
Teraz napiszmy inną klasę, która również dziedziczy po osobie. Tym razem napiszemy klasę Pracownik. Pracownik „jest” osobą, dlatego właściwe jest użycie dziedziczenia:
// Employee publicly inherits from Person
class Employee: public Person
{
public:
double m_hourlySalary{};
long m_employeeID{};
Employee(double hourlySalary = 0.0, long employeeID = 0)
: m_hourlySalary{hourlySalary}, m_employeeID{employeeID}
{
}
void printNameAndSalary() const
{
std::cout << m_name << ": " << m_hourlySalary << '\n';
}
};Pracownik dziedziczy m_name i m_age od Person (jak również dwie funkcje dostępu) i dodaje dwie kolejne zmienne składowe oraz własną funkcję składową. Zauważ, że printNameAndSalary() używa zmiennych zarówno z klasy, do której należy (Employee::m_hourlySalary), jak i klasy nadrzędnej (Person::m_name).
Otrzymujemy wykres wyprowadzania wyglądający tak:

Zauważ, że Pracownik i BaseballPlayer nie mają żadnego bezpośredniego związku, mimo że obaj dziedziczą po Osoba.
Oto pełny przykład użycia pracownika:
#include <iostream>
#include <string>
#include <string_view>
class Person
{
public:
std::string m_name{};
int m_age{};
Person(std::string_view name = "", int age = 0)
: m_name{name}, m_age{age}
{
}
const std::string& getName() const { return m_name; }
int getAge() const { return m_age; }
};
// Employee publicly inherits from Person
class Employee: public Person
{
public:
double m_hourlySalary{};
long m_employeeID{};
Employee(double hourlySalary = 0.0, long employeeID = 0)
: m_hourlySalary{hourlySalary}, m_employeeID{employeeID}
{
}
void printNameAndSalary() const
{
std::cout << m_name << ": " << m_hourlySalary << '\n';
}
};
int main()
{
Employee frank{20.25, 12345};
frank.m_name = "Frank"; // we can do this because m_name is public
frank.printNameAndSalary();
return 0;
}Wypisuje:
Frank: 20.25
Łańcuchy dziedziczenia
Możliwe jest dziedziczenie z klasy, która sama jest pochodną innej klasy. Nie ma w tym nic godnego uwagi ani specjalnego – wszystko przebiega tak, jak w powyższych przykładach.
Na przykład napiszmy klasę Supervisor. Przełożony to Pracownik, czyli Osoba. Napisaliśmy już klasę Pracownik, więc użyjmy jej jako klasy bazowej, z której wyprowadzimy klasę Supervisor:
class Supervisor: public Employee
{
public:
// This Supervisor can oversee a max of 5 employees
long m_overseesIDs[5]{};
};Teraz nasz wykres wyprowadzania wygląda następująco:

Wszystkie obiekty Supervisor dziedziczą funkcje i zmienne zarówno po Pracowniku, jak i Osobie oraz dodają własną zmienną składową m_overseesIDs.
Konstruując takie łańcuchy dziedziczenia, możemy utworzyć zestaw klasy wielokrotnego użytku, które są bardzo ogólne (na górze) i stają się coraz bardziej szczegółowe na każdym poziomie dziedziczenia.
Dlaczego ten rodzaj dziedziczenia jest przydatny?
Dziedziczenie z klasy bazowej oznacza, że nie musimy na nowo definiować informacji z klasy bazowej w naszych klasach pochodnych. Automatycznie otrzymujemy funkcje składowe i zmienne składowe klasy bazowej poprzez dziedziczenie, a następnie po prostu dodajemy potrzebne nam dodatkowe funkcje lub zmienne składowe. To nie tylko oszczędza pracę, ale oznacza również, że jeśli kiedykolwiek zaktualizujemy lub zmodyfikujemy klasę bazową (np. dodamy nowe funkcje lub naprawimy błąd), wszystkie nasze klasy pochodne automatycznie odziedziczą zmiany!
Na przykład, jeśli kiedykolwiek dodamy nową funkcję do Person, wówczas Employee, Supervisor i BaseballPlayer automatycznie uzyskają do niej dostęp. Gdybyśmy dodali nową zmienną do Pracownika, wówczas Przełożony również uzyskałby do niej dostęp. Pozwala nam to konstruować nowe klasy w łatwy, intuicyjny i łatwy w utrzymaniu sposób!
Wnioski
Dziedziczenie pozwala nam na ponowne wykorzystanie klas poprzez dziedziczenie ich członków przez inne klasy. Na przyszłych lekcjach będziemy dalej badać, jak to działa.

