Funkcje wirtualne i polimorfizm
Wykład 7
Mechanizm omawiany w ramach tematu funkcje
wirtualne decyduje o jednej z przewag programowania
obiektowego nad strukturalnym. Rozważmy dziedziczenie
w ramach klas (Grębosz ale wczesniej Eckel w Thinking
in Java):
Instrument: trąbka, bęben, fortepian
Przykład: plik nagłówkowy zawierający klasę z
wirtualną funkcją składową.
#include <iostream.h>
class instrument {
public:
void
virtual
wydaj_dzwiek()
{
cout<<”nieokreslony pisk!\n”;
}
};
//
class trabka: public instrument {
public:
void wydaj_dzwiek()
{
cout<<”tra-ta-ta-ta\n”;
}
};
class beben: public instrument {
public:
void wydaj_dzwiek()
{
cout<<”bum-bum-bum\n”;
}
};
class fortepian: public instrument {
public:
void wydaj_dzwiek()
{
cout<<”plim-plim-plim\n”;
}
};
void muzyk(instrument &instrument);
main()
{
instrument jakis_instrument;
trabka zlota_trabka;
fortepian steinway;
beben beben_dobosza;
cout<<“wywolanie funkcji skladowych na rzecz
obiektow\n“;
}
jakis_instrument.wydaj_dzwiek();
zlota_trabka.wydaj_dzwiek();
steinway.wydaj_dzwiek();
beben_dobosza.wydaj_dzwiek();
///// powinien zadzialac mechanizm przeslaniania
cout<<”wywolanie funkcji na rzecz obiektu\n
pokazanego wskaznikiem instrumentu\n”;
instrument *wskinstr;
//deklaracja wskaznika
//ustawianie wskaznika
wskinstr=&jakis_instrument;
//przypisanie pod wskaźnik
referencji na obiekt
wskinstr-> wydaj_dzwiek(); // wywołanie funkcji na rzecz zawartosci
wyłuskiwanej spod wskaźnika
cout<<”okazuje sie ze możemy pokazac także na obiekty klasy
pochodnej”;
wskinstr=& zlota_trabka;
wskinstr-> wydaj_dzwiek();
wskinstr=& steinway;
wskinstr-> wydaj_dzwiek();
wskinstr=& beben_dobosza;
wskinstr-> wydaj_dzwiek();
cout<<”albo na referencje do funkcji”;
muzyk(jakis_instrument);
//obiekt jest tu abstarkcyjna zmienna
muzyk(zlota_trabka);
muzyk(steinway);
muzyk(beben_dobosza);
}
/////
void muzyk(instrument &pysk);
{
pysk.wydaj_dźwięk();
}
Po uruchomieniu programu trzymamy na ekranie:
wywolanie funkcji skladowych na rzecz obiektow
nieokreslony pisk!
tra-ta-ta-ta
bum-bum-bum
plim-plim-plim
wywolanie funkcji na rzecz obiektu
pokazanego wskaznikiem instrumentu
nieokreslony pisk!
okazuje sie ze możemy pokazac także na obiekty klasy
pochodnej
tra-ta-ta-ta
bum-bum-bum
plim-plim-plim
albo na referencje do funkcji
nieokreslony pisk!
tra-ta-ta-ta
bum-bum-bum
plim-plim-plim
Gdyby jednak usunąć słowo virtual przy funkcji
wydaj_dzwiek w klasie podstawowej, to na ekranie pojawi
się następujący wynik:
wywolanie funkcji skladowych na rzecz obiektow
nieokreslony pisk!
tra-ta-ta-ta
bum-bum-bum
plim-plim-plim
wywolanie funkcji na rzecz obiektu
pokazanego wskaznikiem instrumentu
nieokreslony pisk!
okazuje sie ze możemy pokazac także na obiekty klasy
pochodnej
nieokreslony pisk!
nieokreslony pisk!
nieokreslony pisk!
albo na referencje do funkcji
nieokreslony pisk!
nieokreslony pisk!
nieokreslony pisk!
nieokreslony pisk!
Czyli po wywołaniu funkcji wydaj_dźwięk na rzecz
obiektów z poszczególnych klas wykonała się po prostu
funkcja z każdej z tych klas zgodnie z zaleceniem:
obiekt.wydaj_dźwięk();
Tu działał ukryty wskaźnik this oraz mechanizm
przesłaniania.
Dalej wprowadziliśmy definiowany wskaźnik, który
pokazywał na obiekty klasy instrument. Przy tym
pokazuje na jakis_instrument czyli dowolny obiekt klasy
instrument.
Następnie kierujemy wskaźnik na funkcję, co powoduje,
ze jest ona wykonana na rzecz wskazanego wcześniej
obiektu:
wskaźnik->wydaj_dźwięk();
Potem ustawiliśmy wskaźnik na obiekty klas pochodnych.
Mogliśmy to zrobić bo przy dziedziczeniu następuje
konwersja typów obiektu i wskaźnikiem do obiektu
klasy podstawowej możemy pokazać na obiekt klasy
pochodnej.
Wprawdzie typ wskaźnika jest przy dziedziczeniu ogólnie
różny od typu obiektu ale konwersja działa w ramach
mechanizmu dziedziczenia. Dlaczego jednak kompilator
wybiera właściwą obiektowi funkcję mimo takiej samej
nazwy funkcji? Sprawcą takiego zachowania kompilatora
jest słowo virtual przy funkcji składowej klasy
podstawowej.
To
ono
sprawia,
że
konwersja
przekierowuje kompilator inteligentnie także do funkcji
dla obiektu pokazanego wskaźnikiem.
Gdy słowo virtual zostało usunięte to mechanizm
prawidłowego wykonania funkcji przypisanej obiektowi
nie zadziałał i wykonywała się funkcja tylko z klasy
podstawowej.
Kompilatory języków niezorientowanych
obiektowo używają tzw. wczesnego wiązania
funkcji. Kompilator generuje wywołanie funkcji a
linker zamienia to wywołanie na bezwzględny
adres kodu, który ma być wykonany.
Kompilatory w językach obiektowych stosują tzw.
późne wiązanie. Kod przy takim wiązaniu jest
wywoływany dopiero podczas wykonywania.
Kompilator tylko sprawdza poprawność i
obecność poszczególnych składników w
wiązaniu. W języku C++ takie wywołanie
powoduje słowo kluczowe virtual.
Polimorfizm
Dzięki terminowi virtual fragment kodu funkcji muzyk
podany w formie
&wydaj_dźwięk();
wykonuje się w formie stosownej do zakresu klasy, z
której wskazujemy adresem obiekt:
&instrument::wydaj_dźwięk()
&trabka::wydaj_dźwięk()
&fortepian::wydaj_dźwięk()
zależnie od sytuacji. Czyli funkcja muzyk wykonała się
różnie mimo tej samej formy. To się nazywa
polimorfizmem, co oznacza wielość form. Zastosowanie
funkcji wirtualnej pozwoliło na uzyskanie wielości form.
Dodatkową cechą klasy zawierającej składową funkcję
wirtualną jest to, że
zadziała uniwersalnie dla każdej
klasy pochodnej
wywołującej funkcje wydaj_dźwięk():
#include”instrum.h” // nasze defincje do klasy
instrument zawrzemy w pliku head
/////
class sluchacz:public instrument{
public:
void wydaj_dzwiek();
{
cout<<”jazz-jazz”;
}
////
main()
{
sluchacz bzzzzz;
muzyk(bzzzzz);
}
to na ekranie otrzymamy:
jazz-jazz
Dlaczego? Dlatego, że instrukcja z funkcji muzyk ma
teraz formę:
instrument.sluchacz::wydaj_dźwięk();
Nietrudno zauważyć, że daje to zupełnie nowe możliwości
modyfikacji działania programu w ramach polimorfizmu.
Dlaczego w takim razie nie uznać wszystkich funkcji jako
wirtualnych w trybie domyślnym? Głównie dlatego, że
funkcje wirtualne zabierają znacznie więcej miejsca w
pamięci niż zwykłe funkcje składowe i ich uruchamianie
trwa znacząco dłużej.
Należy pamiętać, że:
•wirtualna może być tylko funkcja składowa, a nie
funkcja globalna;
•słowo virtual występuje tylko przy deklaracji funkcji w
klasie, a ciało funkcji już nie musi go zawierać;
•jeśli klasa pochodna nie zdefiniuje swojej wersji funkcji
wirtualnej, to będzie ona wywoływana z klasy
podstawowej w jej zakresie ważności;
•funkcja wirtualna nie może być funkcją typu static bo
wtedy nie może być stosowana wirtualnie na wielu
obiektach a tylko na tym, na którym jest przypisana jako
static;
•funkcja wirtualna może być funkcją zaprzyjaźnioną ale
straci wówczas możliwość polimorficznego działania czyli
możemy ja zaprzyjaźnić ale za ceną utraty polimorfizmu