1. 程式人生 > >C++中的虛擬函式(表)實現機制詳解

C++中的虛擬函式(表)實現機制詳解

前言

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

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

C++物件的記憶體佈局

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

  1. 輸出成員變數的偏移, 通過offsetof巨集來得到(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

    可知物件佈局:

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

  2. 沒有虛擬函式的物件

    類實現如下:

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

    結果如下:

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

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

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

    類實現如下:

    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)

    看到沒? 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.

    現在的物件佈局如下:

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

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

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

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

    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視窗我們看到:

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

    由此我們可以總結出:

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

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

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

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

    1. 她是編譯器在編譯時期為我們建立好的, 只存在一份
    2. 定義類物件時, 編譯器自動將類物件的__vfptr指向這個虛擬函式表
  5. 單繼承且本身不存在虛擬函式的繼承類的記憶體佈局

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

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

    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().

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

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

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

    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的指標來呼叫此方法, 呼叫的都將是被繼承類重寫後的那個方法(函式), 多型發生鳥!!!

    那麼新的佈局圖:

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

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

    繼續貼程式碼:

    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小節再來討論!

    最新的類物件佈局表示:

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

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

    依然寫點小類玩玩:

    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個直接基類沒有虛擬函式(表)

    這次的程式碼應該比上一個要稍微簡單一些, 因為把第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語言編譯器卻沒有, 因此, 很多事件我們必須手動來完成, 包括但不限於:

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

總之, 要想用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);

輸出結果:

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

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

            
           

相關推薦

C++虛擬函式()實現機制

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

C++虛擬函式儲存位置淺析

關於C++中虛擬函式表,我們知道這樣一些事實: 1. 當class中存在virtual函式時,編譯器會為這個class追加一個void** __vfptr資料成員。 2. C++程式執行時,實際函式的呼叫,是通過查詢__vfptr來獲取的,從而實現多型。 3. 多型的實現,

C++virtual(虛擬函式)的用法

在面向物件的C++語言中,虛擬函式(virtual function)是一個非常重要的概念。因為它充分體現了面向物件思想中的繼承和多型性這兩大特性,在C++語言裡應用極廣。比如在微軟的MFC類庫中,你會發現很多函式都有virtual關鍵字,也就是說,它們都是虛擬函式。難怪有人甚至稱虛擬函

[C/C++]C++虛擬函式的原理和虛擬函式

#include using namespace std; class A{     public:     A();     virtual void fun1();     void fun2(); }; A::A() { } void A::fun1() {     cout<<"I am

C++利用鏈實現一個棧

pop sin 返回 void tac () node bool typedef 在實現棧之前應該思考棧的一些用法: push pop top isempty 想清楚棧頂的組成; 下面是實現代碼: 1 #include<iostream> 2 3 us

c++虛擬函式的理解

虛擬函式的作用,事實上就是實現了多型性,就是實現以共同的方法,但因個體差異而採用不同的策略。下面有程式碼例項來描述: class A{ public: void print(){ cout<<”This is A”<<endl;} }; class B:publ

