Polimorfizm. Funkcje wirtualne. Klasy abstrakcyjne.
Utwórz klasę ksztalt posiadającą atrybuty pole_p, obwod, oraz niech klasa posiada jedną metodę: void pokaz(). Wspomniana metoda powinna być funkcją wirtualną, powinna wypisać jakiś tekst.
class ksztalt { public: double pole_p; float obwod;
}; void ksztalt::pokaz() { cout << "Nieznany kształt" << endl; } |
Nową rzeczą pojawiającą się w tej klasie jest atrybut virtual przed nazwą funkcji w linii 1. Oznacza on, że funkcja będzie funkcją wirtualną (dziedziczoną inteligentnie).
Utwórz klasy pochodne do klasy bazowej ksztalt. Klasy pochodne powinny dziedziczyć publicznie wszystkie elementy klasy bazowej. Dla każdej z klas potomnych należy utworzyć nową funkcję pokaz. Ponadto klasa kwadrat powinna posiadać nowy atrybut bok, klasa kolo - promien a klasa trojkat dwa atrybuty - podstawa i wysokosc. Wszystkie składniki klas powinny być publiczne.
class kwadrat:public ksztalt { public: float bok; void pokaz(); }; void kwadrat::pokaz() { cout << "To jest kwadrat" << endl; } class kolo:public ksztalt { public: float promien; void pokaz(); }; void kolo::pokaz() { cout << "To jest koło" << endl; } class trojkat:public ksztalt { public: float podstawa, wysokosc; void pokaz(); }; void trojkat::pokaz() { cout << "To jest trójkąt" << endl; } |
Tworzymy klasy pochodne do klasy bazowej oraz definiujemy dla każdej z klas funkcję pokaz.
Przetestuj utworzone klasy oraz metody pokaz dla każdej z klas.
|
Wynik wykonania:
Nieznany kształt
To jest kwadrat
To jest koło
To jest trójkąt
Tutaj ujawnia się cel zastosowania funkcji wirtualnej. Dzięki temu, że funkcja pokaz jest funkcją wirtualną, można odwołać się do niej w inteligentny sposób. Proszę spojrzeć na kod programu. W liniach 3-6 tworzymy po jednym obiekcie każdej z klas. W linii 7 tworzymy jeden wskaźnik pokazujący na typ bazowy, czyli ksztalt. Wskaźnik taki może pokazywać nie tylko na typ bazowy. Można go użyć również do pokazywania na typy potomne do typu bazowego. W linii 9 ustawiamy nasz wskaźnik, aby pokazywał na obiekt typu kwadrat. Teraz w linii następnej wywołujemy funkcję pokaz. Dzięki temu, że funkcja pokaz jest funkcją wirtualną to w linii 10 zostanie wywołana nie funkcja pokaz z klasy bazowej a funkcja pokaz z klasy kwadrat. Widać to w oknie przedstawiającym wynik działania programu. W kolejnych liniach wykonane są podobne operacje dla pozostałych klas potomnych.
Usuń słówko virtual stojące przed nazwą metody pokaz w klasie bazowej i przetestuj tak zmieniony program.
class ksztalt { public: double pole_p; float obwod; void pokaz(); }; |
Wynik wykonania:
Nieznany kształt
Nieznany kształt
Nieznany kształt
Nieznany kształt
Po uruchomieniu programu widać, że tym razem kompilator, za każdym razem, gdy pojawiło się wywołanie:
wsk->pokaz();
wywołał funkcję pokaz z klasy bazowej pomimo tego, że wskaźnik wsk pokazywał na obiekty innych klas. Dzieje się tak dlatego, że gdy kompilator nie widzi słówka virtual przed nazwą funkcji to wywołuje metodę z klasy takiej na jaką powinien wskazywać wskaźnik wsk a w naszym przypadku jest to klasa ksztalt
ksztalt *wsk;
Gdy przed nazwą funkcji stoi słówko virtual to kompilator przed wywołaniem funkcji najpierw sprawdza, na jaki typ obiektu wskazuje wskaźnik na rzecz którego wywoływana jest funkcja i wywołuje funkcje odpowiednią dla tego typu.
Zmodyfikuj utworzony wcześniej program tak, aby klasa bazowa była klasą abstrakcyjną (metoda pokaz powinna być funkcją czysto wirtualną). Funkcja pokaz powinna pokazywać pole powierzchni figur. Dodaj do klas pochodnych konstruktory, które ustawią ich atrybuty (policzą pole powierzchni). Utwórz dodatkową klasę czworokat, która będzie pochodną klasy ksztalt. Klasa czworokat niech ma dodatkowy atrybut o nazwie kolor i nie powinna posiadać własnej wersji funkcji pokaz.
#include <iostream.h> class ksztalt { public: double pole_p;
};
{ public: int kolor;
class kwadrat:public czworokat { public: float bok; kwadrat(float =0, int =0); void pokaz(); }; kwadrat::kwadrat(float b, int k) { bok=b; kolor=k; pole_p=bok*bok; } void kwadrat::pokaz() { cout << "Pole kwadratu: " << pole_p << endl; } class kolo:public ksztalt { public: float promien; kolo(float =0); void pokaz(); }; kolo::kolo(float r) { promien=r; pole_p=3.14*r*r; } void kolo::pokaz() { cout << "Pole koła: " << pole_p << endl; } class trojkat:public ksztalt { public: float podstawa, wysokosc; trojkat(float =0, float =0); void pokaz(); }; trojkat::trojkat(float a, float h) { podstawa=a; wysokosc=h; pole_p=0.5*a*h; } void trojkat::pokaz() { cout << "Pole trójkąta: " << pole_p << endl; } void main() { ksztalt *k1 = new kwadrat(5); ksztalt *k2 = new kolo(5); ksztalt *k3 = new trojkat(4,3); k1->pokaz(); k2->pokaz(); k3->pokaz(); } |
Wynik wykonania:
Pole kwadratu: 25
Pole koła: 78.5
Pole trójkąta: 6
1: Nową rzeczą pojawiającą się w tym programie jest definicja funkcji czysto wirtualnej. Oprócz słowa virtual przed nazwą funkcji, za funkcją stoi wyrażenie =0. Oznacza to, że funkcja będzie funkcją czysto wirtualną. Dzięki temu klasa ksztalt staje się klasą abstrakcyjną. Jeżeli klasa jest abstrakcyjna to nie można utworzyć obiektu tej klasy. Próba utworzenia obiektu takiego typu skończy się błędem. Nie trzeba również definiować ciała funkcji czysto wirtualnej.
2 - 3: Klasa czworokat również jest klasą abstrakcyjną gdyż nie ma ona zdefiniowanej własnej wersji funkcji pokaz. Dziedziczy ona tą funkcję z klasy ksztalt a ponieważ, w klasie ksztalt funkcja pokaz jest czysto wirtualna to także w klasie czworokat będzie ona funkcją czysto wirtualną. Jeżeli tak to klasa czworokat ma funkcję czysto wirtualną, więc jest klasą abstrakcyjną, czyli taką, której obiektu nie można utworzyć.
Dalsza część programu nie zawiera nowych rzeczy. W funkcji main następuje sprawdzenie działania funkcji pokaz.
Utwórz klasę bazową kształt posiadającą funkcję wirtualną: void pokaz()oraz destruktor wirtualny. Utwórz klasy pochodne kwadrat posiadającą atrybuty kolor i bok oraz klasę trojkat posiadającą atrybuty kolor, podstawa i wysokość. Zmienne kolor powinny być tablicami znaków. Dla obydwu klas pochodnych zdefiniuj konstruktor, który zainicjuje zmienne liczbowe oraz zarezerwuje pamięć dla zmiennej kolor, destruktor zwalniający zarezerwowaną pamięć oraz własną wersję funkcji pokaz. Przetestuj działanie funkcji wirtualnej pokaz oraz działanie wirtualnego destruktora.
#include <iostream.h> #include <string.h> class ksztalt { public: void virtual pokaz() { cout << "Nieznany kształt" << endl; } virtual ~ksztalt() { cout << "Destruktor kształtu" << endl; } }; class kwadrat:public ksztalt { public: char *kolor; int bok; kwadrat(char * = "", int =0); ~kwadrat(); void pokaz(); }; kwadrat::kwadrat(char *k, int b) { kolor = new char[strlen(k)+1]; strcpy(kolor, k); bok=b; } kwadrat::~kwadrat() { cout << "Destruktor kwadratu" << endl; delete [] kolor; } void kwadrat::pokaz() { cout << "To jest kwadrat" << endl; } class trojkat:public ksztalt { public: char *kolor; int podstawa, wysokosc; trojkat(char * = "", int =0, int =0); ~trojkat(); void pokaz(); }; trojkat::trojkat(char *k, int p, int w) { kolor = new char[strlen(k)+1]; strcpy(kolor, k); podstawa=p; wysokosc=w; } trojkat::~trojkat() { cout << "Destruktor trójkąta" << endl; delete [] kolor; } void trojkat::pokaz() { cout << "To jest trójkąt" << endl; } void main() { ksztalt *k1 = new kwadrat("kwadrat",5); ksztalt *k2 = new trojkat("trójkąt",4,3); k1->pokaz(); k2->pokaz(); delete k1; delete k2; } |
Wynik wykonania:
To jest kwadrat
To jest trójkąt
Destruktor kwadratu
Destruktor kształtu
Destruktor trójkąta
Destruktor kształtu
Gdy usuniemy słówko virtual sprzed destruktora to wynik działania będzie następujący:
Wynik wykonania:
To jest kwadrat
To jest trójkąt
Destruktor kształtu
Destruktor kształtu To jest kwadrat
W drugim przypadku pamięć zajmowana obiekty klas potomnych nie zostanie w całości zwolniona gdyż nie zadziała destruktor klas potomnych a jedynie destruktor klasy bazowej.
Powyższy program tworzy klasę bazową oraz dwie klasy pochodne. Klasa bazowa posiada tylko metodę wirtualną pokaz oraz wirtualny destruktor. Tutaj widać, że destruktor też może być wirtualny (inteligentnie wywoływany). Każda z klas pochodnych posiada konstruktor, swoją wersję destruktora oraz swoją wersję funkcji pokaz.
Nową rzeczą pojawiającą się w tym programie jest wirtualny destruktor. Podobnie jak funkcja wirtualna, wirtualny destruktor jest wywoływany inteligentnie przez kompilator, na rzecz obiektu takiego typu, na jaki wskazuje wskaźnik a nie na rzecz typu, na jaki wskaźnik powinien wskazywać.
Utwórz klasę bazową zwierze i trzy klasy pochodne. Niech każda klasa zawiera konstruktor, destruktor wirtualny, atrybut nazwa oraz metodę glos. Pokaż działanie mechanizmu polimorfizmu na przykładzie obiektów tych klas.
#include <iostream.h> #include <string.h> class zwierze { public: void virtual glos() =0; zwierze() { cout << "Konstruktor zwierzęcia" << endl; } virtual ~zwierze() { cout << "Destruktor zwierzęcia" << endl; } }; class krowa:public zwierze { public: char *nazwa; krowa(char * = ""); ~krowa(); void glos(); }; krowa::krowa(char *k) { nazwa = new char[strlen(k)+1]; strcpy(nazwa, k); cout << "Konstruktor krowy" << endl; } krowa::~krowa() { cout << "Destruktor krowy" << endl; delete [] nazwa; } void krowa::glos() { cout << "Muuuuuuuuuuuu" << endl; } class pies:public zwierze { public: char *nazwa; pies(char * = ""); ~pies(); void glos(); }; pies::pies(char *k) { nazwa = new char[strlen(k)+1]; strcpy(nazwa, k); cout << "Konstruktor psa" << endl; } pies::~pies() { cout << "Destruktor psa" << endl; delete [] nazwa; } void pies::glos() { cout << "Hau Hau" << endl; } class kot:public zwierze { public: char *nazwa; kot(char * = ""); ~kot(); void glos(); }; kot::kot(char *k) { nazwa = new char[strlen(k)+1]; strcpy(nazwa, k); cout << "Konstruktor kota" << endl; } kot::~kot() { cout << "Destruktor kota" << endl; delete [] nazwa; } void kot::glos() { cout << "Miau" << endl; } void main() { zwierze *wsk; int wybor; cout << "Wybierz zwierzę:" << endl << "1 - krowa" << endl << "2 - pies" << endl << "3 - kot" << endl << "4 - koniec" << endl; do { cout << "Wybór: "; cin >> wybor; switch(wybor) { case 1: wsk = new krowa; break; case 2: wsk = new pies; break; case 3: wsk = new kot; break; default: wybor = 4; } if(wybor < 4) { wsk->glos(); delete wsk; } cout << endl; } while(wybor < 4); cout << "Koniec!!!!" << endl; } |
Wynik wykonania:
Wybierz zwierzę:
1 - krowa
2 - pies
3 - kot
4 - koniec
Wybór: 1
Konstruktor zwierzęcia
Konstruktor krowy
Muuuuuuuuuuuu
Destruktor krowy
Destruktor zwierzęcia
Wybór: 2
Konstruktor zwierzęcia
Konstruktor psa
Hau Hau
Destruktor psa
Destruktor zwierzęcia
Wybór: 3
Konstruktor zwierzęcia
Konstruktor kota
Miau
Destruktor kota
Destruktor zwierzęcia
Koniec!!!!
Kod ten pokazuje, w jaki sposób wykorzystać mechanizm polimorfizmu w programie. Widzimy w funkcji main, że tworzenie obiektu danej klasy odbywa się dynamicznie. Również dynamicznie wywoływana jest metoda glos jak i destruktor każdej z klas. Jest to możliwe właśnie dzięki mechanizmowi polimorfizmu.
Z działania programu widzimy że możemy dynamicznie tworzyć obiekty, czyli tworzymy obiekt taki jaki jest nam potrzebny. Dla tego obiektu wywoływane są oczywiście odpowiednie dla niego metody.
Utwórz klasę bazową Bazowa i dwie klasy pochodne. Niech każda klasa zawiera metodę o nazwie msg(). Pokaż działanie mechanizmu polimorfizmu na przykładzie obiektów tych klas.
#include <iostream.h>
class Bazowa {
public:
virtual void msg();
};
class Pochodna1:public Bazowa {
public:
virtual void msg();
};
class Pochodna2:public Bazowa {
public:
virtual void msg();
};
void Bazowa::msg()
{ cout << "Metoda klasy bazowej" << endl;
}
void Pochodna1::msg()
{ cout <<"Metoda klasy pochodnej 1" << endl;
}
void Pochodna2::msg()
{ cout <<"Metoda klasy pochodnej 2" << endl;
}
void main ()
{
Bazowa * ptr;
Bazowa b1;
ptr = &b1;
ptr->msg();//dynamiczne wiązanie
Pochodna1 p1;
ptr = &p1;
ptr->msg();//dynamiczne wiązanie
Pochodna2 p2;
ptr = &p2;
ptr->msg();//dynamiczne wiązanie
}
Wynik wykonania:
Metoda klasy bazowej
Metoda klasy pochodnej 1
Metoda klasy pochodnej 2
Metoda msg() ma trzy implementacji: w klasie Bazowa, w klasie Pochodna1 i w klasie Pochodna2. Referencja ptr na klasę Bazowa jest najpierw inicjowana obiektem klasy Bazowa, a więc instrukcja ptr->msg(); wywoła metodę tej klasy. Następnie referencji ptr zostaje przypisany obiekt klasy Pochodna1, co spowoduje, że następna instrukcja ptr->msg(); wywoła implementację metody msg() zdefiniowaną w klasie Pochodna1. Następnie referencji ptr zostaje przypisany obiekt klasy Pochodna2. Instrukcja ptr->msg(); wywoła implementację metody msg() zdefiniowaną w klasie Pochodna2.
Utwórz klasę bazową Samochod i trzy klasy pochodne. Niech każda klasa zawiera metodę o nazwie kieruj(). Pokaż działanie mechanizmu polimorfizmu na przykładzie obiektów tych klas.
#include <iostream.h>
class Samochod
{public:
virtual void kieruj() = 0;
};
class Osobowy : public Samochod
{public:
virtual void kieruj();
};
class Ciezarowy: public Samochod
{public:
virtual void kieruj();
};
class Sportowy: public Samochod
{public:
virtual void kieruj();
};
void Osobowy::kieruj()//metoda kieruj
{cout << "kierujesz samochodem osobowym"<< endl;}
void Ciezarowy::kieruj()//metoda kieruj
{cout << "kierujesz samochodem ciezarowym"<< endl;}
void Sportowy::kieruj()//metoda kieruj
{cout << "kierujesz samochodem sportowym"<< endl;}
void polecenie_kierowania(Samochod * sam)
{ sam->kieruj();
}
void main ()
{
Samochod * wsk;//utworzenie wskaźnika na obiekt klasy Samochod
delete wsk;
wsk = new Osobowy();//stworzenie obiektu klasy Osobowy
polecenie_kierowania(wsk);//wywołanie metody sterowania pojazdem
delete wsk;
wsk=new Ciezarowy();//stworzenie obiektu klasy Ciezarowy
polecenie_kierowania(wsk);//wywołanie metody sterowania pojazdem
delete wsk;
wsk=new Sportowy();//stworzenie obiektu klasy Sportowy
polecenie_kierowania(wsk);//wywołanie metody sterowania pojazdem
}
Wynik wykonania:
kierujesz samochodem osobowym
kierujesz samochodem ciezarowym
kierujesz samochodem sportowym
Metoda kieruj() w każdej z klas(Osobowy, Ciezarowy, Sportowy) informuje nas jakim pojazdem aktualnie kierujemy. Interpreter, na podstawie typu obiektu wskazywanego przez wskaźnik wsk, sam decyduje którą wersję metody uruchomić. Wskaźnik klasy bazowej może wskazywać na obiekt klasy pochodnej, odwrotny przypadek jest niepoprawny.
Program tworzy kolejno obiekty poszczególnych klas pochodnych i przypisuje je do wskaźnika na klasę bazową wsk. Następnie wywołuje funkcję polecenie_kierowania () przekazując do niej wskaźnik na obiekt. W funkcji na rzecz przekazanego obiektu jest wywoływana metoda kieruj(). W fazie wykonania programu, na podstawie typu obiektu interpreter wybiera metodę z odpowiedniej klasy.
Aby pokazać, że wersja metody wybierana jest w trakcie wykonywania programu, posłużymy się następującym programem. Jest to wcześniejszy program tyle, że z małą przeróbką. Teraz użytkownik decyduje do którego samochodu chce wsiąść.
void main()
{
Samochod * wsk;//utworzenie wskaźnika na obiekt klasy
//Samochod
int wybor; //wybór samochodu
cout << "Wybierz samochod do jazdy:" << endl
<< "1 - osobowy" << endl
<< "2 - ciezarowy" << endl
<< "3 - sportowy" << endl
<< "4 - koniec" << endl;
do{
cout << "? ";
cin >> wybor; //Dokonanie wyboru samochodu
switch(wybor)
{
case 1: wsk = new Osobowy(); break;
case 2: wsk = new Ciezarowy(); break;
case 3: wsk = new Sportowy(); break;
default: wybor = 4;
}
if(wybor != 4)
{ wsk->kieruj();//dynamiczne wywołanie metody
//w tym miejscu interpreter
//decyduje którą wersję metody wybrać
delete wsk;
}
}while(wybor != 4);
cout << "Zakonczyles jazde!!!!" << endl;
}
Wynik wykonania:
Wybierz samochod do jazdy:
1 - osobowy
2 - ciezarowy
3 - sportowy
4 - koniec
? 2
kierujesz samochodem ciezarowym
? 3
kierujesz samochodem sportowym
? 1
kierujesz samochodem osobowym
? 2
kierujesz samochodem ciezarowym
? 4
Zakonczyles jazde!!!!
14