PO wyk12 v1

background image

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.

background image

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.

background image

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.

background image

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ą.

background image

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.

background image

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

background image

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.

background image

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.

background image

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

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.

background image

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.

background image

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.

background image

Przykład

background image

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).

background image

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.

background image

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.

background image

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).

background image

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(),

background image

to kompilator zgłosi błąd:

Kompilator nie może
odnaleźć informacji o
metodzie

Mrucz

w

tablicy

metod

wirtualnych klasy Ssak.

background image

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
.

background image

Ż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.

background image

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ę.

background image

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.

background image

Przykład

background image

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).

background image

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.

background image

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.

background image

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.

background image

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.

background image

background image

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.

background image

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.

background image

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.

background image

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.

background image

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.


Document Outline


Wyszukiwarka

Podobne podstrony:
PO wyk07 v1
postfix krok po kroku v1 1
PO wyk06 v1
PO wyk02 v1
Szkolenie Q Motion Controllers po polsku V1 2 3 11 2003
PO wyk04 v1
PO wyk01 v1
PO wyk05 v1
PO wyk08 v1
PO wyk07 v1
postfix krok po kroku v1 1
Systemy walutowe po II wojnie światowej
HTZ po 65 roku życia

więcej podobnych podstron