runtime polymorphism c
Szczegółowe badanie polimorfizmu środowiska uruchomieniowego w C ++.
Polimorfizm w czasie wykonywania jest również znany jako polimorfizm dynamiczny lub późne wiązanie. W polimorfizmie w czasie wykonywania wywołanie funkcji jest rozwiązywane w czasie wykonywania.
W przeciwieństwie do czasu kompilacji lub statycznego polimorfizmu, kompilator dedukuje obiekt w czasie wykonywania, a następnie decyduje, które wywołanie funkcji ma zostać powiązane z obiektem. W C ++ polimorfizm środowiska uruchomieniowego jest implementowany przy użyciu przesłaniania metody.
W tym samouczku omówimy szczegółowo polimorfizm w czasie wykonywania.
co to jest funkcja znajomego w C ++
=> Sprawdź WSZYSTKIE samouczki C ++ tutaj.
Czego się nauczysz:
- Funkcja zastępująca
- Funkcja wirtualna
- Działanie wirtualnego stołu i _vptr
- Czyste funkcje wirtualne i klasa abstrakcyjna
- Wirtualne niszczyciele
- Wniosek
- rekomendowane lektury
Funkcja zastępująca
Nadpisywanie funkcji to mechanizm, za pomocą którego funkcja zdefiniowana w klasie bazowej jest ponownie definiowana w klasie pochodnej. W tym przypadku mówimy, że funkcja jest zastępowana w klasie pochodnej.
Powinniśmy pamiętać, że przesłanianie funkcji nie może być wykonywane w klasie. Funkcja jest zastępowana tylko w klasie pochodnej. Dlatego dziedziczenie powinno być obecne w celu przesłaniania funkcji.
Po drugie, funkcja z klasy bazowej, którą nadpisujemy, powinna mieć ten sam podpis lub prototyp, tj. Powinna mieć taką samą nazwę, ten sam typ zwracania i taką samą listę argumentów.
Zobaczmy przykład, który demonstruje przesłanianie metody.
#include using namespace std; class Base { public: void show_val() { cout << 'Class::Base'< Wynik:
Klasa :: Podstawa
Klasa :: Pochodne
W powyższym programie mamy klasę bazową i pochodną. W klasie bazowej mamy funkcję show_val, która jest nadpisywana w klasie pochodnej. W funkcji głównej tworzymy obiekt każdej z klas Base i Derived i wywołujemy funkcję show_val z każdym obiektem. Daje pożądaną wydajność.
Powyższe powiązanie funkcji korzystających z obiektów każdej klasy jest przykładem powiązania statycznego.
Zobaczmy teraz, co się stanie, gdy użyjemy wskaźnika klasy bazowej i przypiszemy jako zawartość obiekty klasy pochodnej.
Przykładowy program przedstawiono poniżej:
#include using namespace std; class Base { public: void show_val() { cout << 'Class::Base'; } }; class Derived:public Base { public: void show_val() //overridden function { cout <<'Class::Derived'; } }; int main() { Base* b; //Base class pointer Derived d; //Derived class object b = &d; b->show_val(); //Early Binding }
Wynik:
Klasa :: Podstawa
Teraz widzimy, że wyjście to „Class :: Base”. Zatem niezależnie od tego, jaki obiekt typu przechowuje wskaźnik bazowy, program wyprowadza zawartość funkcji klasy, której typem jest wskaźnik bazowy. W tym przypadku wykonywane jest również linkowanie statyczne.
Aby uzyskać wynik wskaźnika bazowego, poprawną zawartość i poprawne linkowanie, przechodzimy do dynamicznego wiązania funkcji. Osiąga się to za pomocą mechanizmu funkcji wirtualnych, który jest wyjaśniony w następnej sekcji.
Funkcja wirtualna
Aby przesłonięta funkcja powinna być dynamicznie powiązana z ciałem funkcji, uczynimy funkcję klasy bazowej wirtualną za pomocą słowa kluczowego „virtual”. Ta funkcja wirtualna jest funkcją, która jest zastępowana w klasie pochodnej, a kompilator wykonuje późne lub dynamiczne wiązanie dla tej funkcji.
Teraz zmodyfikujmy powyższy program, aby zawierał wirtualne słowo kluczowe w następujący sposób:
#include using namespace std;. class Base { public: virtual void show_val() { cout << 'Class::Base'; } }; class Derived:public Base { public: void show_val() { cout <<'Class::Derived'; } }; int main() { Base* b; //Base class pointer Derived d; //Derived class object b = &d; b->show_val(); //late Binding }
Wynik:
Klasa :: Pochodne
W powyższej definicji klasy Base utworzyliśmy funkcję show_val jako „wirtualną”. Ponieważ funkcja klasy bazowej jest wirtualna, kiedy przypisujemy obiekt klasy pochodnej do wskaźnika klasy bazowej i wywołujemy funkcję show_val, powiązanie następuje w czasie wykonywania.
Tak więc, ponieważ wskaźnik klasy bazowej zawiera obiekt klasy pochodnej, treść funkcji show_val w klasie pochodnej jest powiązana z funkcją show_val, a tym samym z danymi wyjściowymi.
W C ++ przesłonięta funkcja w klasie pochodnej może być również private. Kompilator sprawdza typ obiektu tylko w czasie kompilacji i wiąże funkcję w czasie wykonywania, więc nie robi to żadnej różnicy, nawet jeśli funkcja jest publiczna czy prywatna.
Zauważ, że jeśli funkcja jest zadeklarowana jako wirtualna w klasie bazowej, będzie wirtualna we wszystkich klasach pochodnych.
Ale do tej pory nie rozmawialiśmy o tym, jak dokładnie funkcje wirtualne odgrywają rolę w identyfikowaniu właściwej funkcji, która ma zostać związana, lub innymi słowy, jak późne wiązanie faktycznie ma miejsce.
Funkcja wirtualna jest dokładnie powiązana z treścią funkcji w czasie wykonywania przy użyciu koncepcji wirtualna tabela (VTABLE) i ukryty wskaźnik o nazwie _vptr.
Obie te koncepcje są wdrożeniem wewnętrznym i nie mogą być używane bezpośrednio przez program.
Działanie wirtualnego stołu i _vptr
Najpierw zrozumiemy, czym jest wirtualna tabela (VTABLE).
Kompilator w czasie kompilacji ustawia po jednej tabeli VTABLE dla każdej klasy mającej funkcje wirtualne, jak również dla klas wyprowadzonych z klas posiadających funkcje wirtualne.
VTABLE zawiera wpisy będące wskaźnikami funkcji do funkcji wirtualnych, które mogą być wywoływane przez obiekty klasy. Dla każdej funkcji wirtualnej istnieje jeden wpis wskaźnika funkcji.
W przypadku czystych funkcji wirtualnych ten wpis ma wartość NULL. (To jest powód, dla którego nie możemy utworzyć instancji klasy abstrakcyjnej).
Następna jednostka, _vptr, która jest nazywana wskaźnikiem vtable, jest ukrytym wskaźnikiem, który kompilator dodaje do klasy bazowej. Ten _vptr wskazuje na vtable klasy. Wszystkie klasy pochodne tej klasy bazowej dziedziczą _vptr.
Każdy obiekt klasy zawierającej funkcje wirtualne wewnętrznie przechowuje ten _vptr i jest przezroczysty dla użytkownika. Każde wywołanie funkcji wirtualnej za pomocą obiektu jest następnie rozwiązywane za pomocą tego _vptr.
Weźmy przykład, aby zademonstrować działanie vtable i _vtr.
#include using namespace std; class Base_virtual { public: virtual void function1_virtual() {cout<<'Base :: function1_virtual()
';}; virtual void function2_virtual() {cout<<'Base :: function2_virtual()
';}; virtual ~Base_virtual(){}; }; class Derived1_virtual: public Base_virtual { public: ~Derived1_virtual(){}; virtual void function1_virtual() { coutfunction2_virtual(); delete (b); return (0); }
Wynik:
Derived1_virtual :: function1_virtual ()
Base :: function2_virtual ()
jakie jest najlepsze oprogramowanie do rozpoznawania głosu
W powyższym programie mamy klasę bazową z dwiema wirtualnymi funkcjami i wirtualnym destruktorem. Wyprowadziliśmy również klasę z klasy bazowej iw tym; nadpisaliśmy tylko jedną funkcję wirtualną. W funkcji głównej wskaźnik klasy pochodnej jest przypisywany do wskaźnika podstawowego.
Następnie wywołujemy obie funkcje wirtualne za pomocą wskaźnika klasy bazowej. Widzimy, że nadpisana funkcja jest wywoływana, gdy jest wywoływana, a nie funkcja podstawowa. Podczas gdy w drugim przypadku, ponieważ funkcja nie jest nadpisywana, wywoływana jest funkcja klasy bazowej.
Zobaczmy teraz, jak powyższy program jest reprezentowany wewnętrznie za pomocą vtable i _vptr.
Zgodnie z wcześniejszym wyjaśnieniem, ponieważ istnieją dwie klasy z funkcjami wirtualnymi, będziemy mieć dwie tabele vtables - po jednej dla każdej klasy. Ponadto _vptr będzie obecny dla klasy bazowej.

