C++中的虚函数表介绍
????????在C++語言中,當(dāng)我們使用基類的引用或指針調(diào)用一個(gè)虛成員函數(shù)時(shí)會(huì)執(zhí)行動(dòng)態(tài)綁定。因?yàn)槲覀冎钡竭\(yùn)行時(shí)才能知道到底調(diào)用了哪個(gè)版本的虛函數(shù),所以所有虛函數(shù)都必須有定義。通常情況下,如果我們不使用某個(gè)函數(shù),則無須為該函數(shù)提供定義。但是我們必須為每一個(gè)虛函數(shù)都提供定義,而不管它是否被用到了,這是因?yàn)檫B編譯器也無法確定到底會(huì)使用哪個(gè)虛函數(shù)。虛函數(shù)的作用就是實(shí)現(xiàn)多態(tài)性。
????????對(duì)虛函數(shù)的調(diào)用可能在運(yùn)行時(shí)才被解析:當(dāng)某個(gè)虛函數(shù)通過指針或引用調(diào)用時(shí),編譯器產(chǎn)生的代碼直到運(yùn)行時(shí)才能確定應(yīng)該調(diào)用哪個(gè)版本的函數(shù)。被調(diào)用的函數(shù)是與綁定到指針或引用上的對(duì)象的動(dòng)態(tài)類型相匹配的那一個(gè)。
????????動(dòng)態(tài)綁定只有當(dāng)我們通過指針或引用調(diào)用虛函數(shù)時(shí)才會(huì)發(fā)生。當(dāng)我們通過一個(gè)具有普通類型(非引用非指針)的表達(dá)式調(diào)用虛函數(shù)時(shí),在編譯時(shí)就會(huì)將調(diào)用的版本確定下來。當(dāng)我們使用基類的引用或指針調(diào)用基類中定義的一個(gè)函數(shù)時(shí),我們并不知道該函數(shù)真正作用的對(duì)象是什么類型,因?yàn)樗赡苁且粋€(gè)基類的對(duì)象也可能是一個(gè)派生類的對(duì)象。如果該函數(shù)是虛函數(shù),則直到運(yùn)行時(shí)才會(huì)決定到底執(zhí)行哪個(gè)版本,判斷的依據(jù)是引用或指針?biāo)壎ǖ膶?duì)象的真實(shí)類型。對(duì)非虛函數(shù)的調(diào)用在編譯時(shí)進(jìn)行綁定。類似的,通過對(duì)象進(jìn)行的函數(shù)(虛函數(shù)或非虛函數(shù))調(diào)用也在編譯時(shí)綁定。對(duì)象的類型是確定不變的,我們無論如何都不可能令對(duì)象的動(dòng)態(tài)類型與靜態(tài)類型不一致。因此,通過對(duì)象進(jìn)行的函數(shù)調(diào)用將在編譯時(shí)綁定到該對(duì)象所屬類中的函數(shù)版本中。當(dāng)且僅當(dāng)對(duì)通過指針或引用調(diào)用虛函數(shù)時(shí),才會(huì)在運(yùn)行時(shí)解析該調(diào)用,也只有在這種情況下對(duì)象的動(dòng)態(tài)類型才有可能與靜態(tài)類型不同。
????????派生類中的虛函數(shù):當(dāng)我們?cè)谂缮愔懈采w了某個(gè)虛函數(shù)時(shí),可以再一次使用virtual關(guān)鍵字指出該函數(shù)的性質(zhì)。然而這么做并非必須,因?yàn)橐坏┠硞€(gè)函數(shù)被聲明成虛函數(shù),則在所有派生類中它都是虛函數(shù)。一個(gè)派生類的函數(shù)如果覆蓋了某個(gè)繼承而來的虛函數(shù),則它的形參類型必須與被它覆蓋的基類函數(shù)完全一致。同樣,派生類中虛函數(shù)的返回類型也必須與基類函數(shù)匹配。該規(guī)則存在一個(gè)例外,當(dāng)類的虛函數(shù)返回類型是類本身的指針或引用時(shí),上述規(guī)則無效。基類中的虛函數(shù)在派生類中隱含地也是一個(gè)虛函數(shù)。當(dāng)派生類覆蓋了某個(gè)虛函數(shù)時(shí),該函數(shù)在基類中的形參必須與派生類中的形參嚴(yán)格匹配。
????????虛函數(shù)與默認(rèn)實(shí)參:和其它函數(shù)一樣,虛函數(shù)也可以擁有默認(rèn)實(shí)參。如果某次函數(shù)調(diào)用使用了默認(rèn)實(shí)參,則該實(shí)參值由本次調(diào)用的靜態(tài)類型決定。換句話說,如果我們通過基類的引用或指針調(diào)用函數(shù),則使用基類中定義的默認(rèn)實(shí)參,即使實(shí)際運(yùn)行的是派生類中的函數(shù)版本也是如此。此時(shí),傳入派生類函數(shù)的將是基類函數(shù)定義的默認(rèn)實(shí)參。如果派生類函數(shù)依賴不同的實(shí)參,則程序結(jié)果將與我們的預(yù)期不符。如果虛函數(shù)使用默認(rèn)實(shí)參,則基類和派生類中定義的默認(rèn)實(shí)參最好一致。
????????回避虛函數(shù)的機(jī)制:在某些情況下,我們希望對(duì)虛函數(shù)的調(diào)用不要進(jìn)行動(dòng)態(tài)綁定,而是強(qiáng)迫其執(zhí)行虛函數(shù)的某個(gè)特定版本。使用作用域運(yùn)算符可以實(shí)現(xiàn)這一目的。通常情況下,只有成員函數(shù)(或友元)中的代碼才需要使用作用域運(yùn)算符來回避虛函數(shù)的機(jī)制。什么時(shí)候我們需要回避虛函數(shù)的默認(rèn)機(jī)制呢?通常是當(dāng)一個(gè)派生類的虛函數(shù)調(diào)用它覆蓋的基類的虛函數(shù)版本時(shí)。在此情況下,基類的版本通常完成繼承層次中所有類型都要做的共同任務(wù),而派生類中定義的版本需要執(zhí)行一些與派生類本身密切相關(guān)的操作。如果一個(gè)派生類虛函數(shù)需要調(diào)用它的基類版本,但是沒有使用作用域運(yùn)算符,則在運(yùn)行時(shí)該調(diào)用將被解析為對(duì)派生類版本自身的調(diào)用,從而導(dǎo)致無限遞歸。
??????? 純虛函數(shù):一個(gè)純虛函數(shù)無須定義。我們通過在函數(shù)體的位置(即在聲明語句的分號(hào)之前)書寫=0就可以將一個(gè)虛函數(shù)說明為純虛函數(shù)。其中,=0只能出現(xiàn)在類內(nèi)部的虛函數(shù)聲明語句處。我們也可以為純虛函數(shù)提供定義,不過函數(shù)體必須定義在類的外部。也就是說,我們不能在類的內(nèi)部為一個(gè)=0的函數(shù)提供函數(shù)體。
??????? 含有純虛函數(shù)的類是抽象基類:含有(或者未經(jīng)覆蓋直接繼承)純虛函數(shù)的類是抽象基類(abstract base class)。抽象基類負(fù)責(zé)定義接口,而后續(xù)的其它類可以覆蓋該接口。我們不能(直接)創(chuàng)建一個(gè)抽象基類的對(duì)象。
??????? 派生類構(gòu)造函數(shù)只初始化它的直接基類。
????????多重繼承(multiple inheritance):是指從多個(gè)直接基類中產(chǎn)生派生類的能力。多重繼承的派生類繼承了所有父類的屬性。
????????虛繼承(virtual inheritance):盡管在派生列表中同一個(gè)基類只能出現(xiàn)一次,但實(shí)際上派生類可以多次繼承同一個(gè)類。派生類可以通過它的兩個(gè)直接基類分別繼承同一個(gè)間接基類,也可以直接繼承某個(gè)基類,然后通過另一個(gè)基類再一次間接繼承該類。在默認(rèn)情況下,派生類中含有繼承鏈上每個(gè)類對(duì)應(yīng)的子部分。如果某個(gè)類在派生過程中出現(xiàn)了多次,則派生類中將包含該類的多個(gè)子對(duì)象。虛繼承的目的是令某個(gè)類做出聲明,承諾愿意共享它的基類。其中,共享的基類子對(duì)象稱為虛基類(virtual base class)。在這種機(jī)制下,不論虛基類在繼承體系中出現(xiàn)了多少次,在派生類中都只包含唯一一個(gè)共享的虛基類子對(duì)象。虛派生只影響從指定了虛基類的派生類中進(jìn)一步派生出的類,它不會(huì)影響派生類本身。
????????使用虛基類:我們指定虛基類的方式是在派生列表中添加關(guān)鍵字virtual。virtual說明符表明了一種愿望,即在后續(xù)的派生類當(dāng)中共享虛基類的同一份實(shí)例。至于什么樣的類能夠作為虛基類并沒有特殊規(guī)定。如果某個(gè)類指定了虛基類,則該類的派生仍按常規(guī)方式進(jìn)行。不論基類是不是虛基類,派生類對(duì)象都能被可訪問基類的指針或引用操作。
????????虛基類成員的可見性:因?yàn)樵诿總€(gè)共享的虛基類中只有唯一一個(gè)共享的子對(duì)象,所以該基類的成員可以被直接訪問,并且不會(huì)產(chǎn)生二義性。此外,如果虛基類的成員只被一條派生路徑覆蓋,則我們?nèi)匀豢梢灾苯釉L問這個(gè)被覆蓋的成員。但是如果成員被多余一個(gè)基類覆蓋,則一般情況下派生類必須為該成員自定義一個(gè)新的版本。
????????在虛派生中,虛基類是由最低層的派生類初始化的。
????????虛繼承的對(duì)象的構(gòu)造方式:含有虛基類的對(duì)象的構(gòu)造順序與一般的順序稍有區(qū)別:首先使用提供給最低層派生類構(gòu)造函數(shù)的初始值初始化該對(duì)象的虛基類子部分,接下來按照直接基類在派生列表中出現(xiàn)的次序依次對(duì)其進(jìn)行初始化。虛基類總是先于非虛基類構(gòu)造,與它們?cè)诶^承體系中的次序和位置無關(guān)。
????????構(gòu)造函數(shù)與析構(gòu)函數(shù)的次序:一個(gè)類可以有多個(gè)虛基類。此時(shí),這些虛的子對(duì)象按照它們?cè)谂缮斜碇谐霈F(xiàn)的順序從左向右依次構(gòu)造。和往常一樣,對(duì)象的銷毀順序與構(gòu)造順序正好相反。
????????每個(gè)含有虛函數(shù)的類有一張?zhí)摵瘮?shù)表,表中的每一項(xiàng)是一個(gè)虛函數(shù)的地址。虛函數(shù)表的指針占4個(gè)字節(jié)大小,存在于對(duì)象實(shí)例中最前面的位置。我們通過對(duì)象實(shí)例的地址得到這張?zhí)摵瘮?shù)表,然后就可以遍歷其中的函數(shù)指針,并調(diào)用相應(yīng)的函數(shù)。
????????類的虛函數(shù)表是一塊連續(xù)的內(nèi)存,每個(gè)內(nèi)存單元中記錄一個(gè)JMP指令的地址。編譯器會(huì)為每個(gè)有虛函數(shù)的類創(chuàng)建一個(gè)虛函數(shù)表,該虛函數(shù)表將被該類的所有對(duì)象共享。類的每個(gè)虛成員占據(jù)虛函數(shù)表中的一行。
????????虛函數(shù)(Virtual Function)是通過一張?zhí)摵瘮?shù)表(VirtualTable)來實(shí)現(xiàn)的,簡稱為V-Table。
????????虛函數(shù)表的結(jié)構(gòu):它是一個(gè)函數(shù)指針表,每一個(gè)表項(xiàng)都指向一個(gè)函數(shù)。任何一個(gè)包含至少一個(gè)虛函數(shù)的類都會(huì)有這樣一張表。Virtual Table只包含虛函數(shù)的指針,沒有函數(shù)體。每個(gè)派生類的Virtual Table繼承了它各個(gè)基類的Virtual Table,如果基類Virtual Table中包含某一項(xiàng),則其派生類的Virtual Table中也將包含同樣的一項(xiàng),但是兩項(xiàng)的值可能不同。如果派生類覆蓋了該項(xiàng)對(duì)應(yīng)的虛函數(shù),則派生類Virtual Table的該項(xiàng)指向覆蓋后的虛函數(shù),沒有覆蓋的話,則沿用基類的值。
????????每一個(gè)類只有唯一的一個(gè)Virtual Table,不是每個(gè)對(duì)象都有一個(gè)VirtualTable。虛函數(shù)表是屬于類的,而不是屬于某個(gè)具體的對(duì)象,一個(gè)類只需要一個(gè)虛函數(shù)表即可。同一個(gè)類的所有對(duì)象都使用同一個(gè)虛函數(shù)表。Virtual Table是編譯期間建立,執(zhí)行期間查表執(zhí)行。
????????在C++的標(biāo)準(zhǔn)規(guī)格說明書中說到,編譯器必需要保證虛函數(shù)表的指針存在于對(duì)象實(shí)例中最前面的位置(這是為了保證正確取到虛函數(shù)的偏移量)。所以,在類對(duì)象的內(nèi)存布局中,首先是該類的Virtual Table指針,然后才是對(duì)象數(shù)據(jù)。這意味著我們通過對(duì)象實(shí)例的地址得到這張?zhí)摵瘮?shù)表,然后就可以遍歷其中函數(shù)指針,并調(diào)用相應(yīng)的函數(shù)。
????????虛函數(shù)表是一個(gè)指針數(shù)組,其元素是虛函數(shù)的指針,每個(gè)元素對(duì)應(yīng)一個(gè)虛函數(shù)的函數(shù)指針。需要指出的是,普通的函數(shù)即非虛函數(shù),其調(diào)用并不需要經(jīng)過虛函數(shù)表,所以虛函數(shù)表的元素并不包括普通函數(shù)的函數(shù)指針。虛函數(shù)表內(nèi)的條目,即虛函數(shù)指針的賦值發(fā)生在編譯器的編譯階段,也就是說在代碼的編譯階段,虛函數(shù)表就可以構(gòu)造出來了。
????????為了指定對(duì)象的虛函數(shù)表,對(duì)象內(nèi)部包含一個(gè)虛函數(shù)表的指針,來指向自己所使用的虛函數(shù)表。為了讓每個(gè)包含虛函數(shù)表的類的對(duì)象都擁有一個(gè)虛函數(shù)表指針,編譯器在類中添加了一個(gè)指針即*__vptr,用來指向虛函數(shù)表。這樣,當(dāng)類的對(duì)象在創(chuàng)建時(shí)便擁有了這個(gè)指針,且這個(gè)指針的值會(huì)自動(dòng)被設(shè)置為指向類的虛函數(shù)表。
????????C++中的虛函數(shù)(Virtual Function)是用來實(shí)現(xiàn)動(dòng)態(tài)多態(tài)性的,指的是當(dāng)基類指針指向其派生類實(shí)例時(shí),可以用基類指針調(diào)用派生類中的成員函數(shù)。如果基類指針指向不同的派生類,則它調(diào)用同一個(gè)函數(shù)就可以實(shí)現(xiàn)不同的邏輯,這種機(jī)制可以讓基類指針有”多種形態(tài)”,它的實(shí)現(xiàn)依賴于虛函數(shù)表。虛函數(shù)表(Virtual Table)是指在每個(gè)包含虛函數(shù)的類中都存在著一個(gè)函數(shù)地址的數(shù)組。
????A virtual methodtable (VMT), virtual function table, virtual call table, dispatch table,vtable, or vftable is a mechanism used in a programming language to support dynamic dispatch (or run-time method binding).
????????Whenever a class defines a virtual function(ormethod), most compilers add a hidden member variable to the class which pointsto an array of pointers to (virtual) functions called the virtual method table.These pointers are used at runtime to invoke the appropriate function implementations, because at compile time it may not yet be known if the base function is to be called or a derived one implemented by a class that inheritsfrom the base class.
????????An object's virtual method table will contain the addresses of the object's dynamically bound methods. Method calls are performedby fetching the method's address from the object's virtual method table. The virtual method table is the same for all objects belonging to the same class,and is therefore typically shared between them.Objects belonging to type-compatible classes (for example siblings in an inheritance hierarchy) will have virtual method tables with the same layout: the address of a given method will appear at the same offset for all type-compatible classes. Thus, fetching the method's address from a given offset into a virtual method table will get the method corresponding to the object's actual class.
????????To implement virtual functions, C++ uses a special form of late binding known as the virtual table. The virtual table is a lookup table of functions used to resolve function calls in a dynamic/late binding manner. The virtual table sometimes goes by other names,such as “vtable”, “virtual function table”, “virtual method table”, or“dispatch table”.
????????First, every class that uses virtual functions (or is derived from a class that uses virtual functions) is given its own virtual table. This table is simply a static array that the compiler sets up at compile time. A virtual table contains one entry for each virtual function that can be called by objects of the class. Each entry in this table is simply a function pointer that points to the most-derived function accessible by that class. Second, the compiler also adds a hidden pointer to the base class, which we will call *__vptr. *__vptr is set(automatically) when a class instance is created so that it points to the virtual table for that class. Unlike the *this pointer, which is actually a function parameter used by the compiler to resolve self-references, *__vptr isa real pointer. Consequently, it makes each class object allocated bigger by the size of one pointer. It also means that *__vptr is inherited by derived classes, which is important.
????????Calling a virtual function is slower than calling anon-virtual function for a couple of reasons:First, we have to use the *__vptr to get to the appropriate virtual table.Second, we have to index the virtual table to find the correct function to call. Only then can we call the function. As a result, we have to do 3 operations to find the function to call, as opposed to 2 operations for a normal indirect function call, or one operation for a direct function call.However, with modern computers, this added time is usually fairly insignificant.
????????The virtual table is a structure used at runtime to resolve function calls. But it doesn't control access -- the compiler handles whether you should or should not be able to call a function.
????? ? 以上內(nèi)容主要摘自:《C++Primer(Fifth Edition 中文版)》、Learn C++? 、?陳皓 Blog?
????? ? 測(cè)試代碼:
#include "virtual_function_table.hpp"
#include <iostream>// Blog: http://blog.csdn.net/fengbingchun/article/details/79592347namespace virtual_function_table_ {// reference: http://www.learncpp.com/cpp-tutorial/125-the-virtual-table/
class Base {
public:Base() { fprintf(stdout, "Base::Base\n"); }virtual void function1() { fprintf(stdout, "Base::function1\n"); }virtual void function2() { fprintf(stdout, "Base::function2\n"); }void f1() { fprintf(stdout, "Base::f1\n"); }virtual ~Base() { fprintf(stdout, "Base::~Base\n"); }
};class D1 : public Base {
public:D1() { fprintf(stdout, "D1::D1\n"); }virtual void function1() override { fprintf(stdout, "D1::function1\n"); }virtual void function3() { fprintf(stdout, "D1::function3\n"); }void f2() { fprintf(stdout, "D1::f2\n"); }virtual ~D1() { fprintf(stdout, "D1::~D1\n"); }
};class D2 : public Base {
public:D2() { fprintf(stdout, "D2::D2\n"); }virtual void function2() override { fprintf(stdout, "D2::function2\n"); }void f3() { fprintf(stdout, "D2::f3\n"); }virtual ~D2() { fprintf(stdout, "D2::~D2\n"); }
};class D3 {
public:D3() { fprintf(stdout, "D3::D3\n"); }void f4() { fprintf(stdout, "D3::f4\n"); }~D3() { fprintf(stdout, "D3::~D3\n"); }
};int test_virtual_function_table_1()
{D1* p1 = new D1();fprintf(stdout, "p1 address: %p\n", (void*)p1);Base* b = p1;fprintf(stdout, "b address: %p\n", (void*)b);b->function1();b->function2();// 任何妄圖使用父類指針想調(diào)用子類中的未覆蓋父類的成員函數(shù)的行為都會(huì)被編譯器視為非法//b->function3(); // Error: class "virtual _function_table_::Base" 沒有成員 "function3"delete p1;fprintf(stdout, "\n");Base* p2 = new D1();fprintf(stdout, "p2 address: %p\n", (void*)p2);p2->function1();p2->function2();delete p2;fprintf(stdout, "\n");D1 d1 = D1();fprintf(stdout, "d1 address: %p\n", (void*)&d1);d1.function1();d1.function2();fprintf(stdout, "\n");D2 d2 = D2();fprintf(stdout, "d2 address: %p\n", (void*)&d2);Base* b2 = &d2;fprintf(stdout, "b2 address: %p\n", (void*)b2);b2->function1();b2->function2();fprintf(stdout, "\n");D3 d3 = D3();fprintf(stdout, "\n");return 0;
}} // namespace virtual_function_table_
?
????? ? 以上測(cè)試代碼,可以在VS2013的debug模式下,通過局部變量窗口查看相應(yīng)變量的虛函數(shù)表的信息,如下:
?
????? ? GitHub:https://github.com/fengbingchun/Messy_Test??
總結(jié)
以上是生活随笔為你收集整理的C++中的虚函数表介绍的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: K-均值聚类(K-Means) C++代
- 下一篇: 深度学习中的随机梯度下降(SGD)简介