什么是虛函數(shù)碟婆?
- 簡(jiǎn)單來說电抚,虛函數(shù)是動(dòng)態(tài)調(diào)用。相比于一般的函數(shù)調(diào)用在編譯期確定了函數(shù)地址竖共,而調(diào)用虛函數(shù)是在運(yùn)行時(shí)決定調(diào)用的函數(shù)地址蝙叛。
- 虛函數(shù)怎么使用相信大家都比較清楚,這里簡(jiǎn)單帶過一下公给。C++中父類的指針可以指向子類實(shí)例借帘,通過父類指針調(diào)用虛函數(shù)時(shí)會(huì)因?yàn)橹赶虻牟煌膶?shí)例類型來調(diào)用不同的函數(shù)蜘渣。
- C++多態(tài)性主要體現(xiàn)在虛函數(shù)上,某種程度上來說也只體現(xiàn)在虛函數(shù)上肺然。(泛型是否屬于多態(tài)這種PL問題我并不能準(zhǔn)確回答蔫缸。)
虛函數(shù)是如何實(shí)現(xiàn)的?
- 虛表指針(vfptr)
- 虛函數(shù)表(vtable)
- 動(dòng)態(tài)調(diào)用际起。
虛表指針
在單繼承情況下拾碌,如果父類存在虛函數(shù),子類實(shí)例首地址開始4字節(jié)(在32位編譯器下)會(huì)用來存放虛表指針街望。比如下面這兩個(gè)類:
struct base {
int x;
virtual void func1() {
printf("base func1\n");
}
};
struct sub:base {
int y;
};
在這個(gè)例子中校翔,父類和子類分別擁有一個(gè)4字節(jié)成員。如果沒有虛函數(shù)的情況下灾前,sub類型所占的空間應(yīng)該是8字節(jié)防症,但是因?yàn)楦割愔写嬖谔摵瘮?shù),就需要額外4字節(jié)用來存放虛表指針豫柬,所以用sizeof(sub)返回的結(jié)果會(huì)是12字節(jié)告希。
虛函數(shù)表
在sub實(shí)例開始的4字節(jié)所指向的就是虛函數(shù)表,這個(gè)表可以認(rèn)為是一個(gè)由函數(shù)指針組成的數(shù)組∩崭現(xiàn)在我們來看一下剛才示例中兩個(gè)類的虛表,下面是base實(shí)例對(duì)象的內(nèi)存喝噪,前4個(gè)字節(jié)就是虛表指針础嫡,后四個(gè)字節(jié)是成員x,因?yàn)閐ebug沒有初始化所以是CC酝惧。
base實(shí)例對(duì)象(其中00420F94指向的就是base的虛表, 虛表只有一個(gè)元素就是base::func1的指針
0040101E):
x86內(nèi)存中因?yàn)槭切《舜鎯?chǔ)榴鼎,所以“看起來”是反的,需要肉眼parse...
0019FF38 94 0F 42 00 // 虛表指針
0019FF3C CC CC CC CC
base的虛表:
00420F94 1E 10 40 00 // base::func1的函數(shù)指針
base::func1函數(shù):
0040101E E9 4D 00 00 00 jmp base::func1 (00401070)
sub的虛表內(nèi)容相信大家也已經(jīng)想到了晚唇,雖然在內(nèi)存中這是兩張表巫财,但是因?yàn)閟ub并沒有重寫任何虛函數(shù),所以虛表的內(nèi)容和base是完全一樣的哩陕,都是只有一個(gè)指向base::func1的函數(shù)指針平项。
sub的虛表:
0042003C 1E 10 40 00 // 和base虛表內(nèi)容一樣指向base::func1
示例2(重寫了父類虛函數(shù)):
寫到這里我發(fā)現(xiàn)剛剛的例子可能不太好,因?yàn)樽宇愔袥]有重寫虛函數(shù)悍及,而且只有一個(gè)虛函數(shù)闽瓢,沒有直觀的體現(xiàn)出作用。現(xiàn)在讓我們來豐富一下剛才的兩個(gè)類心赶。
struct base {
int x;
virtual void func1() {
printf("base func1\n");
}
virtual void func2() {
printf("base func2\n");
}
};
struct sub:base {
int y;
void func1() {
printf("sub func1\n");
}
void func4() {
printf("sub func4\n");
}
};
void vPrint(base* ptr) {
ptr->func1(); // 通過虛表指針動(dòng)態(tài)調(diào)用函數(shù)
}
在這個(gè)例子中扣讼,sub重寫了父類的func1函數(shù)。并且我們也可以通過傳給vPrint不同類型的指針缨叫,來調(diào)用不同的函數(shù):
int main() {
base b;
vPrint(&b);
sub s;
vPrint(&s);
return 0;
}
輸出如下:
base func1
sub func1
這個(gè)效果和我們想要的一樣椭符,現(xiàn)在我們?cè)倏匆幌聅ub的虛函數(shù)表荔燎。
sub的虛表:
00420058 6E 10 40 00 // sub::func1的指針
0042005C 5A 10 40 00 // base::func2的指針
0040105A E9 11 03 00 00 jmp base::func2 (00401370)
0040106E E9 AD 03 00 00 jmp sub::func1 (00401420)
可以看到因?yàn)橹貙懥薴unc1,所以虛表中第一個(gè)位置換成了sub::func1销钝。func2沒有被重寫湖雹,所以還是和父類一樣指向base::func2. 而func4因?yàn)闆]有被聲明為虛函數(shù),所以不會(huì)在虛表中存在曙搬。(func1雖然在子類中沒有聲明確聲明為虛函數(shù)摔吏,但是C++中規(guī)定與父類虛函數(shù)同名的函數(shù)都會(huì)自動(dòng)被聲明為虛函數(shù),當(dāng)然這個(gè)同名指的是包括整個(gè)函數(shù)名纵装、返回值征讲、參數(shù)列表。)
base的虛表就不貼出來了橡娄,因?yàn)閎ase沒有繼承其他類诗箍,所以base的虛表中只能是func1和func2兩個(gè)函數(shù)的指針。
動(dòng)態(tài)調(diào)用
說了這么久的虛表指針和虛表挽唉,現(xiàn)在終于可以看看是如何使用它們來實(shí)現(xiàn)動(dòng)態(tài)調(diào)用的滤祖。
讓我們來看一下vPrint函數(shù)的反匯編:
mov eax,dword ptr [ebp+8] // ebp+8是vPrint函數(shù)的第一個(gè)參數(shù)base* ptr
mov edx,dword ptr [eax] // 將實(shí)例對(duì)象的前4個(gè)字節(jié),也就是虛表指針放在edx中
mov ecx,dword ptr [ebp+8] // 傳參this指針瓶籽,不用管
call dword ptr [edx] // call虛表中的第一個(gè)元素匠童,根據(jù)傳進(jìn)來的對(duì)象的虛表不同,調(diào)用不同的函數(shù)
可以清晰的看到call的函數(shù)地址并不是一個(gè)立即數(shù)塑顺,而是edx指向的數(shù)據(jù)汤求。這就是動(dòng)態(tài)調(diào)用實(shí)現(xiàn)的關(guān)鍵了,根據(jù)對(duì)象中攜帶的虛表指針严拒,來調(diào)用不同對(duì)象關(guān)聯(lián)的不同函數(shù)扬绪。
總結(jié)
虛函數(shù)調(diào)用是通過call虛表數(shù)據(jù)來實(shí)現(xiàn)運(yùn)行時(shí)調(diào)用。傳遞的參數(shù)類型不同裤唠,虛表指針就不同挤牛、虛表指針不同,指向的虛函數(shù)表就不同种蘸、虛函數(shù)表不同墓赴,指向的函數(shù)就不同。
附言
這篇文章中只講了單繼承的情況劈彪,而在多繼承的情況下子類對(duì)象實(shí)例中就會(huì)有多個(gè)虛表指針竣蹦,為了不使文章太過冗長(zhǎng),就不一一列出來了沧奴。
大家感興趣的可以自己動(dòng)手試一下痘括,看看多繼承的情況下內(nèi)存分布如何,在傳參時(shí)的偏移如何。
趙克纲菌,寫于2017年01月13日挠日。
如需轉(zhuǎn)載請(qǐng)與我聯(lián)系,并注明出處翰舌。