1. 程式人生 > >C++ 虛擬函式表 vfptr

C++ 虛擬函式表 vfptr

前言

大家都應該知道C++的精髓是虛擬函式吧? 虛擬函式帶來的好處就是: 可以定義一個基類的指標, 其指向一個繼承類, 當通過基類的指標去呼叫函式時, 可以在執行時決定該呼叫基類的函式還是繼承類的函式. 虛擬函式是實現多型(動態繫結)/介面函式的基礎. 可以說: 沒有虛擬函式, C++將變得一無是處!

既然是C++的精髓, 那麼我們有必要了解一下她的實現方式嗎? 有必要! 既然C++是從C語言的基礎上發展而來的, 那麼我們可以嘗試用C語言來模擬實現嗎? 有可能! 接下來, 就是我一步一步地來解析C++的虛擬函式的實現方式, 以及用C語言對其進行的模擬.

C++物件的記憶體佈局

要想知道C++物件的記憶體佈局, 可以有多種方式, 比如:

  1. 輸出成員變數的偏移, 通過offsetof巨集來得到
  2. 通過偵錯程式檢視, 比如常用的VS

只有資料成員的物件

類實現如下:

class Base1
{
public:
    int base1_1;
    int base1_2;
};

物件大小及偏移:

sizeof(Base1)               8
offsetof(Base1, base1_1)    0
offsetof(Base1, base1_2)    4

可知物件佈局:
這裡寫圖片描述

可以看到, 成員變數是按照定義的順序來儲存的, 最先宣告的在最上邊, 然後依次儲存!
類物件的大小就是所有成員變數大小之和.

沒有虛擬函式的物件

class Base1
{
public:
    int base1_1;
    int base1_2;

    void foo(){}
};

結果如下:

sizeof(Base1)               8
offsetof(Base1, base1_1)    0
offsetof(Base1, base1_2)    4

和前面的結果是一樣的? 不需要有什麼疑問對吧?
因為如果一個函式不是虛擬函式,那麼他就不可能會發生動態繫結,也就不會對物件的佈局造成任何影響.
當呼叫一個非虛擬函式時, 那麼呼叫的一定就是當前指標型別擁有的那個成員函式. 這種呼叫機制在編譯時期就確定下來了.

擁有僅一個虛擬函式的類物件

類實現如下:

class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
};

結果如下:

sizeof(Base1)               12
offsetof(Base1, base1_1)    4
offsetof(Base1, base1_2)    8

咦? 多了4個位元組? (64位機器上虛擬函式表指標佔8個位元組)且 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的定義虛擬碼大概如下:

void*   __fun[1] = { &Base1::base1_fun1 };
const void**  __vfptr = &__fun[0];

值得注意的是:

  1. 上面只是一種虛擬碼方式, 語法不一定能通過
  2. 該類的物件大小為12個位元組, 大小及偏移資訊如下:
  3. 大家有沒有留意這個__vfptr? 為什麼它被定義成一個指向指標陣列的指標, 而不是直接定義成一個指標陣列呢?
    我為什麼要提這樣一個問題? 因為如果僅是一個指標的情況, 您就無法輕易地修改那個數組裡面的內容, 因為她並不屬於類物件的一部分.屬於類物件的, 僅是一個指向虛擬函式表的一個指標__vfptr而已, 下一節我們將繼續討論這個問題.
  4. 注意到__vfptr前面的const修飾. 她修飾的是那個虛擬函式表, 而不是__vfptr.

現在的物件佈局如下:
這裡寫圖片描述

虛擬函式指標__vfptr位於所有的成員變數之前定義.

注意到: 我並未在此說明__vfptr的具體指向, 只是說明了現在類物件的佈局情況.
接下來看一個稍微複雜一點的情況, 我將清楚地描述虛擬函式表的構成.

擁有多個虛擬函式的類物件

和前面一個例子差不多, 只是再加了一個虛擬函式. 定義如下:

class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

大小以及偏移資訊如下:
這裡寫圖片描述

有情況!? 多了一個虛擬函式, 類物件大小卻依然是12個位元組!

再來看看VS形象的表現:

呀, __vfptr所指向的函式指標陣列中出現了第2個元素, 其值為Base1類的第2個虛擬函式base1_fun2()的函式地址.

