最近在逛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()
通過以上內容,我們可以知道在使用基類指標呼叫虛擬函式的時候,它能夠根據所指的類物件的不同來正確呼叫虛擬函式。而這些能夠正常工作,得益於虛指標和虛擬函式表的引入,使得在程式執行期間能夠動態呼叫函式。
動態繫結有以下三項條件要符合:
- 使用指標進行呼叫
- 指標屬於up-cast後的
- 呼叫的是虛擬函式
靜態繫結,他們是類物件直接可呼叫的,而不需要任何查表操作,因此呼叫的速度也快於虛擬函式。