-
步驟一,先表述虛函數(shù)表的3個(gè)特性來(lái)做引子:
1, 單繼承時(shí),虛函數(shù)表指針通常存儲(chǔ)在類對(duì)象“內(nèi)存布局”的最前面。
2,虛函數(shù)表實(shí)質(zhì)上是一個(gè)“函數(shù)指針”的數(shù)組,
該數(shù)組最后一個(gè)元素必然為0。(很多博客上都說(shuō)虛函數(shù)表的最后一個(gè)元素是0,但我自己用vs2010做的實(shí)驗(yàn)有時(shí)候不是0)。
3,一個(gè)有虛函數(shù)(
無(wú)論是繼承得到的虛函數(shù)還是自身有的)的類,該類的所有對(duì)象,都共用一份虛函數(shù)表。
? ?? ?每個(gè)對(duì)象有一套(這里用套而不用個(gè),是因?yàn)槎嘀乩^承時(shí),可能有多個(gè)指針組成的一套)“虛函數(shù)表指針”,指向該虛函數(shù)表。
-
步驟二,下面來(lái)證明上面幾個(gè)特性,并推導(dǎo)出類對(duì)象的內(nèi)存布局。
//VC++ 32位編譯器下
#include "stdafx.h"
#include <stdio.h>
#include <iostream>
using namespace std;
typedef void(*FUNC)();
class A{
public:
virtual void func(){
cout << " A::func" << endl;
}
virtual void funcA(){
cout << " A::funcA" << endl;
}
public:
int a;
};
class B:public A{
public:
virtual void func(){
cout << " B::func" << endl;
}
virtual void funcB(){
cout << " B::funcB" << endl;
}
public:
int b;
};
int main()
{
B b1;
B b2;
printf("\n b1對(duì)象的首地址里面存放的虛函數(shù)表的指針是:0x%x\n", (*(int*)&b1));
((FUNC)( *(int*)*((int*)&b1) ))();
((FUNC)*((int*)*((int*)&b1) + 1))();
printf(" b2對(duì)象的首地址里面存放的虛函數(shù)表的指針是:0x%x\n", (*(int*)&b2));
system("pause");
return 0;
}
輸出結(jié)果:
由輸出結(jié)果可以看出,程序正確調(diào)用了兩個(gè)虛函數(shù)(先不管調(diào)用的是什么虛函數(shù)),所以找到的虛函數(shù)表的指針是正確的。步驟一中的1得到證實(shí)。又因?yàn)?b1 和 b2 虛函數(shù)表的指針值是相同的,所以步驟一中的3也得到了證實(shí)。
-
步驟三,再來(lái)看一個(gè)單繼承的例子
//VC++ 32位編譯器下:
#include "stdafx.h"
#include <iostream>
using namespace std;
//單繼承下虛函數(shù)表:是如何組織的
class A{
public:
virtual void func(){
cout << "A::funccommon" << endl;
}
virtual void funcA(){
cout << "A::funcA" << endl;
}
};
class B:public A{
public:
virtual void func(){
cout << "B::funccommon" << endl;
}
virtual void funcB(){
cout << "B::funcB" << endl;
}
};
class C:public A{
public:
virtual void func(){
cout << "C::funccommon" << endl;
}
virtual void funcC(){
cout << "C::funcC" << endl;
}
};
typedef void (*FUNC)();
int main()
{
A a;
B b;
C c;
cout << "A::虛表:" << endl;
((FUNC)(*(int*)(*(int*)(&a))))();
((FUNC)(*((int*)(*(int*)(&a)) + 1)))();
cout << "-------------------------------------" << endl;
cout << "B::虛表:" << endl;
((FUNC)(*(int *)(*(int*)(&b))))();
((FUNC)(*((int*)(*(int*)(&b)) + 1)))();
((FUNC)(*((int*)(*(int*)(&b)) + 2)))();
cout << "-------------------------------------" << endl;
cout << "C::虛表:" << endl;
((FUNC)(*(int *)(*(int*)(&c))))();
((FUNC)(*((int*)(*(int*)(&c)) + 1)))();
((FUNC)(*((int*)(*(int*)(&c)) + 2)))();
system("pause");
return 0;
}
輸出結(jié)果:
在分析輸出結(jié)果之前,先看一下這句代碼是什么意思?
( (FUNC) ( *(int*) (*(int*)(&a)) ) )();????(*(int*)(&a))的意思是,從對(duì)象 a 的起始地址所指向的那個(gè)字節(jié)的位置算起,取4個(gè)字節(jié)的一個(gè)整形值。我們知道,在VC++ 32位編譯器下,指針和 int 型一樣,也是占4個(gè)字節(jié)。
????所以實(shí)際上,取出來(lái)的這個(gè)整形值,也可以看作是一個(gè)地址(也稱指針,實(shí)際上該指針就是對(duì)象a指向虛函數(shù)表的指針)。
????( *(int*) ?(*(int*)(&a)) )的意思是,將上面取出的指針,強(qiáng)制轉(zhuǎn)換為 int* 的指針,然后取出該指針?biāo)傅恼沃担ㄍ瑯?,也可以看作是一個(gè)地址),該整形值事實(shí)上是一個(gè)函數(shù)指針。
????由以上分析可以對(duì)代碼進(jìn)行解釋:
QQ截圖20181029164850.png
????我們?cè)賮?lái)看剛才的輸出結(jié)果,我們對(duì)虛函數(shù)表,按照由數(shù)組首地址,計(jì)算偏移獲取到數(shù)組元素的做法,獲取了A, B, C三個(gè)類里的虛函數(shù)的指針,并且都調(diào)用成功了。
????由此步驟一中的2也得到了證實(shí)。
-
步驟四,如果派生類的虛函數(shù)和基類的虛函數(shù)相同,即派生類的虛函數(shù)“覆蓋”了基類的虛函數(shù),則在派生類的虛函數(shù)表中,只有派生類的那個(gè)虛函數(shù)。如果是派生類新增的虛函數(shù),則將該虛函數(shù)追加到派生類虛函數(shù)表的末尾。如下圖,是類B的虛函數(shù)表的產(chǎn)生過(guò)程:

-
步驟五,虛函數(shù)表的生成的時(shí)間
這里說(shuō)明下一個(gè)問(wèn)題,我用 VS2010 的 Debug 模式來(lái)調(diào)試 步驟三 里面的代碼,然后在監(jiān)視窗口里查看變量b, c的虛函數(shù)表,發(fā)現(xiàn)只能查看到 從基類繼承下來(lái)的 和 派生類覆蓋的 虛函數(shù),不能看到派生類自己追加的虛函數(shù)。
而且對(duì)于對(duì) 虛函數(shù)表 沒有深刻理解的人來(lái)說(shuō),VS2010 的顯示方式讓人容易誤解,認(rèn)為存儲(chǔ)的是基類的虛函數(shù)表的指針。如下圖:
image.png
VS運(yùn)行起來(lái),可以打印上圖 b 和 c 的_vfptr[2],但是不明白為什么在調(diào)試器里不能查看到。
??????現(xiàn)在在 linux 下用 gdb 來(lái)查看:

注:上圖中,因?yàn)槲业膅++編譯器是64位的,所以把 int 改成了 long long 類型。因?yàn)樵趃++ 64位編譯器下,指針是占 8個(gè)字節(jié)的,long long 類型也是占8個(gè)字節(jié)的。當(dāng)然直接用 long 也行。
程序構(gòu)建(Build)的四個(gè)過(guò)程(預(yù)編譯、編譯、匯編和鏈接)
虛函數(shù)表應(yīng)該是在編譯期確定的,原因如下:1)預(yù)編譯器主要處理那些源代碼文件中的以“#”開始的預(yù)編譯指令,如“#include”、“#define”。很明顯這個(gè)過(guò)程可以排除。
2)匯編器是將編譯器生成的匯編代碼轉(zhuǎn)變成機(jī)器可以執(zhí)行的指令,每一個(gè)匯編語(yǔ)句幾乎都對(duì)應(yīng)一條機(jī)器指令。匯編過(guò)程相對(duì)于編譯期來(lái)說(shuō)比較簡(jiǎn)單,沒有復(fù)雜的語(yǔ)法,也沒有語(yǔ)義,也不需要做指令優(yōu)化,只是根據(jù)匯編指令和機(jī)器指令的對(duì)照表一一翻譯就行了。所以,匯編期也是可以排除的。
3)鏈接器(現(xiàn)只考慮靜態(tài)鏈接)是將匯編器生成的目標(biāo)文件(和庫(kù))鏈接成一個(gè)可執(zhí)行文件,本質(zhì)上做的是重定位(Relocation)的工作,詳細(xì)可參考《程序員的自我修養(yǎng)》2.3、2.4節(jié)。很明顯鏈接期也是可以排除的。
4)編譯器要做的事情就比較多了,包括詞法分析、語(yǔ)法分析、語(yǔ)義分析及優(yōu)化代碼等,是整個(gè)程序構(gòu)建的核心。所以,排除了預(yù)編譯期、匯編期、鏈接期及考慮到編譯期所做的事情,虛函數(shù)表應(yīng)該是在編譯期建立的。
為什么不是在運(yùn)行時(shí)確定的呢?
C++是編譯型語(yǔ)言,當(dāng)然是在編譯階段把能夠做的工作都做完,執(zhí)行起來(lái)效率更高。像多態(tài)那種因?yàn)橛脩粜袨闀?huì)影響執(zhí)行路徑的,才不得不在執(zhí)行階段確定。
-
步驟五,虛函數(shù)表存放在進(jìn)程(在磁盤上稱 ”可執(zhí)行文件”,在內(nèi)存中就稱 “進(jìn)程”)的哪個(gè)區(qū)?
???用readelf命令查看,這個(gè)以后再回來(lái)學(xué)習(xí)并補(bǔ)充。開始因?yàn)椴恢肋€有readelf這種命令,也不知道有elf文件這種格式,我愚蠢地去學(xué)習(xí)了一下匯編,想用反匯編的方法看進(jìn)程在內(nèi)存中分布,搞得花了一天的時(shí)間來(lái)折騰,還沒有好的結(jié)果。再寫的時(shí)候參考一下 https://blog.csdn.net/chenlycly/article/details/53377942 這篇博文。
???借用別人博客的一句話先告訴結(jié)果:vtable在Linux/Unix中存放在可執(zhí)行文件的只讀數(shù)據(jù)段中(rodata),而微軟的編譯器將虛函數(shù)表存放在常量段。
-
步驟六,多重繼承時(shí) ,派生類對(duì)象的內(nèi)存布局
多重繼承的概念:網(wǎng)上很多人的博客對(duì)多繼承和多重繼承兩個(gè)概念有不同的解釋,搞得很混亂。尤其是多重繼承,有些博客錯(cuò)得離譜,說(shuō)類C繼承自類B,類B繼承自類A,這樣叫多重繼承。對(duì)多繼承歧義的倒比較少。這里我統(tǒng)一一下概念,根據(jù)C++ Primer中文版(第4版) 的說(shuō)法:
??????多重繼承是從多于一個(gè)直接基類派生類的能力,多重繼承的派生類繼承其所有父類的屬性。
??????多繼承 與 多重繼承實(shí)際上是一個(gè)概念。
//多繼承條件下的虛函數(shù)表
#include "stdafx.h"
#include <iostream>
using namespace std;
#include<iostream>
using namespace std;
class A
{
public:
virtual void fun1()
{
printf("A::virtual void fun(int n)\n");
}
int _a;
};
class B
{
public:
virtual void fun2()
{
printf("B::virtual void fun(int n)\n");
}
int _b;
};
class C:public A,public B
{
public:
int _c;
};
int main()
{
A a;
B b;
C c;
a._a = 1;
b._b = 2;
c._a = 3;
c._b = 4;
c._c = 5;
printf("%p\n", &a);
printf("%p\n", &b);
printf("%p\n", &c);
return 0;
}
內(nèi)存窗口分析:


-
步驟七,虛繼承的作用
??????虛繼承是解決C++多重繼承問(wèn)題的一種手段,從不同途徑繼承來(lái)的同一基類,會(huì)在子類中存在多份拷貝。
這將存在兩個(gè)問(wèn)題:
其一,浪費(fèi)存儲(chǔ)空間;
第二,存在二義性問(wèn)題,通??梢詫⑴缮悓?duì)象的地址賦值給基類對(duì)象,實(shí)現(xiàn)的具體方式是,將基類指針指向繼承類(繼承類有基類的拷貝)中的基類對(duì)象的地址,但是多重繼承可能存在一個(gè)基類的多份拷貝,這就出現(xiàn)了二義性。
??????虛繼承是為了解決上述兩個(gè)問(wèn)題而產(chǎn)生的。
??????這個(gè)比較復(fù)雜,用一篇博客里的說(shuō)法就是,其復(fù)雜度遠(yuǎn)遠(yuǎn)大于它的使用價(jià)值,不想花太多時(shí)間研究,僅僅知道其用法就行了。
??????如果很想知道,可以參考一下https://blog.csdn.net/zhourong0511/article/details/79950847這篇博客的最后一張圖,那里講的菱形繼承里有虛繼承的內(nèi)容。
? ? ? 參考了以下兩篇博客:
1,https://blog.csdn.net/zongyinhu/article/details/51276806?tdsourcetag=s_pcqq_aiomsg。發(fā)現(xiàn)作者講得很透徹,為了我能完全弄懂并記住虛函數(shù)表的有關(guān)問(wèn)題,現(xiàn)在用自己的話整理出來(lái),并發(fā)布。
2,https://blog.csdn.net/zhourong0511/article/details/79950847使用的內(nèi)存表示方法非常好,讓我完全看懂了,不過(guò)博客中有少量錯(cuò)誤和歧義的內(nèi)容。