現在, 虛擬函式指標以及虛擬函式表的偽定義大概如下:

void* __fun[] = { &Base1::base1_fun1, &Base1::base1_fun2 };
const void** __vfptr = &__fun[0];

通過上面兩張圖表, 我們可以得到如下結論:

  1. 更加肯定前面我們所描述的: __vfptr只是一個指標, 她指向一個函式指標陣列(即: 虛擬函式表)
  2. 增加一個虛擬函式, 只是簡單地向該類對應的虛擬函式表中增加一項而已, 並不會影響到類物件的大小以及佈局情況

前面已經提到過: __vfptr只是一個指標, 她指向一個數組, 並且: 這個陣列沒有包含到類定義內部, 那麼她們之間是怎樣一個關係呢?

不妨, 我們再定義一個類的變數b2, 現在再來看看__vfptr的指向:

通過Watch 1視窗我們看到:

b1和b2是類的兩個變數, 理所當然, 她們的地址是不同的(見 &b1 和 &b2)
雖然b1和b2是類的兩個變數, 但是: 她們的__vfptr的指向卻是同一個虛擬函式表
由此我們可以總結出:

同一個類的不同例項共用同一份虛擬函式表, 她們都通過一個所謂的虛擬函式表指標__vfptr(定義為void**型別)指向該虛擬函式表.

是時候該展示一下類物件的記憶體佈局情況了:

不出意外, 很清晰明瞭地展示出來了吧? :-) hoho~~

那麼問題就來了! 這個虛擬函式表儲存在哪裡呢? 其實, 我們無需過分追究她位於哪裡, 重點是:

  1. 她是編譯器在編譯時期為我們建立好的, 只存在一份
  2. 定義類物件時, 編譯器自動將類物件的__vfptr指向這個虛擬函式表

單繼承且本身不存在虛擬函式的繼承類的記憶體佈局

前面研究了那麼多啦, 終於該到研究繼承類了! 先研究單繼承!

依然, 簡單地定義一個繼承類, 如下:

class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive1 : public Base1
{
public:
    int derive1_1;
    int derive1_2;
};

我們再來看看現在的記憶體佈局(定義為Derive1 d1):

沒錯! 基類在上邊, 繼承類的成員在下邊依次定義! 展開來看看:

經展開後來看, 前面部分完全就是Base1的東西: 虛擬函式表指標+成員變數定義.
並且, Base1的虛擬函式表的[0][1]兩項還是其本身就擁有的函式: base1_fun1() 和 base1_fun2().

本身不存在虛擬函式(不嚴謹)但存在基類虛擬函式覆蓋的單繼承類的記憶體佈局

標題本身不存在虛擬函式的說法有些不嚴謹, 我的意思是說: 除經過繼承而得來的基類虛擬函式以外, 自身沒有再定義其它的虛擬函式.

Ok, 既然存在基類虛擬函式覆蓋, 那麼來看看接下來的程式碼會產生何種影響:

class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive1 : public Base1
{
public:
    int derive1_1;
    int derive1_2;

    // 覆蓋基類函式
    virtual void base1_fun1() {}
};

可以看到, Derive1類 重寫了Base1類的base1_fun1()函式, 也就是常說的虛擬函式覆蓋. 現在是怎樣佈局的呢?

特別注意我高亮的那一行: 原本是Base1::base1_fun1(), 但由於繼承類重寫了基類Base1的此方法, 所以現在變成了Derive1::base1_fun1()!

那麼, 無論是通過Derive1的指標還是Base1的指標來呼叫此方法, 呼叫的都將是被繼承類重寫後的那個方法(函式), 多型發生了!!!

那麼新的佈局圖:
這裡寫圖片描述

定義了基類沒有的虛擬函式的單繼承的類物件佈局

class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive1 : public Base1
{
public:
    int derive1_1;
    int derive1_2;

    virtual void derive1_fun1() {}
};

和第5類不同的是多了一個自身定義的虛擬函式. 和第6類不同的是沒有基類虛擬函式的覆蓋.

咦, 有沒有發現問題? 表面上看來幾乎和第5種情況完全一樣? 為嘛呢?
現在繼承類明明定義了自身的虛擬函式, 但不見了??
那麼, 來看看類物件的大小, 以及成員偏移情況吧:

居然沒有變化!!! 前面12個位元組是Base1的, 有沒有覺得很奇怪?

好吧, 既然表面上沒辦法了, 我們就只能從彙編入手了, 來看看呼叫derive1_fun1()時的程式碼:

Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun1();

要注意: 我為什麼使用指標的方式呼叫? 說明一下: 因為如果不使用指標呼叫, 虛擬函式呼叫是不會發生動態繫結的哦! 你若直接 d1.derive1_fun1(); , 是不可能會發生動態繫結的, 但如果使用指標: pd1->derive1_fun1(); , 那麼 pd1就無從知道她所指向的物件到底是Derive1 還是繼承於Derive1的物件, 雖然這裡我們並沒有物件繼承於Derive1, 但是她不得不這樣做, 畢竟繼承類不管你如何繼承, 都不會影響到基類, 對吧?

; pd1->derive1_fun1();
00825466  mov         eax,dword ptr [pd1]  
00825469  mov         edx,dword ptr [eax]  
0082546B  mov         esi,esp  
0082546D  mov         ecx,dword ptr [pd1]  
00825470  mov         eax,dword ptr [edx+8]  
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小節再來討論!

最新的類物件佈局表示:

多繼承且存在虛擬函式覆蓋同時又存在自身定義的虛擬函式的類物件佈局

真快, 該看看多繼承了, 多繼承很常見, 特別是介面類中!

依然寫點小類玩玩:

class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Base2
{
public:
    int base2_1;
    int base2_2;

    virtual void base2_fun1() {}
    virtual void base2_fun2() {}
};

// 多繼承
class Derive1 : public Base1, public Base2
{
public:
    int derive1_1;
    int derive1_2;

    // 基類虛擬函式覆蓋
    virtual void base1_fun1() {}
    virtual void base2_fun2() {}

    // 自身定義的虛擬函式
    virtual void derive1_fun1() {}
    virtual void derive1_fun2() {}
};

程式碼變得越來越長啦! 為了程式碼結構清晰, 我儘量簡化定義.

初步瞭解一下物件大小及偏移資訊:

貌似, 若有所思? 不管, 來看看VS再想:

哇, 不擺了! 一絲不掛啊! :-)

結論:

  1. 按照基類的宣告順序, 基類的成員依次分佈在繼承中.
  2. 注意被我高亮的那兩行, 已經發生了虛擬函式覆蓋
  3. 我們自己定義的虛擬函式呢? 怎麼還是看不見?!

好吧, 繼承反彙編, 這次的呼叫程式碼如下:

Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();

反彙編程式碼如下:

; pd1->derive1_fun2();
00995306  mov         eax,dword ptr [pd1]  
00995309  mov         edx,dword ptr [eax]  
0099530B  mov         esi,esp  
0099530D  mov         ecx,dword ptr [pd1]  
00995310  mov         eax,dword ptr [edx+0Ch]  
00995313  call        eax

解釋下, 其實差不多:

第2行: 取d1的地址
第3行: 取Base1::__vfptr的值!!
第6行: 0x0C, 也就是第4個元素(下標為[3])

結論:

Derive1的虛擬函式表依然是儲存到第1個擁有虛擬函式表的那個基類的後面的.

看看現在的類物件佈局圖:

如果第1個基類沒有虛擬函式表呢? 進入第9節!

9.如果第1個直接基類沒有虛擬函式(表)

class Base1
{
public:
    int base1_1;
    int base1_2;
};

class Base2
{
public:
    int base2_1;
    int base2_2;

    virtual void base2_fun1() {}
    virtual void base2_fun2() {}
};

// 多繼承
class Derive1 : public Base1, public Base2
{
public:
    int derive1_1;
    int derive1_2;

    // 自身定義的虛擬函式
    virtual void derive1_fun1() {}
    virtual void derive1_fun2() {}
};

來看看VS的佈局:

這次相對前面一次的圖來說還要簡單啦! Base1已經沒有虛擬函式表了! (真實情況並非完全這樣, 請繼續往下看!)

現在的大小及偏移情況: 注意: sizeof(Base1) == 8;

