Polimorfizm i klasy pochodne
Obiekt klasy Pies jest jednocześnie obiektem klasy Ssak. Oznaczało to, że
obiekt klasy Pies odziedziczył po klasie bazowej atrybuty (czyli dane) i
umiejętności (czyli metody). W C++ istnieje możliwość jeszcze głębszego
określenia relacji „jest... "
Przykład:
Metody wirtualne
Polimorfizm C++ pozwala na przypisanie wskaźnikowi do klasy bazowej
obiektu klasy pochodnej.
Ssak *pSsak = new Pies;
W ten sposób tworzymy na
stercie obiekt klasy Pies i
otrzymujemy
wskaźnik
do
obiektu klasy Ssak. Wszystko
się zgadza, bo Pies to również
Ssak.
Główną zaletą polimorfizmu w C++ jest możliwość tworzenia różnych typów
okienek (okien dialogowych, pasków, okien edycyjnych, list) i przekazania
każdemu z nich wirtualnej metody Rysuj (). Następnie poprzez stworzenie
wskaźnika do okna i przypisaniu do pól dialogowych i innych typów i
pochodnych, można wywołać metodę Rysuj () bez zastanawiania się, jaki jest
typ aktualnie obsługiwanego okna. Właściwa metoda zostanie wywołana bez
ingerencji kreatora programu.
Otrzymany wskaźnik można wykorzystać do wywoływania dowolnej metody
klasy Ssak. Jedyne co jest niezbędne to możliwość wywoływania
odpowiednich metod nadpisanych w klasie Pies. Pozwalają na to metody
wirtualne.
Przykład
Deklarowana jest wirtualna metoda Mow().
Projektant tej klasy sygnalizuje w ten sposób, że
klasa Ssak będzie klasą bazową innych klas.
Pochodne
klasy
prawdopodobnie
nadpiszą
metodę Mow() .
Tworzony wskaźnik pPies do
klasy Ssak. Przypisujemy mu
adres obiektu klasy Pies.
Przypisanie jest poprawne,
ponieważ obiekt klasy Pies
jest jednocześnie obiektem
klasy
Ssak.
Stworzony
wskaźnik wykorzystujemy do
wywołania metody Ruch ().
Kompilator wie, że pPies
wskazuje na klasę Ssak i
wywołuje
metodę
klasy
Ssak.
Wykorzystujemy wskaźnik pPies do wywołania
metody Mow(). Metoda ta jest wirtualna, dlatego
wywołana zostaje funkcja nadpisana w klasie Pies.
Bardzo ciekawa jest następująca własność, z poziomu wskaźnika na obiekt
klasy Ssak mamy możliwość wywoływania metod klasy Pies. Gdyby miało
się tablicę wskaźników do obiektów klasy Ssak i każdy z nich wskazywałby
na inną klasę pochodną, to można by kolejno wywoływać metodę Mow() i za
każdym razem zostałaby wykonana ta właściwa.
Bardzo ciekawa jest następująca własność, z poziomu wskaźnika na obiekt
klasy Ssak mamy możliwość wywoływania metod klasy Pies. Gdyby miało
się tablicę wskaźników do obiektów klasy Ssak i każdy z nich wskazywałby
na inną klasę pochodną, to można by kolejno wywoływać metodę Mow() i za
każdym razem zostałaby wykonana ta właściwa.
Ssak, deklarujemy wirtualną
metodę Mow(). Każda z
kolejnych klas nadpisuję ją
własną implementacją.
Bardzo ciekawa jest następująca własność, z poziomu wskaźnika na obiekt
klasy Ssak mamy możliwość wywoływania metod klasy Pies. Gdyby miało
się tablicę wskaźników do obiektów klasy Ssak i każdy z nich wskazywałby
na inną klasę pochodną, to można by kolejno wywoływać metodę Mow() i za
każdym razem zostałaby wykonana ta właściwa.
Zauważ,
że
w
momencie
kompilacji nie wiadomo, które
obiekty zostaną stworzone i co
się z tym wiąże, które metody
Mow
()
będą
wywoływane.
Obiekty są przypisywane do
wskaźników już po uruchomieniu
programu.
Nazywamy
to
dynamicznym przypisywaniem (z
ang. dynamic bind). Jest to
przeciwieństwo
przypisywania
statycznego lub przypisywania
przy kompilacji.
Jak działają metody wirtualne?
W momencie tworzenia obiektu klasy pochodnej (takiej jak np. Pies),
najpierw jest wywoływany konstruktor klasy bazowej, a potem konstruktor
klasy pochodnej.
Ssak
Pies
Część od klasy
Ssak
Obiekt klasy Pies
Zauważmy, że klasa Ssak współistnieje z klasą Pies.
W momencie tworzenia w obiekcie funkcji wirtualnej, obiekt musi
przechowywać "ślad" tej funkcji. Większość kompilatorów tworzy w tym celu
specjalną tablicę funkcji wirtualnych, którą w dalszej części będziemy
nazywać v-table. Dla każdego typu tworzona jest jedna taka tablica i każdy
obiekt danego typu przechowuje wskaźnik do tej tablicy (Wskaźnik ten
będziemy nazywać dalej vWsk).
Implementacje mogą się różnić, ale każdy kompilator musi realizować to
zadanie.
vWsk każdego obiektu wskazuje na tablicę v-table, która z kolei
przechowuje wskaźniki wszystkich funkcji wirtualnych danej klasy. Kiedy
tworzone jest część obiektu Pies pochodząca od klasy Ssak, to wskaźnik
vWsk inicjalizowany jest adresem odpowiedniej tablicy v-table.
vWsk
Ssak
&Ruch
&Mow
Następnie, w momencie wywołania konstruktora klasy Pies i tworzenia części
obiektu pochodzącej od klasy Pies, tablica wskazywana przez vWsk jest
aktualizowana tak, aby wskazywała na nadpisane metody wirtualne (jeśli
takie są).
vWsk
Ssak
& Ssak::Ruch()
& Pies::Mow()
Pies
Zauważmy, że w momencie odwołania do wskaźnika vWsk otrzymamy adres
właściwej funkcji, zależny od rzeczywistego typu aktualnego obiektu. Z tego
powodu, gdy wywołaliśmy metodę Mow() została wywołana funkcja
zdefiniowana w klasie Pies.
Przejścia niedozwolone
Gdyby klasa Pies miałaby zadeklarowaną metodę MachajOgonem(), która
nie byłaby uwzględniona w deklaracji klasy Ssak, to niemożliwe byłoby
wywołanie tej metody z poziomu wskaźnika do obiektu typu Ssak. Ponieważ
funkcja MachajOgonem() nie jest wirtualna i nie jest zadeklarowana w
klasie Ssak, to wywołanie jej jest możliwe tylko poprzez obiekt klasy Pies lub
wskaźnik do takiego obiektu.
Można w prosty sposób zmienić wskaźnik na obiekt klasy Ssak na wskaźnik
na obiekt klasy Pies. Takie podejście jest najprostszą i najbezpieczniejszą
metodą, wywołania funkcji MachajOgonem().
W C++ nie stosuje się bezpośrednich odwołań do funkcji klas bazowych, gdyż
takie rozwiązanie jest bardzo podatne na błędy.
Obcinanie danych
Metody wirtualne współpracują, jedynie ze wskaźnikami i referencjami. Przekazywanie
obiektu przez wartość nie daje możliwości wykorzystania funkcji wirtualnych.
Przykład
deklarowane
są
trzy
funkcje:
FunkcjaWsk(),
FunkcjaRef()
i
FunkcjaWar(). Każda z
nich pobiera odpowiednio:
wskaźnika na obiekt klasy
Ssak,
referencje
do
obiektu klasy Ssak i
obiekt
Ssak
przez
wartość. Wszystkie trzy
funkcje wykonują to samo
zadanie
-
wywołują
metodę Mow() .
Użytkownik jest proszony o wybór typu obiektu do
stworzenia (Pies albo Kot). Na tej podstawie
otrzymuje się wskaźnik do obiektu odpowiedniej klasy.
Za pierwszym razem użytkownik wybrał psa. Stworzony
zatem zostaje obiekt klasy Pies. Obiekt ten jest następnie
przekazywany kolejno do trzech funkcji, do pierwszej przez
wskaźnik, do drugiej przez referencję i do trzeciej przez
wartość. Wskaźnik i referencja wywołują wirtualną metodę
Pies->Mow().
Trzecia funkcja pobrała obiekt przez wartość.
Ponieważ wymaga ona obiektu klasy Ssak to
kompilator zredukował obiekt klasy Pies i
pozostawił w nim jedynie część pochodzącą od
klasy Ssak. Dlatego została wywołana metoda
Mow() zdefiniowana w klasie Ssak.
Wirtualne destruktory
Często spotykanym rozwiązaniem jest przekazywanie wskaźnika do klasy
pochodnej w miejscu, w którym wymagany jest wskaźnik do klasy bazowej.
Co się jednak stanie, gdy taki wskaźnik usunie się za pomocą delete?
Jeśli destruktor będzie wirtualny (a powinien być!), to wszystko będzie w
porządku, gdyż zostanie wywołany destruktor klasy pochodnej. Destruktor
klasy pochodnej automatycznie wywoła destruktor klasy bazowej dzięki
czemu cały obiekt zostanie poprawnie usunięty z pamięci.
Zasada: Jeśli chociaż jedna z funkcji w klasie jest wirtualna to destruktor
tej klasy również powinien być wirtualny.
Wirtualne konstruktory kopiujące
Konstruktor nie może być metodę wirtualną. Jednak czasami istnieje potrzeba
przekazania wskaźnika do obiektu klasy bazowej i otrzymania kopii obiektu
właściwej klasy pochodnej. Najwygodniejszym rozwiązaniem tego problemu
jest stworzenie w klasie bazowej wirtualnej metody klonującej obiekt. Metoda
klonująca tworzy nową kopię aktualnego obiektu i zwraca ten obiekt.
Ponieważ każda klasa pochodna nadpisze tę metodę, to zawsze będzie
tworzona kopia obiektu danej klasy pochodnej.
Przykład
Przykład
Do
klasy
Ssak
została
dodana
wirtualna metoda Klonuj(). Zwraca
ona wskaźnik do nowego obiektu klasy
Ssak poprzez wywołanie konstruktora
kopiującego z parametrem będącym
stałą referencją do aktualnego obiektu
(*this).
Przykład
Klasy Pies i Kot nadpisują metodę
Klonuj(). Ich implementacja zawiera
wywołanie
własnych
konstruktorów
kopiujących.
Ponieważ
metoda
Klonuj() jest wirtualna to tworzy ona
coś w rodzaju wirtualnego konstruktora
kopiującego.
Instrukcja
przypisania
wskaźników
obiektów do odpowiednich elementów
tablicy.
W pętli, wywoływane są z poziomu
obiektów w tablicy, metody Mow() i
Klonuj(). Efektem wywołania funkcji
Klonuj() jest wskaźnik do kopii obiektu
oryginalnego.
Wskaźnik
ten
jest
wstawiany do drugiej tablicy.
W pierwszej linii wydruku wyjściowego
użytkownik wybrał wartość 1 (obiekt
klasy Pies). Wywoływane są kolejno
konstruktory klasy Ssak i Pies.
Podobnie dzieje się w przypadku
obiektów Kot i Ssak).
Wynik wywołania metody Mow()
pierwszego obiektu, będącego klasy
Pies. Ponieważ metoda Mow() jest
wirtualna, to została wywołana funkcja
zaimplementowana
w
tej
klasie.
Następnie jest wywoływana wirtualna
metoda Klonuj(). Zostaje wywołana
funkcja klonująca z klasy Pies, co
powoduje wywołanie konstruktora klasy
Ssak i klasy Pies
Na końcu są wywoływane metody
Mow() obiektów, których wskaźniki
znajdują się w drugiej tablicy.
Koszt metod wirtualnych
Każdy obiekt z zadeklarowanymi metodami wirtualnymi musi przechowywać
tablicę
v-table. Wiąże się z tym pewne koszty posiadania i wykorzystywania metod
wirtualnych. Jeżeli stworzy się małą klasę i nie będzie ona bazą dla żadnej
innej klasy to nie ma żadnego powodu, aby deklarować w niej jej metod
wirtualne.
Jeżeli natomiast zadeklaruje się chociaż jedną metodę wirtualną to będzie
musiała ona ponosić koszty tablice v-table (każdy element takiej tablicy
zajmuje trochę pamięci). Następnym krokiem będzie stworzenie wirtualnego
destruktora i prawdopodobnie innych wirtualnych funkcji.
Zastanów się nad każdą, nie-wirtualną metodą i upewnijmy się, czy
rozumiemy dlaczego nie jest ona wirtualna.
•Zawsze wykorzystuj metody wirtualne przy tworzeniu klas pochodnych.
•Zawsze deklaruj destruktor jako wirtualny, jeżeli stworzyłeś w klasie
jakąkolwiek wirtualną metodę.
Zapamiętaj:
•Nigdy nie twórz wirtualnych konstruktorów.
Problemy z pojedynczym dziedziczeniem
Poprzednio pokazałem Państwu, że jeżeli klasa bazowa ma zadeklarowaną metodę
Mow() i metoda ta zostanie nadpisana w klasie pochodnej, to w momencie wywołania
tej metody ze wskaźnika na klasę bazową, wskazującego na obiekt klasy pochodnej,
zostanie wywołana metoda zaimplementowana w klasie pochodnej. Ilustruje to
następujący przykład:
Deklarujemy wirtualną metodę
Mow().
Funkcja
ta
jest
następnie
nadpisywana
w
klasie
Kot
i
wywoływana.
Zauważmy,
że
pKot
jest
zadeklarowany jako wskaźnik
na obiekt klasy Ssak. Na tym
polega
główna
cecha
polimorfizmu C++ (omówiliśmy
to w poprzednio).
Co się jednak stanie, gdy zechce się dodać do klasy Kot metodę niezadeklarowaną w
klasie Ssak?
Załóżmy, że dodajemy metodę Mrucz(). Każdy kot mruczy, jednak żaden inny ssak nie.
Zapewne chcielibyśmy, aby klasa Kot wyglądała następująco:
class Kot : public Ssak
{
public:
Kot() {cout « "Konstruktor Kota...\n";}
~Kot() {cout « "Destruktor Kota...\n";}
void Mow () const {cout « "Miau! \n"; }
void Mrucz () const {cout « "Mrrrrrrrrrrrrrrrr\n"; }
};
Pojawia się następujący problem: Jeżeli spróbuje się wykorzystać stworzony wskaźnik
pKot do wywołania metody Mrucz(),
to kompilator zgłosi błąd:
Kompilator nie może
odnaleźć informacji o
metodzie
Mrucz
w
tablicy
metod
wirtualnych klasy Ssak.
Możesz przenieść deklarację metody Mrucz() do klasy bazowej, ale jest to bardzo zły
pomysł. Chociaż takie rozwiązanie będzie działać, to trudno nazwać eleganckim styl
pisania, w którym klasa bazowa zawiera metody charakterystyczne dla jej klasy
pochodnej.
W tym momencie można zaproponować całą serię niewłaściwych rozwiązań:
Tak naprawdę, cały problem wynika z błędnego projektowania. Ogólnie mówiąc, jeżeli
ma się wskaźnik do klasy bazowej wskazujący na obiekt klasy pochodnej, to oznacza to,
że zamierza się wykorzystywać ten obiekt polimorficznie. Wiąże się z tym proste
ograniczenie - nie można wywoływać metod specyficznych dla klas pochodnych.
Innymi słowy, problemem nie jest istnienie specyficznych metod danej klasy pochodnej
lecz próba ich wywołania ze wskaźnika do klasy bazowej. W świecie rzeczywistym nigdy
nie próbowałoby się odwoływać się do tych metod.
Jednak świat programowania nie jest ani rzeczywisty, ani idealny i czasami trzeba dać
sobie radę z dużym zbiorem obiektów bazowych, np. ogrodem zoologicznym pełnym
ssaków. Może się w tym zbiorze znaleźć obiekt klasy Kot i możesz sobie życzyć, aby on
mruczał. W tym momencie pojawia się jedyne rozwiązanie: programistyczne
oszustwo.
Żeby móc wykonać taką operację, trzeba posłużyć się specjalnym operatorem
dynamic_cast (dynamiczna redukcja, odrzucenie).
Operator (dynamic_cast) ten pozwala na bezpieczną redukcję obiektu.
Dodatkową zaletą tego operatora jest późniejsza możliwość wyszukania w
programie miejsc, w których zastosowało się redukcję. Będzie można zamieniać
to rozwiązanie na inne.
Jeżeli ma się wskaźnik na klasę bazową, np. na klasę Ssak i przypisze się temu
wskaźnikowi obiekt klasy pochodnej, np. Kot, to wskaźnik ten można wykorzystywać
polimorficznie. Jeżeli teraz chce się wywołać metodę klasy Kot, np. Mrucz(), to trzeba
stworzyć wskaźnik na klasę Kot korzystając z operatora dynamic_cast.
W pierwszej kolejności zostanie sprawdzony obiekt klasy bazowej. Jeżeli konwersja
przebiegnie pomyślnie, to otrzyma się nowy wskaźnik na klasę Kot. W każdym innym
przypadku otrzymany wskaźnik będzie równy null.
To oszustwo polegać będzie na redukcji wskaźnika do klasy bazowej do wskaźnika do
klasy pochodnej. To tak jakby powiedziało się kompilatorowi:
"Słuchaj stary, tak się składa, iż wiem, że ten obiekt to Kot, więc nic nie mów i rób co Ci
każę".
Brzmi to nieco brutalnie, ale z programistycznego punktu widzenia jest faktycznie
dosyć radykalne posunięcie, ponieważ izoluje się zasady funkcjonowania obiektu klasy
Kot od wskaźnika na klasę Ssak.
Przeglądanie w pętli całej tablicy stworzonych
obiektów i dla każdego obiektu wywołanie
metody Mow(). Funkcje te są wywoływane
zgodnie z zasadami polimorfizmu - Pies szczeka,
a Kot miauczy.
Chcemy wywołać z obiektu klasy Kot metodę
Mrucz() (nie jest to oczywiście możliwe dla
obiektów klasy Pies).
Wykorzystujemy operator dynamic_cast w celu
sprawdzenia, czy aktualny obiekt to na pewno
Kot. Jeśli uzyskany wskaźnik nie jest równy null,
to możemy wywołać funkcję.
Abstrakcyjne typy danych
Załóżmy, że stworzyło się klasę Ksztalt, z której buduje się następnie klasy pochodne
Prostokat i Kolo. Z klasy Prostokat tworzy się klasę pochodną Kwadrat.
Każda z klas pochodnych nadpisze metody: Rysuj(), PobierzPole() itp. Następujący
przykład ilustruje szkieletową implementację klasy Ksztalt i jej klas pochodnych
Prostokat i Kolo.
Przykład
Przykład
Funkcje PobierzPole() i PobierzObwod()
zwracają jedynie kod błędu. Funkcja
Rysuj() nie robi nic, ponieważ trudno kazać
komputerowi narysować kształt. Można
narysować konkretny rodzaj kształtu (koło,
prostokąt itp.). Kształt jest pojęciem
abstrakcyjnym i nie może być narysowany.
Klasa Kolo jest pochodną klasy Ksztalt i
nadpisuje trzy metody tej klasy. Zauważmy,
że nie ma w tym przypadku konieczności
deklarowania metod jako virtual, ale nic
nie stoi również na przeszkodzie, żeby to
słowo kluczowe zastosować (tak jak w
klasie Prostokat).
Przykład
Problematyczna jest możliwość utworzenia obiektu
klasy Ksztalt. Dobrze by było, żeby uczynić to
niemożliwym. Klasa Ksztalt istnieje tylko jako baza
dla klas pochodnych. Takie klasy nazywamy
abstrakcyjnym typem danych (w skrócie będziemy to
pojęcia nazywać ATD).
Abstrakcyjny typ danych reprezentuje pewną
koncepcję (czy np. kształt), a nie konkretny typ
obiektu (jak np. koło). W C++ ATD jest zawsze klasą
bazową innych klas i nie jest możliwe tworzenie
obiektów ATD.
Klasa Kwadrat jest pochodną klasy Prostokat. Jednak
w tym przypadku nadpisywana jest tylko metoda
PobierzObwod(). Pozostałe metody są dziedziczone
bez nadpisywania.
Funkcje czysto wirtualne
W C++, abstrakcyjny typ danych tworzy się z wykorzystaniem funkcji czysto
wirtualnych. Takie funkcje powstają w wyniku inicjalizacji wartością 0.
virtual void Rysuj() = 0;
Każda klasa, w której zadeklarowano przynajmniej jedną funkcję czysto wirtualną
zalicza się do ATD. Tworzenie obiektów tej klasy jest niedozwolone. Próba stworzenia
takiego obiektu spowoduje błąd kompilacji. Wystąpienie w klasie metod czysto
wirtualnych oznacza dla użytkownika tej klasy, że:
nie można tworzyć obiektów tej klasy. Należy wykorzystywać ją jako klasę bazową dla
innych klas,
trzeba nadpisać w klasach pochodnych funkcje czysto wirtualne.
Każda pochodna klasa klasy ATD dziedziczy funkcje czysto wirtualne bez zmiany ich
statusu. Dlatego niezbędne jest nadpisanie tych metod, jeżeli będzie się chciało żądać
tworzenia obiektów danej klasy. Dlatego właśnie, klasa Prostokat musi nadpisać
wszystkie trzy czysto wirtualne funkcje klasy Ksztalt. W przeciwnym wypadku klasa
Prostokat będzie również zaliczać się do grupy ATD.
class Ksztalt
{
public:
Ksztalt(){};
~Ksztalt(){};
virtual long PobierzPole() = 0;
virtual long PobierzObwod() = 0;
virtual void Rysuj() = 0;
private:
};
Po wprowadzeniu modyfikacji klasy
Ksztalt klasa ta staje się ATD.
Jak widać, działanie programu nie uległo
zmianie. Jedyna różnica polega na tym,
że w chwili obecnej nie jest możliwe
stworzenie obiektu klasy Ksztalt.
Implementowanie funkcji czysto wirtualnych
Na ogół w ATD, nie implementuje się funkcji czysto wirtualnych. Nie ma takiej potrzeby,
ponieważ nie tworzy obiektów klas należących do ATD.
Jednak implementacja funkcji czysto wirtualnej jest możliwa. Taka funkcja może być
wywołana przez obiekt klasy pochodnej, np. do przeprowadzenie jakiś typowych dla
wszystkich nadpisanych w funkcjach pochodnych operacji. Następny przykład to
modyfikacja poprzedniego przykładu. Tym razem Ksztalt należy do ATD i dodatkowo
zawiera implementację funkcji Rysuj(). Klasa Kolo nadpisuje funkcję Rysuj(). W
funkcji zostaje wywołana metoda zaimplementowana w klasie Ksztalt, wypisująca na
ekranie komunikat.
W tym przykładzie, rola implementacji funkcji czysto wirtualnej w klasie bazowej
ogranicza się do wypisania komunikatu, można jednak wyobrazić sobie, że klasa
bazowa dostarcza mechanizmów dzielonego rysowania, np. okien, z których korzystają
klasy pochodne.
Deklarowany jest abstrakcyjny typ danych – klasa
Ksztalt. Jej trzy funkcje dostępu zadeklarowane są
jako metody czysto wirtualne. Zauważmy, że
teoretycznie nie jest to konieczne, jednak takie
podejście powoduje, że klasa należy do ATD.
Metody
PobierzPole()
i
PobierzObwod()
nie
zostały
zaimplementowane,
w
przeciwieństwie
do
funkcji
Rysuj(). Zarówno klasa Kolo jak i
Prostokat nadpisują tę metodę.
W funkcjach nadpisujących zostaje
wywołana funkcja Rysuj() z klasy
Ksztalt.
Hierarchie abstrakcji
Czasami tworzy się klasy ATD z innych klas ATD. Powodem tego może być konieczność
zmiany statusy funkcji czysto wirtualnych (na zwykłe) przy jednoczesnym
pozostawieniu statusu innych.
Jeżeli tworzy się przykładowo klasę Zwierze i deklaruje się w niej metody czysto
wirtualne: Jedz(), Spij(), Ruch(), Reprodukcja().
Następnie tworzy się klasy pochodne Ssak i Ryba.
Następnie ustala się, że wszystkie ssaki rozmnażają się w ten sam sposób, więc
tworzy się funkcję Ssak:: Reprodukcja() jako zwykłą funkcję wirtualną (a nie
czysto wirtualną).
Metody Jedz() i Spij() pozostawia się jako czysto wirtualne.
Kolejnym krokiem jest stworzenie klasy Pies, pochodnej od klasy Ssak. Trzeba
nadpisać wszystkie trzy funkcje wirtualne.
Jako projektant klasy wiesz, ze nie można stworzyć obiektu klasy Zwierze i Ssak.
Klasa Ssak dziedziczy z klasy Zwierze metodę Reprodukcja(), jednak nie
nadpisuje jej.
Deklarujemy klasę ATD o nazwie Zwierze.
Klasa Ssak jest pochodną klasy Zwierze. Nie
wprowadza ona żadnych nowych danych.
Nadpisuje jednak metodę Reprodukcja(),
wprowadzając wspólny sposób rozmnażania
wszystkich obiektów klasy Ssak.
Klasa Ryba, również nadpisuje metodę
Reprodukcja(), ponieważ, podobnie jak
klasa Ssak, jest bezpośrednią pochodną klasy
Zwierze.
Klasy Ryba, Kon i Pies nie zawierają już
żadnej funkcji czysto wirtualnej, co
pozwala na tworzenie obiektów tych klas.
Pochodne klasy Ssak nie muszą już
nadpisywać metody Reprodukcja(). Nie
jest to jednak zabronione, co widać w
klasie Pies.
W głównym programie wykorzystujemy
wskaźnik
na
klasę
Zwierze
do
wywoływania metod obiektów różnych
klas. Wszystkie wywoływane metody są
wirtualne, dlatego właściwa funkcja jest
wywoływana na podstawie aktualnej
wartości wskaźnika (typu obiektu na
który wskazuje).
Próba stworzenia obiektu klasy Zwierze
albo
Ssak
spowodowałaby
błąd
kompilacji.
Hierarchie abstrakcji
Klasa Zwierze, w zależności od konkretnego programu, może być abstrakcyjna, ale nie musi. Czym
należy się zatem kierować przy deklarowaniu klasy jako ATD.
Odpowiedz na to pytanie nie jest podyktowana przez żaden wskaźnik świata rzeczywistego, lecz
przez logikę programu. Jeżeli pisze się program, którego zadaniem jest symulacja farmy albo
ogrodu zoologicznego, to klasa Zwierze będzie w twoim programie typem abstrakcyjnym. Klasa
Pies już nie, ponieważ będziesz musiał tworzyć obiekty tej klasy.
Jednak z drugiej strony, jeśli chciałoby się stworzyć psi cyrk, to klasa Pies również powinna być
typem abstrakcyjnym i dopiero konkretne rasy (Terrier, Dog itd.) będą pozwalały na tworzenie
obiektów. Poziom abstrakcji jest podyktowany przez konieczność rozróżnienia typów.