關於C++虛函數表構造?

1.構造函數是如何構造虛函數表的?

2.假設A繼承自B,那麼在構造A的對象時,A(子類)的構造函數構造的虛函數表的地址為什麼會與B(父類)構造函數構造的虛函數表的地址不一樣?這其中發生了什麼??

3.看代碼:

上述代碼為什麼輸出是:f()~P

Q類的f明明不是虛函數啊。

但是下面代碼輸出: A。

class A

{

public:

void f(){cout &<&< "A" &<&< endl;}

};

class B

{

public:

void f(){cout &<&<"B" &<&< endl;}

}

int main()

{

A * p = new B;

p-&>f();

}


1. 編譯器會構建一張虛表( vtable ),每一個類都有自己獨特的虛表。同時,在這個繼承鏈上,編譯器會為基類插入一個隱式的指針,指向虛表,稱為__vptr。然後,子類繼承父類時,會獲得繼承下來的__vptr. 於是,當你構建具體的類時,若是基類類型,__vptr就會指向父類的vtable,若是子類類型,__vptr就會指向子類的vtable

2. 不同的類型,__vptr指向的vtable不同

3. 父類的函數為virtual時,子類繼承下來的這個函數也是虛函數,這被稱為覆寫。以前我們一般會推薦大家在子類也為這個函數標記為virtual,提醒我們這是虛函數,而在C++11中,我們更提倡使用override來提醒我們在覆寫父類的虛函數,比如:

class X
{
public:
virtual void f() {}
};

class Y : public X
{
void f() override {}
};

使用override的好處可以參看我以前的文章:Effective Modern C++ Note 05


一、函數指針:
函數的本質,其實就是一段二進位的代碼,它寫在內存中,
可以通過指針來指向這段代碼的開頭,計算機就會從開頭
一直往下執行,直到函數的結尾,並通過指令返回回來 。函數的指針與普通的指針,本質上
是一樣的,它也是由 4 個基本的內存單元組成,存儲著一個內存地址,也
就是函數的首地址

二、虛函數的實現原理

舉例:定義一個形狀類:Shape,其中有一個計算面積的虛函數calcArea()和一個數據成員 ,再定義一個圓類:Circle,它公有繼承了 Shape 類,其中並沒有給
Circle 定義一個計算面積的虛函數,即 Circle 所使用的是 Shape 的
虛函數calcArea() 來計算面積

當實例化一個Shape 對象時,這個對象中除了數據成員 之外,
它還會有另一個數據成員:虛函數表指針,虛函數表指針,也是一個指針,佔有 4 個基本的內存單元,虛函數表指針,顧名思義,它指向一個虛函數表,該虛函數表會與Shape
類的定義同時出現在計算機中,虛函數表也是佔有一定空間的,假設虛函數表的起始位置是
0xCCFF,那麼這個虛函數表指針的值就是 0xCCFF,父類的虛函數表只有一個,通過父類實例化出來的所有對象,它們的虛函數表指針的值都是
0xCCFF,以確保每一個對象的虛函數表指針都指向自己的虛函數表 ,在父類Shape 的虛函數表中,肯定定義了這樣一個函數指針,該函數指針
就是計算面積calcArea() 這個函數的入口地址,如果 calcArea() 的入口地
址是0x3355,則虛函數表中 calcArea_ptr 的值就是 0x3355,調用時,就
可以先找到虛函數表指針,再通過虛函數表指針找到虛函數表,再通過位
置的偏移找到相應的虛函數的入口地址(即函數指針),從而最終找到當前定義的虛函數calcArea() ,當實例化一個 Circle 對象時,因為 Circle 中並沒有定義虛函數,但卻從父類中繼承了虛函數calcArea(),所以,在實例化 Circle 對象時也會產生一個虛函數表

注意:這個虛函數表是Circle 的虛函數表,和 Shape 的虛函數表不同, 假如它的起始位置是0x6688,但是在 Circle 的虛函數表中,計算面積的函
數指針卻是一樣的,都是0x3355

這就能夠保證:在 Circle 中去訪問父類計算面積的函數calcArea(),
也能通過 虛函數表指針 找到自己的虛函數表,在自己的虛函數表中
通過偏移找到的計算面積的函數指針calcArea_ptr,也是指向父類的
計算面積的函數入口

如果在Circle 中定義了計算面積的函數,又會是怎樣的呢?

對於 Shape 類來說,它的情況不變:有自己的虛函數表,並且在
實例化一個Shape 的對象之後,通過 虛函數表指針 指向自己的
虛函數表,然後虛函數表中有一個指向計算面積的函數指針

對於Circle 類來說,則有些變化:它的虛函數表和之前的虛函數表
是一樣的,但因為Circle 此時已經定義了自己的計算面積的函數,
所以它的虛函數表中關於計算面積的函數指針,已經覆蓋掉了父類
中原有的函數指針的值

即Circle 類0x6688 中計算面積的函數指針的值是 0x4B2C(假設),
而Shape 類 0xCCFF 中計算面積的函數指針的值是 0x3355

二者是不一樣的,如果用 Shape 的指針去指向 Circle 的對象,就會
通過Circle 對象中的 虛函數表指針 找到 Circle 的虛函數表,通過偏
移就能在Circle 的虛函數表中找到 Circle 的虛函數的函數入口地址,
從而執行子類中的虛函數。


最好看一下 深入探索C++對象模型


T里f面為虛函數,子類的f都是虛函數,不需要virtual


1 虛表構造不受到構造函數控制,由編譯器產生,標準只規定多態行為,不控制具體實現方法,編譯器完全可以不產生虛表這個玩意

2 藍色已經說的很清楚了,兩個類型,虛表自然不同,但是編譯器可以實現成同一個虛表,如果繼承樹足夠簡單的話。

3 是虛函數,這個只要把課本再看一遍就好,入門級問題。


1. 首先虛函數表只有在類中有虛函數或者繼承了虛函數的情況下才會有,所以如果根本沒虛函數也就沒有動態執行了,那就是靜態綁定了!

2.對於從父類繼承而來,子類沒有 override 的函數,一般直接將其添加到子類的 vptr 中,要是 子類 override 了父類的虛函數,那麼直接在子類 vptr 中覆寫點父類的相應虛函數!

3.所有的類實例共享一份虛函數表,在編譯器編譯源碼時會自動為類實例安插一個 _vptr 指針,指向虛函數表


推薦閱讀:

是否有去除c++多餘頭文件的工具?
C/C++編譯器是否會無條件消除空函數調用?
用 Visual Studio 2013 能學好 C++ 嗎?
使用Visual studio 2015 找不見C++?

TAG:C | 編譯原理 | 編譯器 |