重點是看虛擬函式的位置, 進入函式呼叫(和前一次是一樣的):

Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();

反彙編呼叫程式碼:

; pd1->derive1_fun2();
012E4BA6  mov         eax,dword ptr [pd1]  
012E4BA9  mov         edx,dword ptr [eax]  
012E4BAB  mov         esi,esp  
012E4BAD  mov         ecx,dword ptr [pd1]  
012E4BB0  mov         eax,dword ptr [edx+0Ch]  
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 兩個基類都沒有虛擬函式表

class Base1
{
public:
    int base1_1;
    int base1_2;
};

class Base2
{
public:
    int base2_1;
    int base2_2;
};

// 多繼承
class Derive1 : public Base1, public Base2
{
public:
    int derive1_1;
    int derive1_2;

    // 自身定義的虛擬函式
    virtual void derive1_fun1() {}
    virtual void derive1_fun2() {}
};

前面吃了個虧, 現在先來看看VS的基本佈局:

可以看到, 現在__vfptr已經獨立出來了, 不再屬於Base1和Base2!

看看求偏移情況:

Ok, 問題解決! 注意高亮的那兩行, &d1==&d1.__vfptr, 說明虛擬函式始終在最前面!

不用再廢話, 相信大家對這種情況已經有底了.

11.如果有三個基類: 虛擬函式表分別是有, 沒有, 有!

這種情況其實已經無需再討論了, 作為一個完結篇….

上程式碼:

class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Base2
{
public:
    int base2_1;
    int base2_2;
};

class Base3
{
public:
    int base3_1;
    int base3_2;

    virtual void base3_fun1() {}
    virtual void base3_fun2() {}
};

// 多繼承
class Derive1 : public Base1, public Base2, public Base3
{
public:
    int derive1_1;
    int derive1_2;

    // 自身定義的虛擬函式
    virtual void derive1_fun1() {}
    virtual void derive1_fun2() {}
};

只需要看看偏移就行了:

只需知道: 誰有虛擬函式表, 誰就往前靠!

C++中父子物件指標間的轉換與函式呼叫

講了那麼多佈局方面的東東, 終於到了尾聲, 好累呀!!!

通過前面的講解內容, 大家至少應該明白了各類情況下類物件的記憶體佈局了. 如果還不會…..呃….. [email protected]#$%^&*

進入正題~

由於繼承完全擁有父類的所有, 包括資料成員與虛擬函式表, 所以:把一個繼承類強制轉換為一個基類是完全可行的.

如果有一個Derive1的指標, 那麼:

  • 得到Base1的指標: Base1* pb1 = pd1;
  • 得到Base2的指標: Base2* pb2 = pd1;
  • 得到Base3的指標: Base3* pb3 = pd1;

非常值得注意的是:

這是在基類與繼承類之間的轉換, 這種轉換會自動計算偏移! 按照前面的佈局方式!
也就是說: 在這裡極有可能: pb1 != pb2 != pb3 ~~, 不要以為她們都等於 pd1!

至於函式呼叫, 我想, 不用說大家應該知道了:

  1. 如果不是虛擬函式, 直接呼叫指標對應的基本類的那個函式
  2. 如果是虛擬函式, 則查詢虛擬函式表, 並進行後續的呼叫. 虛擬函式表在定義一個時, 編譯器就為我們建立好了的. 所有的, 同一個類, 共用同一份虛擬函式表.

用C語言完全模擬C++虛擬函式表的實現與運作方式

如果對前面兩大節的描述仔細瞭解了的話, 想用C語言來模擬C++的虛擬函式以及多型, 想必是輕而易舉的事情鳥!

前提

但是, 話得說在前面, C++的編譯器在生成類及物件的時候, 幫助我們完成了很多事件, 比如生成虛擬函式表!
但是, C語言編譯器卻沒有, 因此, 很多事件我們必須手動來完成, 包括但不限於:

  • 手動構造父子關係
  • 手動建立虛擬函式表
  • 手動設定__vfptr並指向虛擬函式表
  • 手動填充虛擬函式表
  • 若有虛擬函式覆蓋, 還需手動修改函式指標
  • 若要取得基類指標, 還需手動強制轉換
  • ……

