Menu

Dziedziczenie w C++: klasy bazowe i pochodne

Jak dziedziczenie w C++ pozwala klasie pochodnej wykorzystać i rozszerzyć klasę bazową: składnia, dziedziczenie public a private, kolejność konstruktorów i destruktorów oraz pułapki takie jak object slicing.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Ponowne użycie klasy przez jej rozszerzenie

Umiesz już budować klasy z konstruktorami i sprzątać po nich destruktorami. Dziedziczenie to kolejny krok: zamiast kopiować składowe jednej klasy do drugiej, deklarujesz, że nowa klasa jest wyspecjalizowaną wersją istniejącej, i pozwalasz jej wszystko automatycznie odziedziczyć.

Istniejąca klasa to klasa bazowa (albo rodzic); nowa to klasa pochodna (albo dziecko). Klasa pochodna zaczyna ze wszystkimi danymi i zachowaniem bazy, a potem dodaje lub zmienia to, co ją wyróżnia. To główne narzędzie C++ do wyrażania relacji „jest”: Dog jest Animal, SavingsAccount jest BankAccount.

Podstawowa składnia: class Derived : public Base

Dziedziczysz, pisząc po nazwie klasy pochodnej dwukropek, specyfikator dostępu i nazwę klasy bazowej. Najczęstsza forma to dziedziczenie public.

Dog nigdzie nie deklaruje name ani eat(), a jednak oba działają na obiekcie Dog, bo zostały odziedziczone po Animal. Klasa pochodna może swobodnie dodawać składowe takie jak bark(), o których baza nic nie wie.

protected: składowe tylko dla dzieci

Składowa private klasy bazowej nie jest dostępna w klasie pochodnej: dziedziczenie nie łamie hermetyzacji. Gdy chcesz ukryć wnętrze bazy przed światem zewnętrznym, ale udostępnić je podklasom, użyj specyfikatora dostępu protected.

Wyobraź sobie te trzy poziomy jako koncentryczne pierścienie: private to „tylko ta klasa”, protected to „ta klasa i jej potomkowie”, a public to „wszyscy”. Pełny obraz znajdziesz na stronie o specyfikatorach dostępu.

Kolejność konstruktorów i destruktorów

Obiekt pochodny zawiera podobiekt bazowy, a ta część bazowa musi istnieć, zanim zostanie zbudowana część pochodna. Dlatego konstrukcja zaczyna się od bazy, a destrukcja od klasy pochodnej (dokładnie odwrotnie). Jeśli baza potrzebuje argumentów konstruktora, przekazujesz je przez listę inicjalizacyjną.

Wynik pokazuje kolejność w praktyce:

Animal ctor: Rex   // najpierw budowana jest baza
Dog ctor           // potem część pochodna
Dog dtor           // niszczenie w odwrotnej kolejności...
Animal dtor: Rex   // ...baza na końcu

Jeśli zapomnisz o : Animal(n), a baza nie ma konstruktora domyślnego, kod się nie skompiluje: C++ nie wie, jak zbudować część bazową. Klasa bazowa, po której zamierzasz dziedziczyć, powinna niemal zawsze deklarować destruktor (i, jak pokaże następna strona, często wirtualny).

Nadpisywanie: ponowna definicja metody bazy

Klasa pochodna może zastąpić odziedziczoną metodę, deklarując metodę o tej samej sygnaturze. Do oryginału nadal możesz dotrzeć przez Base::method().

To zwykłe ukrywanie nazw, a nie polimorfizm: to, które describe() się wykona, rozstrzyga się podczas kompilacji na podstawie statycznego typu zmiennej. To poważne ograniczenie: jeśli wywołasz metodę przez Shape& albo Shape*, które w rzeczywistości wskazuje na Circle, i tak dostaniesz Shape::describe(). Naprawa wymaga virtual, czemu poświęcona jest następna strona.

Uwaga na object slicing

