深入理解C++虛函數表

什麼是虛表

虛表全稱爲虛擬函數表。在C++語言中,每個有虛函數的類或者虛繼承的子類,編譯器都會爲它生成一個虛擬函數表,表中的每一個元素都指向一個虛函數的地址。(注意:虛表是從屬於類的)

此外,編譯器會爲包含虛函數的類加上一個成員變量,是一個指向該虛函數表的指針(常被稱爲vptr),每一個由此類別派生出來的類,都有這麼一個vptr。虛表指針是從屬於對象的。也就是說,如果一個類含有虛表,則該類的所有對象都會含有一個虛表指針,並且該虛表指針指向同一個虛表。

爲什麼要有虛表

c++中的虛表是用來實現c++的動態多態性的,指的是當基類指針指向其派生類實例時,可以用基類指針調用派生類中的成員函數。如果基類指針指向不同的派生類,則它調用同一個函數就可以實現不同的邏輯,這種機制可以讓基類指針有“多種形態”,它的實現依賴於虛表。

內存佈局

在程序的內存佈局中,虛表的地址總是存在於對象實例中的最前面(這是爲了保證取虛函數表有最高的性能)。下圖中的vptr就是虛函數表的指針,指向一個函數地址。
在這裏插入圖片描述

單繼承

派生類未覆蓋基類虛函數

在這個例子中,子類沒有重寫父類的任何方法,而是加入了一個新的虛函數。從圖中可以看出,子類虛表中先存放基類的虛函數地址,再存放子類的虛函數地址。其中dfunc1()非虛函數,故不放在虛函數表中。

在這裏插入圖片描述

派生類覆蓋基類虛函數

在這個例子中,子類重寫了父類的vfunc2()函數。從圖中可以看出,虛表中派生類覆蓋的虛函數的地址被放在了基類相應函數原始的位置, 派生類未覆蓋的基類虛函數的地址也繼續沿用基類的地址。

在這裏插入圖片描述

多繼承

派生類未覆蓋基類虛函數

多重繼承稍微複雜一點,本例中Deirved類分別繼承了Base,Base2,Base3,但是沒有覆蓋父類中的虛函數。如下圖可以看出,有幾個基類就有幾個虛函數表,子類的虛成員函數被放到了第一個父類的表中(第一個父類是按照聲明的順序來判斷的)。

在這裏插入圖片描述

派生類覆蓋基類虛函數

本例中,子類覆寫了三個基類中各一個函數,從圖中可以看到,仍然是有三個虛表,其中每個虛表有一個函數的地址已經換成了子類函數的地址。子類自己的函數仍然放在第一個父類的表中。

在這裏插入圖片描述

一個問題

如果綁定了子類對象的基類指針調用子類覆寫的函數,會調用基類的還是子類的?

答:會調用子類的。

因爲雖然是基類的指針,但是指向的是子類的地址,找到的也是子類的虛函數表,自然會調用子類的虛函數。

C++中虛表存在的問題

  1. 通過父類指針訪問子類特有的虛函數

這種情況下雖然父類指向子類的虛表,但是編譯器會對這種行爲報錯,編譯無法通過。但是在運行時通過指針強行找到位置進行讀寫的情況編譯器無法判斷。

  1. 通過子類指針訪問父類的private虛函數

如果父類的虛函數時private或protected的,但這些非public的虛函數也同樣會存在於虛表中。所以同樣可以通過使用指針訪問虛表強行訪問這些函數。

附錄

下面這個程序演示瞭如何使用指針強行訪問類中的private虛函數。證明了虛表確實存在於對象實例的最前面,並且虛表的排列方式正如我們所述。基於此原理,可以自行做子類訪問父類/父類訪問子類的private虛函數的實驗。

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

private:
    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 main()
{
    Base b;

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

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

    // 對象b的第一個虛函數的地址
    // 這裏每個指針+2是因爲測試時64位機,指針大小爲64位,而int只有32位,所以是兩倍的int
    // 32位機自行改爲+1即可
    int *pFunc1 = (int *)*(vtptr + 0);
    int *pFunc2 = (int *)*(vtptr + 2);
    int *pFunc3 = (int *)*(vtptr + 4);
    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);

    typedef void (*FUNC)(void);

    // 調用虛函數
    (FUNC(pFunc1))();
    (FUNC(pFunc2))();
    (FUNC(pFunc3))();
    return 0;
}
# 輸出
vtptr: 0x00400cb0
	 vfunc1addr: 0x00400af4 
	 vfunc2addr: 0x00400b20 
	 vfunc3addr: 0x00400b4c 
m_iMem1: 0 
m_iMem2: 1 

In vfunc1()
In vfunc2()
In vfunc3()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章