C++虛函數表


C++中的虛函數(Virtual Function)是用來實現動態多態性的,指的是當基類指針指向其派生類實例時,可以用基類指針調用派生類中的成員函數。如果基類指針指向不同的派生類,則它調用同一個函數就可以實現不同的邏輯,這種機制可以讓基類指針有“多種形態”,它的實現依賴於虛函數表。虛函數表(Virtual Table)是指在每個包含虛函數的類中都存在着一個函數地址的數組。本文將詳細介紹虛函數表的實現及其內存布局。

1. 虛函數表概述

首先我們要知道虛函數表的地址總是存在於對象實例中最前面的位置,其后依次是對象實例的成員。下圖中vtptr就是虛函數表的地址,可看出虛函數表中的每個成員都對應類中的一個虛函數的地址。據圖所述,我們可以使用對象實例的地址來得到虛函數表的地址,進而獲得具體的虛函數的地址,然后進行調用。

假如有如下定義 Base b; 那么虛函數表的地址vtptr的值就是:(int*)*(int*)&b,第一個虛函數vfunc1的地址就是:*(int*)*(int*)&b,vfunc2的地址是:*( (int*)*(int*)&b + 1 ),詳見本節后文所附代碼。

下文為驗證代碼,其中Base類包含3個虛函數 vfunc1~vfunc3和兩個數據成員m_iMem1, m_iMem2,該類與上圖中的保持一致。在main中,詳細描述了怎么獲取虛表的地址,怎么獲取成員變量,怎么通過虛表地址獲取虛函數的地址

class Base
{
public:
    Base(int mem1 = 1, int mem2 = 2) : m_iMem1(mem1), m_iMem2(mem2){ ; }

    virtual void vfunc1() { std::cout << "In vfunc1()" << std::endl; }
    virtual void vfunc2() { std::cout << "In vfunc2()" << std::endl; }
    virtual void vfunc3() { std::cout << "In vfunc3()" << std::endl; }

private:
    int m_iMem1;
    int m_iMem2;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Base b;

    // 對象b的地址
    int *bAddress = (int *)&b;    

    // 對象b的vtptr的值
    int *vtptr = (int *)*(bAddress + 0);
    printf("vtptr: 0x%08x\n", vtptr);

    // 對象b的第一個虛函數的地址
    int *pFunc1 = (int *)*(vtptr + 0);
    int *pFunc2 = (int *)*(vtptr + 1);
    int *pFunc3 = (int *)*(vtptr + 2);
    printf("\t vfunc1addr: 0x%08x \n" 
           "\t vfunc2addr: 0x%08x \n" 
           "\t vfunc3addr: 0x%08x \n",
           pFunc1, 
           pFunc2, 
           pFunc3);

    // 對象b的兩個成員變量的值(用這種方式可輕松突破private不能訪問的限制)
    int mem1 = (int)*(bAddress + 1);
    int mem2 = (int)*(bAddress + 2);
    printf("m_iMem1: %d \nm_iMem2: %d \n\n",mem1, mem2);

    // 調用虛函數
    (FUNC(pFunc1))();
    (FUNC(pFunc2))();
    (FUNC(pFunc3))();
    return 0;
}

程序運行結果如下面兩幅圖所示,其中左邊部分是程序運行結果,右邊部分為調試窗口中顯示的類中各成員的值,可以發現兩者結果一致。同時在運行結果窗口中可見直接使用地址調用虛函數的方法也是正確的,這就驗證了我們本節開始部分的闡述。

2. 單繼承下的虛函數表
2.1 派生類未覆蓋基類虛函數

下面我們來看下派生類沒有覆蓋基類虛函數的情況,其中Base類延用上一節的定義。從圖中可看出虛函數表中依照聲明順序先放基類的虛函數地址,再放派生類的虛函數地址。

其對應的代碼如下所示:

class Derived : public Base
{
public:
    Devired(int mem = 3) : m_iDMem1(mem){ ; }
    
    virtual void vdfunc1() { std::cout << "In Devired vfunc3()" << std::endl; }

    void dfunc1() { std::cout << "In Devired dfunc1" << std::endl; }

private:     
    int m_iDMem1;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Derived d;
    int *dAddress = (int*)&d;

    /* 1. 獲取對象的內存布局信息 */
    // 虛表地址
    int *vtptr = (int*)*(dAddress + 0);

    // 數據成員的地址
    int  mem1  = (int)*(dAddress + 1);
    int  mem2  = (int)*(dAddress + 2);
    int dmem1  = (int)*(dAddress + 3);

