程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++多態篇3——虛函數表詳解之多繼承、虛函數表的打印

C++多態篇3——虛函數表詳解之多繼承、虛函數表的打印

編輯:C++入門知識

C++多態篇3——虛函數表詳解之多繼承、虛函數表的打印


在上上一篇C++多態篇1一靜態聯編,動態聯編、虛函數與虛函數表vtable中,我最後簡單了剖析了一下虛函數表以及vptr。
而在上一篇文章C++多態篇2——虛函數表詳解之從內存布局看函數重載,函數覆蓋,函數隱藏中我詳細介紹了虛函數的函數重載,函數覆蓋以及函數隱藏的問題,其實在那一篇文章中,對單繼承的虛函數已經做了十分詳細的解答了,如果對前面有興趣的人可以先看一下那篇文章。
在這一篇中,我會具體的分析一下在不同繼承中(單繼承,多繼承)關於虛函數表在內存中的布局以及如何打印虛函數表。但是有關在虛繼承也就是大名鼎鼎的帶有虛函數的菱形繼承,我會放到後面繼續寫一篇文章詳細剖析它在內存中的布局。
一、虛函數表的打印
在前面的文章中我僅僅通過內存以及匯編代碼中分析了虛函數表,現在我就再介紹一種查看虛函數表的方法, 即打印虛函數表
我們都知道如果類中一旦有了虛函數,那麼編譯器會自動給類中加四個字節的數據,這四個字節為指向虛函數表的指針,存放的是虛函數表在內存中的地址。
如果用代碼表示即:

void* vftable_of_A[] = {A::v_fn, ...}; 
//A類的虛函數表
class A 
{
    const void* vftable = vftable_of_A;
    //指向虛函數表的指針vftable
    virtual void v_fn() 
    {}
};
void* vftable_of_B[] = {B::v_fn,...};
//B類的虛函數表
class B 
{ 
    const void *vftable = vftable_of_B; 
    //指向B類虛函數表的指針vftable
    vritual void v_fn() 
    {} 
};

其實上面的代碼我們還需要注意兩個問題:
1.為什麼虛函數表指針的類型為void*?
答:上面vftable的類型之所以用void*表示,實際上一個類中所有虛函數的地址都被放到這個表中,不同虛函數對應的函數指針類型不盡相同,所以這個表的類型無法確定,但是在機器級裡都是入口地址,即一個32位的數字(32位系統),等到調用時,因為編譯器預先知道了函數的參數類型,返回值等,可以自動做好處理。
2.為什麼虛函數表前要加const?
答:因為虛函數表是一個常量表,在編譯時,編譯器會自動生成,並且不會改變,所以如果有多個B類的實例,每個實例中都會有一個vftable指針,但是它們指向的是同一個虛函數表。
那麼我們如何才能打印虛函數表呢?
首先我們用上一篇用過的例子。


class Base
{
public:
    Base(int data = 1)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base::Test3()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = 2)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test1()
    {
        cout << "Derive::Test1()" << endl;
    }   
    void Test2()
    {
        cout << "Derive::Test2()" << endl;
    }
    int d;
};

基類和派生類的定義如上:
那麼我們再定義兩個函數,分別用來打印基類虛函數表以及派生類虛函數表。

typedef void(*VTable)();//定義函數指針
void PrintBase(Base &b)
{
    VTable vtb = (VTable)(*((int *)*(int *)&b));
    //vtb就是函數的地址
    int i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*(int *)&b)) + i);
        //向後偏移四個字節
    }
    cout << "End" << endl;
}
void PrintDerive(Derive &b)
{
    VTable vtb = (VTable)(*((int *)*(int *)&b));
    //vtb就是函數的地址
    int i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*(int *)&b)) + i);
        //向後偏移四個字節
    }
    cout << "End" << endl;
}

在main函數中進行調用:

int main()
{
    Base b;
    Derive d;
    PrintBase(b);
    PrintDerive(d);
    return 0;
}

打斷點一步一步運行得到:
這裡寫圖片描述
現在我對上面的代碼進行一下解釋:
首先我們要定義一個函數指針用來調用函數。
即:

typedef void(*VTable)();//定義函數指針

接著我們將基類對象b的首四個字節給vtb變量。

VTable vtb = (VTable)(*((int *)*(int *)&b));

這裡寫圖片描述vc/C0ru49rqvyv21xLXY1rfBy6GjPGJyIC8+DQrL+dLUo7o8L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;"> vtb();//先調用函數 vtb = (VTable)*(((int*)(*(int *)&b)) + i); //再偏移四個字節

這樣知道vtb指向的是NULL,即到了虛函數的結束部分,就跳出循環。
這樣就可以打印出虛函數表的內容了。
派生類的虛函數表打印同理,因為虛函數表在派生類中存儲的機制相同。只不過形參類型變了而已。

二、單繼承
因為上一篇文章中我已經講了很多關於單繼承時虛函數表的問題,在這裡我不再贅述很多基礎的問題,如果對這裡不熟悉的人可以去看我上一篇文章,在這裡我舉一個簡單的例子說明一下在單繼承時,虛函數表的內存布局。

class Base
{
public:
    Base(int data = 1)
        :b(data)
    {
        cout << "Base()" << endl;
    }
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base::Test3()" << endl;
    }
    int b;
};
class Derive :public Base
{
public:
    Derive(int data = 2)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test1()
    {
        cout << "Derive::Test1()" << endl;
    }   
    void Test2()
    {
        cout << "Derive::Test2()" << endl;
    }
    int d;
};

