Virtuální funkce v C++
Virtuální funkce (také známá jako virtuální metody) je členská funkce, která je deklarována v rámci základní třídy a je předefinována (přepsána) odvozenou třídou. Když odkazujete na objekt odvozené třídy pomocí ukazatele nebo odkazu na základní třídu, můžete pro tento objekt zavolat virtuální funkci a spustit verzi metody odvozené třídy.
- Virtuální funkce zajišťují volání správné funkce pro objekt bez ohledu na typ odkazu (nebo ukazatele) použitého pro volání funkce.
- Používají se hlavně k dosažení Runtime polymorfismu.
- Funkce jsou deklarovány pomocí a virtuální klíčové slovo v základní třídě.
- Řešení volání funkce se provádí za běhu.
Pravidla pro virtuální funkce
Pravidla pro virtuální funkce v C++ jsou následující:
- Virtuální funkce nemohou být statické.
- Virtuální funkce může být funkce přítele jiné třídy.
- Virtuální funkce by měly být přístupné pomocí ukazatele nebo reference typu základní třídy, aby se dosáhlo polymorfismu za běhu.
- Prototyp virtuálních funkcí by měl být stejný v základní i odvozené třídě.
- Jsou vždy definovány v základní třídě a přepsány v odvozené třídě. Není povinné, aby odvozená třída přepsala (nebo předefinovala virtuální funkci), v takovém případě se použije verze základní třídy funkce.
- Třída může mít virtuální destruktor, ale nemůže mít virtuální konstruktor.
Doba kompilace (časná vazba) Chování virtuálních funkcí VS runtime (pozdní vazba).
Zvažte následující jednoduchý program ukazující běhové chování virtuálních funkcí.
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->print();> > // Non-virtual function, binded at compile time> > bptr->show();> > return> 0;> }> |
Výstup
print derived class show base class
Vysvětlení: Runtime polymorfismu je dosaženo pouze prostřednictvím ukazatele (nebo odkazu) typu základní třídy. Ukazatel základní třídy může také ukazovat na objekty základní třídy i na objekty odvozené třídy. Ve výše uvedeném kódu obsahuje ukazatel základní třídy ‚bptr‘ adresu objektu ‚d‘ odvozené třídy.
Pozdní vazba (Runtime) se provádí v souladu s obsahem ukazatele (tj. umístění, na které ukazuje ukazatel) a časná vazba (doba kompilace) se provádí podle typu ukazatele, protože funkce print() je deklarována pomocí virtuálního klíčové slovo, takže bude svázáno za běhu (výstup je třída odvozená z tisku protože ukazatel ukazuje na objekt odvozené třídy) a show() je nevirtuální, takže bude vázán během kompilace (výstup je zobrazit základní třídu protože ukazatel je základního typu).
Poznámka: Pokud jsme vytvořili virtuální funkci v základní třídě a ta je přepsána v odvozené třídě, pak nepotřebujeme virtuální klíčové slovo v odvozené třídě, funkce jsou automaticky považovány za virtuální funkce v odvozené třídě.
Práce s virtuálními funkcemi (koncept VTABLE a VPTR)
Jak je uvedeno zde, pokud třída obsahuje virtuální funkci, pak samotný kompilátor dělá dvě věci.
- Pokud je vytvořen objekt této třídy, pak a virtuální ukazatel (VPTR) je vložen jako datový člen třídy, aby ukazoval na VTABLE této třídy. Pro každý nový vytvořený objekt je vložen nový virtuální ukazatel jako datový člen dané třídy.
- Bez ohledu na to, zda je objekt vytvořen nebo ne, třída obsahuje jako člen statické pole ukazatelů funkcí s názvem VTABLE . V buňkách této tabulky jsou uloženy adresy každé virtuální funkce obsažené v dané třídě.
Zvažte příklad níže:
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->fun_1();> > // Late binding (RTP)> > p->fun_2();> > // Late binding (RTP)> > p->fun_3();> > // Late binding (RTP)> > p->fun_4();> > // Early binding but this function call is> > // illegal (produces error) because pointer> > // is of base type and function is of> > // derived class> > // p->fun_4(5);> > return> 0;> }> |
Výstup
base-1 derived-2 base-3 base-4
Vysvětlení: Nejprve vytvoříme ukazatel typu základní třídy a inicializujeme jej adresou objektu odvozené třídy. Když vytvoříme objekt odvozené třídy, kompilátor vytvoří ukazatel jako datový člen třídy obsahující adresu VTABLE odvozené třídy.
Podobný koncept Pozdní a časná vazba se používá jako ve výše uvedeném příkladu. Pro volání funkce fun_1() se volá verze základní třídy funkce, fun_2() je přepsána v odvozené třídě, takže je volána verze odvozené třídy, fun_3() není v odvozené třídě přepsána a jde o virtuální funkci takže se volá verze základní třídy, podobně fun_4() není přepsáno, takže se volá verze základní třídy.
Poznámka: fun_4(int) v odvozené třídě se liší od virtuální funkce fun_4() v základní třídě, protože prototypy obou funkcí se liší.
Omezení virtuálních funkcí
- Pomalejší: Volání funkce trvá o něco déle kvůli virtuálnímu mechanismu a ztěžuje optimalizaci kompilátoru, protože přesně neví, která funkce bude v době kompilace volána. Obtížné ladění: Ve složitém systému mohou virtuální funkce trochu ztížit zjištění, odkud je funkce volána.