C++虛機制可以說是類中的產(chǎn)物,我們不會看見一個“non-class function”被定義為虛函數(shù),,它自從誕生起便是為類服務:將類中的函數(shù)定義為虛函數(shù),,則在調(diào)用這個函數(shù)時會執(zhí)行一套完全不同的規(guī)則;將類的繼承使用虛繼承時,,它將會造成一些額外負擔,,因為之前已經(jīng)學習過一次虛機制并且當時已經(jīng)弄懂了,但是并沒有好好整理即時回顧,,導致在寫下這篇文章的時候已經(jīng)忘得差不多了,,所以這一次會好好記下。 這篇文章對虛機制的分析分為兩個主要部分,,虛函數(shù)的實現(xiàn)和虛繼承的實現(xiàn),,會對對象的內(nèi)存模型進行分析,所以會使用到GDB工具,,并且對一些分析必要的知識進行說明,。 工具介紹和分析背景知識GDBGDB在本章中的作用是輸出類對象的內(nèi)存模型,便于我們分析,。在進行編譯是加入-g 參數(shù)方便調(diào)試,,并且通過設置GDB輸出 1
2
3
4
5
6
7
8 | 你也可以在你的~路徑下新建.gdbinit文件一勞永逸
set print pretty on 顯示地更好看
set print object on 在C++中,如果一個對象指針指向其派生類,,如果打開這個選項,,GDB會自動按照虛方法調(diào)用的規(guī)則顯示輸出,,
如果關閉這個選項的話,GDB就不管虛函數(shù)表了,。這個選項默認是off,。
set print vtbl on 當此選項打開時,GDB將用比較規(guī)整的格式來顯示虛函數(shù)表時,。其默認是關閉的,。
p/x 16進制輸出
x/8xg 16進制,8字節(jié)一組,,輸出8組
x/3ig 輸出匯編代碼 8字節(jié)一組,,輸出3組 |
你可以在這里看到更多的相關設置,并且GDB的基本操作可以在這里了解到,。 可以使用g++ -fdump-class-hierarchy XXX 來生成類對象的內(nèi)存布局,,可以使用它分析虛函數(shù)表。 sizeof()的輸出sizeof的輸出本沒什么好講的,,但是這中間夾雜著內(nèi)存對齊和指針大小等問題,,在分析的時候如果不了解這些的話會造成困擾。 內(nèi)存對齊一句話概括就是如果一個類對象中的內(nèi)存占用不滿足2的冪次方,,則會使用多一部分內(nèi)存使其滿足,,不對齊的內(nèi)存在訪問時會有額外的性能開銷,不過目前的部分處理器已經(jīng)實現(xiàn)了無額外消耗的訪問未對齊內(nèi)存的操作,。在C++中對象內(nèi)存對齊后得到的size跟其數(shù)據(jù)定義順序有關,,后續(xù)會寫一篇博客專門講內(nèi)存對齊。 指針大小32位編譯器和64位編譯器中指針的大小是不同的,,指針的大小直接關系到其尋址能力的大小,,在32位編譯器中得到的指針大小是4個字節(jié),表示其最大的尋址能力可以達到4GB,,64位以此類推,。 在了解了上面兩個知識點之后,我們可以正式開始虛機制的學習了,。 虛函數(shù)為什么需要虛函數(shù),,可能是很多人都不會去想的,因為“存在即合理”這句話,,導致了大家只關心了怎么做,,卻不關心為什么。虛函數(shù)實現(xiàn)了運行時多態(tài),,定義一個函數(shù)為虛函數(shù)是為了允許使用基類的指針來調(diào)用子類的函數(shù),。虛函數(shù)并不是虛擬的沒有實現(xiàn)的,純虛函數(shù)才代表這個函數(shù)沒有被實現(xiàn),定義一個純虛函數(shù)的目的是為了實現(xiàn)一個接口,,起到規(guī)范化的作用,,程序員編寫派生類時必須實現(xiàn)這個函數(shù)。 下面來講虛函數(shù)的實現(xiàn),,虛函數(shù)的調(diào)用跟普通函數(shù)不同,,普通函數(shù)調(diào)用時,直接使用內(nèi)存偏移量便可以確定函數(shù)地址(Non-static class function member和static class function member調(diào)用略有不同,,但不是本文討論的重點),,但在虛函數(shù)調(diào)用中,是通過一個虛函數(shù)表(virtual table)進行對應虛函數(shù)地址查找,,最后再訪問該地址進行調(diào)用的,。虛函數(shù)表每個類只有一份,存放在數(shù)據(jù)區(qū),,這個表上的元素一般而言是被聲明的virtual function數(shù)量,,在加上多個的slot用于運行時類型轉(zhuǎn)換(后面會講),每個對象中會多出一個指針的內(nèi)存占用,,這個指針叫做虛函數(shù)指針(vptr),,它指向了該類的虛函數(shù)表,就原理而言就是這么簡單,,每個對象通過一個自己的虛函數(shù)指針,,訪問該類的虛函數(shù)表,獲得相應虛函數(shù)地址,,執(zhí)行,。 下面就不同情況下的虛函數(shù)表和對象內(nèi)存地址做分析。 實驗1.1無繼承–單個虛函數(shù)代碼如下 1
2
3
4
5
6
7
8 | class A{
public: int a = 1;
public: void A_Func_1(){}
};
int main(){
A a;
return 0;
} |
編譯后進行gdb,,輸出對象a的size和a的內(nèi)存布局如下 1
2
3
4
5
6
7 | (gdb) p a
$1 = (A) {
_vptr.A = 0x555555754da0 <vtable for A+16>,
a = 1
}
(gdb) p sizeof(a)
$2 = 16 |
可以看到A中只有一個名為_vptr.A的虛函數(shù)指針,,對象總共占用了16個字節(jié),包含了一個虛函數(shù)指針(8字節(jié))和一個int(4字節(jié))和內(nèi)存對齊的4字節(jié),。而里面<vtable for A+16> 指的虛函數(shù)指針_vptr.A指向了虛表起始位置+16個字節(jié)的地方,。經(jīng)過-fdump-class-hierarchy參數(shù)輸出的文件可以證明這一點,如下: 1
2
3
4
5
6
7
8
9
10
11 | Vtable for A
A::_ZTV1A: 3 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1A)
16 (int (*)(...))A::A_Func_1
Class A
size=8 align=8
base size=8 base align=8
A (0x0x7f356cdc5960) 0 nearly-empty
vptr=((& A::_ZTV1A) + 16) |
因此我們可以得出結論: 實驗1.2無繼承下–多個虛函數(shù)我們可以很理所當然的猜測一個類中的虛函數(shù)在虛函表中的位置是按聲明先后排放的(構造函數(shù)初始化列表),驗證代碼如下: 1
2
3
4
5
6
7
8
9
10
11
12 | class A{
public: virtual void A_Func_1();
public: virtual void A_Func_2();
public: virtual void A_Func_3();
};
void A::A_Func_3(){}
void A::A_Func_2(){}
void A::A_Func_1(){}
int main(){
A a;
return 0;
} |
最后得到的.class文件也驗證了我的猜想,其實想想也可以得到答案,,虛函數(shù)表跟運行時的狀態(tài)沒有關系,,它在編譯時就已經(jīng)確定了地址(只讀數(shù)據(jù)段)。 1
2
3
4
5
6
7
8
9
10
11
12
13 | Vtable for A
A::_ZTV1A: 5 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1A)
16 (int (*)(...))A::A_Func_1
24 (int (*)(...))A::A_Func_2
32 (int (*)(...))A::A_Func_3
Class A
size=8 align=8
base size=8 base align=8
A (0x0x7fe64807a960) 0 nearly-empty
vptr=((& A::_ZTV1A) + 16) |
這個實驗得到結論: 實驗1.3單繼承下的虛函數(shù)表如果基類有虛函數(shù),,而派生類重新定義了,并且派生類新聲明了一個基類沒有的虛函數(shù),則在虛函數(shù)表中該如何表示呢,,類的內(nèi)存布局又是怎樣的呢,?代碼如下: 1
2
3
4
5
6
7
8
9
10
11 | class A{
public: long long int a = 0x01020304;
};
class B:public A{
public:virtual void B_func_1(){};
public:virtual void A_func_1(){};
};
int main(){
B b;
return 0;
} |
對象內(nèi)存布局如下: 1
2
3
4
5
6
7 | (gdb) p b
$1 = (B) {
<A> = {
_vptr.A = 0x555555754d80 <vtable for B+16>
}, <No data fields>}
(gdb) p sizeof(b)
$2 = 8 |
虛函數(shù)表為: 1
2
3
4
5
6
7
8
9
10
11
12 | Vtable for A
A::_ZTV1A: 3 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1A)
16 (int (*)(...))A::A_func_1
Vtable for B
B::_ZTV1B: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1B)
16 (int (*)(...))B::A_func_1
24 (int (*)(...))B::B_func_1 |
可以看到class B中的虛函數(shù)表的A_func_1函數(shù)已經(jīng)變成了B::前綴了,從這個實驗我們可以得到結論: 一個類中只有一個虛函數(shù)指針(別急,,這只是當前結論),。 派生類虛函數(shù)表中的函數(shù)排列是先基類的函數(shù)再是派生類的函數(shù)。
實驗1.4多繼承下的虛函數(shù)這個實驗中我們定義了三個類A,B,C每個類中都有一個虛函數(shù),,C繼承自A和B,,先繼承B后繼承A。代碼如下: 1
2
3
4
5
6
7
8
9
10
11
12
13 | class A{
public: virtual void A_func_1(){};
};
class B{
public:virtual void B_func_1(){};
};
class C:public B,A{
public:virtual void C_func_1(){};
};
int main(){
C c;
return 0;
} |
虛函數(shù)表如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 | Vtable for A
A::_ZTV1A: 3 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1A)
16 (int (*)(...))A::A_func_1
Vtable for B
B::_ZTV1B: 3 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1B)
16 (int (*)(...))B::B_func_1
Vtable for C
C::_ZTV1C: 7 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1C)
16 (int (*)(...))B::B_func_1
24 (int (*)(...))C::C_func_1
32 (int (*)(...))-8
40 (int (*)(...))(& _ZTI1C)
48 (int (*)(...))A::A_func_1
Class C
size=16 align=8
base size=16 base align=8
C (0x0x7f1bdbaaf310) 0
vptr=((& C::_ZTV1C) + 16)
B (0x0x7f1bdbc08ae0) 0 nearly-empty
primary-for C (0x0x7f1bdbaaf310)
A (0x0x7f1bdbc08b40) 8 nearly-empty
vptr=((& C::_ZTV1C) + 48) |
可以看到class C占用了16字節(jié)的內(nèi)存,,我們輸出對象c的內(nèi)存布局: 1
2
3
4
5
6
7
8 | (gdb) p c
$1 = (C) {
<B> = {
_vptr.B = 0x555555754d38 <vtable for C+16>
},
<A> = {
_vptr.A = 0x555555754d58 <vtable for C+48>
}, <No data fields>} |
這里我們可以觀察到c中竟然有兩個虛函數(shù)指針,,這是多繼承造成的影響,但是函數(shù)之間并不是緊密連接的,,它們之間會夾雜著一個8位數(shù)值對象和一個(& _ZTI1C) 對象,,這里我也不賣關子直接給出它們的具體含義,C的虛函數(shù)包中,,數(shù)值對象可以看到是-8,,這8個字節(jié)所代表的是offset_to_top 字段,它的英文解釋如下: The offset to top holds the displacement to the top of the object from the location within the object of the virtual table pointer that addresses this virtual table, as a ptrdiff_t. It is always present. The offset provides a way to find the top of the object from any base subobject with a virtual table pointer. This is necessary for dynamic_cast in particular. (In a complete object virtual table, and therefore in all of its primary base virtual tables, the value of this offset will be zero. […])
簡單地說就是該類型的起始地址相對于原本對象的起始地址的偏移量,,c中含有兩個虛函數(shù)指針,,并分別被{} 包圍,這表明它們屬于不同的類并且被C的{} 包圍,,一個指針的字節(jié)為8,,如果這個字段表示的就是,如果想用A類型的指針指向一個C類的對象時,,將對象起始地址設置+8字節(jié)就好了,。那么如果判定這個偏移后的地址屬于哪個對象呢,(& _ZTI1C) 所代表的字段typeinfo 就起到作用了 The typeinfo pointer points to the typeinfo object used for RTTI. It is always present. All entries in each of the virtual tables for a given class must point to the same typeinfo object. A correct implementation of typeinfo equality is to check pointer equality, except for pointers (directly or indirectly) to incomplete types. The typeinfo pointer is a valid pointer for polymorphic classes, i.e. those with virtual functions, and is zero for non-polymorphic classes.
簡單地說就是記錄對象指針的真實類型,,用于運行時多態(tài),。 這個實驗的知識有點多,我們稍微總結一下,,從A* a = new C() 這個代碼出發(fā)進行解釋,,new C() 毫無疑問是一個class C的對象,a則是一個class A的指針,,我們可以很輕易地理解C對象占用的內(nèi)存是比A對象要大的,,再當a指向一個C對象時,基類的指針無法去調(diào)用基類中沒有定義的東西(在這里特指_vptr.B),,所以需要通過字段offset_to_top 進行對象指針偏移訪問,,再通過字段typeinfo 進行類型判別,,到此,a的表現(xiàn)會完全跟指向A對象沒什么兩樣,,但是還是會調(diào)用C的虛函數(shù)表,,實現(xiàn)了動態(tài)多態(tài)。 因此我們可以得到結論: 在多繼承中會出現(xiàn)多個虛函數(shù)指針,。 多繼承中的虛函數(shù)表中的順序是跟其派生類中的繼承順序相關,。 前面說的slot中包含兩個字段:offset_to_top 和typeinfo ,分別表示對象地址和指向類型的數(shù)據(jù)偏移量和真實數(shù)據(jù)類型,。 可以使用基類指針指向派生類,,不能使用派生類指針指向基類。因為派生類中有些動作在基類中沒有定義,,編譯不能通過,。
總結至此我們討論了虛函數(shù)指針和虛函數(shù)表在單類和多繼承下的表現(xiàn),雖然沒有全部舉例,,但是通過上面的結論便可以輕易應對其他情況?,F(xiàn)在將上面得到的所有結論放在一起展示: 虛函數(shù)指針并不是指向了虛函數(shù)表的開頭,而是指向了第一個虛函數(shù)所存的地址,。 虛函數(shù)表在編譯時就已經(jīng)確定,。 派生類虛函數(shù)表中的函數(shù)排列是先基類的函數(shù)再是派生類的函數(shù)。 在多繼承中會出現(xiàn)多個虛函數(shù)指針,。 多繼承中的虛函數(shù)表中的順序是跟其派生類中的繼承順序相關,。 前面說的slot中包含兩個字段:offset_to_top 和typeinfo ,分別表示對象地址和指向類型的數(shù)據(jù)偏移量和真實數(shù)據(jù)類型,。 可以使用基類指針指向派生類,,不能使用派生類指針指向基類。(因為派生類中有些動作在基類中沒有定義,,編譯不能通過,。)
內(nèi)存布局的變化在講虛繼承之前,我們先來了解一下虛函數(shù)指針在對象內(nèi)存中的位置,,是會在class最前面的8個字節(jié)中呢,,還是會在尾端,其實不同編譯器是不同的,,早期的cfront編譯器傾向于放在class尾端,,因為只有這樣才能在C程序代碼中使用,,這才符合base class C struct的對象布局,。當然放在最前面也有他的好處,好處一是放在最前面不必使用off_set定位,,二是對虛函數(shù)的調(diào)用相對于尾端會少執(zhí)行一次尋址,。但是同時也失去了C兼容,,下面我們就來看一下g++編譯器下的虛擬指針布局吧。代碼如下: 1
2
3
4
5
6
7
8
9
10
11 | class A{
public:long long int a = 0x01020304;
public:virtual void A_func_1(){} //有虛函數(shù)
};
class B:public virtual A{
public:long long int b = 0x05060708;
};
int main(){
B b;
return 0;
} |
編譯時加入-std=c++11選項,,之后進入GDB中查看b的內(nèi)存布局 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | (gdb) p &b
$3 = (B *) 0x7fffffffe0e0
(gdb) p/x b +---------+
$4 = (B) { | _vptr.A |
<A> = { +---------+
_vptr.A = 0x555555754d88 <vtable for B+16>, | A::a |
a = 0x1020304 +---------+
}, | B::b |
members of B: +---------+
b = 0x5060708
}
(gdb) p sizeof(b)
$5 = 24
(gdb) x/3xg 0x7fffffffe0e0
0x7fffffffe0e0: 0x0000555555754d88 0x0000000001020304
0x7fffffffe0f0: 0x0000000005060708 |
從上面的輸出我們可以看到,,_vptr.A 是在第一個字節(jié)的,隨后是類A中的成員數(shù)據(jù)和類B中的成員數(shù)據(jù),。因此我們可以得到結論
虛繼承繼承也可以使用虛機制來完成,,被稱為虛繼承,虛繼承的提出解決了面向?qū)ο笕匦灾坏睦^承中的菱形繼承的問題,,下面我們來分析,。 實驗2.1單虛繼承實驗中只有兩個類A,B,B繼承自A,,代碼如下: 1
2
3
4
5
6
7
8
9
10 | class A{
public:long long int a = 0x01020304;
};
class B:public virtual A{
public:long long int b = 0x05060708;
};
int main(){
B b;
return 0;
} |
對象b的內(nèi)存布局如下: 1
2
3
4
5
6
7
8
9 | (gdb) p/x b
$1 = (B) {
<A> = {
a = 0x01020304
},
members of B:
_vptr.B = 0x555555754d78 <VTT for B>,
b = 0x05060708
} |
我們可以看到,,指針_vptr.B指向的并不是vtable for B 了,而是<VVT for B> 而VTT又是何物呢,,可以從虛函數(shù)表中看出一二,。那么它在內(nèi)存空間的位置是不是發(fā)生變化了呢,,使用gdb查看如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | (gdb) p/x b
$1 = (B) {
<A> = {
a = 0x1020304
},
members of B:
_vptr.B = 0x555555754d78 <VTT for B>,
b = 0x5060708
}
(gdb) p &b
$2 = (B *) 0x7fffffffe0e0
(gdb) p sizeof(b)
$3 = 24
(gdb) x/3xg 0x7fffffffe0e0
0x7fffffffe0e0: 0x0000555555754d78 0x0000000005060708
0x7fffffffe0f0: 0x0000000001020304 |
可以看到,,類A中的數(shù)據(jù)已經(jīng)放入類B的尾端了,這是由虛繼承引起的,。虛繼承會將虛基類對象放在最后,,并通過在vtable中加入vbase_off 字段進行偏移查找,。 所以我們會得到虛函數(shù)表如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | Class A
size=8 align=8
base size=8 base align=8 +-------------------+
A (0x0x7f25a1c36960) 0 +---------------+ | 16(vbase_off) |
| _vptr.B |----+ +-------------------+
Vtable for B +---------------+ | | 0(offset_to_top) |
B::_ZTV1B: 3 entries | B::b | | +-------------------+
0 16 +---------------+ | | ptr_typeinfo::B |
8 (int (*)(...))0 | A::a | +--> +-------------------+
16 (int (*)(...))(& _ZTI1B) +---------------| | VTT for B |
24 +-------------------+
VTT for B
B::_ZTT1B: 1 entries
0 ((& B::_ZTV1B) + 24)
Class B
size=24 align=8
base size=16 base align=8
B (0x0x7f25a1acc1a0) 0
vptridx=0 vptr=((& B::_ZTV1B) + 24)
A (0x0x7f25a1c369c0) 16 virtual
vbaseoffset=-24 |
現(xiàn)在給出VTT的定義: An array of virtual table addresses, called the VTT, is declared for each class type that has indirect or direct virtual base classes.
VTT是緊接著vtable在其后面的字段放置。虛函數(shù)指針指向了VTT,。實際上VTT是可以幫助vptr實現(xiàn)快速跳轉(zhuǎn)的,,后面的例子會更加清晰。 Following the primary virtual table of a derived class are secondary virtual tables for each of its proper base classes, except any primary base(s) with which it shares its primary virtual table.
并且在虛函數(shù)表中新增了一個字段vbase_offset ,,在本例中為16,,表示this指針對基類的偏移,用于共享基類,,也就是說在本例中,,共享基類在24個字節(jié)中的最后8個字節(jié)。 這里我們可以得到的結論是 實驗2.2派生類未實現(xiàn)虛函數(shù)的虛繼承這個實驗中只有基類中有虛函數(shù),派生類中沒有實現(xiàn),。代碼如下: 1
2
3
4
5
6
7
8
9
10
11
12
13 | //虛繼承,,派生類并沒有實現(xiàn)基類的虛函數(shù)
class A{
public:long long int a = 0x01020304;
public:virtual void A_func(){}
};
class B:public virtual A{
public:long long int b = 0x05060708;
};
int main(){
B b;
return 0;
} |
而虛函數(shù)表為 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | Vtable for A
A::_ZTV1A: 3 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1A)
16 (int (*)(...))A::A_func
Class A +-------------------+
size=16 align=8 | 16(vbase_off) |
base size=16 base align=8 +-----------+ +-------------------+
A (0x0x7ff8638cc960) 0 | _vptr.B | ---+ | 0(offset_to_top) |
vptr=((& A::_ZTV1A) + 16) +-----------+ | +-------------------+
| B::b | | | ptr_typeinfo::B |
Vtable for B +-----------+ +------> +-------------------+
B::_ZTV1B: 7 entries | _vptr.A | ---+ | 0(vcall_offset) |
0 16 +-----------+ | +-------------------+
8 (int (*)(...))0 | A::a | | | -16(offset_to_top)|
16 (int (*)(...))(& _ZTI1B) +-----------+ | +-------------------+
24 0 | | ptr_typeinfo::B |
32 (int (*)(...))-16 +------> +-------------------+
40 (int (*)(...))(& _ZTI1B) | A::A_func() |
48 (int (*)(...))A::A_func +-------------------+
| VTT for B |
VTT for B +-------------------+
B::_ZTT1B: 2 entries
0 ((& B::_ZTV1B) + 24)
8 ((& B::_ZTV1B) + 48)
Class B
size=32 align=8
base size=16 base align=8
B (0x0x7ff8637621a0) 0
vptridx=0 vptr=((& B::_ZTV1B) + 24)
A (0x0x7ff8638cca20) 16 virtual
vptridx=8 vbaseoffset=-24 vptr=((& B::_ZTV1B) + 48) |
類B中因為沒有實現(xiàn)虛函數(shù),,所以vptr.B所指的位置沒有虛函數(shù)字段,第一個vbase_off字段表示的意思是,,從當前指針出發(fā),,需要16個字節(jié)的偏移就可以到達虛基類對象中,這16個字節(jié)分別是_vptr.B和B::b,;offset_to_top字段剛好相反,,表示的是到首地址的距離。 這里出現(xiàn)了一個新的字段vcall_offset ,,作用是在基類指針指向派生類時,,調(diào)用函數(shù)時,如果函數(shù)被派生類重寫了,,則需要通過vcall_offset進行偏移,,這里沒有重寫所以是0,下面的實驗會具體講解,。 在這里我們可得到的結論是: virtual base class 中有 virtual function時,,派生類虛函數(shù)表會在后面增加基類虛函數(shù)字段,并且生類對象中會增加一個指向這些字段的虛函數(shù)指針_vptr.A,。 虛基類中有虛函數(shù)的話,,派生類的虛函數(shù)表中會相應增加函數(shù)數(shù)量個vcall_offset 字段,這些字段是當前指針對派生類對象起始位置的偏移,,在運行時多態(tài)會使用到,。
實驗2.3派生類實現(xiàn)了虛函數(shù)的虛繼承這個實驗有點復雜,因為融合了兩種情況,,首先A類中實現(xiàn)了兩個虛函數(shù),,而B中只繼承實現(xiàn)了一個虛函數(shù),但是自己又聲明了一個虛函數(shù),。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | class A{
public:virtual void func1(){}
public:virtual void func2(){}
public:long long int a = 0x01020304;
};
class B:public virtual A{
public:virtual void func1(){}
public:virtual void func3(){}
public:long long int b = 0x05060708;
};
int main(){
B b;
return 0;
} |
查看.class文件如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 | Vtable for A
A::_ZTV1A: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1A) +----------------------------+
16 (int (*)(...))A::func1 | 16(vbase_off) |
24 (int (*)(...))A::func2 +----------------------------+
| 0(offset_to_top) |
Class A +-----------+ +----------------------------+
size=16 align=8 | _vptr.B |---+ | ptr_typeinfo::B |
base size=16 base align=8 +-----------+ +------> +----------------------------+
A (0x0x7f156715a960) 0 | B::b | | B::func1() |
vptr=((& A::_ZTV1A) + 16) +-----------+ +----------------------------+
| _vptr.A |---+ | B::func3() |
Vtable for B +-----------+ | +----------------------------+
B::_ZTV1B: 11 entries | A::a | | | 0(vcall_offset) | (func2)
0 16 +-----------+ | +----------------------------+
8 (int (*)(...))0 | | -16(vcall_offset) | (func1) o--+
16 (int (*)(...))(& _ZTI1B) | +----------------------------+ |
24 (int (*)(...))B::func1 | | -16(offset_to_top) | |
32 (int (*)(...))B::func3 | +----------------------------+ |
40 0 | | ptr_typeinfo::B | |
48 18446744073709551600 +------> +----------------------------+ |
56 (int (*)(...))-16 | virtual thunk for B::func1 |o-----------+
64 (int (*)(...))(& _ZTI1B) +----------------------------+
72 (int (*)(...))B::_ZTv0_n24_N1B5func1Ev | A::func2() |
80 (int (*)(...))A::func2 +----------------------------+
VTT for B
B::_ZTT1B: 2 entries asm in virtual thunk for B::func1: (gdb) x/3ig &virtual thunk
0 ((& B::_ZTV1B) + 24) <_ZTv0_n24_N1B5func1Ev>: mov (%rdi),%r10
8 ((& B::_ZTV1B) + 72) <_ZTv0_n24_N1B5func1Ev+3>: add -0x18(%r10),%rdi (這里的-0x18就是從
當前指針位置_vptr.A到-16vcall_offset
<_ZTv0_n24_N1B5func1Ev+7>: jmp 0x555555554900 <B::func1()>
Class B
size=32 align=8
base size=16 base align=8
B (0x0x7f1566ff0208) 0
vptridx=0 vptr=((& B::_ZTV1B) + 24)
A (0x0x7f156715aa80) 16 virtual
vptridx=8 vbaseoffset=-24 vptr=((& B::_ZTV1B) + 72) |
在本實驗中出現(xiàn)了一個vcall_offset字段為-16,,我們可以看到B中是重新實現(xiàn)了func1()函數(shù),所以-16的作用是:A* a = new B() ,,在調(diào)用a->func1() 時,,通過vcall_offset字段進行偏移,找到_vptr.B的指針,,從而調(diào)用B中實現(xiàn)的func1版本,。如何找到vcall_set字段上面的匯編代碼進行了演示。 這里我們可以得到結論: 實驗2.4 菱形繼承看看在虛繼承下的菱形繼承是如何實現(xiàn)的: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | class A{
public:long long int a = 0x00000000000102;
public:virtual void a_func(){}
};
class B:public virtual A{
public:long long int b = 0x00000000000304;
public:virtual void a_func(){}
public:virtual void b_func(){}
};
class C:public virtual A{
public:long long int c = 0x00000000000506;
public:virtual void a_func(){}
public:virtual void c_func(){}
};
class D:public B,public C{
public:long long int d = 0x00000000000708;
public:virtual void a_func(){}
public:virtual void d_func(){}
};
int main(){
D d;
return 0;
} |
.class文件輸出如下; 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56 | vatble for D
\\因為A,B,C的內(nèi)存布局在前面實驗都有展示,,所以這里省略 +-----------------------------+
low | 40(vbase_off) |
Vtable for D +-----------+ +-----------------------------+
D::_ZTV1D: 15 entries | _vptr.B |--+ | 0(offset_to_top) |
0 40 +-----------+ | +-----------------------------+
8 (int (*)(...))0 | B::b | | | ptr_typeinfo::D |
16 (int (*)(...))(& _ZTI1D) +-----------+ +--> +-----------------------------+
24 (int (*)(...))D::a_func | _vptr.C |--+ | D::a_func |
32 (int (*)(...))B::b_func +-----------+ | +-----------------------------+
40 (int (*)(...))D::d_func | C::c | | | B::b_func |
48 24 +-----------+ | +-----------------------------+
56 (int (*)(...))-16 | D::d | | | D::d_func |
64 (int (*)(...))(& _ZTI1D) +-----------+ | +-----------------------------+
72 (int (*)(...))D::_ZThn16_N1D6a_funcEv +-| _vptr.A | | | 24(vbase_off) |
80 (int (*)(...))C::c_func | +-----------+ | +-----------------------------+
88 18446744073709551576 | | A::a | | | -16(offset_to_top) |
96 (int (*)(...))-40 | +-----------+ | +-----------------------------+
104 (int (*)(...))(& _ZTI1D) | high | | ptr_typeinfo::D |
112 (int (*)(...))D::_ZTv0_n24_N1D6a_funcEv | +--> +-----------------------------+
| | virtual thunk for C::c_func |
Construction vtable for B (0x0x7f099da94410 instance) in D | +-----------------------------+
D::_ZTC1D0_1B: 9 entries | | C::c_func |
0 40 | +-----------------------------+
8 (int (*)(...))0 | | -40(vcall_offset) |
16 (int (*)(...))(& _ZTI1B) | +-----------------------------+
24 (int (*)(...))B::a_func | | -40(offset_to_top) |
32 (int (*)(...))B::b_func | +-----------------------------+
40 18446744073709551576 | | ptr_typeinfo::D |
48 (int (*)(...))-40 +-------------------> +-----------------------------+
56 (int (*)(...))(& _ZTI1B) | virtual thunk for D::a_func |
64 (int (*)(...))B::_ZTv0_n24_N1B6a_funcEv +-----------------------------+
Construction vtable for C (0x0x7f099da94478 instance) in D
D::_ZTC1D16_1C: 9 entries
0 24
8 (int (*)(...))0 B-in-D vtable
16 (int (*)(...))(& _ZTI1C) +---------------------+
24 (int (*)(...))C::a_func +---------+ | 40(vbase_off) |
32 (int (*)(...))C::c_func | _vptr.B |----+ +---------------------+
40 18446744073709551592 +---------+ | | 0(offset_to_top) |
48 (int (*)(...))-24 | B::b | | +---------------------+
56 (int (*)(...))(& _ZTI1C) +---------+ | | ptr_typeinfo::B |
64 (int (*)(...))C::_ZTv0_n24_N1C6a_funcEv | _vptr.C | +---------> +---------------------+
+---------+ | B::a_func |
VTT for D | C::c | +---------------------+
D::_ZTT1D: 7 entries +---------+ | B::b_func |
0 ((& D::_ZTV1D) + 24) | D::d | +---------------------+
8 ((& D::_ZTC1D0_1B) + 24) +---------+ | -32(vcall_offset) |
16 ((& D::_ZTC1D0_1B) + 64) | _vptr.A |----+ +---------------------+
24 ((& D::_ZTC1D16_1C) + 24) +---------+ | | -40(offset_to_top) |
32 ((& D::_ZTC1D16_1C) + 64) | A::a | | +---------------------+
40 ((& D::_ZTV1D) + 112) +---------+ | | ptr_typeinfo::B |
48 ((& D::_ZTV1D) + 72) +---------> +---------------------+
| thunk for B::b_func |
+---------------------+ |
這里需要特別注意:.class文件中多了兩個表,,分別是B-in-D vtable 和C-in-D vtable,因為在我們從實際類型為B的對象內(nèi)存分布和實際類型為D的對象內(nèi)存分布中看到,,兩者的偏移量其實不一樣,,在B中,A對象對B的首地址偏移量為16,,而在D中則是32,。如果在構建D虛表的時候使用的還是原來的B虛表,會由于偏移量不同而產(chǎn)生錯誤,,所以編譯器在編譯階段直接生成了B-in-D vtable和C-in-D vtable來幫助其D的虛表進行構建,。上面代碼中畫出了B-in-D的虛函數(shù)表,B-in-D中兩個指針,,C-in-D中同樣有兩個指針,,加上D中的三個指針,這樣總共有7個指針,,它們存放在VTT(virtual tables table)中,,在構造函數(shù)或者析構函數(shù)被調(diào)用的時候,子類的構造函數(shù)或析構函數(shù)向基類傳遞一個合適的,、指向VTT某個部分指針,,使得父類的構造函數(shù)或析構函數(shù)獲取到正確的虛表。 總結虛繼承是為了解決在菱形繼承下的問題,,菱形繼承在生活中很常見,,例如生物學中的分類,但是一旦跟計算機牽扯到一起,,要想表現(xiàn)得盡量完美無缺就需要巨大的努力了,。上面就是虛繼承解決菱形繼承問題的原理和虛函數(shù)表的變化,總結幾點如下: virtual base class 中有 virtual function時,,派生類虛函數(shù)表會在后面增加基類虛函數(shù)字段,,并且生類對象中會增加一個指向這些字段的虛函數(shù)指針_vptr.A。 虛基類中有虛函數(shù)的話,,派生類的虛函數(shù)表中會相應增加函數(shù)數(shù)量個vcall_offset 字段,,這些字段是當前指針對派生類對象起始位置的偏移,在運行時多態(tài)會使用到,。 vcall_offset 字段的排列是按照聲明逆序排列的,,這是因為指向它的指針需要逆序–。
虛繼承會增加一個叫做vbase_offset 的字段,,該字段用于定位虛基類在派生類對象中的位置,。
結語C虛機制分為兩個部分,虛函數(shù)和虛繼承,,虛函數(shù)是為了實現(xiàn)C運行時多態(tài)而產(chǎn)生的,;虛繼承則是為了解決繼承問題中的菱形問題所實現(xiàn)的機制,。 這篇文章前后花了足兩天的時間,有些地方會有表述問題,,有些實驗設計的不夠好,,在后面會進行改進。
|