int main()
{
    Base b;
    b.Test1();
    b.Test2();
    b.Test3();
    Derive d;
    d.Test1();
    d.Test2();
    d.Test3();
    return 0;
}

從代碼可以看出在基類定義了三個虛函數,根據我們以前所說的知識,我們知道基類會生成一個虛函數表,那麼派生類中我們定義了兩個同名同參數的函數,為了讓函數覆蓋的現象更加明顯,我特意沒有將Test3()定義,那麼我們現在看一下運行結果:
這裡寫圖片描述
由結果可知,基類對象調用的是基類的函數。派生類對象調用的是什麼呢?
我們進入內存中查看一下:
這裡寫圖片描述
三、多繼承
1.不帶函數覆蓋
先看一下下面的例子:

class Base1
{
public:
    Base1(int data = 1)
        :b1(data)
    {
        cout << "Base1()" << endl;
    }
    ~Base1()
    {
        cout << "~Base1()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base1::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base1::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base1::Test3()" << endl;
    }
    int b1;
};
class Base2
{
public:
    Base2(int data = 2)
        :b2(data)
    {
        cout << "Base2()" << endl;
    }
    ~Base2()
    {
        cout << "~Base2()" << endl;
    }
    virtual void Test4()
    {
        cout << "Base2::Test4()" << endl;
    }
    virtual void Test5()
    {
        cout << "Base2::Test5()" << endl;
    }
    virtual void Test6()
    {
        cout << "Base2::Test6()" << endl;
    }
    int b2;
};
class Derive :public Base1,public Base2
{
public:
    Derive(int data = 3)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    int d;
};

我們看一下這個多繼承中派生類d的內存布局:
這裡寫圖片描述
現在我們知道了內存布局,我們就可以打印出派生類的虛函數表了。
因為在多繼承中有多個虛函數表,所以我們現在要改一下打印函數,下面的代碼是我已經改過的。

void PrintDerive(Derive &b)
{
    VTable vtb = (VTable)(*((int *)*(int *)&b));
    //打印Derive中Base1的虛函數表
    //vtb就是函數的地址
    int i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*(int *)&b)) + i);
        //向後偏移四個字節
    }
    cout << "End" << endl;
    /*************************/
    //打印Derive中Base2的虛函數表
    vtb = (VTable)(*((int *)*((int *)&b+2)));
    //vtb就是函數的地址
     i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*((int *)&b+2))) + i);
        //向後偏移四個字節
    }
    cout << "End" << endl;
}

因為我們已經知道了派生類的內存布局,所以我們打印Base2的虛函數表的時候,改一下vtb的值即可,我現在以內存的角度剖析一下如何改變vtb的值。
這裡寫圖片描述
我們將打印函數加入main函數中,打印出來的結果為:
這裡寫圖片描述
2.帶函數覆蓋
下面我們再來看看帶函數覆蓋的多繼承的情況:


class Base1
{
public:
    Base1(int data = 1)
        :b1(data)
    {
        cout << "Base1()" << endl;
    }
    ~Base1()
    {
        cout << "~Base1()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base1::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base1::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base1::Test3()" << endl;
    }
    int b1;
};
class Base2
{
public:
    Base2(int data = 2)
        :b2(data)
    {
        cout << "Base2()" << endl;
    }
    ~Base2()
    {
        cout << "~Base2()" << endl;
    }
    virtual void Test1()
    {
        cout << "Base2::Test1()" << endl;
    }
    virtual void Test2()
    {
        cout << "Base2::Test2()" << endl;
    }
    virtual void Test3()
    {
        cout << "Base2::Test3()" << endl;
    }
    virtual void Test4()
    {
        cout << "Base2::Test4()" << endl;
    }
    virtual void Test5()
    {
        cout << "Base2::Test5()" << endl;
    }
    virtual void Test6()
    {
        cout << "Base2::Test6()" << endl;
    }
    int b2;
};
class Derive :public Base1,public Base2
{
public:
    Derive(int data = 3)
        :d(data)
    {
        cout << "Derive()" << endl;
    }
    ~Derive()
    {
        cout << "~Derive()" << endl;
    }
    void Test1()
    {
        cout << "Derive::Test1()" << endl;
    }   
    void Test4()
    {
        cout << "Derive::Test4()" << endl;
    }
    int d;
};

typedef void(*VTable)();//定義函數指針
void PrintDerive(Derive &b)
{
    VTable vtb = (VTable)(*((int *)*(int *)&b));
    //打印Derive中Base1的虛函數表
    //vtb就是函數的地址
    int i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*(int *)&b)) + i);
        //向後偏移四個字節
    }
    cout << "End" << endl;
    /*************************/
    //打印Derive中Base2的虛函數表
    vtb = (VTable)(*((int *)*((int *)&b+2)));
    //vtb就是函數的地址
     i = 0;
    cout << "Vtable is " << endl;
    while (vtb != NULL)
    {
        cout << "NUM " << ++i << "Function " << endl;
        cout << "------->";
        vtb();
        vtb = (VTable)*(((int*)(*((int *)&b+2))) + i);
        //向後偏移四個字節
    }
    cout << "End" << endl;
}
int main()
{
    Derive d;
    PrintDerive(d);
    return 0;
}

運行結果為:
這裡寫圖片描述
由結果可以得到,Test1()不僅覆蓋了Base1中的Test1()函數,也覆蓋了Base2類中的Test1()函數。
現在關於單繼承和多繼承中虛函數表的問題,應該都解釋的差不多啦,如果還有問題,歡迎留言提出~

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved