1、虛函數表
虛函數表是C++實現多態的基礎,多態是面向對象的三大特性之一,多態有利於提高代碼的可讀性,便於后期代碼的擴展和維護。我們都知道多態的實現是基於虛函數表,那么虛函數表是什么時候創建的呢?虛函數表是怎么實現多態的功能的呢?
首先應該明確多態也稱為動態多態,他是在程序運行時候確定函數地址的,也就是程序在運行時,如果類成員函數加了virtual關鍵字,就會建立一個虛函數指針(vfptr)指針指向一個虛函數表,這個虛函數表就保存了虛函數的地址,子類繼承父類也自然繼承了虛函數指針,當子類重寫父類的虛函數時,虛函數指針所指向的虛函數表中的虛函數地址就會被覆蓋,替換成子類的虛函數地址。也就是通過父類的虛函數指針找到了子類的虛函數地址,進而執行這個函數。下面我們通過代碼進行詳細說明:
#include <iostream> using namespace std; class Base{ public: void func(){ cout << "Base func" << endl; } }; class Son: public Base{ void func(){ cout << "Son func" << endl; } }; void test(Base& base) { base.func(); } int main () { Son son; cout << "sizeof(Base) = " << sizeof(Base) << endl; cout << "sizeof(Son) = " << sizeof(Son) << endl; test(son); system("pause"); return 0; }
代碼運行結果為:
因為函數成員不占用類的大小,所以對Base類和Son類輸出大小,都是一個字節,這一個字節是為了可以實例化類,通過引用基類引用派生類,調用func函數,函數調用了基類的func,那么如果我們加上virtual關鍵字后,就不是這種情況了。
#include <iostream> using namespace std; class Base{ public: virtual void func(){ cout << "Base func" << endl; } }; class Son: public Base{ void func(){ cout << "Son func" << endl; } }; void test(Base& base) { base.func(); } int main () { Son son; cout << "sizeof(Base) = " << sizeof(Base) << endl; cout << "sizeof(Son) = " << sizeof(Son) << endl; test(son); system("pause"); return 0; }
代碼運行結果為:
可以看到加了virtual關鍵字后,父類和子類的大小都變成了四字節,這是因為生成了虛函數指針,指針指向虛函數表,虛函數表存儲了虛函數地址,繼承了父類的子類重寫了虛函數,虛函數表中的函數地址被替換,再次調用虛函數就是調用了子類的函數func。
2、虛析構
虛析構主要是為了解決子類中有屬性開辟到堆區,父類指針調用函數時,無法調用到子類的析構代碼,導致子類堆區內存無法釋放。
首先我們看一下子類堆區內存開辟,通過父類指針來調用函數,捕捉他們的構造函數和析構函數看下運行結果:
#include <iostream> using namespace std; class Base{ public: Base(){ cout << "Base 的構造函數調用" << endl; } ~Base(){ cout << "Base 的析構函數調用" << endl; } virtual void func(){ cout << "Base func" << endl; } }; class Son: public Base{ public: Son(int val):m_val(new int (val)) { cout << "Son 的構造函數調用" << endl; } ~Son(){ cout << "Son 的析構函數調用" << endl; if (m_val != NULL) { delete m_val; cout << "Son 析構函數的堆內存釋放" << endl; m_val = NULL; } } void func(){ cout << "Son func" << endl; } void funcTest(){ cout << "funcTest 函數調用" << endl; } int* m_val = NULL; }; void test() { Base *base = new Son(10); base->func(); //base->funcTest(); //無法調用,因為虛函數表中不能找到這個函數的地址 delete base; base = NULL; } int main () { test(); system("pause"); return 0; }
代碼運行結果為:
可以明確,通過父類指針來調用函數的時候,無法調用Son類的析構函數,在Son類在堆區上申請的內存就無法釋放,造成內存泄漏。Son類的析構函數不能調用的主要原因就是在虛函數表中找不到Son的析構函數地址,解決辦法就是把Base類的寫成虛析構函數或者純虛析構函數,下面給出Base類為純虛析構函數的代碼和運行結果:
#include <iostream> using namespace std; class Base{ public: Base(){ cout << "Base 的構造函數調用" << endl; } virtual ~Base() = 0; virtual void func(){ cout << "Base func" << endl; } }; Base :: ~Base(){ cout << "Base 的析構函數調用" << endl; } class Son: public Base{ public: Son(int val):m_val(new int (val)) { cout << "Son 的構造函數調用" << endl; } ~Son(){ cout << "Son 的析構函數調用" << endl; if (m_val != NULL) { delete m_val; cout << "Son 析構函數的堆內存釋放" << endl; m_val = NULL; } } void func(){ cout << "Son func" << endl; } void funcTest(){ cout << "funcTest 函數調用" << endl; } int* m_val = NULL; }; void test() { Base *base = new Son(10); base->func(); //base->funcTest(); //無法調用,因為虛函數表中不能找到這個函數的地址 delete base; base = NULL; } int main () { test(); system("pause"); return 0; }
代碼運行結果為:
可以看到只要把Base類的析構函數寫成虛析構函數或純虛析構函數,通過父類指針調用函數,子類的析構代碼會被調用,子類堆區內存得到釋放。