Zaawansowany polimorfizm


Zaawansowany polimorfizm.

Na poprzednich zajęciach mówiliśmy jak tworzyć w klasach funkcje wirtualne. Podstawą polimorfizmu jest możliwość przypisywania konkretnych obiektów klas pochodnych do wskaźników na klasę bazową nie w momencie kompilacji, lecz w trakcie działania programu. Na dzisiejszych zajęciach powiemy:

Problemy z pojedynczym dziedziczeniem.

W ciągu ostatnich zajęć omawialiśmy polimorficzne podejście do klas bazowych i pochodnych. Pokazaliśmy, że jeśli klasa bazowa ma zadeklarowaną metodę Mow() i metoda ta zostanie nadpisana w klasie pochodnej i 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. Co się jednak stanie gdy zechcemy dodać np. do klasy Kot (pochodnej klasy Ssak) metodę niezadeklarowaną w klasie Ssak? Załóżmy, że dodamy metodę Mrucz(). Każdy kot mruczy, jednak żaden inny ssak nie. Nasza klasa Kot wyglądałaby 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<<”Mru.\n”;}

};

Pojawia się następny problem: Jeśli spóbujemy wykorzystać stworzony wskaźnik np. pKot (ssak *pKot = new Kot) do wywołania metody Miaucz(), to kompilator zgłosi błąd:

Error C2039: `Miaucz': is not member of: `Ssak'

(`Miaucz' nie jest elementem klasy `Ssak')

Kompilator nie może odnaleźć informacji o metodzie Miaucz() w tablicy metod wirtualnych klasy Ssak. W tym momencie można państwu zaserwować całą serię niewłaściwych rozwiązań tego problemu. Można przenieść metodę Miaucz() do klasy bazowej, co jest na pewno złym pomysłem, ale będzie działać. Można zrobić również inne rzeczy, ale nie tędy droga. Tak naprawdę to problem wynika z błędnego projektowania. Ogólnie mówiąc, jeśli mamy wskaźnik do klasy bazowej wskazujący na obiekt klasy pochodnej, to oznacza, że zamierzamy wykorzystywać ten obiekt polimorficznie. Wiąże się z tym proste ograniczenie - nie możemy wywoływać metod specyficznych dla klas pochodnych. Innymi słowy, problemem nie jest istnienie specyficznych metod klasy pochodnej lecz próba ich wywołania ze wskaźnika do klasy bazowej w świecie rzeczywistym nigdy nie próbowalibyście odwoływać się do tych metod. Jednak świat programowania nie jest rzeczywisty ani idealny i czasami musimy 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żemy sobie życzyć, aby on mruczał. W tym momencie pojawia się jedyne rozwiązanie: programistyczne oszustwo. To oszustwo polegać będzie na redukcji wskaźnika do klasy bazowej do wskaźnika do klasy pochodnej. To tak jakbyśmy powiedzieli 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 to faktycznie dosyć radykalne posunięcie, ponieważ izolujemy zasadę funkcjonowania obiektu klasy Kot do wskaźnika na klasę Ssak. Żeby móc wykonać taką operację, trzeba posłużyć się specjalnym operatorem dynamic_cast (dynamiczna redukcja, odrzucenie). Operator ten pozwala na bezpieczną redukcję obiektu. Dodatkową zaletą tego operatora jest późniejsza możliwość wyszukania w programie miejsc, w których zastosowaliśmy redukcję. Dzięki temu będzie można na przykład zmienić to rozwiązanie na inne. Jeśli mamy wskaźnik na klasę bazową np. na klasę Ssak i przypiszemy temu wskaźnikowi obiekt klasy pochodnej np. Kot, to wskaźnik ten możemy wykorzystywać polimorficznie. Jeśli teraz chcemy wywołać metodę klasy Kot np. Miaucz(), to musimy stworzyć wskaźnik na klasę Kot korzystając z operatora dynamic_const. W pierwszej kolejności zostanie sprawdzony obiekt klasy bazowej. Jeśli konwersja przebiegnie pomyślnie, to otrzymamy nowy wskaźnik na klasę Kot. W każdym innym przypadku otrzymany wskaźnik będzie równy null. Spójrzmy na przykład:

#include<iostream.h>

class Ssak

{

Public:

Ssak() {cout<<”Konstruktor Ssaka.\n”;}

~Ssak() {cout<<”Destruktor Ssaka.\n“;}

virtual void Mow() const {cout<<”Odgłos Ssaka.\n”;}

void Miaucz() const {cout<<Mru.\n”;}

protected:

int jegoWiek;

};

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<<”Mru.\n”;}

};

