24.2 — Podstawowe dziedziczenie w C++

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:

  1. 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.
  2. 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.
  3. 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.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
94 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze