Funkcja wirtualna (znana również jako metody wirtualne) to funkcja składowa zadeklarowana w klasie bazowej i ponownie zdefiniowana (przesłonięta) przez klasę pochodną. Kiedy odwołujesz się do obiektu klasy pochodnej za pomocą wskaźnika lub odwołania do klasy bazowej, możesz wywołać funkcję wirtualną dla tego obiektu i wykonać wersję metody klasy pochodnej.
- Funkcje wirtualne zapewniają, że dla obiektu zostanie wywołana właściwa funkcja, niezależnie od typu referencji (lub wskaźnika) użytego do wywołania funkcji.
- Stosowane są głównie w celu osiągnięcia polimorfizmu środowiska wykonawczego.
- Funkcje deklaruje się za pomocą a wirtualny słowo kluczowe w klasie bazowej.
- Rozwiązywanie wywołań funkcji odbywa się w czasie wykonywania.
Reguły funkcji wirtualnych
Reguły funkcji wirtualnych w C++ są następujące:
- Funkcje wirtualne nie mogą być statyczne.
- Funkcja wirtualna może być funkcją zaprzyjaźnioną innej klasy.
- Dostęp do funkcji wirtualnych należy uzyskać za pomocą wskaźnika lub referencji typu klasy bazowej, aby osiągnąć polimorfizm środowiska wykonawczego.
- Prototyp funkcji wirtualnych powinien być taki sam w klasie bazowej i pochodnej.
- Są one zawsze definiowane w klasie bazowej i zastępowane w klasie pochodnej. Przesłonięcie (lub ponowne zdefiniowanie funkcji wirtualnej) przez klasę pochodną nie jest obowiązkowe; w takim przypadku używana jest wersja funkcji klasy bazowej.
- Klasa może mieć wirtualny destruktor, ale nie może mieć wirtualnego konstruktora.
Czas kompilacji (wczesne wiązanie) VS zachowanie funkcji wirtualnych w czasie wykonywania (późne wiązanie).
Rozważmy następujący prosty program pokazujący zachowanie funkcji wirtualnych w czasie wykonywania.
Znak ciągu Java
C++
przeciążanie metody
// C++ program to illustrate> // concept of Virtual Functions> #include> using> namespace> std;> class> base {> public>:> >virtual> void> print() { cout <<>'print base class
'>; }> >void> show() { cout <<>'show base class
'>; }> };> class> derived :>public> base {> public>:> >void> print() { cout <<>'print derived class
'>; }> >void> show() { cout <<>'show derived class
'>; }> };> int> main()> {> >base* bptr;> >derived d;> >bptr = &d;> >// Virtual function, binded at runtime> >bptr->drukuj();> >// Non-virtual function, binded at compile time> >bptr->pokaż();> >return> 0;> }> |
>
>Wyjście
print derived class show base class>
Wyjaśnienie: Polimorfizm środowiska wykonawczego osiąga się jedynie poprzez wskaźnik (lub odwołanie) typu klasy bazowej. Ponadto wskaźnik klasy bazowej może wskazywać na obiekty klasy bazowej, a także na obiekty klasy pochodnej. W powyższym kodzie wskaźnik klasy bazowej „bptr” zawiera adres obiektu „d” klasy pochodnej.
Późne wiązanie (czas wykonywania) odbywa się zgodnie z zawartością wskaźnika (tj. lokalizacją wskazywaną przez wskaźnik), a wczesne wiązanie (czas kompilacji) odbywa się zgodnie z typem wskaźnika, ponieważ funkcja print() jest zadeklarowana za pomocą wirtualnego słowo kluczowe, więc zostanie powiązane w czasie wykonywania (wyjście to klasa pochodna wydruku ponieważ wskaźnik wskazuje obiekt klasy pochodnej), a show() nie jest wirtualne, więc zostanie powiązane w czasie kompilacji (wyjście to pokaż klasę bazową ponieważ wskaźnik jest typu podstawowego).
kółko przewijania nie działa
Notatka: Jeśli utworzyliśmy funkcję wirtualną w klasie bazowej i jest ona nadpisywana w klasie pochodnej, to nie potrzebujemy słowa kluczowego virtual w klasie pochodnej, funkcje są automatycznie uznawane za funkcje wirtualne w klasie pochodnej.
Działanie funkcji wirtualnych (koncepcja VTABLE i VPTR)
Jak omówiono tutaj, jeśli klasa zawiera funkcję wirtualną, kompilator sam robi dwie rzeczy.
- Jeśli zostanie utworzony obiekt tej klasy, wówczas a wskaźnik wirtualny (VPTR) jest wstawiany jako element danych klasy, aby wskazać VTABLE tej klasy. Dla każdego nowo utworzonego obiektu wstawiany jest nowy wirtualny wskaźnik jako element danych tej klasy.
- Niezależnie od tego, czy obiekt został utworzony, czy nie, klasa zawiera jako element członkowski statyczna tablica wskaźników funkcji zwana VTABLE . Komórki tej tabeli przechowują adres każdej funkcji wirtualnej zawartej w tej klasie.
Rozważ poniższy przykład:

