C++/Dziedziczenie
Wstęp - Co to jest dziedziczenie
edytujCzęsto podczas tworzenia klasy napotykamy na sytuację, w której nowa klasa powiększa możliwości innej (wcześniejszej) klasy, nierzadko precyzując jednocześnie jej funkcjonalność. Dziedziczenie daje nam możliwość wykorzystania nowych klas w oparciu o stare klasy. Nie należy jednak traktować dziedziczenia jedynie jako sposobu na współdzielenie kodu między klasami. Dzięki mechanizmowi rzutowania możliwe jest interpretowanie obiektu klasy tak, jakby był obiektem klasy z której się wywodzi. Umożliwia to skonstruowanie szeregu klas wywodzących się z tej samej klasy i korzystanie w przejrzysty i spójny sposób z ich wspólnych możliwości. Należy dodać, że dziedziczenie jest jednym z czterech elementów programowania obiektowego (obok abstrakcji, enkapsulacji i polimorfizmu).
Klasę z której dziedziczymy nazywamy klasą bazową, zaś klasę, która po niej dziedziczy nazywamy klasą pochodną. Klasa pochodna może korzystać z funkcjonalności klasy bazowej i z założenia powinna rozszerzać jej możliwości (poprzez dodanie nowych metod, lub modyfikację metod klasy bazowej).
Składnia
edytujSkładnia dziedziczenia jest bardzo prosta. Przy definicji klasy należy zaznaczyć po których klasach dziedziczymy. Należy tu zaznaczyć, że C++ umożliwia Wielodziedziczenie, czyli dziedziczenie po wielu klasach na raz. Jest ono opisane w rozdziale Dziedziczenie wielokrotne.
class nazwa_klasy :[operator_widocznosci] nazwa_klasy_bazowej, [operator_widocznosci] nazwa_klasy_bazowej ...
{
definicja_klasy
};
operator_widoczności może przyjmować jedną z trzech wartości: public, protected, private. Operator widoczności przy klasie, z której dziedziczymy pozwala ograniczyć widoczność elementów publicznych z klasy bazowej.
- public - oznacza, że dziedziczone elementy (np. zmienne lub funkcje) mają taką widoczność jak w klasie bazowej.
- public public
- protected protected
- private brak dostępu w klasie pochodnej
- protected - oznacza, że elementy publiczne zmieniają się w chronione.
- public protected
- protected protected
- private brak dostępu w klasie pochodnej
- private - oznacza, że wszystkie elementy klasy bazowej zmieniają się w prywatne.
- public private
- protected private
- private brak dostępu w klasie pochodnej
- brak operatora - oznacza, że niejawnie (domyślnie) zostanie wybrany operator private.
- public private
- protected private
- private brak dostępu w klasie pochodnej
Dostęp do elementów klasy bazowej można uzyskać jawnie w następujący sposób:
[klasa_bazowa::...]klasa_bazowa::element
Zapis ten umożliwia dostęp do elementów klasy bazowej, które są "przykryte" przez elementy klasy nadrzędnej (mają takie same nazwy jak elementy klasy nadrzędnej). Jeżeli nie zaznaczymy jawnie o który element nam chodzi kompilator uzna że chodzi o element klasy nadrzędnej, o ile taki istnieje (przeszukiwanie będzie prowadzone w głąb aż kompilator znajdzie "najbliższy" element).
Przykład 1
edytujDefinicja i sposób wykorzystania dziedziczenia
edytujNajczęstszym powodem korzystania z dziedziczenia podczas tworzenia klasy jest chęć sprecyzowania funkcjonalności jakiejś klasy wraz z implementacją tej funkcjonalności. Pozwala to na rozróżnianie obiektów klas i jednocześnie umożliwia stworzenie funkcji korzystających ze wspólnych cech tych klas. Załóżmy że piszemy program symulujący zachowanie zwierząt. Każde zwierze powinno móc jeść. Tworzymy odpowiednią klasę:
class Zwierze
{
public:
Zwierze();
void jedz();
};
Następnie okazuje się, że musimy zaimplementowac klasy Ptak i Ryba. Każdy ptak i ryba jest zwierzęciem. Oprócz tego ptak może latać, a ryba płynąć. Wykorzystanie dziedziczenia wydaje się tu naturalne.
class Ptak : public Zwierze
{
public:
Ptak();
void lec();
};
class Ryba : public Zwierze
{
public:
Ryba();
void plyn();
};
Co istotne tworząc takie klasy możemy wywołać ich metodę pochodzącą z klasy Zwierze:
Ptak ptak;
ptak.jedz(); //metoda z klasy Zwierze
ptak.lec(); //metoda z klasy Ptak
Ryba *ryba=new Ryba();
ryba->jedz(); //metoda z klasy Zwierze
ryba->plyn(); //metoda z klasy Ryba
Możemy też zrzutować obiekty klasy Ptak i Ryba na klasę Zwierze:
Ptak *ptak=new Ptak();
Zwierze *zwierze;
zwierze=ptak;
zwierze->jedz();
Ryba ryba;
((Zwierze)ryba).jedz();
Jeżeli tego nie zrobimy, a rzutowanie jest potrzebne, kompilator sam wykona rzutowanie niejawne:
Zwierze zwierzeta[2];
zwierzeta[0] = Ryba(); //rzutowanie niejawne
zwierzeta[1] = Ptak(); //rzutowanie niejawne
for (int i = 0; i < 2; ++i)
zwierzeta[i].jedz();
Dostęp do elementów przykrytych
edytujElementy chronione - operator widoczności protected
edytujSekcja protected klasy jest ściśle związana z dziedziczeniem - elementy i metody klasy, które się w niej znajdują, mogą być swobodnie używane w klasie dziedziczonej ale poza klasą dziedziczoną i klasą bazową nie są widoczne.
Elementy powiązane z dziedziczeniem
edytujChciałbym zwrócić uwagę na inne, bardzo istotne elementy dziedziczenia, które są opisane w następnych rozdziałach tego podręcznika, a które mogą być wręcz niezbędne w prawidłowym korzystaniu z dziedziczenia (przede wszystkim Funkcje wirtualne).
Funkcje wirtualne
edytujPrzykrywanie metod, czyli definiowanie metod w klasie pochodnej o nazwie i parametrach takich samych jak w klasie bazowej, ma zwykle na celu przystosowanie metody do nowej funkcjonalności klasy. Bardzo często wywołanie metody klasy bazowej może prowadzić wręcz do katastrofy, ponieważ nie bierze ona pod uwagę zmian miedzy klasą bazową a pochodną. Problem powstaje, kiedy nie wiemy jaka jest klasa nadrzędna obiektu, a chcielibyśmy żeby zawsze była wywoływana metoda klasy pochodnej. W tym celu język C++ posiada funkcje wirtualne. Są one opisane w rozdziale Funkcje wirtualne.
Wielodziedziczenie - czyli dziedziczenie wielokrotne
edytujJęzyk C++ umożliwia dziedziczenie po wielu klasach bazowych na raz. Proces ten jest opisany w rozdziale Dziedziczenie wielokrotne.
Przykład 2
edytuj #include <iostream>
class Zwierze
{
public:
Zwierze()
{ }
void jedz( )
{
for (int i = 0; i < 10; ++i)
std::cout << "Om Nom Nom Nom\n";
}
void pij( )
{
for (int i = 0; i < 5; ++i)
std::cout << "Chlip, chlip\n";
}
void spij( )
{
std::cout << "Chrr...\n";
}
};
class Pies : public Zwierze
{
public:
Pies()
{ }
void szczekaj()
{
std::cout << "Hau! hau!...\n";
}
void warcz()
{
std::cout << "Wrrrrrr...\n";
}
};
...
Za pomocą
...
class Pies : public Zwierze
{
...
utworzyliśmy klasę Psa, która dziedziczy klasę Zwierze. Dziedziczenie umożliwia przekazanie zmiennych, metod itp. z jednej klasy do drugiej. Możemy funkcję main zapisać w ten sposób:
...
int main()
{
Pies burek;
burek.jedz();
burek.pij();
burek.warcz();
burek.pij();
burek.szczekaj();
burek.spij();
return 0;
}
Zabronienie dziedziczenia
edytujNiekiedy zachodzi potrzeba uniemożliwienia dziedziczenia po podanej klasie. Przed C++11 można to było uzyskać trochę naokoło:
- utworzyć prywatny konstruktor,
- dodać do klasy statyczną metodę tworzącą instancję.
Tu przykład:
class Klasa {
private:
Klasa();
public:
static Klasa* utworz() {
return new Klasa();
}
};
class Pochodna: public Klasa {};
int main() {
// Pochodna p; // błąd kompilacji: konstruktor jest prywatny
Klasa* k = Klasa::utworz();
delete k;
return 0;
}
Problemy z tym podejściem są co najmniej trzy:
- Po pierwsze jest to mocno nieczytelne,
- Po drugie jeśli klasa ma więcej konstruktorów trzeba dla każdego pisać nową wersję metody utworz.
- Po trzecie błąd kompilacji pojawi się dopiero przy instantacji klasy. Powyższy program się kompiluje, dopiero odkomentowanie pierwszego wiersza main powoduje błąd, który jedynie stwierdzi, że konstruktor klasy jest prywatny.
W C++11 problem ten został usunięty, wprowadzono słowo kluczowe final, które dodane po nazwie klasy powoduje, że dziedziczenie stanie się w ogóle niemożliwe. Poniższy program nie kompiluje się, a kompilator powinien jasno podać przyczynę:
class Klasa final {
public:
Klasa();
};
class Pochodna: public Klasa {};