背景
現在有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是真實的。
從圖中可以看出,當發生強制轉換時有兩個步驟:
- 發生CAnimal對象構造,同時將vptr中的值被賦值爲CAnimal::breath的地址
- ((CAnimal)FishObj).breath()調用轉換爲臨時對象的breath調用。
我們也可以從彙編代碼中看出實際執行情況,彙編代碼如下:
圖三 代碼彙編分析
在彙編代碼中也驗證了圖二流程分析的正確性,需要注意的是在強制轉換過程中,編譯器會主動爲我們合成一個構造函數,不是我們定義的那個構造函數,但調用的析構函數都是同一個。
通過以上分析,用例4的測試結果也就不感到意外了。
總結
如果類對象發生了切割或者向上強制轉換,會產生臨時對象,使得這個臨時對象變成真正的CAnimal類對象,而不是CFish對象。
在分析問題過程中,也瞭解到一個類的構造函數有多個,但是其析構函數只有一個,因爲析構函數沒有返回值,沒入參,也就無法實現析構函數的重載。