    /* 2. 輸出對象的內存布局信息 */
    int *pFunc1 = (int *)*(vtptr + 0);
    int *pFunc2 = (int *)*(vtptr + 1);
    int *pFunc3 = (int *)*(vtptr + 2);
    int *pdFunc1 = (int *)*(vtptr + 3);
    
    (FUNC(pFunc1))();
    (FUNC(pFunc2))();
    (FUNC(pFunc3))();
    (FUNC(pdFunc1))();

    printf("\t vfunc1addr: 0x%08x \n"
            "\t vfunc2addr: 0x%08x \n" 
            "\t vfunc3addr: 0x%08x \n"
            "\t vdfunc1addr: 0x%08x \n\n",
            pFunc1, 
            pFunc2, 
            pFunc3,
            pdFunc1
            );


    printf("m_iMem1: %d, m_iMem2: %d, m_iDMem3: %d \n", mem1, mem2, dmem1);
    return 0;
}

其輸出結果如下圖所示,可見與本節開始介紹的結論是一致的。

2.2 派生類覆蓋基類虛函數

我們再來看一下派生類覆蓋了基類的虛函數的情形,可見:1. 虛表中派生類覆蓋的虛函數的地址被放在了基類相應的函數原來的位置  2. 派生類沒有覆蓋的虛函數延用基類的

代碼如下所示,注意這里只給出了類的定義,main函數的測試代碼與上節一樣:

class Devired : public Base
{
public:
    // 覆蓋基類的虛函數
    virtual void vfunc2() { std::cout << "In Devired vfunc2()" << std::endl; }

public:
    Devired(int mem = 3) : m_iDMem1(mem){ ; }

    virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }
    void dfunc1() { std::cout << "In Devired dfunc1" << std::endl; }

private:     
    int m_iDMem1;
};

運行結果如下所示:

3. 多繼承下的虛函數表
3.1 無虛函數覆蓋

如果是多重繼承的話,問題就變得稍微復雜一丟丟,主要有幾點:1. 有幾個基類就有幾個虛函數表   2. 派生類的虛函數地址存依照聲明順序放在第一個基類的虛表最后,見下圖所示:

Base類延用本文之前的定義,其余部分代碼如下所示:

class Base2
{
public:
    Base2(int mem = 3) : m_iBase2Mem(mem){ ; }
    virtual void vBase2func1() { std::cout << "In Base2 vfunc1()" << std::endl; }
    virtual void vBase2func2() { std::cout << "In Base2 vfunc2()" << std::endl; }

private:
    int m_iBase2Mem;
};

class Base3
{
public:
    Base3(int mem = 4) : m_iBase3Mem(mem) { ; }
    virtual void vBase3func1() { std::cout << "In Base3 vfunc1()" << std::endl; }
    virtual void vBase3func2() { std::cout << "In Base3 vfunc2()" << std::endl; }

private:
    int m_iBase3Mem;
};

class Devired: public Base, public Base2, public Base3
{
public:
    Devired(int mem = 7) : m_iMem1(mem) { ; }
    virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }

private:
    int m_iMem1;
};

int _tmain(int argc, _TCHAR* argv[])
{
    // Test_3
    Devired d;
    int *dAddress = (int*)&d;

    /* 1. 獲取對象的內存布局信息 */
    // 虛表地址一
    int *vtptr1  = (int*)*(dAddress + 0);
    int basemem1 = (int)*(dAddress + 1);
    int basemem2 = (int)*(dAddress + 2);

    int *vtpttr2 = (int*)*(dAddress + 3);
    int base2mem = (int)*(dAddress + 4);    

    int *vtptr3  = (int*)*(dAddress + 5);
    int base3mem = (int)*(dAddress + 6);

    /* 2. 輸出對象的內存布局信息 */
    int *pBaseFunc1 = (int *)*(vtptr1 + 0);
    int *pBaseFunc2 = (int *)*(vtptr1 + 1);
    int *pBaseFunc3 = (int *)*(vtptr1 + 2);
    int *pBaseFunc4 = (int *)*(vtptr1 + 3);

    (FUNC(pBaseFunc1))();
    (FUNC(pBaseFunc2))();
    (FUNC(pBaseFunc3))();
    (FUNC(pBaseFunc4))();
    // .... 后面省略若干輸出內容,可自行補充
    return 0;
}

調試輸出如下圖,這里的展示結果與本節開始所展示的內存布局圖是一致的

3.2 有虛函數覆蓋

本節不再給出任何分析,讀者如果想徹底搞明白可以根據本文上述內容自行畫圖寫代碼驗證。

轉自:https://jocent.me/2017/08/07/virtual-table.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM