C++中的虛函數(表)實現機制以及用C語言對其進行的模擬實現
前言
大家都應該知道C++的精髓是虛函數吧? 虛函數帶來的好處就是: 可以定義一個基類的指針, 其指向一個繼承類, 當通過基類的指針去調用函數時, 可以在運行時決定該調用基類的函數還是繼承類的函數. 虛函數是實現多態(動態綁定)/接口函數的基礎. 可以說: 沒有虛函數, C++將變得一無是處!
既然是C++的精髓, 那麼我們有必要了解一下她的實現方式嗎? 有必要! 既然C++是從C語言的基礎上發展而來的, 那麼我們可以嘗試用C語言來模擬實現嗎? 有可能! 接下來, 就是我一步一步地來解析C++的虛函數的實現方式, 以及用C語言對其進行的模擬.
C++對象的內存布局
要想知道C++對象的內存布局, 可以有多種方式, 比如:
1. 輸出成員變量的偏移, 通過offsetof宏來得到
2. 通過調試器查看, 比如常用的VS
1. 只有數據成員的對象
類實現如下:
1 class Base1
2 {
3 public:
4 int base1_1;
5 int base1_2;
6 };
對象大小及偏移:
sizeof(Base1) 8
offsetof(Base1, base1_1) 0
offsetof(Base1, base1_2) 4
可知對象布局:
可以看到, 成員變量是按照定義的順序來保存的, 最先聲明的在最上邊, 然後依次保存!
類對象的大小就是所有成員變量大小之和.
2. 沒有虛函數的對象
類實現如下:
復制代碼
1 class Base1
2 {
3 public:
4 int base1_1;
5 int base1_2;
6
7 void foo(){}
8 };
復制代碼
結果如下:
sizeof(Base1) 8
offsetof(Base1, base1_1) 0
offsetof(Base1, base1_2) 4
和前面的結果是一樣的? 不需要有什麼疑問對吧?
因為如果一個函數不是虛函數, 那麼他就不可能會發生動態綁定, 也就不會對對象的布局造成任何影響.
當調用一個非虛函數時, 那麼調用的一定就是當前指針類型擁有的那個成員函數. 這種調用機制在編譯時期就確定下來了.
3. 擁有僅一個虛函數的類對象
類實現如下:
復制代碼
1 class Base1
2 {
3 public:
4 int base1_1;
5 int base1_2;
6
7 virtual void base1_fun1() {}
8 };
復制代碼
結果如下:
sizeof(Base1) 12
offsetof(Base1, base1_1) 4
offsetof(Base1, base1_2) 8
咦? 多了4個字節? 且 base1_1 和 base1_2 的偏移都各自向後多了4個字節!
說明類對象的最前面被多加了4個字節的"東東", what's it?
現在, 我們通過VS2013來瞧瞧類Base1的變量b1的內存布局情況:
(由於我沒有寫構造函數, 所以變量的數據沒有根據, 但虛函數是編譯器為我們構造的, 數據正確!)
(Debug模式下, 未初始化的變量值為0xCCCCCCCC, 即:-858983460)
看到沒? base1_1前面多了一個變量 __vfptr(常說的虛函數表vtable指針), 其類型為void**, 這說明它是一個void*指針(注意:不是數組).
再看看[0]元素, 其類型為void*, 其值為 ConsoleApplication2.exe!Base1::base1_fun1(void), 這是什麼意思呢? 如果對WinDbg比較熟悉, 那麼應該知道這是一種慣用表示手法, 她就是指 Base1::base1_fun1() 函數的地址.
可得, __vfptr的定義偽代碼大概如下:
1 void* __fun[1] = { &Base1::base1_fun1 };
2 const void** __vfptr = &__fun[0];
值得注意的是:
1. 上面只是一種偽代碼方式, 語法不一定能通過.
2. 該類的對象大小為12個字節, 大小及偏移信息如下:
sizeof(Base1) 12
offsetof(__vfptr) 0
offsetof(base1_1) 4
offsetof(base1_2) 8
3. 大家有沒有留意這個__vfptr? 為什麼它被定義成一個指向指針數組的指針, 而不是直接定義成一個指針數組呢?
我為什麼要提這樣一個問題? 因為如果僅是一個指針的情況, 您就無法輕易地修改那個數組裡面的內容, 因為她並不屬於類對象的一部分.
屬於類對象的, 僅是一個指向虛函數表的一個指針__vfptr而已, 下一節我們將繼續討論這個問題.
4. 注意到__vfptr前面的const修飾. 她修飾的是那個虛函數表, 而不是__vfptr.
現在的對象布局如下:
4. 虛函數指針__vfptr位於所有的成員變量之前定義.
注意到: 我並未在此說明__vfptr的具體指向, 只是說明了現在類對象的布局情況.
接下來看一個稍微復雜一點的情況, 我將清楚地描述虛函數表的構成.
4.擁有多個虛函數的類對象
和前面一個例子差不多, 只是再加了一個虛函數. 定義如下:
復制代碼
1 class Base1
2 {
3 public:
4 int base1_1;
5 int base1_2;
6
7 virtual void base1_fun1() {}
8 virtual void base1_fun2() {}
9 };
復制代碼
大小以及偏移信息如下:
有情況!? 多了一個虛函數, 類對象大小卻依然是12個字節!
再來看看VS形象的表現:
呀, __vfptr所指向的函數指針數組中出現了第2個元素, 其值為Base1類的第2個虛函數base1_fun2()的函數地址.
現在, 虛函數指針以及虛函數表的偽定義大概如下:
1 void* __fun[] = { &Base1::base1_fun1, &Base1::base1_fun2 };
2 const void** __vfptr = &__fun[0];
通過上面兩張圖表, 我們可以得到如下結論:
1. 更加肯定前面我們所描述的: __vfptr只是一個指針, 她指向一個函數指針數組(即: 虛函數表)
2. 增加一個虛函數, 只是簡單地向該類對應的虛函數表中增加一項而已, 並不會影響到類對象的大小以及布局情況.
前面已經提到過: __vfptr只是一個指針, 她指向一個數組, 並且: 這個數組沒有包含到類定義內部, 那麼她們之間是怎樣一個關系呢?
不妨, 我們再定義一個類的變量b2, 現在再來看看__vfptr的指向:
通過Watch 1窗口我們看到:
1. b1和b2是類的兩個變量, 理所當然, 她們的地址是不同的(見 &b1 和 &b2).
2. 雖然b1和b2是類的兩個變量, 但是: 她們的__vfptr的指向卻是同一個虛函數表.
由此我們可以總結出:
同一個類的不同實例共用同一份虛函數表, 她們都通過一個所謂的虛函數表指針__vfptr(定義為void**類型)指向該虛函數表.
是時候該展示一下類對象的內存布局情況了:
不出意外, 很清晰明了地展示出來了吧? :-) hoho~~
那麼問題就來了! 這個虛函數表保存在哪裡呢? 其實, 我們無需過分追究她位於哪裡, 重點是:
1. 她是編譯器在編譯時期為我們創建好的, 只存在一份.
2. 定義類對象時, 編譯器自動將類對象的__vfptr指向這個虛函數表.
5. 單繼承且本身不存在虛函數的繼承類的內存布局
前面研究了那麼多啦, 終於該到研究繼承類了! 先研究單繼承!
依然, 簡單地定義一個繼承類, 如下:
復制代碼
1 class Base1
2 {
3 public:
4 int base1_1;
5 int base1_2;
6
7 virtual void base1_fun1() {}
8 virtual void base1_fun2() {}
9 };
10
11 class Derive1 : public Base1
12 {
13 public:
14 int derive1_1;
15 int derive1_2;
16 };
復制代碼
我們再來看看現在的內存布局(定義為Derive1 d1):
圖1:
沒錯! 基類在上邊, 繼承類的成員在下邊依次定義! 展開來看看:
圖2:
經展開後來看, 前面部分完全就是Base1的東西: 虛函數表指針+成員變量定義.
並且, Base1的虛函數表的[0][1]兩項還是其本身就擁有的函數: base1_fun1() 和 base1_fun2().
現在類的布局情況應該是下面這樣:
6. 本身不存在虛函數(不嚴謹)但存在基類虛函數覆蓋的單繼承類的內存布局
標題`本身不存在虛函數`的說法有些不嚴謹, 我的意思是說: 除經過繼承而得來的基類虛函數以外, 自身沒有再定義其它的虛函數.
Ok, 既然存在基類虛函數覆蓋, 那麼來看看接下來的代碼會產生何種影響:
復制代碼
1 class Base1
2 {
3 public:
4 int base1_1;
5 int base1_2;
6
7 virtual void base1_fun1() {}
8 virtual void base1_fun2() {}
9 };
10
11 class Derive1 : public Base1
12 {
13 public:
14 int derive1_1;
15 int derive1_2;
16
17 // 覆蓋基類函數
18 virtual void base1_fun1() {}
19 };
復制代碼
可以看到, Derive1類 重寫了Base1類的base1_fun1()函數, 也就是常說的虛函數覆蓋. 現在是怎樣布局的呢?
特別注意我高亮的那一行: 原本是Base1::base1_fun1(), 但由於繼承類重寫了基類Base1的此方法, 所以現在變成了Derive1::base1_fun1()!
那麼, 無論是通過Derive1的指針還是Base1的指針來調用此方法, 調用的都將是被繼承類重寫後的那個方法(函數), 多態發生鳥!!!
那麼新的布局圖:
7. 定義了基類沒有的虛函數的單繼承的類對象布局
說明一下: 由於前面一種情況只會造成覆蓋基類虛函數表的指針, 所以接下來我不再同時討論虛函數覆蓋的情況.
繼續貼代碼:
復制代碼
1 class Base1
2 {
3 public:
4 int base1_1;
5 int base1_2;
6
7 virtual void base1_fun1() {}
8 virtual void base1_fun2() {}
9 };
10
11 class Derive1 : public Base1
12 {
13 public:
14 int derive1_1;
15 int derive1_2;
16
17 virtual void derive1_fun1() {}
18 };
復制代碼
和第5類不同的是多了一個自身定義的虛函數. 和第6類不同的是沒有基類虛函數的覆蓋.
咦, 有沒有發現問題? 表面上看來幾乎和第5種情況完全一樣? 為嘛呢?
現在繼承類明明定義了自身的虛函數, 但不見了??
那麼, 來看看類對象的大小, 以及成員偏移情況吧:
居然沒有變化!!! 前面12個字節是Base1的, 有沒有覺得很奇怪?
好吧, 既然表面上沒辦法了, 我們就只能從匯編入手了, 來看看調用derive1_fun1()時的代碼:
1 Derive1 d1;
2 Derive1* pd1 = &d1;
3 pd1->derive1_fun1();
要注意: 我為什麼使用指針的方式調用? 說明一下: 因為如果不使用指針調用, 虛函數調用是不會發生動態綁定的哦! 你若直接 d1.derive1_fun1(); , 是不可能會發生動態綁定的, 但如果使用指針: pd1->derive1_fun1(); , 那麼 pd1就無從知道她所指向的對象到底是Derive1 還是繼承於Derive1的對象, 雖然這裡我們並沒有對象繼承於Derive1, 但是她不得不這樣做, 畢竟繼承類不管你如何繼承, 都不會影響到基類, 對吧?
接下來看看調用反匯編代碼:
復制代碼
1 pd1->derive1_fun1();
2 00825466 mov eax,dword ptr [pd1]
3 00825469 mov edx,dword ptr [eax]
4 0082546B mov esi,esp
5 0082546D mov ecx,dword ptr [pd1]
6 00825470 mov eax,dword ptr [edx+8]
7 00825473 call eax
復制代碼
匯編代碼解釋:
第2行: 由於pd1是指向d1的指針, 所以執行此句後 eax 就是d1的地址
第3行: 又因為Base1::__vfptr是Base1的第1個成員, 同時也是Derive1的第1個成員, 那麼: &__vfptr == &d1, clear?
所以當執行完 mov edx, dword ptr[eax] 後, edx就得到了__vfptr的值, 也就是虛函數表的地址.
第5行: 由於是__thiscall調用, 所以把this保存到ecx中.
第6行: 一定要注意到那個edx+8, 由於edx是虛函數表的地址, 那麼 edx+8將是虛函數表的第3個元素, 也就是__vftable[2]!!!
第7行: 調用虛函數.
結果:
1. 現在我們應該知道內幕了! 繼承類Derive1的虛函數表被加在基類的後面! 事實的確就是這樣!
2. 由於Base1只知道自己的兩個虛函數索引[0][1], 所以就算在後面加上了[2], Base1根本不知情, 不會對她造成任何影響.
3. 如果基類沒有虛函數呢? 這個問題我們留到第9小節再來討論!
最新的類對象布局表示:
8. 多繼承且存在虛函數覆蓋同時又存在自身定義的虛函數的類對象布局
真快, 該看看多繼承了, 多繼承很常見, 特別是接口類中!
依然寫點小類玩玩:
復制代碼
1 class Base1
2 {
3 public:
4 int base1_1;
5 int base1_2;
6
7 virtual void base1_fun1() {}
8 virtual void base1_fun2() {}
9 };
10
11 class Base2
12 {
13 public:
14 int base2_1;
15 int base2_2;
16
17 virtual void base2_fun1() {}
18 virtual void base2_fun2() {}
19 };
20
21 // 多繼承
22 class Derive1 : public Base1, public Base2
23 {
24 public:
25 int derive1_1;
26 int derive1_2;
27
28 // 基類虛函數覆蓋
29 virtual void base1_fun1() {}
30 virtual void base2_fun2() {}
31
32 // 自身定義的虛函數
33 virtual void derive1_fun1() {}
34 virtual void derive1_fun2() {}
35 };
復制代碼
代碼變得越來越長啦! 為了代碼結構清晰, 我盡量簡化定義.
初步了解一下對象大小及偏移信息:
貌似, 若有所思? 不管, 來看看VS再想:
哇, 不擺了! 一絲不掛啊! :-)
結論:
1. 按照基類的聲明順序, 基類的成員依次分布在繼承中.
2. 注意被我高亮的那兩行, 已經發生了虛函數覆蓋!
3. 我們自己定義的虛函數呢? 怎麼還是看不見?!
好吧, 繼承反匯編, 這次的調用代碼如下:
1 Derive1 d1;
2 Derive1* pd1 = &d1;
3 pd1->derive1_fun2();
反匯編代碼如下:
復制代碼
1 pd1->derive1_fun2();
2 00995306 mov eax,dword ptr [pd1]
3 00995309 mov edx,dword ptr [eax]
4 0099530B mov esi,esp
5 0099530D mov ecx,dword ptr [pd1]
6 00995310 mov eax,dword ptr [edx+0Ch]
7 00995313 call eax
復制代碼
解釋下, 其實差不多:
第2行: 取d1的地址
第3行: 取Base1::__vfptr的值!!
第6行: 0x0C, 也就是第4個元素(下標為[3])
結論:
Derive1的虛函數表依然是保存到第1個擁有虛函數表的那個基類的後面的.
看看現在的類對象布局圖:
如果第1個基類沒有虛函數表呢? 進入第9節!
9. 如果第1個直接基類沒有虛函數(表)
這次的代碼應該比上一個要稍微簡單一些, 因為把第1個類的虛函數給去掉鳥!
復制代碼
1 class Base1
2 {
3 public:
4 int base1_1;
5 int base1_2;
6 };
7
8 class Base2
9 {
10 public:
11 int base2_1;
12 int base2_2;
13
14 virtual void base2_fun1() {}
15 virtual void base2_fun2() {}
16 };
17
18 // 多繼承
19 class Derive1 : public Base1, public Base2
20 {
21 public:
22 int derive1_1;
23 int derive1_2;
24
25 // 自身定義的虛函數
26 virtual void derive1_fun1() {}
27 virtual void derive1_fun2() {}
28 };
復制代碼
來看看VS的布局:
這次相對前面一次的圖來說還要簡單啦! Base1已經沒有虛函數表了! (真實情況並非完全這樣, 請繼續往下看!)
現在的大小及偏移情況: 注意: sizeof(Base1) == 8;
重點是看虛函數的位置, 進入函數調用(和前一次是一樣的):
1 Derive1 d1;
2 Derive1* pd1 = &d1;
3 pd1->derive1_fun2();
反匯編調用代碼:
復制代碼
1 pd1->derive1_fun2();
2 012E4BA6 mov eax,dword ptr [pd1]
3 012E4BA9 mov edx,dword ptr [eax]
4 012E4BAB mov esi,esp
5 012E4BAD mov ecx,dword ptr [pd1]
6 012E4BB0 mov eax,dword ptr [edx+0Ch]
7 012E4BB3 call eax
復制代碼
這段匯編代碼和前面一個完全一樣!, 那麼問題就來了! Base1 已經沒有虛函數表了, 為什麼還是把b1的第1個元素當作__vfptr呢?
不難猜測: 當前的布局已經發生了變化, 有虛函數表的基類放在對象內存前面!? , 不過事實是否屬實? 需要仔細斟酌.
我們可以通過對基類成員變量求偏移來觀察:
可以看到:
&d1==0x~d4
&d1.Base1::__vfptr==0x~d4
&d1.base2_1==0x~d8
&d1.base2_2==0x~dc
&d1.base1_1==0x~e0
&d1.base1_2==0x~e4
所以不難驗證: 我們前面的推斷是正確的, 誰有虛函數表, 誰就放在前面!
現在類的布局情況:
那麼, 如果兩個基類都沒有虛函數表呢?
10. What if 兩個基類都沒有虛函數表
代碼如下:
復制代碼
1 class Base1
2 {
3 public:
4 int base1_1;
5 int base1_2;
6 };
7
8 class Base2
9 {
10 public:
11 int base2_1;
12 int base2_2;
13 };
14
15 // 多繼承
16 class Derive1 : public Base1, public Base2
17 {
18 public:
19 int derive1_1;
20 int derive1_2;
21
22 // 自身定義的虛函數
23 virtual void derive1_fun1() {}
24 virtual void derive1_fun2() {}
25 };
復制代碼
前面吃了個虧, 現在先來看看VS的基本布局:
可以看到, 現在__vfptr已經獨立出來了, 不再屬於Base1和Base2!
看看求偏移情況:
Ok, 問題解決! 注意高亮的那兩行, &d1==&d1.__vfptr, 說明虛函數始終在最前面!
不用再廢話, 相信大家對這種情況已經有底了.
對象布局:
11. 如果有三個基類: 虛函數表分別是有, 沒有, 有!
這種情況其實已經無需再討論了, 作為一個完結篇....
上代碼:
復制代碼
1 class Base1
2 {
3 public:
4 int base1_1;
5 int base1_2;
6
7 virtual void base1_fun1() {}
8 virtual void base1_fun2() {}
9 };
10
11 class Base2
12 {
13 public:
14 int base2_1;
15 int base2_2;
16 };
17
18 class Base3
19 {
20 public:
21 int base3_1;
22 int base3_2;
23
24 virtual void base3_fun1() {}
25 virtual void base3_fun2() {}
26 };
27
28 // 多繼承
29 class Derive1 : public Base1, public Base2, public Base3
30 {
31 public:
32 int derive1_1;
33 int derive1_2;
34
35 // 自身定義的虛函數
36 virtual void derive1_fun1() {}
37 virtual void derive1_fun2() {}
38 };
復制代碼
只需要看看偏移就行了:
只需知道: 誰有虛函數表, 誰就往前靠!
C++中父子對象指針間的轉換與函數調用
講了那麼多布局方面的東東, 終於到了發聲, 好累呀!!!
通過前面的講解內容, 大家至少應該明白了各類情況下類對象的內存布局了. 如果還不會.....呃..... !@#$%^&*
進入正題~
由於繼承完全擁有父類的所有, 包括數據成員與虛函數表, 所以:
把一個繼承類強制轉換為一個基類是完全可行的.
如果有一個Derive1的指針, 那麼:
得到Base1的指針: Base1* pb1 = pd1;
得到Base2的指針: Base2* pb2 = pd1;
得到Base3的指針: Base3* pb3 = pd1;
非常值得注意的是:
這是在基類與繼承類之間的轉換, 這種轉換會自動計算偏移! 按照前面的布局方式!
也就是說: 在這裡極有可能: pb1 != pb2 != pb3 ~~, 不要以為她們都等於 pd1!
至於函數調用, 我想, 不用說大家應該知道了:
1. 如果不是虛函數, 直接調用指針對應的基本類的那個函數
2. 如果是虛函數, 則查找虛函數表, 並進行後續的調用.
虛函數表在定義一個時, 編譯器就為我們創建好了的. 所有的, 同一個類, 共用同一份虛函數表.
用C語言完全模擬C++虛函數表的實現與運作方式
如果對前面兩大節的描述仔細了解了的話, 想用C語言來模擬C++的虛函數以及多態, 想必是輕而易舉的事情鳥!
1. 前提
但是, 話得說在前面, C++的編譯器在生成類及對象的時候, 幫助我們完成了很多事件, 比如生成虛函數表!
但是, C語言編譯器卻沒有, 因此, 很多事件我們必須手動來完成, 包括但不限於:
1. 手動構造父子關系
2. 手動創建虛函數表
3. 手動設置__vfptr並指向虛函數表
4. 手動填充虛函數表
5. 若有虛函數覆蓋, 還需手動修改函數指針
6. 若要取得基類指針, 還需手動強制轉換
......
總之, 要想用C語言來實現, 要寫的代碼絕對有點復雜.
2. C++原版調用
接下來, 我們都將以最後那個, 最繁雜的那個3個基類的實例來講解, 但作了一些簡化與改動:
0. 用構造函數初始化成員變量
1. 減少成員變量的個數
2. 減少虛函數的個數
3. 調用函數時產生相關輸出
4. Derive1增加一個基類虛函數覆蓋
以下是對類的改動, 很少:
復制代碼
class Base1
{
public:
Base1() : base1_1(11) {}
int base1_1;
virtual void base1_fun1() {
std::cout << "Base1::base1_fun1()" << std::endl;
}
};
class Base2
{
public:
Base2() : base2_1(21) {}
int base2_1;
};
class Base3
{
public:
Base3() : base3_1(31) {}
int base3_1;
virtual void base3_fun1() {
std::cout << "Base3::base3_fun1()" << std::endl;
}
};
class Derive1 : public Base1, public Base2, public Base3
{
public:
Derive1() : derive1_1(11) {}
int derive1_1;
virtual void base3_fun1() {
std::cout << "Derive1::base3_fun1()" << std::endl;
}
virtual void derive1_fun1() {
std::cout << "Derive1::derive1_fun1()" << std::endl;
}
};
復制代碼
為了看到多態的效果, 我們還需要定義一個函數來看效果:
復制代碼
1 void foo(Base1* pb1, Base2* pb2, Base3* pb3, Derive1* pd1)
2 {
3 std::cout << "Base1::\n"
4 << " pb1->base1_1 = " << pb1->base1_1 << "\n"
5 << " pb1->base1_fun1(): ";
6 pb1->base1_fun1();
7
8 std::cout << "Base2::\n"
9 << " pb2->base2_1 = " << pb2->base2_1
10 << std::endl;
11
12 std::cout << "Base3::\n"
13 << " pb3->base3_1 = " << pb3->base3_1 << "\n"
14 << " pb3->base3_fun1(): ";
15 pb3->base3_fun1();
16
17 std::cout << "Derive1::\n"
18 << " pd1->derive1_1 = " << pd1->derive1_1 << "\n"
19 << " pd1->derive1_fun1(): ";
20 pd1->derive1_fun1();
21 std::cout << " pd1->base3_fun1(): ";
22 pd1->base3_fun1();
23
24 std::cout << std::endl;
25 }
復制代碼
調用方式如下:
1 Derive1 d1;
2 foo(&d1, &d1, &d1, &d1);
輸出結果:
可以看到輸出結果全部正確(當然了! :-), 哈哈~
同時注意到 pb3->base3_fun1() 的多態效果哦!
3. 用C語言來模擬
必須要把前面的理解了, 才能看懂下面的代碼!
為了有別於已經完成的C++的類, 我們分別在類前面加一個大寫的C以示區分(平常大家都是習慣在C++寫的類前面加C, 今天恰好反過來, 哈哈).
0. C語言無法實現的部分
C/C++是兩個語言, 有些語言特性是C++專有的, 我們無法實現! 不過, 這裡我是指調用約定,, 我們應該把她排除在外.
對於類的成員函數, C++默認使用__thiscall, 也即this指針通過ecx傳遞, 這在C語言無法實現, 所以我們必須手動聲明調用約定為:
1. __stdcall, 就像微軟的組件對象模型那樣
2. __cdecl, 本身就C語言的調用約定, 當然能使用了.
上面那種調用約定, 使用哪一種無關緊要, 反正不能使用__thiscall就行了.
因為使用了非__thiscall調用約定, 我們就必須手動傳入this指針, 通過成員函數的第1個參數!
1. 從最簡單的開始: 實現 Base2
由於沒有虛函數, 僅有成員變量, 這個當然是最好模擬的咯!
1 struct CBase2
2 {
3 int base2_1;
4 };
2. 有了虛函數表的Base1, 但沒被覆蓋
下面是Base1的定義, 要復雜一點了, 多一個__vfptr:
1 struct CBase1
2 {
3 void** __vfptr;
4 int base1_1;
5 };
因為有虛函數表, 所以還得單獨為虛函數表創建一個結構體的哦!
但是, 為了更能清楚起見, 我並未定義前面所說的指針數組, 而是用一個包含一個或多個函數指針的結構體來表示!
因為數組能保存的是同一類的函數指針, 不太很友好! 但他們的效果是完全一樣的, 希望讀者能夠理解明白!
1 struct CBase1_VFTable
2 {
3 void(__stdcall* base1_fun1)(CBase1* that);
4 };
注意: base1_fun1 在這裡是一個指針變量!
注意: base1_fun1 有一個CBase1的指針, 因為我們不再使用__thiscall, 我們必須手動傳入! Got it?
Base1的成員函數base1_fun1()我們也需要自己定義, 而且是定義成全局的:
1 void __stdcall base1_fun1(CBase1* that)
2 {
3 std::cout << "base1_fun1()" << std::endl;
4 }
3. 有虛函數覆蓋的Base3
虛函數覆蓋在這裡並不能體現出來, 要在構造對象初始化的時候才會體現, 所以: base3其實和Base1是一樣的.
復制代碼
1 struct CBase3
2 {
3 void** __vfptr;
4 int base3_1;
5 };
6
7 struct CBase3_VFTable
8 {
9 void(__stdcall* base3_fun1)(CBase3* that);
10 };
復制代碼
Base3的成員函數:
1 void __stdcall base3_fun1(CBase1* that)
2 {
3 std::cout << "base3_fun1()" << std::endl;
4 }
4. 定義繼承類CDerive1
相對前面幾個類來說, 這個類要顯得稍微復雜一些了, 因為包含了前面幾個類的內容:
復制代碼
1 struct CDerive1
2 {
3 CBase1 base1;
4 CBase3 base3;
5 CBase2 base2;
6
7 int derive1_1;
8 };
復制代碼
特別注意: CBase123的順序不能錯!
另外: 由於Derive1本身還有虛函數表, 而且所以項是加到第一個虛函數表(CBase1)的後面的, 所以此時的CBase1::__vfptr不應該單單指向CBase1_VFTable, 而應該指向下面這個包含Derive1類虛函數表的結構體才行:
1 struct CBase1_CDerive1_VFTable
2 {
3 void (__stdcall* base1_fun1)(CBase1* that);
4 void(__stdcall* derive1_fun1)(CDerive1* that);
5 };
因為CDerive1覆蓋了CBase3的base3_fun1()函數, 所以不能直接用Base3的那個表:
1 struct CBase3_CDerive1_VFTable
2 {
3 void(__stdcall* base3_fun1)(CDerive1* that);
4 };
Derive1覆蓋Base3::base3_fun1()的函數以及自身定義的derive1_fun1()函數:
復制代碼
1 void __stdcall base3_derive1_fun1(CDerive1* that)
2 {
3 std::cout << "base3_derive1_fun1()" << std::endl;
4 }
5
6 void __stdcall derive1_fun1(CDerive1* that)
7 {
8 std::cout << "derive1_fun1()" << std::endl;
9 }
復制代碼
5. 構造各類的全局虛函數表
由於沒有了編譯器的幫忙, 在定義一個類對象時, 所有的初始化工作都只能由我們自己來完成了!
首先構造全局的, 被同一個類共同使用的虛函數表!
復制代碼
1 // CBase1 的虛函數表
2 CBase1_VFTable __vftable_base1;
3 __vftable_base1.base1_fun1 = base1_fun1;
4
5 // CBase3 的虛函數表
6 CBase3_VFTable __vftable_base3;
7 __vftable_base3.base3_fun1 = base3_fun1;
復制代碼
然後構造CDerive1和CBase1共同使用的虛函數表:
1 // CDerive1 和 CBase1 共用的虛函數表
2 CBase1_CDerive1_VFTable __vftable_base1_derive1;
3 __vftable_base1_derive1.base1_fun1 = base1_fun1;
4 __vftable_base1_derive1.derive1_fun1 = derive1_fun1;
再構造CDerive1覆蓋CBase3後的虛函數表:
注意: 函數覆蓋會替換原來的函數指針
1 CBase3_CDerive1_VFTable __vftable_base3_derive1;
2 __vftable_base3_derive1.base3_fun1 = base3_derive1_fun1;
6. 開始! 從CDerive1構造一個完整的Derive1類
先初始化成員變量與__vfptr的指向: 注意不是指錯了!
復制代碼
1 CDerive1 d1;
d1.derive1 = 1;
2
3 d1.base1.base1_1 = 11;
4 d1.base1.__vfptr = reinterpret_cast<void**>(&__vftable_base1_derive1);
5
6 d1.base2.base2_1 = 21;
7
8 d1.base3.base3_1 = 31;
9 d1.base3.__vfptr = reinterpret_cast<void**>(&__vftable_base3_derive1);
復制代碼
由於目前的CDerive1是我們手動構造的, 不存在真正語法上的繼承關系, 如要得到各基類指針, 我們就不能直接來取, 必須手動根據偏移計算:
1 char* p = reinterpret_cast<char*>(&d1);
2 Base1* pb1 = reinterpret_cast<Base1*>(p + 0);
3 Base2* pb2 = reinterpret_cast<Base2*>(p + sizeof(CBase1) + sizeof(CBase3));
4 Base3* pb3 = reinterpret_cast<Base3*>(p + sizeof(CBase1));
5 Derive1* pd1 = reinterpret_cast<Derive1*>(p);
真正調用:
1 foo(pb1, pb2, pb3, pd1);