天天看點

深入分析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

------------------越是喧嚣的世界,越需要甯靜的思考------------------

合抱之木,生于毫末;九層之台,起于壘土;千裡之行,始于足下。

積土成山,風雨興焉;積水成淵,蛟龍生焉;積善成德,而神明自得,聖心備焉。故不積跬步,無以至千裡;不積小流,無以成江海。骐骥一躍,不能十步;驽馬十駕,功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓無爪牙之利,筋骨之強,上食埃土,下飲黃泉,用心一也。蟹六跪而二螯,非蛇鳝之穴無可寄托者,用心躁也。