C++通過虛擬函式呼叫虛擬函式

    C++的類如果有虛擬函式,則該類的第一個成員的數值,是一個地址,指向其虛擬函式表。例如     class CTest { public: virtual void Test1(void) { cout<&l

C++虛擬函式函式

解構函式為什麼要宣告為虛 函式??? 基類的解構函式需要宣告為虛擬函式:  當派生類物件經由一個基類指標被刪除,而該基類帶著一個non-virtual解構函式,實際執行時通常發生的是物件的派生類成員沒有被銷燬。這也就是區域性銷燬,會發生記憶體洩漏,所以我們通常將基類的解構函式需要宣告為

C++虛擬函式工作原理

C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。 所謂泛型技術,比如:模板技術,RTTI技術,虛擬函式技術,要麼是試圖做到在編譯時決議,要麼試圖做到執行時決議。 虛擬函式表(

C++虛擬函式工作原理和 虛 繼承類的記憶體佔用大小計算

                      虛擬函式的實現要求物件攜帶額外的資訊,這些資訊用於在執行時確定該物件應該呼叫哪一個虛擬函式。典型情況下,這一資訊具有一種被稱為 vptr(virtual table pointer,虛擬函式表指標)的指標的形式。vptr 指向一個被稱為 vtbl(virtual t

c++虛擬函式和純虛擬函式定義

      只有用virtual宣告類的成員函式,使之成為虛擬函式,不能將類外的普通函式宣告為虛擬函式。因為虛擬函式的作用是允許在派生類中對基類的虛擬函式重新定義。所以虛擬函式只能用於類的繼承層次結構中。      一個成員函式被宣告為虛擬函式後,在同一類族中的類就不能

關於c++虛擬函式和介面的關係區分(簡單)

虛擬函式:                 虛擬函式的作用是實現動態聯編,也就是在程式的執行階段動態地選擇合適的成員函式,在定義了虛擬函式後,可以在基類的派生類中對虛擬函式重新定義,在派生類中重新定義的函式應與虛擬函式具有相同的形參個數和形參型別。以實現統一的介面,不同定義

從零開始學C++之虛擬函式與多型(一):虛擬函式指標、虛函式、object slicing與虛擬函式C++物件模型圖

#include <iostream>using namespace std;class CObject {public:     virtual void Serialize()     {         cout << "CObject::Serialize ..." <&

淺析C++虛擬函式的呼叫及物件的內部佈局

     在我那篇《淺析C++中的this指標》中,我通過分析C++程式碼編譯後生成的彙編程式碼來分析this指標的實現方法。這次我依然用分析C++程式碼編譯後生成的彙編程式碼來說明C++中虛擬函式呼叫的實現方法,順便也說明一下C++中的物件內部佈局。下面所有的彙編程式碼都是

C++虛擬函式的作用是什麼?它應該怎麼用呢?

虛擬函式聯絡到多型,多型聯絡到繼承。所以本文中都是在繼承層次上做文章。沒了繼承,什麼都沒得談。下面是對C++的虛擬函式這玩意兒的理解。 一, 什麼是虛擬函式 (如果不知道虛擬函式為何物,但有急切的想知道,那你就應該從這裡開始)簡單地說,那些被virtual關鍵字修飾的成員

Android多執行緒之Java 8ThreadLocal內部實現機制

前言:ThreadLocal是執行緒內部的儲存類,通過它可以實現在每個執行緒中儲存自己的私有資料。即資料儲存以後,只能在指定的執行緒中獲取這個儲存的物件,而其它執行緒則不能獲取到當前執行緒儲存的這個物件。ThreadLocal有一個典型的應用場景,即我們在前文中

C++虛擬函式不能是inline函式的原因

在C++中,inline關鍵字和virtual關鍵字分別用來定義c++中的行內函數和虛擬函式,他們在各自的場合都有其各自的應用,下面將簡單介紹他們各自的功能,然後在說明為什麼一個函式不能同時是虛擬函式和行內函數(inline)

C#Serializable序列化實例

磁盤 close ear 但是 如果 mat 更新數據 eat 新的 本文實例講述了C#中Serializable序列化。分享給大家供大家參考。具體分析如下: 概述: 序列化就是是將對象轉換為容易傳輸的格式的過程,一般情況下轉化打流文件,放入內存或者IO文件 中。例如,可

C++this指針的用法

編譯 ++ call 高級 隱含參數 才有 == 可見 產生 轉自:http://blog.chinaunix.net/uid-21411227-id-1826942.html 1. this指針的用處:   一個對象的this指針並不是對象本身的一部分,不會影響sizeo

C++ queue 、 deque、priority_queue

最近看到一道題用到了佇列,在這裡就具體的分析一下C++中的這三種佇列的區別 queue 用法: #include <iostream> #include <queue> using namespace std; int mai