深入探索C++对象模型-构造、解构、拷贝、语意学

仅记录一些有趣的点

纯虚函数

对于纯虚函数,C++初学者往往都不知道纯虚函数居然是可以定义和进行调用的。

纯虚函数的调用并非是通过虚拟机制调用,只能通过静态调用。

还有一点就是我们纯虚函数一般都是用在接口里面的,我们在接口里面一般都是不定义变量,但是如果所有实现这个接口的类都有一个共同变量,那是否需要把这个共同变量给加到接口类中呢?

如果把将一个共同变量加到了接口类中,那么这个类也就是纯虚类了,纯虚类需要定义构造函数来完成这个共同变量的初始化。到底要不要把这个公共变量提取出来下放到接口类中,这一点并无一个标准到底怎么做好。

纯虚类中,正常来说那些函数我们都无需定义,除了一个函数之外,纯虚析构函数。由于后续继承的子类在析构的过程中会层层调用上级的析构函数,因此缺乏任何一个base class的destructor的定义,就会导致链接失败。

因此,定义析构函数时,不要将其定义为纯虚函数,将其定义为虚函数。

无继承情况下的对象构造

上面我们可以看到定义了3个对象,分别是1个为全局变量,另外两个为局部变量,但是这两个局部变量分别占用堆和栈的内存。

typedef struct {
    float x, y, z;
} Point;

我们需要注意到一点在C里面,全局变量都是保存在BSS段中,BSS段中保存了未初始化和初始化为0的全局变量。

但是对于C++而言,所有全局对象都会当作“初始化过的数据”来对待,因此这个global全局对象变量中的数据并不会存放在BSS段。这是因为class构造行为隐含应用之故。因此,该global对象保存在数据段中。

继承体系下的对象构造

一个类的构造函数往往会经过编译器的扩充:

1、初始化列表中的初始化行为会放到constructor函数中;

2、带有默认构造函数的成员变量进行默认构造函数的调用;

3、vptr的初始化;

4、base class也是同理,从深到浅的顺序;

5、virtual base class则需要考虑额外考虑一些,并不一定会发生constructor的扩充;

同理的还有copy operator以及destructor。如果继承的基类或者是成员对象存在nontrival的实现,那么这个派生类的copy operator和 destructor也会自动合成出来。

cfront和Borland编译器在自动生成copy operator的时候都没有用以下条件句进行筛选,当两个相同对象互相赋值将会发生无意义的赋值的操作。

if (this == &rhs) return *this;

此外,我们在写赋值操作符函数时务必需要考虑这个检查自我指派的操作,这是新手极易陷入的一个错误,特别在以下例子中将会导致一个严重的bug

String& String::operator= (const String &ths) {
    delete []str;
    str = new char[ strlen(rhs.str) + 1 ];
}

以上的代码写出来,当发生自我指派时,就会导致严重错误。

虚拟继承

虚拟继承的效果是让多继承的情况下,避免一个子类继承重复的一个基类的数据。虚拟继承的特性在于共享性

如果任何一个虚继承了基类的类的构造函数中都扩充调用该基类的构造函数,那么就会发生这个基类的构造函数的重复调用。

以上面的类的继承关系图为例,如果避免Vertex3d和PVertex类中Point类的构造函数重复调用的情况呢?

任何虚继承了Point的类的构造函数中,都需要检查自己是否是最底层的类,如果自己是最底层的类,那么就要承担调用Point构造函数的责任。

我们以Point3d的构造函数为例子:

还有Vertex3d的构造函数:

这就很好的解决了虚继承的共享性带来的问题。

virtual base class constructor被调用是有着明确的定义:只有当一个完整的class object被定义出来时,它才会被调用;如果object只是某个完整object的subobject,它就不会被调用。

针对虚继承的这一特点,其实也可以有更效率的解决办法,例如:新近的编译器会产生两个constructor,一个constructor用于作为完整的object产生时调用,另一个constructor用于作为subobject时调用。

vptr的初始化语意学

已知这个继承关系图,当我们定义一个PVertex对象时,constructor的调用顺序依次为:Point->Point3d->Vertex->Vertex3d->PVertex

但是如果这几个类中的构造函数中都调用了一个虚函数size(),负责输出该类的大小。那么定义PVertex时,依次调用的这些构造函数中的size()的输出将如何呢?

由于size函数是一个虚函数,其实这个函数的执行结果取决于调用时对象中的vptr。

那么PVertex的构造过程中从深到浅的依次执行基类的构造函数的过程中,vptr的情况是如何的呢?

答案为:一个类的vptr的初始化操作并不会进入构造函数后立马赋值,而是会等到base class constructors操作结束后,在member intialization list执行之前。这就确保了,每个base class的constructor在调用过程中使用的都是对应base class的vptr,从而实现能够调用正确的virtual function实体。因此,我们也可以得到一个结论,PVertex对象在构造过程中vptr发生了多次赋值。

