Dziedziczenie
Każdy pies dziedziczy z ssaka wszystkie jego cechy. Możemy powiedzieć, że
ponieważ jest ssakiem to umie się poruszać, oddycha powietrzem itp. Jednak
pojęcie pies dodaje do definicji ssaka możliwość szczekania, machania
ogonem itp. Pojęcie pies jest specjalistyczne natomiast ssak ogólne.
C++ pozwala na reprezentowanie takich relacji poprzez definiowanie klas
pochodzących od innych klas. Pochodzenie jest metodą wyrażania relacji
„jest..." .
Ssak
Gad
Kot
Pies
Terier
Myśliwski
Zwierz
ę
Można stworzyć klasę Pies jako pochodną klasy Ssak. Nie trzeba jawnie
określać, że Pies potrafi się poruszać, gdyż ta cecha zostanie odziedziczona z
klasy Ssak. Klasa Pies, poprzez dziedziczenie z klasy Ssak, automatycznie
posiada umiejętność "poruszania się".
Klasa, która wprowadza nowe funkcje do już istniejącej klasy, nazywana
jest pochodną klasy oryginalnej. Klasa oryginalna nazywana jest klasą
bazową.
Jeżeli klasa Pies jest pochodną klasy Ssak, to klasa Ssak jest klasą bazową
klasy Pies. Klasy pochodne są nadzbiorami ich klas bazowych. Tak jak pies
posiada dodatkowe umiejętności w stosunku do statystycznego ssaka, tak i
klasa Pies dodaje nowe metody i dane do klasy Ssak.
Wyobraźmy sobie, że dostaliśmy zadanie zaprojektowania gry dla dzieci -
symulacji farmy. Trzeba napisać dla każdej klasy metody powodujące, że
każde zwierzę będzie się zachowywać zgodnie z oczekiwaniami odbiorcy
programu.
Zadanie
Tworzenie klasy pochodnej
Podczas deklaracji klasy trzeba zaznaczyć, że jest ona pochodną innej klasy
poprzez napisanie dwukropka po nazwie tworzonej klasy, typu pochodzenia
(public albo inny), a następnie nazwy klasy bazowej.
Przykład
class Pies: public Ssak
Przykład
W świecie rzeczywistym ssaki są pochodną
zwierząt. W programie w C++ jesteśmy w
stanie
przedstawić
jedynie
część
posiadanych informacji o danym obiekcie.
Rzeczywistość jest zbyt kompleksowa i nie
można uwzględnić wszystkich jej aspektów.
Każda hierarchia w C++ jest jedynie
odzwierciedleniem fragmentu posiadanych
informacji. Sztuka dobrego projektowania
polega
na
przedstawieniu
ważnych
obszarów w taki sposób, aby całość
maksymalnie przystawała do rzeczywistości.
W poprzednich programach, zmienne wewnętrzne klasy
były deklarowane po słowie kluczowym private. Jednak
zmienne deklarowane jako private nie byłyby widoczne
w klasie pochodnej. Oczywiście można również
zadeklarować zmienne nJegoWiek i nJegoWaga jako
public, ale jest to niewskazane, gdyż umożliwiłoby
bezpośredni dostęp do nich innym klasom.
Private czy Protected ?
Funkcje i zmienne zadeklarowane jako protected są dostępne we
wszystkich klasach pochodnych (i są w nich prywatne).
Zmienne
i
funkcje
zadeklarowane
jako
protected są widoczne
dla wszystkich funkcji
danej klasy i jej klas
pochodnych.
public
protected
private
Jeżeli funkcja ma dostęp
do obiektu danej klasy to
ma również bezpośredni
dostęp do wszystkich jej
zmiennych
i
funkcji
zadeklarowanych
jako
public.
Do zmiennych i funkcji
zadeklarowanych jako
private mają dostęp
tylko
funkcje
wewnętrzne
danej
klasy.
Przykład
Konstruktory i destruktory
Obiekty klasy Pies są również obiektami klasy Ssak. Jest to główna cecha
relacji „jest...".
Podczas usuwania obiektu Chacko z pamięci zachodzi proces odwrotny.
Najpierw jest wywoływany destruktor klasy Pies, a następnie destruktor
klasy Ssak. Każdy destruktor kasuje tę część obiektu Chacko, która należy
do jego klasy.
Kiedy tworzymy obiekt Chacko najpierw jest wywoływany jego konstruktor
bazowy, którego zadaniem jest stworzenie obiektu klasy Ssak. Następnie jest
wywoływany konstruktor klasy Pies, tworzący gotowy obiekt. Ponieważ, przy
deklaracji Chacko, nie podaliśmy żadnych parametrów, to wywoływany jest
domyślny konstruktor klasy Pies. Obiekt Chacko jest w pełni stworzony
dopiero wtedy, gdy zostaną wykonane oba konstruktory: jeden z klasy Ssak i
drugi z klasy Pies.
Przekazywanie argumentów do konstruktora bazowego
Istnieje możliwość przeciążenia konstruktora klasy Ssak tak, aby pobierał on
konkretny wiek. Podobnie można przeciążyć konstruktor klasy Pies, tak aby
pozwalał on na proste określenie rasy. Jak odczytać wartość parametru
przekazanego do konstruktora w klasie Ssak? Co się stanie, gdy klasa Pies
pozwala na inicjalizację wieku, natomiast klasa Ssak nie?
Inicjalizacja klasy bazowej może być przeprowadzona podczas inicjalizacji
klasy poprzez napisanie nazwy klasy bazowej i podanie w nawiasach
parametrów wymaganych przez klasę bazową.
Zauważ, że domyślny konstruktor klasy Pies
wywołuje domyślny konstruktor klasy Ssak.
Takie rozwiązanie nie jest ściśle wymagane,
jest
to
"dokumentacja"
wywołania
domyślnego konstruktora klasy bazowej.
Bazowy konstruktor i tak zostanie wywołany.
Podobnie jak poprzednio, najpierw inicjalizuje on
klasę
bazową
poprzez
wywołanie
odpowiedniego
konstruktora
klasy
Ssak.
Dodatkowo
inicjalizowana
jest
zmienna
wewnętrzna nJegoWaga. Zauważmy, że nie ma
możliwości inicjalizacji zmiennej klasy bazowej
w części inicjalizacyjnej konstruktora. Ponieważ
klasa
Ssak
nie
posiada
konstruktora
pobierającego
wartość
dla
zmiennej
nJegoWaga,
dlatego
inicjalizację
trzeba
przeprowadzić w treści konstruktora.
Nadpisywanie funkcji
Obiekt klasy Pies ma dostęp do wszystkich funkcji wewnętrznych klasy Ssak
tak jak do własnych. Podobnie, jak dodaliśmy metodę MachajOgonem(),
możemy dodać inne funkcje. Istnieje również możliwość nadpisania funkcji
klasy bazowej. Nadpisanie oznacza zmianę implementacji funkcji z klasy
bazowej. Kiedy wywołuje się metodę z klasy pochodnej kompilator wywoła tę
właściwą - stworzoną w tej klasie.
Kiedy klasa pochodna tworzy funkcję z tym samym typem wartości
zwracanej, z tą samą nazwą i listą parametrów (sygnaturą) co jakaś
funkcja w klasie bazowej i definiuje jej nową implementację to mówimy o
nadpisaniu tej funkcji (metody).
UWAGA: Kiedy chce się napisać funkcję, trzeba zapewnić zgodność typu
wartości zwracanej, nazwy i listy parametrów z funkcją klasy bazowej.
Przeciążanie czy nadpisywanie ?
Obie metody dają podobne efekty. Kiedy przeciąża się metodę to tworzy się
kilka różnych metod o tej samej nazwie i o różnej sygnaturze (wartość
zwracana, lista parametrów). Kiedy nadpisuje się metodę to tworzy się w
klasie pochodnej metodę, która zastępuje metodę z klasy bazowej.
W
klasa
Pies
nadpisuję
metodę Mow() tak, aby każdy
obiekt klasy Pies szczekał
(wypisywał na ekranie Hau!) w
momencie wywołania metody
Mow()
Ukrywanie metod klasy bazowej
W ostatnim programie, metoda klasy Pies o nazwie Mow() ukryła metodę
klasy bazowej. O to nam chodziło, ale istnieje możliwość zaistnienia pewnego
efektu ubocznego. Jeżeli klasa Ssak miałaby przeciążoną metodę Ruch(),
którą byśmy nadpisali w klasie Pies, to zostałyby ukryte wszystkie
przeciążone metody Ruch() w klasie Ssak.
Jeżeli klasa Ssak miałaby trzy metody przeciążające funkcję Ruch() - jedną
nie pobierającą parametrów, druga pobierającą wartość całkowitą i trzecią
dwuargumentową i jeżeli klasa Pies nadpisałaby metodę Ruch() funkcją nie
pobierającą parametrów to dostęp do pozostałych dwóch metod z klasy Ssak
byłby bardzo utrudniony.
Linia została zakomentowana ponieważ
powoduje błąd kompilacji. Jeżeli klasa Pies
nie nadpisałyby metody Ruch() to
mogłaby wywołać metodę Ruch(int).
Teraz, jeżeli chcielibyśmy wykorzystać
metodę z parametrem, musielibyśmy
również ją nadpisać.
Bardzo częstym błędem jest nieświadome ukrycie metod klasy bazowej przy
próbie nadpisania ich. Przyczyną jest pominięcie słowa kluczowego const.
const jest częścią sygnatury funkcji i pominięcie go zmienia sygnaturę
powodując ukrycie funkcji zamiast jej nadpisania.
Wywołanie metody bazowej
Jeżeli nadpisało się już metodę bazową to nadal istnieje możliwość wywołania
nadpisanej funkcji. Należy w tym celu podać pełną nazwę metody łącznie z
nazwą klasy bazowej. Oto przykład:
Możliwa jest również pewna ciekawa modyfikacja wykomentowanej linii z
ostatniego programu:
Ssak: :Ruch()
oChcacko.Ssak::Ruch(10) ;
Programista chce wywołać metodę Ruch(int) z
obiektu oChacko klasy Pies. Jest jednak pewien
problem, gdyż klasa Pies nadpisała metodę
Ruch(), ale nie przeciążyła jej czyli funkcja
Ruch(int) nie jest bezpośrednio dostępna.
Rozwiązaniem jest bezpośrednie odwołanie się do
klasy Ssak i wywołanie metody Ruch (int).