class Pies : public Ssak

{

public:

Pies() {cout<<”Konstruktor Psa.\n”;}

~Pies() {cout<<”Destruktor Psa.\n”;}

void Mow() const {cout<<”Hau!.\n”;}

};

int main()

{

const int NumerSsaka = 3;

Ssak* Zoo[NumerSsaka];

Ssak *pSsak;

int wybor, i;

for (i=0; i<NumerSsaka; i++)

{

cout<<”(1)Pies (2)Kot: “;

cin>>wybor;

if (wybor==1)

pSsak = new Pies;

else

pSsak = new Kot;

Zoo[i] = pSsak;

}

cout<<”\n”;

for (i=0; i<NumerSsaka; i++)

{

Zoo[i] -> Mow();

Kot *pPrawdziwyKot = dynamic_cast<Kot”>(Zoo[i]);

if (pPrawdziwyKot)

pPrawdziwyKot -> Miaucz();

else

cout<<”To nie był Kot.\n”;

delete Zoo[i];

cout<<”\n”;

}

return 0;

}

Efekt działania:

(1)Pies (2)Kot: 1

Konstruktor Ssaka.

Konstruktor Psa.

(1)Pies (2)Kot: 2

Konstruktor Ssaka.

Konstruktor Kota.

Hau!

To nie był Kot.

Destruktor Psa.

Miau!

Mru.

Destruktor Kota.

W programie głównym użytkownik proszony jest o podanie typu obiektu który ma zostać wstawiony do tablicy (pierwsza pętla for). Druga pętla for przegląda całą tablicę i dla każdego obiektu wywołuje metodę Mow(). Funkcje te są wywoływane zgodnie z zasadami polimorfizmu - Pies szczeka, a Kot mruczy. W ostatniej instrukcji if chcemy wywołać z obiektu klasy Kot metodę Mrucz() (nie jest to oczywiście możliwe dla obiektów klasy Pies). Linię wcześniej 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 stworzyliśmy klasę Kształt, z której budujemy następne klasy pochodne Prostokąt i Koło. Z klasy Prostokąt tworzymy klasę pochodną Kwadrat. Każda z klas pochodnych nadpisze metody Rysuj(), PobierzPole() i pobierzObwód()

class Kształt

{

public:

Ksztalt() {}

~Ksztalt() {}

virtual long PobierzPole() {return -1;} //błąd

virtual long PobierzObwod() {return -1;}

virtual void Rysuj() {}

};

class Kot : public Kształt

{

public:

Koło (int promien) : jegoPromien(promien) {}

~Koło () {}

long PobierzPole() {return 3*jegoPromien*jegoPromien;}

long PobierzObwod() {return 6*jegoProgram;}

void Rysuj();

private:

int jegoPromien;

};

class Prostokat : public Ksztalt

{

public:

Prostokat(int dlugosc, int szerokosc);

JegoDlugosc(dlugosc);

jegoSzerokosc(szerokosc) {};

~Prostokat() {};

virtual long PobierzPole() {return jegoDlugosc *jegoSzerokosc;};

virtual long PobierzObwow() {return 2*jegoDługosc+2*jegoSzerokosc;};

virtual int PobierzDlugosc() {return jegoDługosc;};

virtual int PobierzSzerokosc() {return jegoSzerokosc;};

virtual void Rysuj();

protected:

int jegoSzerokosc;

int jegoDługosc;

};

class Kwadrat : public Prostokat

{

public:

Kwadrat (int dlugosc);

Kwadrat (int dlugosc, int szerokosc);

~Kwadrat() {};

long PobierzObwod() {return 4*PobierzDługosc();}

};

Na początku programu deklarowana jest klasa Ksztalt. Funkcja PobierzPole() i PobierzObwod zwracają jedynie kod błędu. Funkcja Rysuj() nie robi nic, ponieważ trudno kazać kompilatorowi narysować kształt. Można narysować konkretny rodzaj kształtu (koło, prostokąt itd.) Kształt jest pojęciem abstrakcyjnym i nie może być narysowany. Klasa Koło jest pochodną klasy Kształt i nadpisuje trzy metody tej klasy. Zauważcie, że nie ma w tym przypadku konieczności deklarowania metod jako virtual, ale nic nie stoi na przeszkodzie, żeby to słowo zastosować (tak jak w klasie Prostokat). Klasa Kwadrat jest pochodną klasy Prostokat. Jednak w tym przypadku nadpisywana jest tylko metoda PobierzObwod(). Pozostałe metody są dziedziczone bez nadpisywania. 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ęcie nazywać ATD). Abstrakcyjny typ danych reprezentuje pewną koncepcję (czyli np. Ksztalt), 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.

