閱讀498 返回首頁    go 阿裏雲 go 技術社區[雲棲]


漫談析構函數(一)——從一個麵試題開始

在開始我們的內容前,首先讓我們看一道麵試題,題目如下:

說出下段代碼的輸出:


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,他需要為每個Objectvptr設定初值,使其指向適當的vtbl

擴展:同理當一個Class直接或間接繼承一個virtual base Class時,也不再表現“位逐次拷貝語義”,默認構造函數和拷貝構造函數也不再是trivial,因為需要正確的設置virtual base Class的偏移。


注:如圖:兩個B的對象之間或兩個D的對象之間調用拷貝構造,“位逐次拷貝”不會發生問題,但是用D的對象給B的對象賦值(此時造成切割)就必須調整vptrvirtual base Class偏移。

4.2 虛函數表中的內容

那麼虛函數表中的“虛函數”都包括哪些呢?總共有以下三類:

(1) 被當前Class覆蓋(override)的base Class中的虛函數

(2) 繼承自base Class的沒有被override的虛函數。

(3) 純虛函數(當前類定義的,或者從base Class繼承來的)

4.3 虛函數的布局

    說了這麼多,我們基本可以推斷出程序中Class A,Class B的內存布局情況,如下圖。注:Type_info以後討論,這裏先忽略。

  

      (aClass A內存布局                           (bClass 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

  上一篇:go 【Java】ArrayList 的 toArray() 方法拋出 ClassCastException 異常
  下一篇:go Oracle中的sql語句優化