Funkcja wirtualna w C++
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.
C++
// 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).
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:
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;> }> |
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.