C++虚函数的实现原理
一. 虛函數介紹
C++中的虛函數主要是用來實現多態(面向對象的三大特性之一)的。
下面是一個實現多態的錯誤例子:
程序輸出:
Call Base::Base() Call Derive::Derive() Call Base::Name()輸出內容的第3行為:Call Base::Name(),并不是期望的Call Derive::Name()。
因為void Name()函數不是虛函數,所以pBase->Name()調用的是基類的Name()函數,并不是我們所期望的派生類Derive的Name()函數。 如果將基類中void Name()改成虛函數virtual void Name(),程序輸出就會和我們期望的一樣:
二、 虛析構函數
virtual不僅可以修飾成員函數,也可以用來修飾析構函數,也就是我們常說的虛析構函數。 下面的例子中的基類的析構函數沒有使用virtual修飾,我們先執行程序,觀察運行結果(類似上面的程序,只是在析構函數中多加入了輸出打印語句)。
// 基類 // class Base { public:Base() {printf("Call Base::Base()\n");}~Base() {printf("Call Base::~Base()\n");}virtual void Name() {printf("Call Base::Name()\n");} };// 派生類 // class Derive : public Base { public:Derive() {printf("Call Derive::Derive()\n");}~Derive() {printf("Call Derive::~Derive()\n");}void Name() {printf("Call Derive::Name()\n");} };int main() {Base* pBase = new Derive();pBase->Name();delete pBase;return 0; }程序輸出:
Call Base::Base() Call Derive::Derive() Call Derive::Name() Call Base::~Base()從輸出內容的第4行可以看到,執行delete pBase語句只有基類Base類的析構函數被調用了,而派生類Derive的析構函數卻沒有被調用。如果此時派生類Derive中有需要在析構函數執行的代碼(如內存釋放,句柄關閉等),這些代碼將不會執行,有可能就會造成內存泄漏、句柄泄漏、邏輯錯誤等問題。
正確的做法是:使用virtual修飾基類的析構函數,即虛析構函數。
// 基類 // class Base { public:Base() {printf("Call Base::Base()\n");}virtual ~Base() {printf("Call Base::~Base()\n");}virtual void Name() {printf("Call Base::Name()\n");} };此時程序輸出為:
Call Base::Base() Call Derive::Derive() Call Derive::Name() Call Derive::~Derive() Call Base::~Base()調用delete pBase之后,先執行了派生類Derive的析構函數,然后執行基類Base的析構函數。
在《C++API設計》一書中有明確的說到:“如果希望一個類可以被繼承,那么就應該將它的析構函數使用virtual修飾;反過來可以理解為,如果一個類的析構函數不是虛的,那么這個類是被設計為不可繼承的。”
二. 虛函數的實現原理
2.1 實現原理
C++的虛函數是使用虛函數表(即指針數組,也就是指針的指針)來實現的。 只要在類中聲明了虛函數,編譯器就會在類的對象中自動生成一個虛函數表,但一個對象最多只有一個虛函數表,不管這個類聲明了多少個虛函數。虛函數表是針對于類的對象的。
2.2 虛函數表(指針)存儲位置
不同的編譯器將自動生成的虛函數表指針存放的位置不同,有的存放在類對象所占內存的起始位置,有的存放在類對象所占內存的末尾。 可以通過如下代碼來判斷:
// 返回值: ture - 虛函數指針存放在對象內存起始位置 // false - 虛函數指針存放在對象內存末尾位置 // bool VirtualTableAtFirst() {class _C {public:char _i;virtual void _f() {}};_C c;char * p1 = reinterpret_cast<char*>(&c);char * p2 = reinterpret_cast<char*>(&c._i);return p1 != p2; }通過MSVC2015編譯運行,返回true, 說明MSVC編譯器是將虛函數表指針放置在類對象內存的起始位置處。
2.3 虛函數表存儲方式
既然知道了C++是使用虛函數表的形式來實現虛函數的,那個虛函數表中的數據是以何種形式來存儲的了? 現在我們根據類的繼承方式的不同來分別說明。
1. 單繼承無重載
類結構如圖,Derive繼承于Base,但Derive沒有重載Base類中的任何函數。
需要說明的是,函數f(), g(), h(), f1(), g1(), h1() 均為虛函數,這個在圖上沒有明確的寫出來,后面的圖也是一樣。
這時Base b; 對象b的虛函數表為:
Derive d; 對象d的虛函數表為:
2. 單繼承有重載
Derive重載Base類中的f()函數:
這時Base b; 對象b的虛函數表不變,無論繼承于它派生類如何重載,都不會影響基類的虛函數:
Derive d; 對象d的虛函數表為:
派生類中重載基類的f()函數指針替換了原來基類中虛函數Base::f()的指針; 派生類中其他的虛函數存放在基類虛函數之后。
3. 多繼承無重載
此時,Derive d; 對象d的虛函數表為:
派生類自己的虛函數存放在第一個基類的虛函數表的最后面。
4. 多繼承有重載
此時,Derive d; 對象d的虛函數表為:
圖片引用自 http://blog.csdn.net/haoel/article/details/1948051
總結
以上是生活随笔為你收集整理的C++虚函数的实现原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用C++评选优秀教师和优秀学生
- 下一篇: s3c2440移植MQTT