詳解C++虛函數(shù)的工作原理
靜態(tài)綁定與動(dòng)態(tài)綁定
討論靜態(tài)綁定與動(dòng)態(tài)綁定,首先需要理解的是綁定,何為綁定?函數(shù)調(diào)用與函數(shù)本身的關(guān)聯(lián),以及成員訪問(wèn)與變量?jī)?nèi)存地址間的關(guān)系,稱為綁定。 理解了綁定后再理解靜態(tài)與動(dòng)態(tài)。
- 靜態(tài)綁定:指在程序編譯過(guò)程中,把函數(shù)調(diào)用與響應(yīng)調(diào)用所需的代碼結(jié)合的過(guò)程,稱為靜態(tài)綁定。發(fā)生在編譯期。
- 動(dòng)態(tài)綁定:指在執(zhí)行期間判斷所引用對(duì)象的實(shí)際類型,根據(jù)實(shí)際的類型調(diào)用其相應(yīng)的方法。程序運(yùn)行過(guò)程中,把函數(shù)調(diào)用與響應(yīng)調(diào)用所需的代碼相結(jié)合的過(guò)程稱為動(dòng)態(tài)綁定。發(fā)生于運(yùn)行期。
C++中動(dòng)態(tài)綁定
在C++中動(dòng)態(tài)綁定是通過(guò)虛函數(shù)實(shí)現(xiàn)的,是多態(tài)實(shí)現(xiàn)的具體形式。而虛函數(shù)是通過(guò)虛函數(shù)表實(shí)現(xiàn)的。這個(gè)表中記錄了虛函數(shù)的地址,解決繼承、覆蓋的問(wèn)題,保證動(dòng)態(tài)綁定時(shí)能夠根據(jù)對(duì)象的實(shí)際類型調(diào)用正確的函數(shù)。這個(gè)虛函數(shù)表在什么地方呢?C++標(biāo)準(zhǔn)規(guī)格說(shuō)明書(shū)中說(shuō)到,編譯器必須要保證虛函數(shù)表的指針存在于對(duì)象實(shí)例中最前面的位置(這是為了保證正確取到虛函數(shù)的偏移量)。也就是說(shuō),我們可以通過(guò)對(duì)象實(shí)例的地址得到這張?zhí)摵瘮?shù)表,然后可以遍歷其中的函數(shù)指針,并調(diào)用相應(yīng)的函數(shù)。
虛函數(shù)的工作原理
要想弄明白動(dòng)態(tài)綁定,就必須弄懂虛函數(shù)的工作原理。C++中虛函數(shù)的實(shí)現(xiàn)一般是通過(guò)虛函數(shù)表實(shí)現(xiàn)的(C++規(guī)范中沒(méi)有規(guī)定具體用哪種方法,但大部分的編譯器廠商都選擇此方法)。類的虛函數(shù)表是一塊連續(xù)的內(nèi)存,每個(gè)內(nèi)存單元中記錄一個(gè)JMP指令的地址。編譯器會(huì)為每個(gè)有虛函數(shù)的類創(chuàng)建一個(gè)虛函數(shù)表,該虛函數(shù)表將被該類的所有對(duì)象共享。 類的每個(gè)虛成員占據(jù)虛函數(shù)表中的一行。如果類中有N個(gè)虛函數(shù),那么其虛函數(shù)表將有N*4字節(jié)的大小。
虛函數(shù)(virtual)是通過(guò)虛函數(shù)表來(lái)實(shí)現(xiàn)的,在這個(gè)表中,主要是一個(gè)類的虛函數(shù)的地址表,這張表解決了繼承、覆蓋的問(wèn)題,保證其真實(shí)反映實(shí)際的函數(shù)。這樣,在有虛函數(shù)的類的實(shí)例中分配了指向這個(gè)表的指針的內(nèi)存(位于對(duì)象實(shí)例的最前面),所以,當(dāng)用父類的指針來(lái)操作一個(gè)子類的時(shí)候,這張?zhí)摵瘮?shù)表就顯得尤為重要,指明了實(shí)際所應(yīng)調(diào)用的函數(shù)。它是如何指明的呢?后面會(huì)講到。
JMP指令是匯編語(yǔ)言中的無(wú)條件跳轉(zhuǎn)指令,無(wú)條件跳轉(zhuǎn)指令可轉(zhuǎn)到內(nèi)存中任何程序段。轉(zhuǎn)移地址可在指令中給出,也可以在寄存器中給出,或在儲(chǔ)存器中指出。
首先我們定義一個(gè)帶有虛函數(shù)的基類
class Base { public: virtual void fun1(){ cout<<"base fun1!\n"; } virtual void fun2(){ cout<<"base fun2!\n"; } virtual void fun3(){ cout<<"base fun3!\n"; } int a; };
我們可以看到在Base類的內(nèi)存布局上,第一個(gè)位置上存放虛函數(shù)表指針,接下來(lái)才是Base的成員變量。另外,存在著虛函數(shù)表,該表里存放著B(niǎo)ase類的所有virtual函數(shù)。
既然虛函數(shù)表指針通常放在對(duì)象實(shí)例的最前面的位置,那么我們應(yīng)該可以通過(guò)代碼來(lái)訪問(wèn)虛函數(shù)表,通過(guò)下面這段代碼加深對(duì)虛函數(shù)表的理解:
#include "stdafx.h" #include<iostream> using namespace std; class Base { public: virtual void fun1(){ cout<<"base fun1!\n"; } virtual void fun2(){ cout<<"base fun2!\n"; } virtual void fun3(){ cout<<"base fun3!\n"; } int a; }; int _tmain(int argc, _TCHAR* argv[]) { typedef void(*pFunc)(void); Base b; cout<<"虛函數(shù)表指針地址:"<<(int*)(&b)<<endl; //對(duì)象最前面是指向虛函數(shù)表的指針,虛函數(shù)表中存放的是虛函數(shù)的地址 pFunc pfun; pfun=(pFunc)*((int*)(*(int*)(&b))); //這里存放的都是地址,所以才一層又一層的指針 pfun(); pfun=(pFunc)*((int*)(*(int*)(&b))+1); pfun(); pfun=(pFunc)*((int*)(*(int*)(&b))+2); pfun(); system("pause"); return 0; }
運(yùn)行結(jié)果:
通過(guò)這個(gè)例子,對(duì)虛函數(shù)表指針,虛函數(shù)表這些有了足夠的理解。下面再深入一些。C++又是如何利用基類指針和虛函數(shù)來(lái)實(shí)現(xiàn)多態(tài)的呢?這里,我們就需要弄明白在繼承環(huán)境下虛函數(shù)表是如何工作的。目前只理解單繼承,至于虛繼承,多重繼承待以后再理解。
單繼承代碼如下:
class Base { public: virtual void fun1(){ cout<<"base fun1!\n"; } virtual void fun2(){ cout<<"base fun2!\n"; } virtual void fun3(){ cout<<"base fun3!\n"; } int a; }; class Child:public Base { public: void fun1(){ cout<<"Child fun1\n"; } void fun2(){ cout<<"Child fun2\n"; } virtual void fun4(){ cout<<"Child fun4\n"; } };
內(nèi)存布局對(duì)比:
通過(guò)對(duì)比,我們可以看到:
- 在單繼承中,Child類覆蓋了Base類中的同名虛函數(shù),在虛函數(shù)表中體現(xiàn)為對(duì)應(yīng)位置被Child類中的新函數(shù)替換,而沒(méi)有被覆蓋的函數(shù)則沒(méi)有發(fā)生變化。
- 對(duì)于子類自己的虛函數(shù),直接添加到虛函數(shù)表后面。
另外,我們注意到,類Child和類Base中都只有一個(gè)vfptr指針,前面我們說(shuō)過(guò),該指針指向虛函數(shù)表,我們分別輸出類Child和類Base的vfptr:
int _tmain(int argc, _TCHAR* argv[]) { typedef void(*pFunc)(void); Base b; Child c; cout<<"Base類的虛函數(shù)表指針地址:"<<(int*)(&b)<<endl; cout<<"Child類的虛函數(shù)表指針地址:"<<(int*)(&c)<<endl; system("pause"); return 0; }
運(yùn)行結(jié)果:
可以看到,類Child和類Base分別擁有自己的虛函數(shù)表指針vfptr和虛函數(shù)表vftable。
下面這段代碼,說(shuō)明了父類和基類擁有不同的虛函數(shù)表,同一個(gè)類擁有相同的虛函數(shù)表,同一個(gè)類的不同對(duì)象的地址(存放虛函數(shù)表指針的地址)不同。
int _tmain(int argc, _TCHAR* argv[]) { Base b; Child c1,c2; cout<<"Base類的虛函數(shù)表的地址:"<<(int*)(*(int*)(&b))<<endl; cout<<"Child類c1的虛函數(shù)表的地址:"<<(int*)(*(int*)(&c1))<<endl; //虛函數(shù)表指針指向的地址值 cout<<"Child類c2的虛函數(shù)表的地址:"<<(int*)(*(int*)(&c2))<<endl; system("pause"); return 0; }
在定義該派生類對(duì)象時(shí),先調(diào)用其基類的構(gòu)造函數(shù),然后再初始化vfptr,最后再調(diào)用派生類的構(gòu)造函數(shù)( 從二進(jìn)制的視野來(lái)看,所謂基類子類是一個(gè)大結(jié)構(gòu)體,其中this指針開(kāi)頭的四個(gè)字節(jié)存放虛函數(shù)表頭指針。執(zhí)行子類的構(gòu)造函數(shù)的時(shí)候,首先調(diào)用基類構(gòu)造函數(shù),this指針作為參數(shù),在基類構(gòu)造函數(shù)中填入基類的vfptr,然后回到子類的構(gòu)造函數(shù),填入子類的vfptr,覆蓋基類填入的vfptr。如此以來(lái)完成vfptr的初始化)。也就是說(shuō),vfptr指向vftable發(fā)生在構(gòu)造函數(shù)期間完成的。
動(dòng)態(tài)綁定例子:
#include "stdafx.h" #include<iostream> using namespace std; class Base { public: virtual void fun1(){ cout<<"base fun1!\n"; } virtual void fun2(){ cout<<"base fun2!\n"; } virtual void fun3(){ cout<<"base fun3!\n"; } int a; }; class Child:public Base { public: void fun1(){ cout<<"Child fun1\n"; } void fun2(){ cout<<"Child fun2\n"; } virtual void fun4(){ cout<<"Child fun4\n"; } }; int _tmain(int argc, _TCHAR* argv[]) { Base* p=new Child; p->fun1(); p->fun2(); p->fun3(); system("pause"); return 0; }
運(yùn)行結(jié)果:
結(jié)合上面的內(nèi)存布局:
其實(shí),在new Child時(shí)構(gòu)造了一個(gè)子類的對(duì)象,子類對(duì)象按上面所講,在構(gòu)造函數(shù)期間完成虛函數(shù)表指針vfptr指向Child類的虛函數(shù)表,將這個(gè)對(duì)象的地址賦值給了Base類型的指針p,當(dāng)調(diào)用p->fun1()時(shí),發(fā)現(xiàn)是虛函數(shù),調(diào)用虛函數(shù)指針查找虛函數(shù)表中對(duì)應(yīng)虛函數(shù)的地址,這里就是&Child::fun1。調(diào)用p->fun2()情況相同。調(diào)用p->fun3()時(shí),子類并沒(méi)有重寫父類虛函數(shù),但依舊通過(guò)調(diào)用虛函數(shù)指針查找虛函數(shù)表,發(fā)現(xiàn)對(duì)應(yīng)函數(shù)地址是&Base::fun3。所以上面的運(yùn)行結(jié)果如上圖所示。
到這里,你是否已經(jīng)明白為什么指向子類實(shí)例的基類指針可以調(diào)用子類(虛)函數(shù)?每一個(gè)實(shí)例對(duì)象中都存在一個(gè)vfptr指針,編譯器會(huì)先取出vfptr的值,這個(gè)值就是虛函數(shù)表vftable的地址,再根據(jù)這個(gè)值來(lái)到vftable中調(diào)用目標(biāo)函數(shù)。所以,只要vfptr不同,指向的虛函數(shù)表vftable就不同,而不同的虛函數(shù)表中存放著對(duì)應(yīng)類的虛函數(shù)地址,這樣就實(shí)現(xiàn)了多態(tài)的”效果“。
以上就是詳解C++虛函數(shù)的工作原理的詳細(xì)內(nèi)容,更多關(guān)于C++虛函數(shù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
windows下安裝QT及visual studio 2017搭建開(kāi)發(fā)環(huán)境
這篇文章主要介紹了windows下安裝QT及visual studio 2017搭建開(kāi)發(fā)環(huán)境,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03C語(yǔ)言數(shù)據(jù)結(jié)構(gòu)之?dāng)U展字符詳解
掌握C語(yǔ)言數(shù)據(jù)結(jié)構(gòu)的關(guān)鍵在于理解其核心概念,擴(kuò)展字符作為其中的重要一環(huán),對(duì)于編程人員來(lái)說(shuō)至關(guān)重要,本指南將為您深入剖析擴(kuò)展字符的相關(guān)知識(shí),帶您輕松掌握C語(yǔ)言數(shù)據(jù)結(jié)構(gòu),讓我們一起探索這個(gè)令人著迷的領(lǐng)域吧!2024-03-03c語(yǔ)言如何實(shí)現(xiàn)兩數(shù)之和
這篇文章主要介紹了c語(yǔ)言如何實(shí)現(xiàn)兩數(shù)之和,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07c語(yǔ)言大小端(數(shù)據(jù)在內(nèi)存中的存儲(chǔ))
大小端是內(nèi)存存儲(chǔ)字節(jié)的兩種方式,一個(gè)是大端存儲(chǔ),一個(gè)是小端存儲(chǔ),本文主要介紹了c語(yǔ)言大小端,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09c++10進(jìn)制轉(zhuǎn)換為任意2-16進(jìn)制數(shù)字的實(shí)例
下面小編就為大家?guī)?lái)一篇c++10進(jìn)制轉(zhuǎn)換為任意2-16進(jìn)制數(shù)字的實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06C++?Qt開(kāi)發(fā)之使用QNetworkAccessManager實(shí)現(xiàn)Web網(wǎng)頁(yè)訪問(wèn)
Qt?是一個(gè)跨平臺(tái)C++圖形界面開(kāi)發(fā)庫(kù),利用Qt可以快速開(kāi)發(fā)跨平臺(tái)窗體應(yīng)用程序,本文主要介紹了如何運(yùn)用QNetworkAccessManager組件實(shí)現(xiàn)Web網(wǎng)頁(yè)訪問(wèn),需要的可以參考下2024-03-03C++詳解哈夫曼樹(shù)的概念與實(shí)現(xiàn)步驟
給定N個(gè)權(quán)值作為N個(gè)葉子結(jié)點(diǎn),構(gòu)造一棵二叉樹(shù),若該樹(shù)的帶權(quán)路徑長(zhǎng)度達(dá)到最小,稱這樣的二叉樹(shù)為最優(yōu)二叉樹(shù),也稱為哈夫曼樹(shù)(Huffman?Tree)。哈夫曼樹(shù)是帶權(quán)路徑長(zhǎng)度最短的樹(shù),權(quán)值較大的結(jié)點(diǎn)離根較近2022-04-04C++中CString string char* char 之間的字符轉(zhuǎn)換(多種方法)
在寫程序的時(shí)候,我們經(jīng)常遇到各種各樣的類型轉(zhuǎn)換,比如 char* CString string 之間的互相轉(zhuǎn)換,這里簡(jiǎn)單為大家介紹一下,需要的朋友可以參考下2017-09-09