我们可以看到以下的PVertex的构造函数,按照以上的规则编译器进行扩充的流程:

变成下面的样子

当然同理,编译器给每个类的构造函数提供两个即可,一个用于完整的object实体,另一个用于subobject实体。在subobject的实体中vptr的调用被忽略,同时subobject实体中调用虚函数将不通过虚拟机制来进行,变成静态调用。

虽然上述的方案解决了构造函数中调用虚函数的问题,但是本身这种在构造函数中调用虚函数的做法就是不被提倡的,因为可能这个虚函数还依赖了尚未初始化的member的数值。

对象复制语意学

一个类的的copy assignment operator在以下情况下不会表现出bitwise copy的语意,也就是nontrival的,会被编译器合成出来。

1、class中的一个member object,其class实现了copy assignment operator;

2、class的base class实现了copy assignment operaotr;

3、class的设计中存在虚拟机制,即存在vptr;

4、class存在虚继承机制,即存在vtbl;

下面我们讲述这个问题时,我们重新拿上面的那个继承关系图来讲解:

我们首先给Point类实现一个copy assignment operator:

那么,显然虚拟继承了Point的Point3d和Vertex类,编译器会自动为这两个类合成一个copy assignment operator。

inline Point3d& Point3d::operator= (const Point3d &p) {
    this->Point::operator=(p);
    z_ = p.z_;
    return *this;
}
​
inline Vertex& Vertex::operator= (const Vertex &v) {
    this->Point::operator=(v);
    next_ = v.next_;
    return *this;
}

我们可以看到在虚拟继承的情况下,我们之前碰到的相似问题再次出现了,Point虽然作为公共变量,但是任何一个虚继承了Point的类的operator中都会调用Point的oprator=,这就导致了Point这个公共变量的重复进行operator = 的调用。

特别在Vertex3d类中的copy assignment operator中,我们就可以看到这个现象:

inline Vertex3d& Vertex3d::operator= (const Vertex3d &v) {
    this->Point::operator=(v);
    this->Point3d::operator=(v);
    this->Vertex::operator=(v);
    ...
}

我们可以看到上面这三行代码,其实会执行Point::operator=(v)三次。

那么编译器如何在Point3d和Vertex中压制Point的operator=的调用呢?是否能够像传统constructor那样来解决呢?

答案是不行,传统的constructor是在构造函数中添加了一个参数bool __most__derived,这种附加上额外的参数对于copy assignment operator的扩充是不可行的,因为operator函数是可以取函数地址的,如下所示:

typedef Point3d& (Point3d::*pmfPoint3d)(const Point3d&);
pmfPoint3d pmf = &Point3d::operator=;
(x.*pmf) (x);

因此copy assignment operator无法像constructor那样灵巧。

还有一种方法是采用之前将的split function。当class为most_derived class和中间的base class分别都会对应不同的copy assignment operator,该方案对于那些编译器自行合成的copy assignment operator是可行的,但是如果一个人自行设计函数,在copy assignment operator中加入虚函数的调用,我们在上面的vptr的初始化语意学中可以看到split function会让原function中的所有行为都变得明确,不再有虚拟机制,所有调用都变为静态调用,因此不支持虚函数了。因此,如果如果copy assignment operator的实现中如果加入虚函数,那么split function方案的原则冲突了。

目前copy assignment operator在虚继承情况下就是还有很多问题。因此,现在尽可能不允许一个virtual base class的拷贝操作,或者是不要在任何virtual base class中声明数据。

那么编译器无法处理的情况,我们在代码编写的方式能否解决这一问题呢?

inline Vertex3d& Vertex3d::operator= (const Vertex3d &v) {
    this->Point3d::operator=(v);
    this->Vertex::operator=(v);
    this->Point::operator=(v);
    ...
}

我们仅需把Point::operator放到后面执行,这并不能够省略subobject的多重拷贝,但是可以保证语意的正确,同时写copy assignment operator别忘了自我指派的检查步骤。

解构语意学

Destructor的设计其实和Constructor类似,如果一个类的member object或者是base class存在实现的Destructor,那么编译器就会为这个class生成一个destructor,反之编译器就不会自动合成destructor。

那么destructor中碰到constructor中类似的虚函数的调用以及虚继承的情况该怎么办呢?

解决方案其实和constructor的方案是一模一样的。

那么destructor的执行顺序到底如何,下面列出来:

1、首先执行最外层类的destructor本身其中的代码;

2、执行member object的destructor,如果object中带有vptr,那么会先将vptr更新为member object的vptr,然后再执行其destructor;

3、以相反顺序来执行继承的base class的destructor,同样如果base class有vptr,同样会先重设vptr;

4、执行virtual base class的destructor;

当然目前对于destructor最佳的实现策略还是使用split function,针对每个类都实现两个destructor,分别对应完整的object和subobject。

  • 14
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值