Skoro referencja lub wskaźnik do bazy może odnosić się do obiektu pochodnego, kuszące jest skopiowanie obiektu pochodnego do zmiennej typu bazowego. Nie rób tego: część pochodna zostaje odcięta.

a to prawdziwy Animal, a nie Dog z etykietą bazy, więc breed po prostu w nim nie istnieje. Aby pracować z obiektami pochodnymi polimorficznie, musisz używać referencji do bazy (Animal&) albo wskaźnika (Animal*), nigdy wartości typu bazowego. Slicing jest cichy: kompiluje się bez problemu i po cichu gubi dane, przez co łatwo przepuścić ten błąd dziedziczenia do produkcji.

Częste błędy, których warto unikać

  • Oczekiwanie, że składowe private bazy będą dostępne w dziecku. Nie są. Używaj protected dla danych, których klasa pochodna naprawdę potrzebuje, a naprawdę wewnętrzny stan trzymaj jako private.
  • Zapominanie o przekazaniu argumentów do konstruktora bazy. Jeśli baza nie ma konstruktora domyślnego, musisz wywołać go jawnie na liście inicjalizacyjnej konstruktora pochodnego (: Base(args)).
  • Slicing obiektu pochodnego do wartości bazowej. Skopiowanie Dog do Animal gubi wszystko, co specyficzne dla Dog. Zamiast tego przekazuj i przechowuj referencje albo wskaźniki do bazy.
  • Nadużywanie dziedziczenia do ponownego użycia kodu. Dziedziczenie modeluje relację „jest”. Jeśli relacja to w rzeczywistości „ma” (Car ma Engine), wybierz kompozycję, czyli obiekt składowy, zamiast dziedziczenia.

Dalej: funkcje wirtualne

Nadpisanie, które właśnie widzisz, zostało rozstrzygnięte podczas kompilacji, więc wywołanie przez wskaźnik do bazy zignorowało wersję pochodną. Następna strona, virtual-functions, wprowadza słowo kluczowe virtual i override: mechanizm, dzięki któremu o wywoływanej metodzie decyduje typ w czasie wykonania. To otwiera drogę do prawdziwego polimorfizmu i wyjaśnia, dlaczego klasy bazowe potrzebują wirtualnych destruktorów.

Najczęściej zadawane pytania

Czym jest dziedziczenie w C++?

Dziedziczenie pozwala zdefiniować nową klasę (klasę pochodną) na podstawie istniejącej (klasy bazowej). Klasa pochodna automatycznie dostaje pola i metody klasy bazowej, a może dodawać nowe albo zastępować istniejące zachowanie. Modeluje relację „jest”: Dog jest Animal, i jest głównym sposobem, w jaki C++ wykorzystuje ponownie i rozszerza kod w hierarchiach klas.

Czym się różni dziedziczenie public od private w C++?

Przy dziedziczeniu public (class Dog : public Animal) publiczny interfejs klasy bazowej pozostaje publiczny w klasie pochodnej, więc Dog jest Animal i można go użyć wszędzie tam, gdzie oczekiwany jest Animal. Przy dziedziczeniu private odziedziczone składowe stają się prywatne: klasa pochodna korzysta z implementacji bazy, ale nie może jej zastąpić. Dziedziczenie publiczne to zdecydowanie najczęstszy przypadek; po prywatne sięgaj tylko przy ponownym użyciu typu „zaimplementowane za pomocą”.

W jakiej kolejności działają konstruktory i destruktory przy dziedziczeniu w C++?

Konstruktory działają od bazy: klasa bazowa jest w pełni zbudowana, zanim wykona się ciało konstruktora klasy pochodnej. Destruktory działają dokładnie w odwrotnej kolejności: najpierw pochodna, potem bazowa. To gwarantuje, że podczas budowania i niszczenia obiektu pochodnego każda część, od której on zależy, już istnieje (albo nadal istnieje).

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