背景
现在有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对象。
在分析问题过程中,也了解到一个类的构造函数有多个,但是其析构函数只有一个,因为析构函数没有返回值,没入参,也就无法实现析构函数的重载。