C++中的虛函數(表)實現機制以及用C語言對其進行的模擬實現

C++中的虛函數(表)實現機制以及用C語言對其進行的模擬實現

聲明:本文非博主原創,轉自https://blog.twofei.com/496/,博主讀后受益良多,特地轉載,一是希望好文能有更多人看到,二是為了日后自己查閱。

前言

大家都應該知道C++的精髓是虛函數吧? 虛函數帶來的好處就是: 可以定義一個基類的指針, 其指向一個繼承類, 當通過基類的指針去調用函數時, 可以在運行時決定該調用基類的函數還是繼承類的函數. 虛函數是實現多態(動態綁定)/接口函數的基礎. 可以說: 沒有虛函數, C++將變得一無是處!

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

C++對象的內存布局

要想知道C++對象的內存布局, 可以有多種方式, 比如:

  1. 輸出成員變量的偏移, 通過offsetof宏來得到
  2. 通過調試器查看, 比如常用的VS
  1. 只有數據成員的對象

類實現如下:

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

對象大小及偏移:

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

可知對象布局:

img

可以看到, 成員變量是按照定義的順序來保存的, 最先聲明的在最上邊, 然后依次保存!
類對象的大小就是所有成員變量大小之和.

  1. 沒有虛函數的對象

    類實現如下:

    class Base1
    {
    public:int base1_1;int base1_2;void foo(){}
    };
    

    結果如下:

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

    和前面的結果是一樣的? 不需要有什么疑問對吧?
    因為如果一個函數不是虛函數,那么他就不可能會發生動態綁定,也就不會對對象的布局造成任何影響.
    當調用一個非虛函數時, 那么調用的一定就是當前指針類型擁有的那個成員函數. 這種調用機制在編譯時期就確定下來了.

  2. 擁有僅一個虛函數的類對象

    類實現如下:

    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個字節? 且 base1_1 和 base1_2 的偏移都各自向后多了4個字節!
    說明類對象的最前面被多加了4個字節的"東東", what’s it?
    現在, 我們通過VS2013來瞧瞧類Base1的變量b1的內存布局情況:
    (由于我沒有寫構造函數, 所以變量的數據沒有根據, 但虛函數是編譯器為我們構造的, 數據正確!)
    (Debug模式下, 未初始化的變量值為0xCCCCCCCC, 即:-858983460)

    img

    看到沒? 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個字節, 大小及偏移信息如下:

      sizeof(Base1)12
      offsetof(__vfptr)0
      offsetof(base1_1)4
      offsetof(base1_2)8
    3. 大家有沒有留意這個__vfptr? 為什么它被定義成一個指向指針數組的指針, 而不是直接定義成一個指針數組呢?

      我為什么要提這樣一個問題? 因為如果僅是一個指針的情況, 您就無法輕易地修改那個數組里面的內容, 因為她并不屬于類對象的一部分.
      屬于類對象的, 僅是一個指向虛函數表的一個指針__vfptr而已, 下一節我們將繼續討論這個問題.

    4. 注意到__vfptr前面的const修飾. 她修飾的是那個虛函數表, 而不是__vfptr.

    現在的對象布局如下:

    img

    虛函數指針__vfptr位于所有的成員變量之前定義.

    注意到: 我并未在此說明__vfptr的具體指向, 只是說明了現在類對象的布局情況.
    接下來看一個稍微復雜一點的情況, 我將清楚地描述虛函數表的構成.

  3. 擁有多個虛函數的類對象

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

    class Base1
    {
    public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {}
    };
    

    大小以及偏移信息如下:

    img

    有情況!? 多了一個虛函數, 類對象大小卻依然是12個字節!

    再來看看VS形象的表現:

    img

    呀, __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的指向:

    img

    通過Watch 1窗口我們看到:

    1. b1和b2是類的兩個變量, 理所當然, 她們的地址是不同的(見 &b1 和 &b2)
    2. 雖然b1和b2是類的兩個變量, 但是: 她們的__vfptr的指向卻是同一個虛函數表

    由此我們可以總結出:

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

    是時候該展示一下類對象的內存布局情況了:

    img

    不出意外, 很清晰明了地展示出來了吧? 😃 hoho~~

    那么問題就來了! 這個虛函數表保存在哪里呢? 其實, 我們無需過分追究她位于哪里, 重點是:

    1. 她是編譯器在編譯時期為我們創建好的, 只存在一份
    2. 定義類對象時, 編譯器自動將類對象的__vfptr指向這個虛函數表
  4. 單繼承且本身不存在虛函數的繼承類的內存布局

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

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

    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):

    img

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

    img

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

    現在類的布局情況應該是下面這樣:

    img

  5. 本身不存在虛函數(不嚴謹)但存在基類虛函數覆蓋的單繼承類的內存布局

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

    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()函數, 也就是常說的虛函數覆蓋. 現在是怎樣布局的呢?

    img

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

    那么, 無論是通過Derive1的指針還是Base1的指針來調用此方法, 調用的都將是被繼承類重寫后的那個方法(函數), 多態發生鳥!!!

    那么新的布局圖:

    img

  6. 定義了基類沒有的虛函數的單繼承的類對象布局

    說明一下: 由于前面一種情況只會造成覆蓋基類虛函數表的指針, 所以接下來我不再同時討論虛函數覆蓋的情況.

    繼續貼代碼:

    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類不同的是沒有基類虛函數的覆蓋.

    img

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

    img

    居然沒有變化!!! 前面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小節再來討論!

    最新的類對象布局表示:

    img

  7. 多繼承且存在虛函數覆蓋同時又存在自身定義的虛函數的類對象布局

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

    依然寫點小類玩玩:

    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() {}
    };
    

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

    初步了解一下對象大小及偏移信息:

    img

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

    img

    哇, 不擺了! 一絲不掛啊! 😃

    結論:

    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個擁有虛函數表的那個基類的后面的.

    看看現在的類對象布局圖:

    (注:圖中有點錯誤,右上角應該是 void* __vftable[4],多謝 shadow3002 的提醒)

    (注:圖中有點錯誤,Derive1是存在虛函數覆蓋的。源圖丟失,請讀者注意不要被誤導。多謝 Oyster 的提醒)

    img

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

  8. 如果第1個直接基類沒有虛函數(表)

    這次的代碼應該比上一個要稍微簡單一些, 因為把第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的布局:

    img

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

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

    img

    重點是看虛函數的位置, 進入函數調用(和前一次是一樣的):

    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呢?
    不難猜測: 當前的布局已經發生了變化, 有虛函數表的基類放在對象內存前面!? , 不過事實是否屬實? 需要仔細斟酌.

    我們可以通過對基類成員變量求偏移來觀察:

    img

    可以看到:

    &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
    

    所以不難驗證: 我們前面的推斷是正確的, 誰有虛函數表, 誰就放在前面!

    現在類的布局情況:

    img

    那么, 如果兩個基類都沒有虛函數表呢?

  9. 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的基本布局:

    img

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

    看看求偏移情況:

    img

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

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

    對象布局:

    img

  10. 如果有三個基類: 虛函數表分別是有, 沒有, 有!

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

    上代碼:

    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() {}
    };
    

    以下是偏移圖:

    img

    以下是對象布局圖(多謝 @Oyster 的手繪):

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-23OJVvha-1628692462775)(https://blog.twofei.com/496/11-2.jpg)]

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

C++中父子對象指針間的轉換與函數調用

講了那么多布局方面的東東, 終于到了尾聲, 好累呀!!!

通過前面的講解內容, 大家至少應該明白了各類情況下類對象的內存布局了. 如果還不會…呃… !@#$%^&*

進入正題~

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

如果有一個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語言編譯器卻沒有, 因此, 很多事件我們必須手動來完成, 包括但不限于:

  1. 手動構造父子關系
  2. 手動創建虛函數表
  3. 手動設置__vfptr并指向虛函數表
  4. 手動填充虛函數表
  5. 若有虛函數覆蓋, 還需手動修改函數指針
  6. 若要取得基類指針, 還需手動強制轉換

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

C++原版調用

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

  1. 用構造函數初始化成員變量
  2. 減少成員變量的個數
  3. 減少虛函數的個數
  4. 調用函數時產生相關輸出
  5. 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);

