C++面向對象總結——虛指針與虛函數表
最近在逛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後的
- 調用的是虛函數
靜態綁定,他們是類對象直接可調用的,而不需要任何查表操作,因此調用的速度也快於虛函數。