W klasie pochodnej można zdefiniować metodę o sygnaturze i typie zwracanym (patrz rozdział o funkcjach ) takich samych jak dla pewnej metody z klasy bazowej. Nie jest to przeciążenie, tylko przesłonięcie: sygnatury są bowiem te same (a funkcje przeciążane mają tę samą nazwę, ale różne sygnatury).
Załóżmy następującą sytuację:
class A { // ... void fun() { ... } // ... }; class B : public A { // ... void fun() { ... } // ... };Zdefiniujmy teraz
A a, *pa = new A, *pab = new B, &raa = a, &rab = *pab;A zatem
Przypuśćmy teraz, że wywołujemy funkcję fun za pomocą zmiennych a, pa, pab, raa i rab i pytamy, która z metod: czy ta z klasy A, czy ta z klasy B, zostanie wywołana. Otóż dla wszystkich wywołań decyduje tu typ statyczny; wywołania
a.fun(); pa->fun(); pab->fun(); raa.fun(); rab.fun();wszystkie spowodują wywołanie funkcji fun z klasy A, mimo że w trzecim i piątym przypadku obiekt, na rzecz którego nastąpi wywołanie, jest w rzeczywistości obiektem klasy pochodnej B, a w klasie tej metoda fun została przedefiniowana, przesłaniając wersję odziedziczoną z klasy bazowej.
Dla znających Pythona czy Javę może to być zaskoczenie. Tam bowiem, jak w większości języków obiektowych, decyduje typ dynamiczny: jeśli obiekt, na rzecz którego wywołujemy metodę, jest klasy pochodnej względem tej, która jest typem statycznym wskaźnika (referencji) do tego obiektu, to wywołana będzie wersja tej metody pochodząca z klasy pochodnej (jeśli została tam przedefiniowana). Mówimy wtedy, że metody są wirtualne. A zatem w Javie wszystkie metody (prócz finalnych i prywatnych) są wirtualne. Klasy w których istnieją metody wirtualne, nazywamy klasami polimorficznymi, bo wywołanie ich poprzez wskaźnik (referencję) pewnego typu zależy od typu obiektu na który ten wskaźnik wskazuje, ma zatem „wiele kształtów”. A zatem w Javie klasy, prócz finalnych, są polimorficzne.
Trzeba jednak zdawać sobie sprawę, że ceną za polimorfizm jest pewna utrata wydajności. Dla wywołań na rzecz obiektów klas niepolimorficznych odpowiednia metoda jest wybierana już w czasie kompilacji na podstawie typu statycznego. Mówimy, że następuje wtedy wczesne wiązanie (ang. early binding).
Typ dynamiczny obiektu wskazywanego przez wskaźnik lub referencję może być natomiast określony dopiero w czasie wykonania. Kompilator, napotkawszy wywołanie metody z klasy polimorficznej, nie może umieścić w pliku wykonywalnym kodu odpowiadającego wywołaniu konkretnej funkcji. Zamiast tego umieszczany jest tam kod sprawdzający prawdziwy typ obiektu i wybierający odpowiednią metodę. Mówimy, że następuje wtedy późne wiązanie (ang. late binding). Tak więc każde wywołanie metody wirtualnej powoduje narzut czasowy w trakcie wykonania. Wybranie odpowiedniej metody wymaga też dostępu do informacji o różnych wersjach metody w klasach dziedziczących. Informacja ta jest zwykle umieszczana w specjalnej tablicy, której adres jest przechowywany w każdym obiekcie klasy polimorficznej. Obiekt taki musi być zatem większy niż obiekt analogicznej klasy niepolimorficznej — polimorfizm powoduje zatem również narzut pamięciowy.
W C++, przede wszystkim właśnie ze względu na wydajność, podejście do polimorfizmu jest nieco inne niż w większości innych języków obiektowych. Jako programiści mamy mianowicie możliwość wyboru: czy chcemy, aby definiowana klasa była polimorficzna, czy też z polimorfizmu rezygnujemy na rzecz podniesienia wydajności. Domyślnie nowo definiowane klasy nie są polimorficzne, a zatem definiowane w nich metody nie są wirtualne. Jeśli w programie następuje wywołanie, poprzez wskaźnik lub referencję, dowolnej metody na rzecz obiektu klasy niepolimorficznej, kompilator umieszcza od razu wywołanie konkretnej metody w kodzie wynikowym. Kieruje się przy tym wyłącznie typem zadeklarowanym (statycznym) wskaźnika lub referencji.
Aby definiowana klasa była polimorficzna, wystarczy jeśli choć jedna metoda tej klasy będzie wirtualna. W szczególności może to być destruktor (ale nie konstruktor — ten wirtualny nie może być nigdy).
Deklaracja metody jako wirtualnej musi mieć miejsce w klasie bazowej. Jeśli metoda została zadeklarowana w klasie bazowej jako wirtualna, to wersje przesłaniające tę metodę we wszystkich klasach pochodnych (nie tylko „synach”, ale i „wnukach”, „prawnukach”,...) są też wirtualne. Ponowne deklarowanie ich w klasach pochodnych jako wirtualnych jest dopuszczalne i zalecane, bo zwiększa czytelność kodu, ale w zasadzie zbędne.
Metodę deklarujemy jako wirtualną przez dodanie modyfikatora virtual w jej deklaracji. W klasach pochodnych, jak powiedzieliśmy, powtarzać tego nie musimy; na przykład:
class A { // ... virtual double fun(int,int); // ... }; class B : public A { // ... double fun(int,int); // ... };Zdefiniujmy jak przedtem:
A a, *pa = new A, *pab = new B, &raa = a, &rab = *pab;Teraz klasy są polimorficzne, metoda fun jest wirtualna, a więc zadziała mechanizm późnego wiązania. Wywołania
a.fun(), pa->fun(), pab->fun(), raa.fun(), rab.fun()spowodują teraz w trzecim i piątym przypadku wywołanie metody fun z klasy B, gdyż:
pab->A::fun(); rab.A::fun();spowodują wywołanie wersji funkcji fun z klasy bazowej A nawet jeśli wersja przesłaniająca w klasie B istnieje i mimo że obiekt, na rzecz którego następuje wywołanie, jest typu B, a wywołanie jest poprzez wskaźnik lub referencję. Tak więc jawna kwalifikacja nazwy metody (za pomocą nazwy klasy) powoduje, że normalny mechanizm polimorfizmu nie jest używany — takie wywołanie będzie „wcześnie wiązane”. Skoro tak, to możliwe jest też wywołanie
b.A::fun()bezpośrednio na rzecz obiektu klasy B (nie poprzez wskaźnik lub referencję). Odwrotna sytuacja nie jest oczywiście możliwa nigdy: nie można, nawet używając nazw kwalifikowanych, wywołać metody z klasy B poprzez nazwę obiektu klasy bazowej A (a nie wskaźnika lub referencji):
a.B::fun() // Źle!!byłoby nielegalne.
Przesłaniając w klasie pochodnej metodę dziedziczoną z klasy bazowej możemy zawęzić jej dostępność (ale nie rozszerzyć — odwrotnie niż w Javie!).
Jaka zatem będzie dostępność wirtualnej metody wywoływanej poprzez
wskaźnik typu
A*
do obiektu klasy
B, jeśli w klasie
pochodnej
B
dostępność tej metody zawęziliśmy?
Otóż będzie ona taka, jak w klasie, do której odnosi się wskaźnik
lub referencja (typ statyczny), a nie taka jak w klasie obiektu
(typ dynamiczny). Jeśli metoda jest publiczna w klasie bazowej, to
odpowiednie wersje tej metody z klas pochodnych będą dostępne
poprzez wskaźnik lub referencja do obiektu klasy bazowej, nawet jeśli
w klasach pochodnych ta sama składowa jest prywatna!
Rozpatrzmy przykład:
1. #include <iostream> 2. using namespace std; 3. 4. class Figura { 5. protected: 6. int height; 7. public: 8. Figura(int height = 0) : height(height) 9. { } 10. 11. virtual void what() { 12. cout << "Figura: h=" << height <<endl; 13. } 14. }; 15. 16. class Prostokat : public Figura { 17. private: 18. int base; 19. void what() { 20. cout << "Prostokat: (h,b)=(" << height 21. << "," << base << ")\n"; 22. } 23. public: 24. Prostokat(int height = 0, int base = 0) 25. : Figura(height), base(base) 26. { } 27. }; 28. 29. int main() { 30. Figura *f = new Prostokat(4,5) , &rf = *f; 31. Prostokat *p = new Prostokat(40,50); 32. 33. // what w Prostokat private, ale w Figura nie! 34. f->what(); // Prostokat 35. rf.what(); // Prostokat 36. 37. // p->what(); nie, bo what prywatne w Prostokat 38. // Ale ponizsze legalne! 39. ((Figura*)p)->what(); // Prostokat 40. ((Figura&)*p).what(); // Prostokat 41. 42. // OK: wersja publiczna z klasy bazowej Figura 43. p->Figura::what(); // Figura 44. }
Prostokat: (h,b)=(4,5) Prostokat: (h,b)=(4,5) Prostokat: (h,b)=(40,50) Prostokat: (h,b)=(40,50) Figura: h=40Natomiast zakomentowane wywołanie z linii 37 nie powiodłoby się. Tam bowiem typem statycznym obiektu wskazywanego przez p jest obiekt klasy Prostokat, a w tej klasie metoda what jest prywatna.
Zwróćmy uwagę na wywołania z linii 39 i 40. Zmienna p jest co prawda wskaźnikiem do obiektu typu pochodnego (i na taki obiekt wskazuje), ale przed wywołaniem rzutujemy wartość tego wskaźnika na typ Figura*, a zatem zmieniamy typ statyczny wskazywanego obiektu na Figura, w której to klasie what jest metodą publiczną — analogiczny mechanizm zastosowaliśmy dla referencji rf. Zatem oba te wywołania powiodą się.
Ostatnia linia wydruku jest rezultatem instrukcji z linii 43 programu. Obiektem wskazywanym przez zmienną wskaźnikową p jest co prawda obiekt klasy Prostokat, ale wywołaliśmy jawnie, poprzez kwalifikację nazwą zakresu, metodę what z klasy bazowej Figura. Wywołanie zatem nie jest polimorficzne. Było możliwe, gdyż w klasie Figura metoda what jest publiczna.
Przekonajmy się jeszcze, że polimorfizm rzeczywiście kosztuje,
a zatem nie powinien być stosowany bez potrzeby.
W przykładzie poniżej definiujemy trzy bardzo podobne klasy:
1. #include <iostream> 2. using namespace std; 3. 4. class A { 5. int i; 6. public: 7. A() : i(0) 8. { } 9. }; 10. 11. class B { 12. int i; 13. public: 14. B() : i(0) 15. { } 16. ~B() 17. { } 18. }; 19. 20. class C { 21. int i; 22. public: 23. C() : i(0) 24. { } 25. virtual ~C() 26. { } 27. }; 28. 29. int main() { 30. cout << "sizeof(A): " << sizeof(A) << endl; 31. cout << "sizeof(B): " << sizeof(B) << endl; 32. cout << "sizeof(C): " << sizeof(C) << endl; 33. }
sizeof(A): 4 sizeof(B): 4 sizeof(C): 8Widzimy, że dopóki klasa nie jest polimorficzna, rozmiar obiektu jest taki, jak wynika z rozmiaru pól. Samo dodanie metody (w tym przypadku destruktora) nie zwiększa rozmiaru obiektów. Natomiast dodanie polimorfizmu, jak dla klasy C, powoduje wzrost rozmiaru obiektu, w naszym przykładzie o cztery bajty. W przypadku tej prostej klasy oznacza to zatem wzrost o 100%.
Często się zdarza, że pola klasy są głównie typu wskaźnikowego; obiekty takich klas zwykle nie są duże. Narzut spowodowany polimorfizmem, a więc obecnością dodatkowej informacji (o tzw. tablicy funkcji wirtualnych) w każdym obiekcie klasy może być więc dość spory.
T.R. Werner, 21 lutego 2016; 20:17