連結:
- C++虛函數探索筆記(1)——虛函數的簡單樣本分析
- C++虛函數探索筆記(2)——虛函數與多繼承
- C++虛函數探索筆記(3)——延伸思考:虛函數應用的一些其他情形
關注問題:
- 虛函數的作用
- 虛函數的實現原理
- 虛函數表在物件版面配置裡的位置
- 虛函數的類的sizeof
- 純虛函數的作用
- 多級繼承時的虛函數表內容
- 虛函數如何執行父類代碼
- 多繼承時的虛函數表定位,以及物件版面配置
- 虛解構函式的作用
- 虛函數在QT的訊號與槽中的應用
- 虛函數與inline修飾符,static修飾符
囉嗦兩句
虛函數在C++裡的作用是在是非常非常的大,很多講述C++的文章都會講到它,要用好C++,就一定要學好虛函數。網路上可以google到很多很多關於它的文章,這一次的學習,我不準備去只是簡單的閱讀瞭解那些文章,而是希望通過編寫一些測試代碼,來對虛函數的一些實現機制,以及C++物件版面配置做一下探索。
虛函數的簡單樣本 !
虛函數常常出現在一些抽象介面類定義裡,當然,還有一個更常見的“特例”,那就是虛解構函式,後面會提到這個。
下面是一段關於虛函數的簡單代碼,示範了使用基類介面操作對象時的效果:
//Source filename: Win32Con.cpp#include <iostream>using namespace std;class parent1{public: virtual int fun1()=0;};class child1:public parent1{public: virtual int fun1() { cout<<"child1::fun1()"<<endl; return 0; }};class child2:public parent1{public: virtual int fun1() { cout<<"child2::fun1()"<<endl; return 0; }};void test_func1(parent1 *pp){ pp->fun1();}int main(int argc, char* argv[]){ child1 co1; child2 co2; test_func1(&co1); test_func1(&co2); return 0;}
在上面的代碼裡,類parent1是一個只具有純虛函數的介面類,這個類不能被執行個體化,它唯一的用途就是抽象一些特定的介面函數,當然,在這裡這個介面函數就是純虛函數 parent1::fun1()。
而類child1和child2則是兩個從parent1繼承的類,我們要使用它定義具體的類執行個體,所以它實現了由parent1繼承得來的fun1介面,並且各自的實現是不同的。
函數 test_func1 的參數是一個parent1類型的指標,它所要完成的功能就是調用這個parent1對象的fun1()函數。
讓我們編譯運行一下上面的代碼,可以看到下面的輸出
child1::fun1() child2::fun1() |
很顯然,在兩次調用test_func1函數的時候,雖然傳入的參數都是一個parent1的指標,但是卻都分別執行了child1和child2各自的fun1函數!這就是C++裡類的多態。然而,這一切是怎麼發生的呢?test_func1函數怎麼會知道應該調用哪個函數的呢?我不準備像其他人一樣畫若干圖來說明,我準備用具體某個編譯器產生的物件版面配置以及相應的彙編代碼來說明這個過程(這個編譯器是vs2008裡的vc9)。
我們先開啟一個VS2008命令提示視窗,改變目錄到上面的代碼Win32Con.cpp所在目錄,輸入下面的命令:
cl win32con.cpp /d1reportSingleClassLayoutchild |
上面的命令可以編譯win32con.cpp源碼,同時產生裡面類名包含child 的類的物件版面配置(layout)
注意:d1reportSingleClassLayout和後面的child是相連的!
輸入上面的命令後看到的物件版面配置如下,紅色字為我添加的注釋
class child1 size(4): 子類child1的物件版面配置,只包含一個vfptr,大小為4位元組 +--- | +--- (base class parent1) 這是被嵌套的父類parent1的物件版面配置 0 | | {vfptr} | +--- +---這是child1的vfptr所指的虛函數表的布局,只包含一個函數的地址,就是child1的fun1函數child1::$vftable@: | &child1_meta | 0 0 | &child1::fun1child1::fun1 this adjustor: 0class child2 size(4): 子類child2的物件版面配置,只包含一個vfptr,大小為4位元組 +--- | +--- (base class parent1) 這是被嵌套的父類parent1的物件版面配置 0 | | {vfptr} | +--- +---這是child2的vfptr所指的虛函數表的布局,只包含一個函數的地址,就是child2的fun1函數child2::$vftable@: | &child2_meta | 0 0 | &child2::fun1child2::fun1 this adjustor: 0
從上面的物件版面配置可以知道:
- 每個子物件都有一個隱藏的成員變數vfptr(你當然不能用這個名字訪問到它),它的值是指向該子物件的虛函數表,而虛函數表裡填寫的函數地址是該子物件的fun1函數地址。
- 對一個包含有虛函數的類做sizeof操作的時候,除了能直接看到的成員變數,還得增加4位元組(在32位機器上),就是vfptr這個指標的大小。
所以當test_func1進行pp->fun1()調用的時候,會首先取出pp所指的記憶體位址並按照parent1的記憶體布局,擷取到vfptr指標(由於pp在兩次調用中分別指向co1和co2所以這裡取得的實際上是co1的vfptr和co2的vfptr),然後從vfptr所指的虛函數表第一項(現在也只有 1 項)取出作為將要調用的函數,由於co1和co2在各自的虛函數表裡填寫了各自的fun1的地址,於是pp->fun1()最終就調用到了co1和co2各自的fun1,輸出自然也就不同了。
讓我們看看test_func1的反組譯碼代碼:
void test_func1(parent1 *pp){001C1530 push ebp001C1531 mov ebp,esp001C1533 sub esp,0C0h001C1539 push ebx001C153A push esi001C153B push edi001C153C lea edi,[ebp-0C0h]001C1542 mov ecx,30h001C1547 mov eax,0CCCCCCCCh001C154C rep stos dword ptr es:[edi] pp->fun1();001C154E mov eax,dword ptr [pp] //取得pp的值放到eax,即對象的地址//取得對象的vfptr地址放到edx(因為vfptr在物件版面配置裡拍在第一)001C1551 mov edx,dword ptr [eax]001C1553 mov esi,esp001C1555 mov ecx,dword ptr [pp]001C1558 mov eax,dword ptr [edx] //取出vfptr的第一個虛函數的地址到eax001C155A call eax //調用虛函數,即fun1()
至此,應該比較清楚虛函數機制的基本實現了。然而,也許你還會有這些問題:
- 虛函數表是每個子物件都有的嗎?
- 虛函數是存在一個表裡的,表的資料結構是怎樣的,如何定位表裡哪個才是我們要調用的虛函數?
略作變化
讓我們對前面的代碼做以下修改:
- 定義一個普通類
- 修改parent類,在fun1前增加虛函數fun2
- 在child1裡和child2裡編寫fun2的具體實現,一個在fun1之前編寫,另外一個在之後編寫
修改後的編碼大致如下:
class parent1{public: virtual int fun2()=0; virtual int fun1()=0;};class child{ int a;};class child1:public parent1{public: virtual int fun1() { cout<<"child1::fun1()"<<endl; return 0; } virtual int fun2() { cout<<"child1::fun2()"<<endl; return 0; }};
然後我們再使用cl命令以及/d1reportSingleClassLayout選項輸出相關的類物件版面配置情況:
class child size(4): //在普通類child裡,看不到vfptr的身影! +--- 0 | a +---class child1 size(4): //child1的物件版面配置,和之前沒有變化! +--- | +--- (base class parent1) 0 | | {vfptr} | +--- +---//child1的虛函數表多了fun2,並且兩個虛函數在表裡的順序相同於在parent類裡聲明的順序child1::$vftable@: | &child1_meta | 0 0 | &child1::fun2 1 | &child1::fun1child1::fun1 this adjustor: 0child1::fun2 this adjustor: 0
結論很明顯:
- 虛函數表指標vfptr只在類裡有虛擬函數的時候才會存在
- 當有多個虛函數的時候,虛函數在虛函數表裡的順序由父類裡虛函數的定義順序決定
並且我們還可以觀察到:
- 這個vfptr指標會放在類的起始處(這是必須的,vfptr在父類和子類的物件版面配置上必須一致!)
- 虛函數表是以一個NULL指標標識結束
讓我們對這次簡單的範例程式碼測試來做個小小總結:
- 有虛函數的類,一定會有一個虛函數表指標vfptr
- 這個vfptr指標會放在類的起始處
- 虛函數表裡會按基類聲明虛函數的順序在vfptr裡存放函數地址
- 虛函數表裡存放的是函數地址是具體子類的實現函數的地址
- 調用虛函數的時候,是從vfptr所指的函數表裡擷取到函數地址,然後才調用具體的代碼