C++提供了继承机制和虚拟,并通过(且只能通过)指向同一类族的指针或者引用来实现多态,否则多态不起作用。原因之一就是这里要说一下的著名的对象切片(Object slicing)问题。
无虚拟机制的继承的切片问题首先,类中毫无疑问地需要有继承和虚拟。没有这两者,就不存在多态(注意,重载并不属于多态——个人理解,欢迎来搞)。由于虚拟机制的复杂性,先用一个小例子来说明一下只有继承时的切片问题。假定有两个类:
class MyBase
{
public:
void Get(){};
void Set(){};
public:
int b;
};
class DerivedMyBase: public MyBase
{
public:
void Print(){};
void GetD(){};
};
如果有下面的语句:
DerivedMyBase aDMB;
MyBase aMB = aDMB;
那么,通过aMB来访问d或者Print()就是非法的:
// Illegal to access GetD() or Print() through aMB
aMB.GetD();
aMB.Print();
这是因为在将aDMB拷贝给aMB时发生了对象切片,在aMB对象中只有MyBase的信息,所有的关于DerivedMyBase类的信息都被切片了。在“MyBase aMB = aDMB;”还涉及到默认拷贝构造函数的问题,下文会详细描述。
这仅仅是最简单的一种情况。要注意区分下面这种情况:
DerivedMyBase aDMB;
MyBase * pMB = &aDMB;
通过pMB来访问d或者Print()仍然是非法的:
// Illegal to access GetD() or Print() through pMB
pMB->GetD(); // Of course one can use dynamic_cast<> to make this call legal.
pMB->Print();
由于没有虚拟机制,多态在这里仍然不起作用,然而,这里并没有对象切片的发生。因为DerivedMyBase是一个MyBase,所以“MyBase * pMB = &aDMB;”是合法的。而pMB仅仅是一个指针,通过该指针引用的是aDMB,但编译器对于该指针应用对象的了解仅限于MyBase,对于DerivedMyBase类的信息一无所知——这也就是在实践中通常将基类作为抽象类来实现多态的原因,此时派生类中的所有不属于基类的信息都无法通过基类指针或引用来获取,因为编译器在解析该指针或引用指向的内存区时是按照基类的信息来解释的。
对象切片的机理那么,对象切片是如何发生的?简而言之,是由compiler向拷贝构造函数中插入的代码来做。由于在“MyBase aMB = aDMB;”中由编译器生成的拷贝构造函数不需要对虚拟机制进行额外的处理,此时依照bitwise copy,所有属于DerivedMyBase的信息都丢掉了。而在“ MyBase * pMB = &aDMB;”中,根本就不需要调用copy ctor,所以切片不会发生。
下面,为MyBase和DerivedMyBase加入虚拟机制,看看情况有什么变化:
class MyBase
{
public:
virtual void Get(){};
virtual void Set(){};
public:
int b;
};
class DerivedMyBase: public MyBase
{
public:
void Print(){};
void GetD(){};
};
首先编译器会在你的ctor或者编译器为你生成的ctor中加入对虚拟机制的处理代码,这也使得默认拷贝构造函数及对象切片问题变得异常复杂。——此处虚拟机制包括virtual函数和virtual基类。
memberwise copy和bitwise copy首先说一下深拷贝(memberwise copy)和浅拷贝(bitwise copy)的问题。一般来说,自己定义的copy ctor对于对象的拷贝会有严格的、符合语义的定义(人为错误、破坏因素除外)。然而,无论是自定义的还是默认的ctor,编译器都会插入对虚拟机制的处理代码,这就保证对象切片和拷贝正确的发生——可能会出乎你的意料,但符合C++的语法语义。
虚拟机制与拷贝方式当类中没有虚拟机制、没有其他类对象的成员时(只包含built-in类型、指针或者数组),默认copy ctor进行的是bitwise copy,这会导致对象切片的发生。然而,当类中有虚拟机制,或者有其他类对象成员时,默认copy ctor采用的是memberwise copy,并且会对虚拟机制进行正确的拷贝。
因为包含虚拟机制的类在定义一个对象时,编译器会向ctor中添加初始化vtable和vbaseclasstable(依赖于具体编译器)的代码,这样可以保证vtable中的内容与类型完全匹配。也就是说MyBase和DerivedMyBase有这相似的VTABLE,但不是完全相同——例如DerivedMyBase中还可以定义自己的virtual函数,这样它的VTABLE就会有更多表项。
而多态的实现是通过将函数调用解析为VTABLE中的偏移量来实现。pMB->Get()可能会被编译器解析成:
(*pMB->__vtable[Offset_of_Get])();
而当MyBase作为虚基类时,访问其中的数据成员可能就是:
pMB->__vBaseClassMyBase->b;
那么,当“aMB = aDMB;”,copy ctor会执行memberwise copy,正确的初始化aMB的VTABLE,而不是仅仅将aDMB的VTABLE拷贝过来。如果是bitwise copy,aMB对象中的VTABLE将是aDMB的,aMB.Get()调用的将是DervieMyBase定义的Get(),这显然是不符合语义和逻辑的。
总而言之
对象切片和copy ctor是一个很复杂的东西,在有虚拟机制的情况下两者是紧密结合在一起的。因为对象切片和拷贝构造函数的问题,不通过指针或者引用无法达到多态的目的。
还有一个问题是赋值拷贝的问题,这个机制更复杂,因此Lippman建议不要再虚基类中使用数据成员。C#和java禁止了多重继承,并将interface作为一个单独的东西,消除了赋值拷贝带来的复杂性。关于赋值拷贝的问题,有机会再讨论。
PS:上述代码均能由g++ 3.4.4编译。对于C++的复杂性,想必很多人都有切身感受。
顺祝ChinaUnix的所有朋友们新年快乐,万事大吉,新年发大财!
参考:
Inside the C++ Object Model, by Stanley B Lippman.
Copyleft (C) 2007-2009 raof01.
本文可以用于除商业外的所有用途。此处“用途”包括(但不限于)拷贝/翻译(部分或全部),不包括根据本文描述来产生代码及思想。若用于非商业,请保留此
权利声明,并标明文章原始地址和作者信息;若要用于商业,请与作者联系(raof01@gmail.com),否则作者将使用法律来保证权利。
阅读(4425) | 评论(2) | 转发(0) |