对于虚基类子对象进行默认初始化----读书笔记《C++ gotchas》

一个class对象中的虚基类子对象和非虚基类子对象,布局不同。

非虚基类子对象如同它是派生类中的一个普通数据成员,可以出现多次:

class A {memebers};

class B : public A { members };

class C : public A {members };

class D : public B, public C { members };

c  gotchas_5

而虚基类类型子对象在派生类对象中出现一次

class A {memebers};

class B : public virtual A { members };

class C : public virtual A {members };

class D : public B, public C { members };


c  gotchas_6

 

为演示方便,此处使用的是相当老旧的指针式虚基类实现:在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)) {

    //正确

}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值