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 

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 

są 

trzy 

funkcje: 

FunkcjaWsk(), 

FunkcjaRef() 

i 

FunkcjaWar().  Każda  z 
nich  pobiera  odpowiednio: 
wskaźnika  na  obiekt  klasy 
Ssak

referencje 

do 

obiektu  klasy  Ssak  
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 

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 

klasie 

Kot 

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 

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, 

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ę 

Zwierz

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