類對象切割對虛函數調用的影響

背景

現在有CFish和CAnimal兩個類,並且CFish類繼承於CAnimal類,它們都有breath這樣的接口,只是表現形式不同,所以用虛函數來定義,類關係如下圖所示;
這裏寫圖片描述
圖一 類圖關係

其代碼實現如下:

//基類
class CAnimal
{
public:
    CAnimal()
    {
        //構造函數
        cout << "CAnimal Constructor" << endl;
    }
    ~CAnimal()
    {
        //析構函數
        cout << "CAnimal Destructor" << endl;
    } 
    virtual void breath()
    {
       cout << "CAnimal breath" <<endl;
    }
};
//繼承類CFish
class CFish:public CAnimal
{
public:
    CFish()
    {
        //構造函數
        cout << "CFish Constructor" << endl;
    }
    ~CFish()
    {
        //析構函數
        cout << "CFish Destructor" << endl;
    } 

    virtual void breath()
    {
        cout << "CFish breath" << endl;
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

現在我們使用這兩個類來分析對象切割,也就是發生Object slicing時對虛函數有何影響,示例代碼如下:

int _tmain(int argc, _TCHAR* argv[])
{
    CFish FishObj;
    CFish *pFish = new CFish;

    cout << "case test begin..." << endl << endl;
    //case1
    cout << "case1" <<endl;
    FishObj.breath();

    //case2
    cout << "case2" <<endl;
    pFish->breath();

    //case3
    cout << "case3" <<endl;
    ((CAnimal*)(&FishObj))->breath();

    //case4, 對象切割,對象發生向上強制轉換
    cout << "case4" <<endl;
    ((CAnimal)FishObj).breath();


    cout << "case test end..." << endl << endl;

    if (NULL != pFish)
    {
        delete pFish;
        pFish = NULL;
    }

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

經vs2008輸出如下的測試結果:

//CFish FishObj 定義對象輸出
CAnimal Constructor
CFish Constructor
//new CFish new對象時輸出
CAnimal Constructor
CFish Constructor

case test begin...

case1
CFish breath
case2
CFish breath
case3
CFish breath

case4
CAnimal breath      //------> 出乎意外,竟不是CFish breath
CAnimal Destructor  //------> 出乎意外,竟有調用析構函數
case test end...

//函數返回棧對象析構
//以及delete對象析構
CFish Destructor
CAnimal Destructor
CFish Destructor
CAnimal Destructor
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

毫無疑問,case1-case3都是調用CFish類中的breath函數,因爲pFish 、FishObj在構造對象結束後,他們的虛函數表內容已經確定,都是存在CFish::breath接口,但case4輸出結果卻比較特殊,因爲語句((CAnimal)FishObj)發生了向上強制轉換,導致對象切割,在切割過程有對象重新構造,導致虛函數表中的內容發生變化,具體分析如下;

對象切割分析

我們知道,派生類通常會比基類大,因爲派生類不僅有基類的成員還有派生類本身的成員,經過向上轉換(派生類對象強制轉換爲基類對象),就會造成對象內容被切割(object slicing).

當代碼執行到((CAnimal)FishObj).breath()語句時,發生了object slicing,其過程如下:

這裏寫圖片描述
圖二 對象切割流程

備註:本例中m_data1和m_data2是虛擬的,只有vptr是真實的。

從圖中可以看出,當發生強制轉換時有兩個步驟:

  1. 發生CAnimal對象構造,同時將vptr中的值被賦值爲CAnimal::breath的地址
  2. ((CAnimal)FishObj).breath()調用轉換爲臨時對象的breath調用。

我們也可以從彙編代碼中看出實際執行情況,彙編代碼如下:

這裏寫圖片描述
圖三 代碼彙編分析

在彙編代碼中也驗證了圖二流程分析的正確性,需要注意的是在強制轉換過程中,編譯器會主動爲我們合成一個構造函數,不是我們定義的那個構造函數,但調用的析構函數都是同一個

通過以上分析,用例4的測試結果也就不感到意外了。

總結

如果類對象發生了切割或者向上強制轉換,會產生臨時對象,使得這個臨時對象變成真正的CAnimal類對象,而不是CFish對象。

在分析問題過程中,也瞭解到一個類的構造函數有多個,但是其析構函數只有一個,因爲析構函數沒有返回值,沒入參,也就無法實現析構函數的重載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章