Wiemy już na czym polega dziedziczenie i znamy zasady tworzenia i funkcjonowania hierarchii klas pochodnych. Wiemy również w jaki sposób w klasie pochodnej można nadpisać metody klasy bazowej. 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.

Metody wirtualne

Definiujemy sobie dwie klasy PiesSsak. Obiekt klasy Pies jest jednocześnie obiektem klasy Ssak. Oznacza 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 tej relacji.

Polimorfizm C++ pozwala na przypisanie wskaźnikowi do klasy bazowej obiektu klasy pochodnej, na przykład:

Ssak* pSsak = New Pies;

W ten sposób tworzymy na stercie obiekt klasy Pies i otrzymujemy wskaźnik do obiektu klasy Ssak. Wszystko jest w porządku bo Pies to również Ssak. Otrzymany wskaźnik można wykorzystać do wywoływania dowolnej metody klasy Ssak. Jedyne czego potrzeba to możliwość wywoływania odpowiednich metod nadpisanych w klasie Pies. Pozwalają na to właśnie metody wirtualne.

W momencie tworzenia obiektu klasy pochodnej (np. Pies) najpierw jest wywoływany konstruktor klasy bazowej, a potem konstruktor klasy pochodnej. klasa Ssak współistnieje z klasą Pies. Schematycznie przedstawia to poniższy rysunek:

00029606.jpg 

W obiekcie tworzenia w obiekcie funkcji wirtualnej, obiekt musi przechowywać "ślad" tej funkcji. Większość kompilatorów tworzy w tym celu specjalną tablicę funkcji wirtualnych, tzw. v-table. Dla każdego typu tworzona jest jedna taka tablica i każdy obiekt danego typu przechowuje wskaźnik do tej tablicy (vWsk). vWsk każdego obiektu wskazuje na tablicę v-table, która z kolei przechowuje wskaźniki wszystkich funkcji wirtualnych danej klasy. Kiedy tworzona jest część obiektu Pies pochodząca od klasy Ssak to wskaźnik vWsk inicjalizowany jest adresem odpowiedniej tablicy v-table.

00029607.jpg 

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.

00029608.jpg 

W momencie odwołania do wskaźnika vWsk otrzymamy adres właściwej funkcji, zależny od rzeczywistego typu aktualnego obiektu. Dlatego, gdy wywołaliśmy metodę Mow() została wywołana funkcja zdefiniowana w klasie Pies. Metody wirtualne współpracują jedynie ze wskaźnikami i referencjami. Przekazywanie obiektu przez wartość nie daje możliwości wykorzystania funkcji wirtualnych.

Przejścia niedozwolone

Jeśli 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 przez obiekt klasy Pies lub wskaźnik do takiego obiektu. Najprostszą i najbezpieczniejszą metodą wywołania MachajOgonem() jest zamiana wskaźnika obiektu klasy Ssak na obiekt klasy Pies.

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. Należy trzymać się zasady, że jeżeli chociaż jedna z funkcji w klasie jest wirtualna to destruktor tej klasy również powinien być wirtualny.

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 jest stworzenie w klasie bazowej wirtualnej metody klonującej obiekt. Metoda klonująca tworzy nową kopię aktualnego obiektu i zwraca ten obiekt. Ponieważ klasa pochodna nadpisze tę metodę, to zawsze będzie tworzona kopia obiektu danej klasy pochodnej.

Koszt metod wirtualnych

Każdy obiekt z zadeklarowanymi metodami wirtualnymi musi przechowywać tablicę v-table. Wiążą się z tym pewne koszty posiadania i wykorzystywania metod wirtualnych. Jeśli stworzymy małą klasę i nie będzie ona bazą dla żadnej innej klasy to nie ma żadnego powodu, aby deklarować w niej metody wirtualne. Jeśli zadeklarujemy chociaż jedną metodę wirtualną to będziemy musieli ponosić koszty tej tablicy (każdy element takiej tablicy zajmuje miejsce w pamięci). Następnie należy stworzyć wirtualny destruktor i inne wirtualne funkcje.

Przykład zastosowania

Możemy stworzyć różne typy okienek - okna dialogowe, paski, okna edycyjne, listy i dać każdemu wirtualną metodę Rysuj() . Następnie poprzez stworzenie wskaźnika do okna i przypisaniu do pól dialogowych i innych typów pochodnych, możemy wywołać metodę Rysuj() bez zastanawiania się, jaki jest typ aktualnie obsługiwanego okna. Właściwa metoda zostanie wywołana bez naszej ingerencji. W momencie kompilacji nie wiadomo, które obiekty zostaną stworzone i które metody Rysuj() będą wywoływane. Obiekty są przypisane do wskaźników już po uruchomieniu programu. Nazywa się to dynamicznym przypisywaniem.

Podsumowanie

Należy zawsze wykorzystywać metody wirtualne przy tworzeniu klas pochodnych. Zawsze deklarujemy destruktor jako wirtualny jeżeli w klasie stworzyliśmy jakąkolwiek wirtualną metodę, gwarantuje to że pochodne części obiektu również będą zwalniane w momencie kasowania wskaźnika do obiektu. Nigdy nie tworzymy wirtualnych konstruktorów. Polimorficzne podejście do klas bazowych polega na tym, że jeżeli klasa bazowa ma zadeklarowaną metodę i metoda ta zostanie nadpisana w klasie pochodnej to w momencie wywołania tej metody ze wskaźnika na klasę bazową, wskazującego na obiekty klasy pochodnej, zostanie zaimplementowana metoda w klasie pochodnej.