
>Z punktu widzenia programowania obiektowego (OO) jest to najważniejsza właściwość C++: [6.8], [6.9].
Funkcja wirtualna, czyli zadeklarowana przy użyciu słowa kluczowego virtual pozwala w klasach pochodnych zastępować implementację metod dostarczonych przez klasę bazową. Kompilator dba o to by metoda zastępująca była wywoływana zawsze gdy obiekt na którego rzecz ma ona być wywołana należy do klasy pochodnej, nawet wtedy gdy obiekt ten jest wskazywany nie przez wskaźnik do klasy pochodnej a przez wskaźnik do klasy bazowej. Pozwala to zastępować w klasach pochodnych algorytmy użyte w klasach bazowych innymi algorytmami, nawet gdy użytkownik nie wie o tym, że wywołuje klasę pochodną.
Klasa pochodna może albo całkowicie zastąpić (ang. "override") metodę klasy bazowej lub częściowo zastąpić (ang. "augment", pol. wzbogacić) metodę klasy bazowej. To drugie osiąga się poprzez wywołanie w metodzie klasy pochodnej metody klasy bazowej.
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
Kiedy rozważamy wskaźnik do obiektu, wówczas wskazywany obiekt może być obiektem klasy pochodnej względem klasy, która jest klasą pochodną w stosunku do klasy której typ stanowi typ bazowy wskaźnika (przykładowo Vehicle*, który w rzeczywistości wskazuje na obiekt typu "Ca"r, taką sytuację nazywamy polimorfizmem, ang. "polymorphism"). Mamy zatem dwa typy: (statyczny) typ wskaźnika (w tym wypadku Vehicle) i (dynamiczny) typ wskazywanego obiektu (w tym wypadku Car).
Statyczna kontrola typów oznacza, że legalność wywołania metody jest sprawdzana w najwcześniejszym możliwym momencie: przez kompilator w chwili kompilacji. Kompilator używa statycznego typu wskaźnika by sprawdzić czy wywołanie metody jest legalne. Jeśli typ wskaźnika pozwala na wywołanie metody, wówczas wskazywany obiekt również sobie z nim poradzi. Przykładowo jeśli klasa Vehicle ma pewną metodę, to na pewno klasa Car ma ją również, ponieważ Car (samochód) jest rodzajem Vehicle (pojazdu).
Łączenie dynamiczne oznacza, że adres w kodzie wywołania metody ustalany jest w najpóźniejszym możliwym momencie: w czasie działania programu, zależnie od dynamicznego typu obiektu. Nazywamy to łączeniem dynamicznym (ang. "dynamic binding") ponieważ połączenie z kodem, który zostanie wywołany następuje w sposób dynamiczny (w czasie działania programu). Łączenie dynamiczne jest skutkiem działania metod zadeklarowanych ze słowem kluczowym virtual (metody wirtualne).
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
Metody, które są zadeklarowane bez słowa kluczowego virtual (niewirtualne) są wyznaczane (ang. resolve) statycznie. Oznacza to, że funkcja, która zostanie wywołana jest wybierana w sposób statyczny (w momencie kompilacji) zależnie od typu wskaźnika (lub referencji) do obiektu.
Inaczej dzieje się w wypadku metod wirtualnych. Metody zadeklarowane z użyciem słowa kluczowego virtual są wyznaczane w sposób dynamiczny (w czasie działania programu). Oznacza to, że funkcja, która zostanie wywołana jest wybierana dynamicznie (w czasie działania programu) w zależności od typu obiektu, a nie typu wskaźnika/referencji do tego obiektu. Nazywa się to łączeniem dynamicznym. Większość kompilatorów używa jakiegoś wariantu poniższej techniki: jeśli dla obiektu istnieje jedna lub więcej funkcji wirtualnych, wówczas kompilator dodaje do obiektu ukryty wskaźnik zwany wskaźnikiem do tablicy funkcji wirtualnych (ang. "virtual-pointer" lub "v-pointer"). Wskaźnik ten wskazuje na globalną tablicę zwaną tablicą funkcji wirtualnych (ang. "virtual-table" lub "v-table.").
Dla każdej klasy, która ma przynajmniej jedną funkcję wirtualną kompilator tworzy osobną tablicę funkcji wirtualnych. Przykładowo jeśli klasa Circle posiada metody draw(), move() i resize() zadeklarowane jako wirtualne, wówczas istnieje dokładnie jedna tablica funkcji wirtualnych związana z klasą Circle, nawet gdyby istniał zylion [p.tłum. bardzo duża liczba :)] obiektów klasy Circle. Wskaźnik do tablicy funkcji wirtualnych (v-pointer) każdego z tych obiektów wskazuje na tablicę fukcji wirtualnych (v-table) klasy Circle. Sama tablica funkcji funkcji wirtualnych posiada wskaźniki do wszystkich funkcji wirtualnych danej klasy. Przykładowo tablica funkcji wirtualnych klasy Circle będzie miała trzy wskaźniki: wskaźnik do Circle::draw(), wskaźnik do Circle::move() i wskaźnik do Circle::resize().
Podczas wywoływania funkcji wirtualnej (ang. dispatch), kod podąża śladem wskaźnika do tablicy funkcji wirtualnych odpowiednej klasy, a następnie odnajduje stosowny wpis w tablicy funkcji wirtualnych dotyczący kodu właściwej metody.
Dodatkowy koszt związany z powyższą techniką (ang. overhead) jest następujący: dodatkowy wskaźnik dla kaźdego obiektu (ale wyłącznie dla wskaźników, które będą wymagać łączenia dynamicznego), plus dodatkowy wskaźnik dla kaźdej metody wirtualnej (ale tylko dla metod wirtualnych). Jeśli chodzi o zwiększenie czasu działania, to mamy co następuje: w porównaniu z wywołaniem normalnej funkcji, funkcja wirtualna wymaga dwóch dodatkowych odwołań do pamięci (jednego dla pobrania wartości wskaźnika do tablicy funkcji wirtualnych, drugiego do pobrania adresu metody). Żadna z tych czynności nie jestpotrzebna w wypadku funkcji niewirtualnych, gdyż kompilator wyznacza funkcje niewirtualne w czasie kompilacji, odpowiednio do typu wskaźnika.
Uwaga: powyższa dyskusja jest w znaczący sposób uproszczona, gdyż nie bierze pod uwagę takich aspektów jak dziedziczenie wielokrotne (ang. multiple inheritance), dziedziczenie wirtualne, RTTI, itp., ani też nie uwzględnia takich czynników (wpływających na wydajność) jak błędy stron, wywoływanie funkcji poprzez wskaźnik do funkcji, itp. Jeśli chcesz dowiedzieć się więcej o tych rzeczach, pytaj na comp.lang.c++; PROSZĘ, NIE WYSYŁAJ ŻADNYCH MAILI DO MNIE!
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
To zaskakująco proste.
Wyobraźmy sobie, że istnieje klasa bazowa Vehicle (pojazd) z dwiema klasami pochodnymi: Car (samochód) i "Truck" (ciężarówka). Kod przegląda listę pojazdów (obiektów klasy Vehicle) i robi różne rzeczy, w zależności od tego jakiego typu jest dany pojazd. Przykładowo może ona ważyć ciężarówki (aby sprawdzić czy nie wiozą zbyt dużego ładunku), a dla samochodów może robić coś innego np. sprawdzać tablice rejestracyjne.
Pierwszym rozwiązaniem, które przychodzi do głowy (przynajmniej większości ludzi) jest użycie instrukcji if. Przykładowo, gdy obiektem jest "Truck" rób cośtam, gdy jest Car rób coś innego, w przeciwnym wypadku (else) rób coś jeszcze innego:
The problem with this is what I call "else-if-heimer's disease" (say it fast and you'll understand). The above code gives you else-if-heimer's disease because eventually you'll forget to add an else if when you add a new derived class, and you'll probably have a bug that won't be detected until run-time, or worse, when the product is in the field.
The solution is to use dynamic binding rather than dynamic typing. Instead of having (what I call) the live-code dead-data metaphor (where the code is alive and the car/truck objects are relatively dead), we move the code into the data. This is a slight variation of Bertrand Meyer's Inversion Principle.
The idea is simple: use the description of the code within the {...} blocks of each if (in this case it is "the foo-bar operation"; obviously your name will be different). Just pick up this descriptive name and use it as the name of a new virtual member function in the base class (in this case we'll add a fooBar() member function to class Vehicle).
Then you remove the whole if...else if... block and replace it with a simple call to this virtual function:
Finally you move the code that used to be in the {...} block of each if into the fooBar() member function of the appropriate derived class:
If you actually have an else block in the original myCode() function (see above for the "semi-generic code that does the 'foo-bar' operation on something other than a Car or Truck"), change Vehicle's fooBar() from pure virtual to plain virtual and move the code into that member function:
That's it!
The point, of course, is that we try to avoid decision logic with decisions based on the kind-of derived class you're dealing with. In other words, you're trying to avoid if the object is a car do xyz, else if it's a truck do pqr, etc., because that leads to else-if-heimer's disease.
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]

When someone will delete a derived-class object via a base-class pointer.
In particular, here's when you need to make your destructor virtual:
Confused? Here's a simplified rule of thumb that usually protects you and usually doesn't cost you anything: make your destructor virtual if your class has any virtual functions. Rationale:
Note: if your base class has a virtual destructor, then your destructor is automatically virtual. You might need an explicit destructor for other reasons, but there's no need to redeclare a destructor simply to make sure it is virtual. No matter whether you declare it with the virtual keyword, declare it without the virtual keyword, or don't declare it at all, it's still virtual.
BTW, if you're interested, here are the mechanical details of why you need a virtual destructor when someone says delete using a Base pointer that's pointing at a Derived object. When you say delete p, and the class of p has a virtual destructor, the destructor that gets invoked is the one associated with the type of the object *p, not necessarily the one associated with the type of the pointer. This is A Good Thing. In fact, violating that rule makes your program undefined. The technical term for that is, "Yuck."
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
An idiom that allows you to do something that C++ doesn't directly support.
You can get the effect of a virtual constructor by a virtual clone() member function (for copy constructing), or a virtual create() member function (for the default constructor).
In the clone() member function, the new Circle(*this) code calls Circle's copy constructor to copy the state of this into the newly created Circle object. (Note: unless Circle is known to be final (AKA a leaf), you can reduce the chance of slicing by making its copy constructor protected.) In the create() member function, the new Circle() code calls Circle's default constructor.
Users use these as if they were "virtual constructors":
This function will work correctly regardless of whether the Shape is a Circle, Square, or some other kind-of Shape that doesn't even exist yet.
Note: The return type of Circle's clone() member function is intentionally different from the return type of Shape's clone() member function. This is called Covariant Return Types, a feature that was not originally part of the language. If your compiler complains at the declaration of Circle* clone() const within class Circle (e.g., saying "The return type is different" or "The member function's type differs from the base class virtual function by return type alone"), you have an old compiler and you'll have to change the return type to Shape*.
Amazingly Microsoft Visual C++ is one of those compilers that does not, as of version 6.0, handle Covariant Return Types. This means:
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
E-mail the author
[ C++ FAQ Lite
| Spis treści
| Skorowidz
| O autorze
| ©
| Pobierz swoją własną kopię ]
Ostatnia aktualizacja Jun 17, 2002
Wersja polska: 0.1i Jul 12, 2004