rdzeń języka Java
C++
// C++ program to illustrate> // working of Virtual Functions> #include> using> namespace> std;> class> base {> public>:> >void> fun_1() { cout <<>'base-1
'>; }> >virtual> void> fun_2() { cout <<>'base-2
'>; }> >virtual> void> fun_3() { cout <<>'base-3
'>; }> >virtual> void> fun_4() { cout <<>'base-4
'>; }> };> class> derived :>public> base {> public>:> >void> fun_1() { cout <<>'derived-1
'>; }> >void> fun_2() { cout <<>'derived-2
'>; }> >void> fun_4(>int> x) { cout <<>'derived-4
'>; }> };> int> main()> {> >base* p;> >derived obj1;> >p = &obj1;> >// Early binding because fun1() is non-virtual> >// in base> >p->zabawa_1();> >// Late binding (RTP)> >p->zabawa_2();> >// Late binding (RTP)> >p->zabawa_3();> >// Late binding (RTP)> >p->zabawa_4();> >// Early binding but this function call is> >// illegal (produces error) because pointer> >// is of base type and function is of> >// derived class> >// p->zabawa_4(5);> >return> 0;> }> |
data lokalna
>
>Wyjście
base-1 derived-2 base-3 base-4>
Wyjaśnienie: Początkowo tworzymy wskaźnik typu klasa bazowa i inicjujemy go adresem obiektu klasy pochodnej. Kiedy tworzymy obiekt klasy pochodnej, kompilator tworzy wskaźnik jako element danych klasy zawierający adres VTABLE klasy pochodnej.
Podobna koncepcja Późne i wczesne wiązanie jest używany jak w powyższym przykładzie. W przypadku wywołania funkcji fun_1() wywoływana jest wersja funkcji w klasie bazowej, fun_2() jest zastępowana w klasie pochodnej, więc wywoływana jest wersja klasy pochodnej, fun_3() nie jest zastępowana w klasie pochodnej i jest funkcją wirtualną więc wywoływana jest wersja klasy bazowej, podobnie fun_4() nie jest zastępowana, więc wywoływana jest wersja klasy bazowej.
Notatka: fun_4(int) w klasie pochodnej różni się od funkcji wirtualnej fun_4() w klasie bazowej, ponieważ prototypy obu funkcji są różne.
Ograniczenia funkcji wirtualnych
- Wolniej: Wywołanie funkcji trwa nieco dłużej ze względu na mechanizm wirtualny i utrudnia optymalizację kompilatorowi, ponieważ nie wie on dokładnie, która funkcja zostanie wywołana w czasie kompilacji. Trudne do debugowania: w złożonym systemie funkcje wirtualne mogą nieco utrudnić ustalenie, skąd funkcja jest wywoływana.