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

歡迎訪問 生活随笔!

生活随笔

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

c/c++

C++ 虚函数详解(虚函数表、vfptr)——带虚函数表的内存分布图

發布時間:2023/12/15 c/c++ 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++ 虚函数详解(虚函数表、vfptr)——带虚函数表的内存分布图 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

總所周知,虛函數是實現多態的基礎。

  • 引用或指針的靜態類型與對象本身的動態類型的不同,才是C++支持多態的根本所在。
  • 當使用基類的引用或指針調用一個虛函數成員時,會執行動態綁定。
  • 所有的虛函數都必須有定義,因為編譯器直到運行前也不知道到底要調用哪個版本的虛函數。
  • 只有通過指針或引用調用虛函數才會發生動態綁定,因為只有這種情況,引用或指針的靜態類型與對象本身的動態類型才會不同。

關于另一篇博客

大家在網上搜索關于虛函數的博客應該都會搜到陳皓寫的那篇C++ 虛函數表解析吧,這篇文章確實不錯,畫的圖也比較好理解,對于指針理解比較深刻的人應該不會理解錯誤,但對于新人來說可能還是有點不友好。以下幾點我覺得需要強調:

  • 虛函數表的指針,實質是指針的指針。
  • 虛函數表的內容,實質是一個指針的數組。(同時輔證了上一點)
  • 在圖例中,所以就會兩次指針指向的過程。

還有一點就是在該大神的例子程序的輸出中,給出的中文解釋我認為是錯誤的,看起來是很容易誤導人的。 最開始的例子程序中的:
cout << "虛函數表地址:" << (int*)(&b) << endl;
cout << "虛函數表 — 第一個函數地址:" << (int*)*(int*)(&b) << endl;
這兩句明顯錯誤,本人在困惑之余便開始了自己的驗證。
而且圖例中也應該有兩次指針指向的過程。

虛函數表(vfptr)

虛函數表的指針存儲在對象實例中最前面的位置。
這意味著我們可以通過對象實例的地址得到這個虛函數表的指針,然后就遍歷虛函數表中的各個函數指針,然后調用相應的函數。
下面開始各個例子程序的實驗!(win10+vs2017)

只有基類

#include "pch.h" #include <iostream> using namespace std; class Base {public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }}; int main() {typedef void(*Fun)(void);Base b;Fun pFun = NULL;Base * p = &b;cout << "該對象的地址:" << p << endl;cout << "虛函數表的指針也是從這個地址"<< (int*)(&b) <<"開始存的" << endl << endl;cout << "虛函數表的指針指向的地址10進制:" << *(int*)(&b) << "即虛函數表的指針存的內容"<<endl;cout << "即虛函數表的地址:" << (int*)*(int*)(&b) << endl << endl;pFun = (Fun)*(int*)*(int*)(&b);//第一個虛函數的指針cout << "第一個虛函數的地址:" << pFun << endl;pFun();Fun gFun = NULL;gFun = (Fun)*((int*)*(int*)(&b) + 1);//第二個虛函數的指針Fun hFun = NULL;hFun = (Fun)*((int*)*(int*)(&b) + 2);//第三個虛函數的指針 }
  • 理解內存里每個字節是有編號,這個編號便是我們說的地址。
  • 指針存的是一個地址,我們只關心指針指向的地址(指針存的地址)和指向對象的類型,而不關心指針這個對象本身的地址。
  • 對指針解引用,實際是從指針指向的地址的那個字節開始,按照指向對象的類型的字節大小n,讀取n個字節出來,來組成這個類型的對象。
  • 打印指針時,會打印出來指針指向的地址,以16進制。
  • b返回Base類型的對象。
  • &b返回Base *類型的指針。
  • (int *)(&b)將Base *類型的指針轉換為int *類型的指針,轉換后指針指向地址沒變,但指向對象的類型變了。
  • *(int *)(&b)對int *類型的指針解引用,從地址開始的那個字節開始,取出sizeof(int)個字節,賦值給一個int對象(因為指針認為自己指向一個int對象)。
  • (int *)*(int *)(&b)相當于 (int *)后接一個int值,返回一個int指針,將這個int值作為該指針指向的地址值。
  • *(int *)*(int *)(&b)對int *類型的指針解引用,返回int值。
  • (Fun)*(int *)*(int *)(&b),Fun是函數指針,后接一個int值,將這個int值作為該函數指針指向的地址值。
  • 如果以上過程你都正確理解,那么你就能理解這句gFun = (Fun)*((int*)*(int*)(&b) + 1);了。首先(int*)*(int*)(&b)將虛函數表的指針轉換為指向指針數組首元素的指針(即轉換過程中,指針指向地址沒變的),然后((int*)*(int*)(&b) + 1)這里就是數組的指針的正常操作,現在這個指針指向了數組的第二個元素(即第二個虛函數指針),最后就是解引用,然后轉換為Fun函數指針。
  • 如果你還沒有理解某個步驟,建議直接查看以下圖例的大圖,配合debug顯示的局部變量表使用,再回頭看整個過程。


    上圖解釋了虛函數的實現機制:

  • 在有虛函數的基類對象中,肯定至少有三塊不同的內存存儲區域。
  • 首先是對象內存空間,其開始區域,存了虛函數表的指針。
  • 虛函數表實際是一個指針的數組,這些指針就是虛函數的函數指針。
  • 最后是各個虛函數的存儲區域。
  • 虛函數表的結束標志

    在上面例子中還需要講一個細節,在虛函數表最后位置有一個字節用來標志虛函數表的結束。

    char* end = NULL;end = (char*)((int*)*(int*)(&b) + 3);

    加入如上代碼便可以得到結束標志,((int*)*(int*)(&b) + 3)這里指向了虛函數表即指針數組的第四個元素,但實際上數組里只有三個指針,所以這里便剛好指向了結束標志。再通過(char*)轉換指針類型,代表指向的是一個字節。

    由于我是第二次運行程序,所以地址有點不一樣。這里end指針存的地址,按照之前的例子應該是0x00305b38再加12。
    這里你最好再明確下char型存儲的含義:(即ASCII表中:是int型<---->char型的相互轉換關系)

    char end1 = '\0';//字符串的結束符char end2 = 0;//字符串的結束符char zero1 = '0';//這才是真正的字符0char zero2 = 48;

    單繼承(無虛函數覆蓋)

    在此例中,基類有三個虛函數,派生類也有三個虛函數,但派生類一個虛函數也沒有去重寫。

    #include "pch.h" #include <iostream> using namespace std; class Base { public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }}; class Derive : public Base { public:virtual void f1() { cout << "Derive::f" << endl; }virtual void g1() { cout << "Derive::g" << endl; }virtual void h1() { cout << "Derive::h" << endl; }}; int main() {typedef void(*Fun)(void);Derive d;Base *p = &d;Fun fFun = NULL;fFun = (Fun)*((int*)*(int*)(&d) + 0);//第一個虛函數的指針Fun gFun = NULL;gFun = (Fun)*((int*)*(int*)(&d) + 1);//第二個虛函數的指針Fun hFun = NULL;hFun = (Fun)*((int*)*(int*)(&d) + 2);//第三個虛函數的指針Fun f1 = NULL;f1 = (Fun)*((int*)*(int*)(&d) + 3);Fun g1 = NULL;g1 = (Fun)*((int*)*(int*)(&d) + 4);Fun h1 = NULL;h1 = (Fun)*((int*)*(int*)(&d) + 5);char* end = NULL;end = (char*)((int*)*(int*)(&d) + 6); }


    雖然虛函數表里只能顯示父類的虛函數,但通過增加數組指針的方法,一樣可以獲得派生類的虛函數指針。就算這里是Derive *p1 = &d;也一樣,只顯示基類的三個虛函數。

    • 虛函數指針按照聲明順序放在虛函數表里面。
    • 基類的虛函數在派生類的虛函數前面。

    虛函數表的內存模型如下:

    但這里我已經厭倦了給每個虛函數生成一個函數指針,所以可以用以下循環:

    int main() {typedef void(*Fun)(void);Derive d;int *vTable = (int *)*(int *)(&d);//虛函數表的指針for (int i = 0; i<6; ++i)//判斷條件寫成vTable[i] != 0,有可能會報異常{printf("function : %d :0X%x->", i, vTable[i]);Fun f = (Fun)(vTable[i]);f(); //訪問虛函數} }


    vTable[i]相當于給vTable指針加i,再解引用。其實就是數組的用法啦,所以就少了解引用的一步。
    打印出來的是各個虛函數的地址。

    單繼承(有虛函數覆蓋)

    在此例中,派生類只覆蓋了基類的一個函數:f()。

    #include "pch.h" #include <iostream> using namespace std; class Base { public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }}; class Derive : public Base { public:virtual void f() { cout << "Derive::f" << endl; }virtual void g1() { cout << "Derive::g" << endl; }virtual void h1() { cout << "Derive::h" << endl; }}; int main() {typedef void(*Fun)(void);Derive d;int *vTable = (int *)*(int *)(&d);for (int i = 0; i<5; ++i){printf("function : %d :0X%x->", i, vTable[i]);Fun f = (Fun)(vTable[i]);f(); //訪問虛函數} }


    可以看出:

    • 由于f虛函數被重寫,原本虛函數表(即指針數組)第一個元素是Base::f()的指針,現在被替換為了Derive::f()的指針
    • 其他虛函數按照之前的順序排列

    虛函數表的內存模型如下:

    多重繼承(無虛函數覆蓋)

    在此例中,有三個基類,一個派生類,且派生類一個虛函數也沒有去重寫。

    #include "pch.h" #include <iostream> using namespace std; class Base1 { public:virtual void f() { cout << "Base1::f" << endl; }virtual void g() { cout << "Base1::g" << endl; }virtual void h() { cout << "Base1::h" << endl; } }; class Base2 { public:virtual void f() { cout << "Base2::f" << endl; }virtual void g() { cout << "Base2::g" << endl; }virtual void h() { cout << "Base2::h" << endl; } }; class Base3 { public:virtual void f() { cout << "Base3::f" << endl; }virtual void g() { cout << "Base3::g" << endl; }virtual void h() { cout << "Base3::h" << endl; } }; class Derive : public Base1,public Base2, public Base3 { public:virtual void f1() { cout << "Derive::f" << endl; }virtual void g1() { cout << "Derive::g" << endl; }virtual void h1() { cout << "Derive::h" << endl; } }; typedef void(*Fun)(void); void printVfun(int n,int * vTable) {for (int i = 0; i < n; ++i){printf("function : %d :0X%x->", i, vTable[i]);Fun f = (Fun)(vTable[i]);f(); //訪問虛函數}cout << "" << endl; } int main() {Derive d;int *vTable1 = (int *)*(int *)(&d);//第一個虛函數表的指針printVfun(6, vTable1);int *vTable2 = (int *)*((int *)(&d)+1);//第二個虛函數表的指針printVfun(3, vTable2);int *vTable3 = (int *)*((int *)(&d) + 2);//第三個虛函數表的指針printVfun(3, vTable3); }



    可以看到:

    • 對于繼承到的每個基類,都有一個對應的虛函數表。
    • 派生類的虛函數的指針,被放進了第一個基類對應的虛函數表里。(按照聲明順序來判斷的)

    內存模型如下:

    多重繼承(有虛函數覆蓋)

    在此例中,有三個基類,一個派生類,且派生類重寫了三個基類的同一個虛函數。

    #include "pch.h" #include <iostream> using namespace std; class Base1 { public:virtual void f() { cout << "Base1::f" << endl; }virtual void g() { cout << "Base1::g" << endl; }virtual void h() { cout << "Base1::h" << endl; } }; class Base2 { public:virtual void f() { cout << "Base2::f" << endl; }virtual void g() { cout << "Base2::g" << endl; }virtual void h() { cout << "Base2::h" << endl; } }; class Base3 { public:virtual void f() { cout << "Base3::f" << endl; }virtual void g() { cout << "Base3::g" << endl; }virtual void h() { cout << "Base3::h" << endl; } }; class Derive : public Base1, public Base2, public Base3 { public:virtual void f() { cout << "Derive::f" << endl; }virtual void g1() { cout << "Derive::g" << endl; }virtual void h1() { cout << "Derive::h" << endl; } }; typedef void(*Fun)(void); void printVfun(int n, int * vTable) {for (int i = 0; i < n; ++i){printf("function : %d :0X%x->", i, vTable[i]);Fun f = (Fun)(vTable[i]);f(); //訪問虛函數}cout << "" << endl; } int main() {Derive d;int *vTable1 = (int *)*(int *)(&d);//第一個虛函數表的指針printVfun(5, vTable1);int *vTable2 = (int *)*((int *)(&d) + 1);//第二個虛函數表的指針printVfun(3, vTable2);int *vTable3 = (int *)*((int *)(&d) + 2);//第三個虛函數表的指針printVfun(3, vTable3); }



    可以看到:

    • 三個基類的虛函數表的第一項,都被替換為Derive::f的指針
    • 這樣任意基類指針指向派生類對象,都可以調用到Derive::f

    對象模型如下:

    類與虛函數表與虛函數的對應關系

    注意本章中的示意圖都只會關注基類的虛函數指針。或者因為重寫,而導致在虛函數表中基類的虛函數指針被替換的情況。(就像局部變量圖中的一樣)

    單繼承(無虛函數覆蓋)

    在該例中運行:

    Base b1;Base b2;Derive d1;Derive d2;


    • 每一個類對應到一個虛函數表。
    • 兩個虛函數表里各個指針指向的地址都是相同的。

    單繼承(有虛函數覆蓋)

    Base b;Derive d;


    • 基類的虛函數表的三項還是沒有變化
    • 派生類的虛函數表的第一項被替換了

    多重繼承(無虛函數覆蓋)

    Base1 b1;Base2 b2;Base3 b3;Derive d;


    • 派生類因為繼承了三個基類,所以會有三張虛函數表。

    多重繼承(有虛函數覆蓋)


    • 派生類的每個虛函數表的第一項都被替換為Derive::f()的指針了,因為它把三個基類的f虛函數都重寫了。

    總結

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

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