漫談析構函數(一)——從一個麵試題開始
在開始我們的內容前,首先讓我們看一道麵試題,題目如下:
說出下段代碼的輸出:
class A { public: virtual void g() { cout<<"A::g()"<<endl; } private: virtual void f() { cout<<"A::f()"<<endl; } }; class B:public A { public: virtual void g() { cout<<"B::g()"<<endl; } private: virtual void h() { cout<<"B::h()"<<endl; } }; typedef void(*Fun)(void); int _tmain(int argc, _TCHAR* argv[]) { B b; Fun pFun; for(int i=0;i<3;i++) { pFun=(Fun)*((int*)*(int*)(&b)+i); pFun(); } };
思考幾分鍾,看一下程序輸出:
如果你有疑問,或者不理解,我們就開徹底分析下這個程序。首先簡單討論一下虛函數在內存中與Class的關係。
4.1 虛函數的維護
帶有一個虛函數的Class,以下3個操作會在編譯期間發生:
1. 一個虛函數表(vtbl)會被編譯器產生出來,內部存放Class的虛函數地址。(虛函數表是以Class維護的)
2. 每一個Class Object中,一個額外的虛函數表指針(vptr)會被編譯器合成出來,內含vtbl的地址。(vptr是以Class Object維護的)
3. 對虛函數的調用會被改寫,比如b.f(),(f()為虛函數)會被改寫成:(*b.vptr[1])(&b),其中1表示f()在vtbl中的索引,&b代表調用f()的this指針。(可以看出虛函數的調用是需要以間接調用完成,效率相對普通成員函數要低)
此外帶有一個虛函數的Class的默認合成構造函數以及拷貝構造函數不再是trivial,他需要為每個Object的vptr設定初值,使其指向適當的vtbl。
擴展:同理當一個Class直接或間接繼承一個virtual base Class時,也不再表現“位逐次拷貝語義”,默認構造函數和拷貝構造函數也不再是trivial,因為需要正確的設置virtual base Class的偏移。
注:如圖:兩個B的對象之間或兩個D的對象之間調用拷貝構造,“位逐次拷貝”不會發生問題,但是用D的對象給B的對象賦值(此時造成切割)就必須調整vptr和virtual base Class偏移。
4.2 虛函數表中的內容
那麼虛函數表中的“虛函數”都包括哪些呢?總共有以下三類:
(1) 被當前Class覆蓋(override)的base Class中的虛函數
(2) 繼承自base Class的沒有被override的虛函數。
(3) 純虛函數(當前類定義的,或者從base Class繼承來的)
4.3 虛函數的布局
說了這麼多,我們基本可以推斷出程序中Class A,Class B的內存布局情況,如下圖。注:Type_info以後討論,這裏先忽略。
(a)Class A內存布局 (b)Class B 內存布局
從上圖可以看出A,B的對象中沒有數據成員,隻有一個vptr,這個我們可以輸出驗證:
cout<<sizeof(A)<<" "<<sizeof(B)<<endl;(4,4)。
注:虛函數在vtbl中的順序和虛函數在Class中的聲明順序一致。
4.4 pFun=(Fun)*((int*)*(int*)(&b)+i);
相信不少人對程序中這條語句都有迷惑,下麵我們來逐步分析下這條語句的含義。
首先,由程序中typedef語句我們知道Fun是一個函數指針,也就是這句話是要將某個地址轉換成函數地址(指針)。具體步驟如下:
(1) (Fun)*((int*)*(int*)(&b)+i)----------------------------取對象b的地址,由對象的內存布局我們知道對象b的地址和vptr的地址相同,即取vptr的地址。
(2) (Fun)*((int*)*(int*)(&b)+i)----------------------------將vptr的地址強轉成int型地址(指針),即讓編譯器將vptr的地址當做int型指針對待。否則*(&b)會被當做對象b。
(3) (Fun)*((int*)*(int*)(&b)+i)----------------------------得到vptr地址中存放的內容,即vptr(或者說vptr的值,或者說vtbl的地址)。
(4) (Fun)*((int*)*(int*)(&b)+i)----------------------------將vtbl的地址轉為int指針,為之後與整型類型i相加做準備。
(5) (Fun)*((int*)*(int*)(&b)+i)---------------------------- 我們知道指針與整數的相加,移動的是指針所指對象的大小,因為上一步已經轉換為int型指針,所以會移動i*sizeof(int)個字節。
(6) (Fun)*((int*)*(int*)(&b)+i)-----------------------------其實上一步就是將指針移動到vtbl中相應表條目處(存放虛函數的地址),所以這裏取出地址的內容就是對應虛函數的地址。
(7) (Fun)*((int*)*(int*)(&b)+i)------------------------------最後這一步將函數的地址轉換成函數指針,以後後麵調用。
綜上,我們可以知道這句活就是根據i的遞增逐個調用B中的虛函數。而B中的虛函數布局我們已經知道,所以輸出就不難理解了。
注:通過這個例子我們還發現了一個問題,函數f()在基類A中是私有的,而我們卻訪問到了。其實我們將B中的h()聲明為private,輸出結果依然不變,並不會引起訪問權限問題。也就是說可以外界訪問到Class的私有虛函數。這是為什麼呢?我個人的理解是:訪問限定符隻在編譯檢查時候起作用,而在程序執行期間沒有作用,因為從C++的函數名稱修飾規則來看,並沒有將訪問限定符納入其中,所以我們隻要通過了編譯,找到對應的函數地址就能夠調用私有函數,因為在內存中私有函數和公有函數並沒有什麼區別。
最後更新:2017-04-03 12:56:00