總之, 要想用C語言來實現, 要寫的程式碼絕對有點複雜.

C++原版呼叫

接下來, 我們都將以最後那個, 最繁雜的那個3個基類的例項來講解, 但作了一些簡化與改動:

  • 用建構函式初始化成員變數
  • 減少成員變數的個數
  • 減少虛擬函式的個數
  • 呼叫函式時產生相關輸出
  • 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;
    }
};

為了看到多型的效果, 我們還需要定義一個函式來看效果

void foo(Base1* pb1, Base2* pb2, Base3* pb3, Derive1* pd1)
{
    std::cout << "Base1::\n"
        << "    pb1->base1_1 = " << pb1->base1_1 << "\n"
        << "    pb1->base1_fun1(): ";
    pb1->base1_fun1();

    std::cout << "Base2::\n"
        << "    pb2->base2_1 = " << pb2->base2_1
        << std::endl;

    std::cout << "Base3::\n"
        << "    pb3->base3_1 = " << pb3->base3_1 << "\n"
        <<"    pb3->base3_fun1(): ";
    pb3->base3_fun1();

    std::cout << "Derive1::\n"
        << "    pd1->derive1_1 = " << pd1->derive1_1<< "\n"
        <<"    pd1->derive1_fun1(): ";
    pd1->derive1_fun1();
    std::cout<< "    pd1->base3_fun1(): ";
    pd1->base3_fun1();

    std::cout << std::endl;
}

呼叫方式如下:

Derive1 d1;
foo(&d1, &d1, &d1, &d1);

輸出結果:

可以看到輸出結果全部正確(當然了! :-), 哈哈~
同時注意到 pb3->base3_fun1() 的多型效果哦!

用C語言來模擬

必須要把前面的理解了, 才能看懂下面的程式碼!

為了有別於已經完成的C++的類, 我們分別在類前面加一個大寫的C以示區分(平常大家都是習慣在C++寫的類前面加C, 今天恰好反過來, 哈哈).

C語言無法實現的部分

C/C++是兩個語言, 有些語言特性是C++專有的, 我們無法實現! 不過, 這裡我是指呼叫約定, 我們應該把她排除在外.

對於類的成員函式, C++預設使用__thiscall, 也即this指標通過ecx傳遞, 這在C語言無法實現, 所以我們必須手動宣告呼叫約定為:

  1. __stdcall, 就像微軟的元件物件模型那樣
  2. __cdecl, 本身就C語言的呼叫約定, 當然能使用了.

上面那種呼叫約定, 使用哪一種無關緊要, 反正不能使用__thiscall就行了.

因為使用了非__thiscall呼叫約定, 我們就必須手動傳入this指標, 通過成員函式的第1個引數!

從最簡單的開始: 實現 Base2

由於沒有虛擬函式, 僅有成員變數, 這個當然是最好模擬的咯!

struct CBase2
{
    int base2_1;
};

有了虛擬函式表的Base1, 但沒被覆蓋

下面是Base1的定義, 要複雜一點了, 多一個__vfptr:

struct CBase1
{
    void** __vfptr;
    int base1_1;
};

因為有虛擬函式表, 所以還得單獨為虛擬函式表建立一個結構體的哦!
但是, 為了更能清楚起見, 我並未定義前面所說的指標陣列, 而是用一個包含一個或多個函式指標的結構體來表示!
因為陣列能儲存的是同一類的函式指標, 不太很友好!
但他們的效果是完全一樣的, 希望讀者能夠理解明白!

struct CBase1_VFTable
{
    void(__stdcall* base1_fun1)(CBase1* that);
};

注意: base1_fun1 在這裡是一個指標變數!
注意: base1_fun1 有一個CBase1的指標, 因為我們不再使用__thiscall, 我們必須手動傳入! Got it?

Base1的成員函式base1_fun1()我們也需要自己定義, 而且是定義成全域性的:

void __stdcall base1_fun1(CBase1* that)
{
std::cout << “base1_fun1()” << std::endl;
}
有虛擬函式覆蓋的Base3

虛擬函式覆蓋在這裡並不能體現出來, 要在構造物件初始化的時候才會體現, 所以: base3其實和Base1是一樣的.