輸出結果:

img

可以看到輸出結果全部正確(當然了! 😃, 哈哈~
同時注意到 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);

調用結果:

img

結果相當正確!!!

源代碼

我以為我把源代碼搞丟了,結果過了一年多發現其實并沒有。— 2015-12-24(每個圣誕我都在寫代碼)

有兩個,忘了區別了:Source1.cpp, Source2.cpp.

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/532831.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/532831.shtml
英文地址,請注明出處:http://en.pswp.cn/news/532831.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

php 前端模板 yii,php – Yii2高級模板:添加獨立網頁

我在backend / views / site下添加了help.php,并在SiteController.php下聲明了一個能夠識別鏈接的函數public function behaviors(){return [access > [class > AccessControl::className(),rules > [[actions > [login, error],allow > true,],[actions > […

C++中數組和指針的關系(區別)詳解

C中數組和指針的關系&#xff08;區別&#xff09;詳解 本文轉自&#xff1a;http://c.biancheng.net/view/1472.html 博主在閱讀后將文中幾個知識點提出來放在前面&#xff1a; 沒有方括號和下標的數組名稱實際上代表數組的起始地址&#xff0c;這意味著數組名稱實際上就是…

安裝php獨立環境,0507-php獨立環境的安裝與配置 Web程序 - 貪吃蛇學院-專業IT技術平臺...

1.在一個純英文目錄下新建三個文件夾2.安裝apache(選擇好版本)過程中該填的按格式填好&#xff0c;其余的只更改安裝目錄即可如果報錯1901是安裝版本的問題。檢查&#xff1a;安裝完成后localhost打開為It works!添加到電腦屬性環境變量&#xff1a;3.將php文件解壓文檔放到AMP…

linux中PATH變量-詳細介紹

轉自&#xff1a;https://blog.csdn.net/haozhepeng/article/details/100584451 轉載者勘誤 原文最后提到的 echo 命令對于環境變量的修改無影響。這是肯定的&#xff0c;echo 命令相當于只是一個打印的函數&#xff08;比如 Python 中的 print&#xff09;。這里要修改環境變…

php assert eval,代碼執行函數之一句話木馬

前言大家好&#xff0c;我是阿里斯&#xff0c;一名IT行業小白。非常抱歉&#xff0c;昨天的內容出現瑕疵比較多&#xff0c;今天重新整理后再次發出&#xff0c;修改并添加了細節&#xff0c;另增加了常見的命令執行函數如果哪里不足&#xff0c;還請各位表哥指出。eval和asse…

顯卡、顯卡驅動、CUDA、CUDA Toolkit、cuDNN 梳理

顯卡、顯卡驅動、CUDA、CUDA Toolkit、cuDNN 梳理 轉自&#xff1a;https://www.cnblogs.com/marsggbo/p/11838823.html#nvccnvidia-smi GPU型號含義 顯卡&#xff1a; 簡單理解這個就是我們前面說的GPU&#xff0c;尤其指NVIDIA公司生產的GPU系列&#xff0c;因為后面介紹的…

php中msubstr,PHP學習:thinkphp中字符截取函數msubstr()用法分析

《PHP學習&#xff1a;thinkphp中字符截取函數msubstr()用法分析》要點&#xff1a;本文介紹了PHP學習&#xff1a;thinkphp中字符截取函數msubstr()用法分析&#xff0c;希望對您有用。如果有疑問&#xff0c;可以聯系我們。本文實例講述了thinkphp中字符截取函數msubstr()用法…

VS Code的Error: Running the contributed command: ‘_workbench.downloadResource‘ failed解決

VS Code的Error: Running the contributed command: _workbench.downloadResource failed解決 轉自&#xff1a;https://blog.csdn.net/ibless/article/details/118610776 1 問題描述 此前&#xff0c;本人參考網上教程在VS Code中配置了“Remote SSH”插件&#xff08;比如這…

Oracle閃回報錯,oracle 閃回區滿了,ORA-19815

oracle 閃回區滿了&#xff0c;查看日志報錯&#xff1a;ORA-19815&#xff0c;命令行輸入&#xff1a;sqlplus / as sysdbastartup mount //如果你的數據庫出現了無法連接的情況時&#xff0c;可以加上這句select file_type, percent_space_used as used,percent_space_rec…

[2021-ICCV] MUSIQ Multi-scale Image Quality Transformer 論文簡析

[2021-ICCV] MUSIQ: Multi-scale Image Quality Transformer 論文簡析 論文&#xff1a;https://arxiv.org/abs/2108.05997 代碼&#xff1a;https://github.com/google-research/google-research/tree/master/musiq 概述 當前SOTA的IQA&#xff08;圖像質量評估&#xff0…

安裝oracle不動了,windows2008安裝ORACLE到2%不動的問題 | 信春哥,系統穩,閉眼上線不回滾!...

最近又有網友遇到在windows2008服務器上安裝ORACLE軟件時到2%就卡住不動的問題&#xff0c;下面是該網友的描述&#xff1a;oralce 11g r2 windows server 2008 R2安裝到最后一步復制數據文件時卡到2% 不走了內存一直飆升求解決這個問題前段時間也有人遇到過&#xff0c;但是他…

手把手教你入門Git --- Git使用指南(Linux)

手把手教你入門Git — Git使用指南&#xff08;Linux&#xff09; 系統&#xff1a;ubuntu 18.04 LTS 本文所有git命令操作實驗具有連續性&#xff0c;git小白完全可以從頭到尾跟著本文所有給出的命令走一遍&#xff0c;就會對git有一個初步的了解&#xff0c;應當能做到會用并…

linux修改主機名后oracle em控制臺起不來,更改計算機名后導致Oracle dbconsole無法啟動問題解決方法...

今天不知道哪根筋搭歪了&#xff0c;看著Oracle EM控制臺的主機名WIN-LOSGI0TCOG0亂七八糟的很不爽&#xff0c;就把它給改了。然后Oracle EM就上不去了&#xff0c;Oracledbconsole服務起不來&#xff0c;我嘗試把所有“WIN-LOSGI0TCOG0”替換成“ggsjy”&#xff0c;找了好多…

Shell腳本多行換行報錯:- unrecognized arguments- \

shell error: unrecognized arguments: \ 在使用linux長命令時&#xff0c;我們通常會用一個shell腳本加\分行的形式來更清晰展示參數。 這時有一個注意事項&#xff1a;在\后必須緊跟回車&#xff0c; 否則會導致后面的參數無法傳入。因為這時程序會將后面的參數算作一條新命…

shell腳本長命令帶換行 注釋方法

shell腳本長命令帶換行 注釋方法 命令行傳參 在訓練深度學習網絡時&#xff0c;我們每次實驗通常會有許多超參數需要設置&#xff0c;如batch size, epoch, gpu id, arch甚至還有一些具體的模型結構等。這事我們通常使用python模塊argparse&#xff0c;在命令行進行傳參。 比…

php對中英文字符串進行截取,利用php怎么對中英文混合的字符串進行截取

利用php怎么對中英文混合的字符串進行截取發布時間&#xff1a;2021-01-04 15:31:24來源&#xff1a;億速云閱讀&#xff1a;103作者&#xff1a;Leah利用php怎么對中英文混合的字符串進行截取&#xff1f;很多新手對此不是很清楚&#xff0c;為了幫助大家解決這個難題&#xf…

Linux查找命令find、loacte、whereis、which、type梳理

Linux查找命令find、loacte、whereis、which、type梳理 Linux操作系統中有5種常用的查找命令&#xff1a;find&#xff0c;locate&#xff0c;whereis&#xff0c;which&#xff0c;type&#xff0c;他們分別用于查找不同的東西&#xff0c;本文將就他們各自的功能進行一下梳理…

php數據關系圖,如何利用navicat查看數據表的ER關系圖

文章背景&#xff1a;(相關推薦&#xff1a;navicat)由于工作需要&#xff0c;現在要分析一個數據庫&#xff0c;然后查看各個表之間的關系&#xff0c;所以需要查看表與表之間的關系圖&#xff0c;專業術語叫做ER關系圖。默認情況下&#xff0c;Navicat顯示的界面是這樣的&…

Linux軟鏈接的使用

Linux軟鏈接的使用 轉自&#xff1a;https://www.cnblogs.com/sueyyyy/p/10985443.html&#xff0c;本博文僅對原博排版稍微優化。 更為細致的硬鏈接、軟鏈接的介紹可參考&#xff1a;Linux中的硬鏈接和軟鏈接 另外&#xff0c;提供筆者對軟連接的一個最簡單的理解&#xff…

oracle死鎖優化,Oracle性能優化之LockContention(轉)

1、概念DML事務使用row-level locks,查詢不會鎖定數據。鎖有兩種模式&#xff1a;exlusive、share。鎖的類型&#xff1a;? DML or data locks:– Table-level locks(TM)– Row-level locks(TX)? DDL or dictionary locks一個transaction至少獲得兩個鎖&#xff1a;一個共享的…