C++ 多型的實現原理分析
阿新 • • 發佈:2019-02-04
一、什麼是多型
在面向物件開發中,多型是一個很重要的特性。
什麼是多型呢?就是程式執行時,父類指標可以根據具體指向的子類物件,來執行不同的函式,表現為多型。
二、C++ 多型的實現原理
1. 實現原理
- 當類中存在虛擬函式時,編譯器會在類中自動生成一個虛擬函式表
- 虛擬函式表是一個儲存類成員函式指標的資料結構
- 虛擬函式表由編譯器自動生成和維護
- virtual 修飾的成員函式會被編譯器放入虛擬函式表中
- 存在虛擬函式時,編譯器會為物件自動生成一個指向虛擬函式表的指標(通常稱之為 vptr 指標)
2. 舉個例子
看完上面的實現原理,你可能會覺得有點懵,接下來我們就一點點分析和驗證上面的結論。
#include <iostream> using namespace std; class Parent { public: // 父類虛擬函式必須要有 virtual 關鍵字 virtual void fun() { cout << "父類" << endl; } }; class Child : public Parent { public: // 子類有沒有 virtual 關鍵字都可以 void fun() { cout << "子類" << endl; } }; int main() { Parent *p = NULL; // 建立一個父類的指標 Parent parent; Child child; p = &parent; // 指向父類的物件 p->fun(); // 執行的是父類的 fun() 函式 p = &child; // 指向子類的物件 p->fun(); // 執行的是子類的 fun() 函式 return 0; }
如上例程式碼所示,當我們傳入父類物件時,將呼叫和執行父類的函式,當我們傳入子類物件時,將呼叫和執行子類的函式。而 C++ 編譯器的執行過程其實是這樣的:
- 父類的 fun() 是個虛擬函式,所以編譯器給父類物件自動添加了一個 vptr 指標,指向父類的虛擬函式表,這個虛擬函式表裡存放了父類的 fun() 函式的函式指標
- 子類的 fun() 函式是重寫了父類的,即寫不寫 virtual 編譯器都會為其自動新增一個 virtual,然後編譯器給子類物件自動添加了一個 vptr 指標,指向子類的虛擬函式表,這個虛擬函式表裡存放了子類的 fun() 函式的函式指標
- 執行 p->fun() 時,編譯器檢測到 fun() 是一個虛擬函式,所以不會靜態的將 Parent 類的 fun() 方法直接編譯過來,而是是執行的時候,動態的根據 base 指向的物件,找到這個物件的 vptr 指標,然後找到這個物件的虛擬函式表,最後呼叫虛擬函式表裡對應的函式,實現多型
3. 證明 vptr 的存在
上面說了這麼多,那麼怎麼證明說的都是對的呢?vptr 指標真的存在麼?
其實要證明 vptr 的存在很簡單,我們只需要建立兩個相同的類,一個類有虛擬函式,一個類沒有虛擬函式,然後通過 sizeof() 方法打印出類物件的大小就行。如下:
#include <iostream>
using namespace std;
class Parent1
{
public:
int a;
void fun() {} // 非虛擬函式
};
class Parent2
{
public:
int a;
virtual void fun() {} // 虛擬函式
};
int main()
{
Parent1 p1;
Parent2 p2;
cout << sizeof(p1) << endl;
cout << sizeof(p2) << endl;
return 0;
}
執行結果:
4
8
可以看到,存在虛擬函式的類的物件,大小大了4個位元組,這正好是一個指標物件的大小(指標物件的大小可能會根據執行環境而改變,32位的系統指標的大小是4個位元組),這說明編譯器確實給我們添加了這麼一個指標物件 vptr。
4. 父類的構造方法中呼叫虛擬函式,會發生多型嗎
看下面這個例子:
#include <iostream>
using namespace std;
class Parent
{
public:
Parent()
{
// 父類的構造方法中執行虛擬函式,會發生多型嗎?
fun();
}
virtual void fun()
{
cout << "父類" << endl;
}
};
class Child : public Parent
{
public:
Child()
{
fun();
}
void fun()
{
cout << "子類" << endl;
}
};
void main()
{
Child c;
return 0;
}
執行程式會發現,建立子類物件時,會先建立父類物件,而父類的構造方法中呼叫虛擬函式,執行的並不是子類的 fun() 函式,而是父類自己的 fun() 函式,並沒有發生多型。
即答案是:父類的構造方法中呼叫虛擬函式,不會發生多型。這個和 vptr 的分步初始化有關。
5. vptr 的分步初始化
從上例中我們看到,在父類中呼叫虛擬函式時,執行的還是父類的函式,沒有發生多型。這是因為當建立子類物件時,編譯器的執行順序其實是這樣的:
- 物件在建立時,由編譯器對 vptr 進行初始化
- 子類的構造會先呼叫父類的建構函式,這個時候 vptr 會先指向父類的虛擬函式表
- 子類構造的時候,vptr 會再指向子類的虛擬函式表
- 物件的建立完成後,vptr 最終的指向才確定