1. 程式人生 > >C++ 多型的實現原理分析

C++ 多型的實現原理分析

一、什麼是多型

在面向物件開發中,多型是一個很重要的特性。
什麼是多型呢?就是程式執行時,父類指標可以根據具體指向的子類物件,來執行不同的函式,表現為多型。

二、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++ 編譯器的執行過程其實是這樣的:

  1. 父類的 fun() 是個虛擬函式,所以編譯器給父類物件自動添加了一個 vptr 指標,指向父類的虛擬函式表,這個虛擬函式表裡存放了父類的 fun() 函式的函式指標
  2. 子類的 fun() 函式是重寫了父類的,即寫不寫 virtual 編譯器都會為其自動新增一個 virtual,然後編譯器給子類物件自動添加了一個 vptr 指標,指向子類的虛擬函式表,這個虛擬函式表裡存放了子類的 fun() 函式的函式指標
  3. 執行 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 的分步初始化

從上例中我們看到,在父類中呼叫虛擬函式時,執行的還是父類的函式,沒有發生多型。這是因為當建立子類物件時,編譯器的執行順序其實是這樣的:

  1. 物件在建立時,由編譯器對 vptr 進行初始化
  2. 子類的構造會先呼叫父類的建構函式,這個時候 vptr 會先指向父類的虛擬函式表
  3. 子類構造的時候,vptr 會再指向子類的虛擬函式表
  4. 物件的建立完成後,vptr 最終的指向才確定