struct CBase3
{
    void** __vfptr;
    int base3_1;
};
struct CBase3_VFTable
{
    void(__stdcall* base3_fun1)(CBase3* that);
};

Base3的成員函式:

void __stdcall base3_fun1(CBase3* that)
{
    std::cout << "base3_fun1()" << std::endl;
}

定義繼承類CDerive1

相對前面幾個類來說, 這個類要顯得稍微複雜一些了, 因為包含了前面幾個類的內容:

struct CDerive1
{
    CBase1 base1;
    CBase3 base3;
    CBase2 base2;

    int derive1_1;
};

特別注意: CBase123的順序不能錯!

另外: 由於Derive1本身還有虛擬函式表, 而且所以項是加到第一個虛擬函式表(CBase1)的後面的, 所以此時的CBase1::__vfptr不應該單單指向CBase1_VFTable, 而應該指向下面這個包含Derive1類虛擬函式表的結構體才行:

struct CBase1_CDerive1_VFTable
{
    void (__stdcall* base1_fun1)(CBase1* that);
    void(__stdcall* derive1_fun1)(CDerive1* that);
};

因為CDerive1覆蓋了CBase3的base3_fun1()函式, 所以不能直接用Base3的那個表:

struct CBase3_CDerive1_VFTable
{
    void(__stdcall* base3_fun1)(CDerive1* that);
};

Derive1覆蓋Base3::base3_fun1()的函式以及自身定義的derive1_fun1()函式:

void __stdcall base3_derive1_fun1(CDerive1* that)
{
    std::cout << "base3_derive1_fun1()" << std::endl;
}

void __stdcall derive1_fun1(CDerive1* that)
{
    std::cout << "derive1_fun1()" << std::endl;
}

構造各類的全域性虛擬函式表

由於沒有了編譯器的幫忙, 在定義一個類物件時, 所有的初始化工作都只能由我們自己來完成了!

首先構造全域性的, 被同一個類共同使用的虛擬函式表!

// CBase1 的虛擬函式表
CBase1_VFTable __vftable_base1;
__vftable_base1.base1_fun1 = base1_fun1;

// CBase3 的虛擬函式表
CBase3_VFTable __vftable_base3;
__vftable_base3.base3_fun1 = base3_fun1;
然後構造CDerive1和CBase1共同使用的虛擬函式表:

// CDerive1 和 CBase1 共用的虛擬函式表
CBase1_CDerive1_VFTable __vftable_base1_derive1;
__vftable_base1_derive1.base1_fun1 = base1_fun1;
__vftable_base1_derive1.derive1_fun1 = derive1_fun1;

再構造CDerive1覆蓋CBase3後的虛擬函式表: 注意: 數覆蓋會替換原來的函式指標

CBase3_CDerive1_VFTable __vftable_base3_derive1;
__vftable_base3_derive1.base3_fun1 = base3_derive1_fun1;

開始! 從CDerive1構造一個完整的Derive1類
先初始化成員變數與__vfptr的指向: 注意不是指錯了!

CDerive1 d1;
d1.derive1 = 1;

d1.base1.base1_1 = 11;
d1.base1.__vfptr = reinterpret_cast<void**>(&__vftable_base1_derive1);

d1.base2.base2_1 = 21;

d1.base3.base3_1 = 31;
d1.base3.__vfptr = reinterpret_cast<void**>(&__vftable_base3_derive1);

由於目前的CDerive1是我們手動構造的, 不存在真正語法上的繼承關係, 如要得到各基類指標, 我們就不能直接來取, 必須手動根據偏移計算:

char* p = reinterpret_cast<char*>(&d1);
Base1* pb1 = reinterpret_cast<Base1*>(p + 0);
Base2* pb2 = reinterpret_cast<Base2*>(p + sizeof(CBase1) + sizeof(CBase3));
Base3* pb3 = reinterpret_cast<Base3*>(p + sizeof(CBase1));
Derive1* pd1 = reinterpret_cast<Derive1*>(p);

真正呼叫:

foo(pb1, pb2, pb3, pd1);

呼叫結果:

結果相當正確!!!

原始碼

我以為我把原始碼搞丟了,結果過了一年多發現其實並沒有。— 2015-12-24(每個聖誕我都在寫程式碼)

Reference