常见错误53: 对于虚基类子对象进行默认初始化----读书笔记《c++ gotchas》...
时间:2010-08-17 来源:lzueclipse
一个class对象中的虚基类子对象和非虚基类子对象,布局不同。
非虚基类子对象如同它是派生类中的一个普通数据成员,可以出现多次:
class A {memebers};
class B : public A { members };
class C : public A {members };
class D : public B, public C { members };
而虚基类类型子对象在派生类对象中出现一次:
class A {memebers};
class B : public virtual A { members };
class C : public virtual A {members };
class D : public B, public C { members };
为掩饰方便,此处使用的是相当老旧的指针式虚基类实现:在A类型子对象本来应该出现的地方,放置了一个指向A类型子对象的指针。
在新的编译器视线中,这种手法一般是不用的,取而代之的是一个偏移量,或者是虚函数表中的附加信息来完成。
典型地,虚基类子对象是附在完整对象之后。
上例中,完整对象是D,因此虚基类子对象A是放在D的数据成员之后。
只有最深派生类才知道虚基类子对象的精确地址。
在最深派生类类型为B的情况下,就是B的构造函数来初始化虚基类子对象A,并将指针指向它:
B::B(int arg)
: A(arg) {}
图5-2中,最深的派生类是D,所以D的构造函数负责初始化虚基类子对象A,并在B和C中准备好指向该对象的指针,另外还将完成
直接继承基类B和C的子对象的初始化工作:
D::D(int arg)
: A(arg), B(arg), C(arg+1) {}
这样,一旦虚基类子对象被D的构造函数初始化,它就不会被B或C的构造函数再初始化一遍(一种编译器可能采用的实现策略
是它会向B或C的构造函数传递一个flag,或是A类型的指针,提醒不要再把A类型子对象再初始化一遍)。
再来看一个D的构造函数:
D::D()
: B(11), D(12) {}
D的构造函数仍然初始化了虚基类子对象A,通过隐式调用A的默认构造函数来完成;当调用B和C的构造函数时,也不会再把
A的子对象初始化一遍。
我们知道复制赋值运算符应该和对应的构造函数有相同的语义。下面我们来讨论复制赋值,使得它也不会对虚基类多次
赋值:
意图设计成虚基类的class,最好是把它们设计成“接口类”(interface class)——不包含数据成员,
一般而言其成员函数(也许唯一例外的是析构函数)全是纯虚函数,不声明任何构造函数:
class A {
public:
virtual ~A();
virtual void op1() = 0;
virtual int op2(int src, int dest) = 0;
//...
};
inline A::~A() {}
对于复制赋值而言,编译器提供的复制赋值运算符可能会,也可能不会把一个虚基类子对象赋值多次;
如果所有的虚基类都是接口类的话,那么复制操作符肯定实现为空操作。(因为指向虚函数表的指针,不受赋值操作符的影响,
只在初始化时设置)。如此一来,多次赋值也不会导致缺陷。
考虑图5-1所示的class D的实现,它包含两个A类型的子对象,在此情形中,我们撰写一个复制赋值运算符,
该运算符基于直接基类(immediate base class)实现:
D & D::operator = (const D &rhs) {
if(this != &rhs) {
B::operator=(*this);//对B类型子对象赋值,同时完成A类型子对象赋值
C::operator=(*this);//对C类型子对象赋值,同时完成A类型子对象赋值
//D的特定数据成员赋值
}
}
这种分层的赋值实现对于存在虚继承的情况下,玩不转。
最深派生类应该完成虚基类子对象赋值的同时,应该阻止对于该虚基类子对象的重复赋值行为:
D & D::operator = (const D &rhs) {
if(this != &rhs) {
A::operator = (*this);//对虚基类子对象A赋值
B::nonvirtAssign(*this);//对B类型子对象赋值,除了A部分
C::nonvirtAssign(*this);//对C类型子对象赋值,出了A部分
//对D特有数据成员赋值
}
}
这里,在B和C引入了特殊的assignment-like member function,它们完成的操作和复制赋值运算符很相似,只是把
虚基类复制那部分赋值去掉了。但是有个问题,引入了复杂性,要密切关注整个继承谱系,任何继承谱系的改动,
都会带来重写D的实现的需求。打算用作虚基类的class最好还是实现为接口类。
虚基类子对象在一个完整对象中的布局隐含着一个推论,那就是不允许使用static_cast向下强制类型转换(static downcast)
,将虚基类对象转换至其派生类类型。
A *ap = gimmeanA();
D *dp = static_cast<D *> (ap);//错误
dp = (D *) ap;//错误
使用reinterpret_cast把一个虚基类转换成某个派生类是合法的,但结果可能是一个无效地址。
唯一可靠的是dynamic_cast:
if(D *dp = dynamic_cast<D *> (ap)) {
//正确
}