郑重声明:以下文字“借鉴”自侯捷老师的译作《深度探索C++对象模型》部分内容【5.2节,大概在211页左右】,写在这里,算是加深自己对此书内容的记忆,因为鄙人水平太浅,难免有理解错误的地方,如果有朋友看出来,还请费神指出,鄙人不胜感激!
让我们先来看这样一个继承体系,首先声明一个Point类作为基类,然后再声明两个Point3d和Vertex,它们俩都虚拟继承自Point类,接着再申请一个Vertex3d类,它继承自Point3d和Vertex类,最后再申请一个PVertex类,它继承自Vertex3d类,它们的继承的关系图如下:
代码如下:
#include
#include
using namespace std;
class Point{
public:
Point(int x = 0, int y = 0):_x(x),_y(y){
cout << "This is Point construct!" << endl;
};
protected:
int _x, _y;
};
class Point3d :virtual public Point{
public:
Point3d(int z=0) :_z(z){
cout << "This is Point3d construct!" << endl;
};
protected:
int _z;
};
class Vertex :virtual public Point{
public:
Vertex():_pv(NULL){
cout << "this is Vertex construct!" << endl;
};
protected:
Vertex *_pv;
};
class Vertex3d : public Vertex,public Point3d{
public:
Vertex3d(){
cout << "This is Vertex3d construct" << endl;
};
};
class PVertex :public Vertex3d{
public:
PVertex(){
cout << "This is PVertex construct" << endl;
}
};
int main()
{
PVertex pv;
return 0;
}
根据这个图片我们可以看出,对于虚继承体系来说,任何一个对象的构造执行顺序总是从深到浅、从远到近,既最先构造的总是最底层的基类。
这样看起来似乎并没有什么值得奇怪的地方,但是让我们想想,如果我们把主函数main中的代码改成【Point3d p3d;】呢?相信按下F5后,会输出两句执行结果:【This is Point construct!】和【This is Point3d construct!】,这是因为在构造Point3d对象时,会先执行Point的构造器。看到这里,细心的朋友或许会生出疑问了:不是说在构造PVertex对象时,执行完Point的构造器,还会执行Vertex、Point3d等等类的构造器吗,而在这些类的构造中,不还是要重新执行Point类的构造器吗?如果真是这样,那Point构造器应该被执行很多次啊,那为什么我们只看到一句【This is Point construct!】呢??
嗯,按道理来说,是应该如此,但这种“讲道理”实在是太浪费效率了:因为对于那些多层继承来说,这种构造方式无疑是一种冗余大灾难!所以,C++的先驱们,就设计出一种黑魔法,让编译器可以不讲道理式的高效率构造对象,而这种黑魔法,我称之为“隐参选构法”。顾名思义,就是在执行一个派生类对象的构造时,会有条件的选择要不要执行基类的构造器。具体做法就是,给继承体系中所有的构造器传入一个隐形参数,根据这个参数的具体数值来确定要不要调用基类的构造器。再具体一点的说,根据书中的描述,这个隐形参数的名字叫做【_most_derived】,是紧跟隐形this指针参数的第二个参数,它有两个值,一个为true,一个为false,当参数为true时,那么它就调用最基类的Point构造器,否则就不调用。
那么什么时候值才是true呢?答案是当我们声明一个完整的派生类对象时,比如我们例子中的代码【PVertex pv;】,因为pv是一个完整派生类对象,所以编译器在调用它的构造器时,会传入一个true值【1】,所以代码【PVertex pv;】会被编译器扩展成大概这样的形式【PVertex pv(&pv,1);】。那么什么时候传入的值是false呢?答案是在任何一个派生类对象的构造器中,递归调用上一层类的构造器时。比如我们的PVertex类,在编译器给它合成的构造器里面,一定会调用它的继承类Vertex3d类的构造器,而在调用后者时,一定会传入一个false值【0】,以此类推,Vertex3d在调用Vertex和Point3d时,也会传入false值;
说了这么多,让我们来看一下编译器给PVertex类合成的构造器内部代码大概是什么样子的【以下伪代码只是鄙人推测,未必如实!】:
PVertex* PVertex::PVertex(PVertex *this, bool _most_derived, int x, int y, int z)
{
//如果是声明一个完整类对象,那么默认传入一个true值,所
//以在我们的例子中表达式结果为真,Point构造器得以执行。
if (_most_derived != false)
this->Point::Point(x, y);
//只要是在派生类的构造器中递归调用上一层类的构造器,传
//入的值一定是false,所以Vertex3d构造器中的Point构造器
//不会得到执行!以此类推,在Vertex3d中调用Vertex和
//Point3d时,Point构造器也不会执行
this->Vertex3d::Vertex3d(false, x, y, z);
//接着我们需要设置一下虚表指针等
this->_vptr_PVertex = _vtbl_PVertex;
this->vptr_PVertex_Point = _vtbl_PVertex_Point;
/*****************************************
这里可以用来安插一些用户自己写的构造器代码
******************************************/
//最后返回this指针
return this;
}
在现代编译器中,会把每一个construct一分为二,一种针对完整的继承对象,另一种针对子对象【subobject】;完整版无条件调用virtual base construct,设置所有的vptrs,而“subobject”则不调用virtual base construct,也可能不设置vptrs。无疑这是一种提高对象构造效率的方法,毕竟在构造器里面省却了条件分支判断了,代价则是编译后代码的臃肿。
讲完了constructor,接下来再讲一讲destructor的工作原理,我们就只说一下它的工作步骤好了,如果有读者感兴趣,请自行阅读《深度探索C++对象模型》一书第五章最后一节的内容,大约在235页:
1):destructor函数本身首先被执行。
2):如果该类拥有成员类对象,而这些成员类对象又拥有destructor,那么它们会以声明次序的相反顺序逐个被调用。
3):如果object内带一个vptr,则就在现在被重新设定,指向适当的基类的虚函数表。
4):如果该类有上一层的nonvirtual base class,并且这些基类们拥有destructor,那么就按照它们的声明次序的相反顺序调用。
5):如果有任何的virtual base class拥有destructor,而当前讨论的这个类是最尾端【most derived】的类,那么它们会以其原来的构造顺序相反的顺序被调用。
类似constructor,目前对于destructor的实现策略也是维护两份destructor实体:
1):一个complete object【完全对象】实体,它总是设定好vptrs,并调用virtual base class的destructor。
2):一个base class object实体,除非在destructor调用一个virtual function,否则它绝对不会调用virtual function或设定vptrs。