Funkcje czysto wirtualne.

W C++, abstrakcyjny typ danych tworzy się z wykorzystaniem funkcji czysto wirtualnych. Takie funkcje powstają przy 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:

  1. Nie można tworzyć obiektów tej klasy. Należy wykorzystywać ją jako bazową dla innych klas.

  2. Trzeba nadpisać w klasach pochodnych funkcje czysto wirtualne.

Każda pochodna klasy ATD dziedziczy funkcje czysto wirtualne bez zmiany ich statusu. Dlatego niezbędne jest nadpisanie 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. Poniższy fragment programu zawiera zmodyfikowaną klasę Ksztalt, która stała się ATD.

class Ksztalt

{

public:

Ksztalt();

~Ksztalt();

virtual long PobierzPole()=0;

virtual long PobierzObwod()=0;

virtual long Rysuj()=0;

};

Taka zmiana deklaracji klasy nie spowoduje zmiany działania programu. Jedyna różnica polega na tym, że w chwili obecnej nie jest możliwe stworzenie obiektu klasy Kształt.

Abstrakcyjne typy danych. Deklaracja klasy jako ATD polega na umieszczeniu w niej przynajmniej jednej funkcji czysto wirtualnej. Deklaracja funkcji czysto wirtualnej zawiera inicjalizację funkcji wartością 0. Np.:

Class Ksztalt

{

virtual void Rysuj() =0; // funkcja czysto wirtualna

};

Implementowanie funkcji czysto wirtualnych.

Na ogół, w ATD nie implementuje się funkcji czysto wirtualnych. Nie ma takiej potrzeby, ponieważ nie tworzy się obiektów klas należących do ATD. Jednak implementacja funkcji czysto wirtualnych jest możliwa. Taka funkcja może być wywołana przez obiekt klasy pochodnej np. do przeprowadzania jakiś typowych dla wszystkich nadpisanych w funkcjach pochodnych operacji. Spójrzmy na przykład w którym klasa Ksztalt należy do ATD i dodatkowo zawiera implementację funkcji Rysuj. Klasa Kolo i klasa Prostokat odpowiednio nadpisują metodę Rysuj()

class Ksztalt

{

public:

Ksztalt() {};

~Ksztalt() {};

virtual long PobierzPole() = 0;

virtual long PobierzObwod() = 0;

virtual void Rysuj() = 0;

};

void Ksztalt::Rysuj()

{

cout<<”Abstrakcyjny mechanizm rysowania!”\n”;

}

Klasa Kolo również nadpisuje funkcje Rysuj();

void Kolo::Rysuj()

{

cout<<”Tutaj powinna być procedura rysowania koła!\n”;

Ksztalt::Rysuj();

}

Podobnie również w klasie Prostokat:

void Prostokat::Rysuj();

{

cout<<”Tutaj powinna być procedura rysowania prostokata!\n”;

Ksztalt::Rysuj();

}

Klasy Kolo i Prostokat nadpisały metody Rysuj(). Ale w swoich implementacjach wywołują metodę Rysuj() zaimplementowaną w klasie Kształt, wypisującą na ekranie komunikat „Abstrakcyjny mechanizm rysowania!”. W tym przykładzie, rola implementacji funkcji czysto wirtualnej w klasie bazowej ogranicza się do wypisania komunikatu. Możemy jednak wyobrazić sobie, że klasa bazowa dostarcza mechanizmów odzielonego rysowania, np. okien, z których korzystają klasy pochodne.

Hierarchie abstrakcji.

Czasami tworzy się klasy ATD z innych klas ATD. Powodem tego może być konieczność zmiany statusu funkcji, czysto wirtualnych (np. na zwykłe) przy jednoczesnym pozostawieniu statusu innych. Jeśli np. tworzymy klasę Zwierze to deklarujemy w niej metody czysto wirtualne np. Jedz(), Spij(), Ruch(), Reprodukcja(). Następnie tworzymy klasy pochodne: Ssak, Ryba.. Ustalamy, że wszystkie ssaki rozmnażają się w ten sam sposób, więc tworzymy funkcjie Ssak::Reprodukcja() jako zwykłą funkcję wirtualną (a nie czysto wirtualną). Metody Jedz() i Spij() pozostawiamy jako czysto wirtualne. Kolejnym krokiem jest stworzenie klasy Pies, pochodnej od klasy Ssak. Musimy nadpisać wszystkie trzy funkcje wirtualne. Jako projektanci klas wiemy, że nie można stworzyć obiektu klasy Zwierze i Ssak. Klasa Ssak dziedziczy z klasy Zwierze metodę Reprodukcja(), jednak nie nadpisuje jej. Poniższy przykład pokazuje klasę ATD jako pochodną innej klasy ATD.