Powyżej pokazane jest obrazowe przedstawienie tego, jak będzie wyglądał układ tabeli vtable dla powyższego programu. Tabela vtable dla klasy bazowej jest prosta. W przypadku klasy pochodnej nadpisywana jest tylko funkcja function1_virtual.
Stąd widzimy, że w klasie pochodnej vtable wskaźnik funkcji dla function1_virtual wskazuje na nadpisaną funkcję w klasie pochodnej. Z drugiej strony wskaźnik funkcji dla function2_virtual wskazuje na funkcję w klasie bazowej.
Zatem w powyższym programie, gdy wskaźnik bazowy jest przypisany do obiektu klasy pochodnej, wskaźnik bazowy wskazuje na _vptr klasy pochodnej.
Więc kiedy wywoływane jest wywołanie b-> function1_virtual (), wywoływana jest funkcja function1_virtual z klasy pochodnej, a gdy wywołanie funkcji b-> function2_virtual (), ponieważ ten wskaźnik funkcji wskazuje na funkcję klasy bazowej, funkcja klasy bazowej jest nazywany.
Czyste funkcje wirtualne i klasa abstrakcyjna
Widzieliśmy szczegóły dotyczące funkcji wirtualnych w C ++ w naszej poprzedniej sekcji. W C ++ możemy również zdefiniować „ czysta funkcja wirtualna ”, Które jest zwykle równe zeru.
Czysta funkcja wirtualna jest zadeklarowana, jak pokazano poniżej.
virtual return_type function_name(arg list) = 0;
Klasa, która ma co najmniej jedną czystą funkcję wirtualną nazywaną „ Klasa abstrakcyjna ”. Nigdy nie możemy utworzyć instancji klasy abstrakcyjnej, tj. Nie możemy stworzyć obiektu klasy abstrakcyjnej.
Dzieje się tak, ponieważ wiemy, że dla każdej funkcji wirtualnej w tabeli VTABLE (tabela wirtualna) dokonywany jest wpis. Ale w przypadku czystej funkcji wirtualnej ten wpis jest bez adresu, co powoduje, że jest niekompletny. Dlatego kompilator nie pozwala na utworzenie obiektu dla klasy z niepełnym wpisem VTABLE.
To jest powód, dla którego nie możemy utworzyć instancji klasy abstrakcyjnej.
Poniższy przykład zademonstruje czystą funkcję wirtualną oraz klasę abstrakcyjną.
#include using namespace std; class Base_abstract { public: virtual void print() = 0; // Pure Virtual Function }; class Derived_class:public Base_abstract { public: void print() { cout <<'Overriding pure virtual function in derived class
'; } }; int main() { // Base obj; //Compile Time Error Base_abstract *b; Derived_class d; b = &d; b->print(); }
Wynik:
Zastępowanie czystej funkcji wirtualnej w klasie pochodnej
W powyższym programie mamy klasę zdefiniowaną jako Base_abstract, która zawiera czystą funkcję wirtualną, co czyni ją klasą abstrakcyjną. Następnie tworzymy klasę „Derived_class” z Base_abstract i nadpisujemy w niej czystą wirtualną funkcję print.
W funkcji głównej nie jest komentowana pierwsza linia. Dzieje się tak, ponieważ jeśli go odkomentujemy, kompilator wyświetli błąd, ponieważ nie możemy utworzyć obiektu dla klasy abstrakcyjnej.
Ale druga linia dalej kod działa. Możemy z powodzeniem utworzyć wskaźnik klasy bazowej, a następnie przypisać do niego obiekt klasy pochodnej. Następnie wywołujemy funkcję print, która wypisuje zawartość funkcji print nadpisanej w klasie pochodnej.
Wymieńmy w skrócie niektóre cechy klasy abstrakcyjnej:
- Nie możemy utworzyć instancji klasy abstrakcyjnej.
- Klasa abstrakcyjna zawiera co najmniej jedną czystą funkcję wirtualną.
- Chociaż nie możemy utworzyć instancji klasy abstrakcyjnej, zawsze możemy utworzyć wskaźniki lub odwołania do tej klasy.
- Klasa abstrakcyjna może mieć pewne implementacje, takie jak właściwości i metody, a także czyste funkcje wirtualne.
- Kiedy wyprowadzamy klasę z klasy abstrakcyjnej, klasa pochodna powinna przesłonić wszystkie czyste funkcje wirtualne w klasie abstrakcyjnej. W przeciwnym razie klasa pochodna będzie również klasą abstrakcyjną.
Wirtualne niszczyciele
Destruktory tej klasy można zadeklarować jako wirtualne. Za każdym razem, gdy wykonujemy upcast, tj. Przypisujemy obiekt klasy pochodnej do wskaźnika klasy bazowej, zwykłe destruktory mogą dawać niedopuszczalne wyniki.
Na przykład,rozważ następujące upcasting zwykłego destruktora.
#include using namespace std; class Base { public: ~Base() { cout << 'Base Class:: Destructor
'; } }; class Derived:public Base { public: ~Derived() { cout<< 'Derived class:: Destructor
'; } }; int main() { Base* b = new Derived; // Upcasting delete b; }
Wynik:
jak otworzyć plik .dat?
Klasa podstawowa :: Destructor
W powyższym programie mamy odziedziczoną klasę pochodną z klasy bazowej. Przede wszystkim przypisujemy obiekt klasy pochodnej do wskaźnika klasy bazowej.
Idealnie byłoby, gdyby destruktor wywoływany podczas wywoływania funkcji „usuń b” był tym z klasy pochodnej, ale na podstawie danych wyjściowych można zobaczyć, że destruktor klasy bazowej jest wywoływany, ponieważ wskaźnik klasy bazowej wskazuje na to.
Z tego powodu destruktor klasy pochodnej nie jest wywoływany, a obiekt klasy pochodnej pozostaje nienaruszony, powodując przeciek pamięci. Rozwiązaniem tego problemu jest uczynienie konstruktora klasy bazowej wirtualnym, tak aby wskaźnik obiektu wskazywał na poprawny destruktor i przeprowadzane było właściwe niszczenie obiektów.
Poniższy przykład ilustruje użycie wirtualnego destruktora.
#include using namespace std; class Base { public: virtual ~Base() { cout << 'Base Class:: Destructor
'; } }; class Derived:public Base { public: ~Derived() { cout<< 'Derived class:: Destructor
'; } }; int main() { Base* b = new Derived; // Upcasting delete b; }
Wynik:
Klasa pochodna :: Destructor
Klasa podstawowa :: Destructor
Jest to ten sam program, co poprzedni, z tą różnicą, że dodaliśmy wirtualne słowo kluczowe przed destruktorem klasy bazowej. Tworząc wirtualny destruktor klasy bazowej, osiągnęliśmy pożądany wynik.
Widzimy, że kiedy przypisujemy obiekt klasy pochodnej do wskaźnika klasy bazowej, a następnie usuwamy wskaźnik klasy bazowej, destruktory są wywoływane w odwrotnej kolejności tworzenia obiektów. Oznacza to, że najpierw wywoływany jest destruktor klasy pochodnej, obiekt jest niszczony, a następnie niszczony jest obiekt klasy bazowej.
Uwaga: W C ++ konstruktory nigdy nie mogą być wirtualne, ponieważ konstruktorzy są zaangażowani w konstruowanie i inicjowanie obiektów. Dlatego potrzebujemy, aby wszystkie konstruktory były całkowicie wykonane.
Wniosek
Polimorfizm w czasie wykonywania jest implementowany przy użyciu przesłaniania metody. Działa to dobrze, gdy wywołujemy metody z odpowiednimi obiektami. Ale kiedy mamy wskaźnik klasy bazowej i wywołujemy przesłonięte metody przy użyciu wskaźnika klasy bazowej wskazującego na obiekty klasy pochodnej, pojawiają się nieoczekiwane wyniki z powodu łączenia statycznego.
Aby temu zaradzić, używamy koncepcji funkcji wirtualnych. Dzięki wewnętrznej reprezentacji vtables i _vptr funkcje wirtualne pomagają nam dokładnie wywoływać pożądane funkcje. W tym samouczku omówiliśmy szczegółowo polimorfizm środowiska uruchomieniowego używanego w C ++.
Na tym kończymy nasze samouczki dotyczące programowania obiektowego w C ++. Mamy nadzieję, że ten samouczek będzie pomocny w lepszym i dokładnym zrozumieniu koncepcji programowania obiektowego w C ++.
=> Odwiedź tutaj, aby nauczyć się C ++ od podstaw.
rekomendowane lektury
- Polimorfizm w C ++
- Dziedziczenie w C ++
- Funkcje znajomego w C ++
- Klasy i obiekty w C ++
- Wykorzystanie klasy Selenium Select do obsługi elementów rozwijanych na stronie internetowej - Samouczek Selenium nr 13
- Samouczek dotyczący głównych funkcji języka Python z praktycznymi przykładami
- Wirtualna maszyna Java: jak JVM pomaga w uruchamianiu aplikacji Java
- Jak skonfigurować pliki skryptów LoadRunner VuGen i ustawienia środowiska wykonawczego