C++物件導向總結——虛指標與虛擬函式表

唯有自己強大發表於2021-08-12

最近在逛B站的時候發現有候捷老師的課程,如獲至寶。因此,跟隨他的講解又複習了一遍關於C++的內容,收穫也非常的大,對於某些模糊的概念及遺忘的內容又有了更深的認識。

以下內容是關於虛擬函式表、虛擬函式指標,而C++中的動態繫結實現和這兩個內容是分不開的。


一,虛擬函式表、虛指標

​當一個類在實現的時候,如果存在一個或以上的虛擬函式時,那麼這個類便會包含一張虛擬函式表。而當一個子類繼承並重寫了基類的虛擬函式時,它也會有自己的一張虛擬函式表。

當我們在設計類的時候,如果把某個函式設定成虛擬函式時,也就表明我們希望子類在繼承的時候能夠有自己的實現方式;如果我們明確這個類不會被繼承,那麼就不應該有虛擬函式的出現。

下面是某個基類A的實現:

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
            void func1();
            void func2();
private:
    int m_data1, m_data1;
};

從下圖中可以看到該類在記憶體中的存放形式,對於虛擬函式的呼叫是通過查虛擬函式表來進行的,每個虛擬函式在虛擬函式表中都存放著自己的一個地址,而如何在虛擬函式表中進行查詢,則是通過虛指標來呼叫,在記憶體結構中它一般都會放在類最開始的地方,而對於普通函式則不需要通過查表操作。這張虛擬函式表是什麼時候被建立的呢?它是在編譯的時候產生,否則這個類的結構資訊中也不會插入虛指標的地址資訊。

 以下例子包含了繼承關係:

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
            void func1();
            void func2();
private:
    int m_data1, m_data1;
};

class B : public A {
public:
    virtual void vfunc1();
            void func2();
private:
    int m_data3;
};

class C : public B {
public:
    virtual void vfunc1();
            void func2();
private:
    int m_data1, m_data4;
};

以上三個類在記憶體中的排布關係如下圖所示:

  •  對於非虛擬函式,三個類中雖然都有一個叫 func2 的函式,但他們彼此互不關聯,因此都是各自獨立的,不存在過載一說,在呼叫的時候也不需要進行查表的操作,直接呼叫即可。
  • 由於子類B和子類C都是繼承於基類A,因此他們都會存在一個虛指標用於指向虛擬函式表。注意,假如子類B和子類C中不存在虛擬函式,那麼這時他們將共用基類A的一張虛擬函式表,在B和C中用虛指標指向該虛擬函式表即可。但是,上面的程式碼設計時子類B和子類C中都有一個虛擬函式 vfunc1,因此他們就需要各自產生一張虛擬函式表,並用各自的虛指標指向該表。由於子類B和子類C都對 vfunc1 作了過載,因此他們有三種不同的實現方式,函式地址也不盡相同,在使用的時候需要從各自類的虛擬函式表中去查詢對應的 vfunc1 地址。
  • 對於虛擬函式 vfunc2,兩個子類都沒有進行過載操作,所以基類A、子類B和子類C將共用一個 vfunc2,該虛擬函式的地址會分別儲存在三個類的虛擬函式表中,但他們的地址是相同的。
  • 從上圖可以發現,在類物件的頭部存放著一個虛指標,該虛指標指向了各自類所維護的虛擬函式表,再通過查詢虛擬函式表中的地址來找到對應的虛擬函式。
  • 對於類中的資料而言,子類中都會包含父類的資訊。如上例中的子類C,它自己擁有一個變數 m_data1,似乎是和基類中的 m_data1 重名了,但其實他們並不存在聯絡,從存放的位置便可知曉。

 二,關於動態繫結

首先來說一說靜態繫結:靜態繫結是指在程式編譯過程中,把函式(方法或者過程)呼叫與響應呼叫所需的程式碼結合的過程(如何理解呢?)

來看一段程式碼:

#include <iostream> 
using namespace std;

class Shape {
protected:
    int width, height;
public:
    Shape(int a,int b):width(a),height(b){}
    int area()
    {
        cout << "Parent class area :" << endl;
        return 0;
    }
};
//將Rectangle類繼承Shape類
class Rectangle : public Shape {
public:
    Rectangle(int a,int b) :Shape(a, b) { }
    int area()
    {
        cout << "Rectangle class area :" <<width*height<< endl;
        return 0;
    }
};

// 程式的主函式
int main()
{
    Shape* shape;//定義shpae類指標
    Rectangle rec(10, 7);//派生類物件
    // 基類指標指向派生類物件(儲存矩形的地址)
    shape = &rec;
    // 呼叫矩形的求面積函式 area
    shape->area();
    return 0;
}

 

可以看到呼叫的卻是基類的函式。

在沒有加virtual關鍵字的時候,通過基類指標指向派生類物件時,基類指標只能訪問派生類的成員變數,但是不能訪問派生類的成員函式。這是因此在系統編譯過程中,已經將area()函式和shape類繫結在一起了。

而動態繫結是在加了virtual關鍵字以後,派生類中的成員函式在重寫的時候會自動生成自己的虛擬函式表(單獨的一個地址),並通過虛指標指向該地址。

即:shape指標->vptr->Rectangle::area()  

​通過以上內容,我們可以知道在使用基類指標呼叫虛擬函式的時候,它能夠根據所指的類物件的不同來正確呼叫虛擬函式。而這些能夠正常工作,得益於虛指標和虛擬函式表的引入,使得在程式執行期間能夠動態呼叫函式。

動態繫結有以下三項條件要符合:

  1. 使用指標進行呼叫
  2. 指標屬於up-cast後的
  3. 呼叫的是虛擬函式

靜態繫結,他們是類物件直接可呼叫的,而不需要任何查表操作,因此呼叫的速度也快於虛擬函式。

 

相關文章