Class Zwierze \\ wspólna baza wszystkich zwirząt

{

public:

Zwierze(int);

virtual ~Zwierze() {cout<<”Destruktor Zwierzaka.\n”;}

virtual int PobierzWiek() const {return jegoWiek;}

virtual void Ustaw Wiek(int wiek) {jegoWiek=wiek;}

virtual void Spij() const=0;

virtual void Jedz() const=0;

virtual void Reprodukcja() const=0;

virtual void Ruch() const=0;

virtual void Mow() const=0;

private:

int jegoWiek;

};

class Ssak:public Zwierze

{

public:

Ssak(int wiek):Zwierze(wiek) {cout<<”Ssak konstruktor.\n”;}

virtual ~Ssak() {cout<<”Destruktor Ssaka.\n”;}

virtual void Reprodukcja() const {cout<<”Rozmnażanie Ssaka.\n”;}

};

class Pies:public Ssak

{

public:

Pies(int): Ssak(wiek) {cout<<”Konstruktor Psa.\n”;}

virtual ~Pies() {cout<<”Destruktor Psa.\n”;}

virtual void Mow() const {cout<<”Hau!\n”;}

virtual void Spij() const {cout<<”Pies chrapie.\n”;}

virtual void Jedz() const {cout<<”Pies je.\n”;}

virtual void Ruch() const {cout<<”Pies biega.\n”;};

virtual void Reprodukcja() const {cout<<”Reprodukcja Psa.\n”;}

};

Na początku deklarujemy klasę ATD o nazwie Zwierze. Wszystkie jej metody są czysto wirtualne poza jedną - PobierzWiek(). Ta metoda jest wspólna dla wszystkich zwierząt. Pozostałe pięć funkcji to: Spij(), Jedz(), Reprodukcja(), Ruch(), Mow(). Klasa Ssak jest pochodną klasy Zwierze. Nie wprowadza ona żadnych nowych danych. Nadpisuje tylko metodę Reprodukcja(), wprowadzając wspólny sposób rozmnażania wszystkich obiektów klasy Ssak. Pochodne klasy Ssak nie muszą już nadpisywać metody Reprodukcja(). Nie jest to jednak zabronione, co widać w klasie Pies. Klasa Pies nie zawiera już funkcji czysto wirtualnych co pozwala na tworzenie obiektów tych klas.

Które typy są abstrakcyjne?

Klasa Zwierze, w zależności od konkretnego programu może być abstrakcyjna ale nie musi. Czym należy się kierować przy deklarowaniu klasy typu ATD. Odpowiedź na to pytanie podyktowana jest logiką programu. Jeśli piszemy program którego zadaniem jest symulacja ogrodu zoologicznego, to klasa Zwierze będzie typem abstrakcyjnym. Klasa Pies już nie ponieważ będziemy musieli tworzyć obiekty tej klasy. Jednak z drugiej strony, jeżeli będziemy tworzyli np. psi cyrk, to klasa Pies będzie abstrakcyjna i dopiero konkretne rasy będą pozwalały na tworzenie obiektów. Poziom abstrakcji jest podyktowany przez konieczność rozróżniania typów.

8



Wyszukiwarka

Podobne podstrony:
Zaawansowane metody udrażniania dród oddechowych
Zaawansowane zabiegi ratujące życie
Stopnie zaawansowania w ozt
Matematyka zaawansowana rroznic Nieznany
CHROMATOGRAFIA CIECZOWA, I MU, Zaawansowana analiza
Jak stworzyć zaawansowany test wyboru lub quiz, PHP Skrypty
Egzamin zintegrowany chirurgia (6), Pielęgniarstwo- magisterka cm umk, I rok, Zaawansowana praktyka
AutoCAD - Kurs zaawansowany - Lekcja 09, autocad kurs, Zaawansowany
ZRF-zadania-ST i WNIP, SGH, Zaawansowana rachunkowość finansowa - Gawart
Seminarium dyplomowe Stan zaawansowania pracy inżynierskiej
kolos zaawan zaoczni 15
4 Formatowanie zaawansowane punktury i numeratory Cwiczenie 4

więcej podobnych podstron