日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

C++虚函数表,虚表指针,内存分布

發布時間:2025/3/21 c/c++ 15 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++虚函数表,虚表指针,内存分布 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

虛函數表和內存分布那一塊轉載自:https://blog.twofei.com/496/

虛函數效率轉載自:https://www.cnblogs.com/rollenholt/articles/2023364.html

前言

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

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

C++對象的內存布局

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

  • 輸出成員變量的偏移, 通過offsetof宏來得到
  • 通過調試器查看, 比如常用的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個字節? 且 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];

  • 值得注意的是:

  • 上面只是一種偽代碼方式, 語法不一定能通過
  • 該類的對象大小為12個字節, 大小及偏移信息如下:
    sizeof(Base1)12
    offsetof(__vfptr)0
    offsetof(base1_1)4
    offsetof(base1_2)8
  • 大家有沒有留意這個__vfptr? 為什么它被定義成一個指向指針數組的指針, 而不是直接定義成一個指針數組呢?

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

  • 注意到__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];

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

  • 更加肯定前面我們所描述的: __vfptr只是一個指針, 她指向一個函數指針數組(即: 虛函數表)
  • 增加一個虛函數, 只是簡單地向該類對應的虛函數表中增加一項而已, 并不會影響到類對象的大小以及布局情況
  • 前面已經提到過: __vfptr只是一個指針, 她指向一個數組, 并且: 這個數組沒有包含到類定義內部, 那么她們之間是怎樣一個關系呢?
    不妨, 我們再定義一個類的變量b2, 現在再來看看__vfptr的指向:

    通過Watch 1窗口我們看到:

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

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

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

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

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

  • 她是編譯器在編譯時期為我們創建好的, 只存在一份
  • 定義類對象時, 編譯器自動將類對象的__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行: 調用虛函數.

    結果:

  • 現在我們應該知道內幕了! 繼承類Derive1的虛函數表被加在基類的后面! 事實的確就是這樣!
  • 由于Base1只知道自己的兩個虛函數索引[0][1], 所以就算在后面加上了[2], Base1根本不知情, 不會對她造成任何影響.
  • 如果基類沒有虛函數呢? 這個問題我們留到第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再想:

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

    結論:

  • 按照基類的聲明順序, 基類的成員依次分布在繼承中.
  • 注意被我高亮的那兩行, 已經發生了虛函數覆蓋!
  • 我們自己定義的虛函數呢? 怎么還是看不見?!
  • 好吧, 繼承反匯編, 這次的調用代碼如下:

    ?
  • 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節!

    如果第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

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

    現在類的布局情況:

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

    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, 說明虛函數始終在最前面!

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

    對象布局:

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

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

    上代碼:

    ?
  • 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++中父子對象指針間的轉換與函數調用

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

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

    進入正題~

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

    如果有一個Derive1的指針, 那么:

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

    非常值得注意的是:

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

    至于函數調用, 我想, 不用說大家應該知道了:

  • 如果不是虛函數, 直接調用指針對應的基本類的那個函數
  • 如果是虛函數, 則查找虛函數表, 并進行后續的調用. 虛函數表在定義一個時, 編譯器就為我們創建好了的. 所有的, 同一個類, 共用同一份虛函數表.
  • 用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語言無法實現, 所以我們必須手動聲明調用約定為:

  • __stdcall, 就像微軟的組件對象模型那樣
  • __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(每個圣誕我都在寫代碼)

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

    ======================================================================

    ?

    虛函數會存在效率的問題,如果是普通函數,那么在編譯時期,其相對地址是確定的;但如果是虛函數,它執行過程中會跳轉兩次(首先找到對象的虛函數表,其次通過該虛函數表中存的虛函數地址找到真正的執行地址),這樣CPU運行的時候會跳轉兩次,而普通函數只跳一次。CPU每跳轉一次,預取指令基本上就要作廢很多,cache命中率低。所以效率會很低。換而言之,就是虛函數的地址是動態的,譬如取到的地址在eax里,則在call eax之后的那些已經被預取進入流水線的所有指令都將失效,產生大量氣泡。流水線越長,一次分支預測失敗的代價也就越大。

    另外,第二點編譯器不好優化。因為編譯器只知道調用的是一個不確定的地址處的函數,沒法知道更多細節,也就沒法替你做更多優化。

    總結

    以上是生活随笔為你收集整理的C++虚函数表,虚表指针,内存分布的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。