C++对象模型剖析(十三)一一构造、析构、拷贝语义学(三)

构造、析构、拷贝语义学(三)

继承体系下的对象构造

我们接着昨天的内容。

  • 虚拟继承(virtual Inheritance)

    class Point {
    public:
        Point( float x = 0.0, float y = 0.0 );
        Point( const Point& );
        Point& operator=(const Point&);
        
        virtual ~Point();
        virtual float z() { return 0.0; }
        
    protected:
        float _x, _y;
    };
    

    这是我们昨天给出的典例,我们下面实现一个虚拟继承

    class Point3d : virtual public Point {
    public:
    	Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
    		: Point(x, y), _z(z)
    	{}
    	Point3d(const Point3d& rhs)
    		: Point(rhs), _z(rhs._z)
    	{}
    	~Point3d();
    	Point3d& operator= (const Point3d&);
    	virtual float z()
    	{
    		return _z;
    	}
    
    protected:
    	float _z;
    };
    

    我们猜一下,编译器会怎样给这个 Point3d的constructor进行扩张,

    Point3d::Point3d(Point3d* this, float x, float y, float z)
    {
    	this->Point::Point(x, y);
    	this->__vptr_Point3d = __vtbl_Point3d;
    	this->__vptr_Point3d_Point = __vptr_Point3d_point;
    	this->_z = z;
    	return this;
    }
    

    如果是上面的扩张的话,在当前的情况下看是没什么问题,但是我们再考虑一个更深层的继承关系。

    class Vertex : virtual public Point {};
    class Vertex3d : virtual public Vertexv, public Point3d {};
    class PVertex : public Vertex3d {};
    

    在这里插入图片描述
    它们的继承关系是这样的,如果Vertex的构造函数的扩张也像上面的Point3d一样的话,我们可以发现,Point这个虚基类被实例化了两次,分别是在Point3dVertex的构造函数进行实例化的。所以,一般而言,以上面为例,当Point3dVertex同为Vertex3d的subobject时,它们对Point的 constructor 的调用操作一定不可以发生;取而代之的是,作为最底层的 class,Vertex3d有责任将Point初始化。而如果在往下的继承,则是由PVertex来负责完成“被共享的Point constructors的构造。

    所以,Point3d中正确的扩张应该是下面这样的

    Point3d::Point3d(Point3d* this, bool __most_derived, float x, float y, float z)
    {
        // 有派生类对象的constructor决定是否需要构建虚基类
        if (__most_derived != flase)
    		this->Point::Point(x, y);
        
    	this->__vptr_Point3d = __vtbl_Point3d;
    	this->__vptr_Point3d_Point = __vptr_Point3d_point;
    	this->_z = z;
    	return this;
    }
    
    // 在更深层次的情况下,例如Vertex3d调用Point3d和Vertex的constructor时,
    // 总会把__most_derived参数设置为false,于是就压制了两个constructor中
    // 对Point constructor的调用操作
    Vertex3d* Vertex3d::Vertex3d( Vertex3d *this, bool __most_derived,
                                float x, float y, float z)
    {
        if (__most_derived != false)
            this->Point::Point(x, y);
        
        // 调用上一层的base class
        // 设定__most_derived为false
        this->Point3d::Point3d(false, x, y, z);
        this->Vertex::Vertex(fase, x, y);
        
        // 设定vptr
        // 安插用户代码
        
       return this;
    }
    

    通过上面的例子我们可以很好地理解我们上一期所说的操作了,因为在扩充的时候,编译器首先会将virtual base class的constructor进行调用(或者判断是否需要调用),然后再来调用上层的base class,最后递归结束,再调用自己的构造函数的代码。

    在这里插入图片描述

    所以我们可以看到,virtual base class constructor的被调用有着明确的定义:只有当一个完整的 class object 被定义出来的时候,它才会被调用;如果object只是某个完整的class的subobject,那么它就不会被调用。

    书原话:以此为杠杆,我们可以产生更有效率的constructor。某些新进的编译器把每一个constructor分裂为二,一个针对完整的object,另一个针对subobject。“完整object”版无条件地调用 virtual base constructor,设定所有的vptrs等。“subobject”版则不调用virtual base constructor,也可能不设定vptrs等。

  • vptr 初始化语义学 (the semantic of the vptr initialization)

    通过上面的学习,我们知道,当我们定义一个PVertexobject时,constructors的调用顺序是:

    Point(x,y);
    Point3d(x,y,z);
    Vertex(x,y,z);
    Vertex3d(x,y,z);
    Point3d(x,y,z);
    

    **这里我给大家举一反三一下:**我们通过一个小小的案例来更好的说明这一点:

    // 现在我给大家写一个复杂一点的继承体系
    class A;
    class B : virtual public A {};
    class C : virtual public A {};
    class D : public B, public C {};
    class E : virtual public D {};
    class F : virtual public D {};
    class G : virtual public E, virtual public F {};
    class H : public G {};
    class I : public H {};
    /* 继承关系大概这样的
    	  -- B --			-- E -- 
    	/		 \		  / 	   \
    A --		   -- D -- 			 -- G -- H -- I
         \		  /		   \	    /
           -- C --  		 -- F -- 
              
    */
    // 这时候我们初始化一个
    I i;
    // 构造函数的调用顺序是什么样子的呢?
    // 大家思考一下,我们开始下一个部分
    

    好,那现在我们在假设一个情景:这个之前的继承体系中,每一个class都定义了一个virtual function size(),这个函数负责传回class的大小;然后我们在这个继承体系中的每一个构造函数内含一个调用操作,像这样:

    Point3d::Point3d( float x, float y, float z)
        : _x(x), _y(y), _z(z)
    {
    	if (spyOn)  {
            cerr << "Within Point3d::Point3d()" 
                << "size: " << size() << endl;
        }
    }
    

    那么当我们定义PVertexobject的时候,前面五个构造函数会被决议成PVertex::size()吗?还是说,每一次调用会被决议成“目前正在执行的constructor所对应的class的size()的实例?

    C++语言规则告诉我们,在Point3dconstructor中调用的size()函数,必须被决议成Point3d::size()而不是PVertex::size()。更一般地说,在一个class的constructor(和 destructor)中,经由构造中的对象来调用一个virtual function,其函数实例应该是在此class中有作用的那个。由于各个constructor的调用顺序,上述情况是必要的。**Constructor调用的顺序是:由根源到末端(botton up)、由内到外(inside out)。**当base class constructor执行时,derived实例还没有被构造起来。

    所以,当每一个PVertexbase class constructor被调用时,编译系统必须保证有适当的size()实例被调用。**如果调用操作必须在constructor或destructor中直接调用,那么答案十分明显:将每一个调用操作以静态方式决议之,千万不要用到虚拟机制。**例如:如果是在Point3d constructor 中,就显式调用Point3d::size()

    如果在size()中又调用了一个 virtual function(太离谱了,作者的想象无极限)。这个情况下,这个调用也必须决议为Point3d的函数实例。但是在其他情况下,这个调用是一个真正的virtual,必须经由虚拟机制来决定其归向。也就是收,虚拟机制必须知道这个调用是否源自于一个constructor中。

    但是我们还有一种更好的解决方法:引入 virtual table。

    编译系统只需要简单地控制住vptr的初始化和设定操作,就能够控制一个class中的所用函数。而且设定vptr是编译器的操作,我们并不需要担心。

    vptr初始化操作应该如何处理?其实本质上就是vptr需要在什么时候被初始化?我们有三个选择:

    • 在任何操作之前
    • 在base class constructor 调用操作之后,但是在程序员供应的代码之前或是member initalization list中所列的members初始化开始之前。
    • 每一个件事情发生之后。

    毫无疑问,就是第二种。如果没有给 constructor 都一直等到其base class constructors 执行完毕之后才设定其对象的vptr,那么每次它都能够调用正确的virtual function实例。

    **书上原话(有点深奥):另每一个base class constructor 设定其对象的vptr,使它指向相关的virtual table 之后,构造中的对象就可以严格而正确地变成:构造过程中所幻化出来的每一个class”的对象。**也就是说,一个PVertex会先形成一个Point对象,一个Point3d对象,一个Vertex对象,一个Vertex3d对象,然后才成为一个PVertex对象。(下面这句我是真不懂了)**在每一个base class constructor 中,对象可以与 constructor的class的完整对象做比较。对于对象而言,“个体发生学“概括了”系统发生学”。**constructor的执行算法通常如下:

    • 在derived class constructor中,“所有 virtual base classes”及“上一层base class”的constructor会被调用。
    • 上述完成之后,对象的vptr被初始化,指向相关的virtual table。
    • 如果有member initialization list的话,将在constructor体内扩展开来。这必须在vptr被设定之后才做,以免有一个virtual member function调用。
    • 最后执行程序员写的代码

    我们举个新的例子,看看扩展后是怎么样的:

    PVertex::PVertex( float x, float y, float z)
        : _next(0), Vertex3d(x, y, z), Pont(x, y)
        {
            if (spyOn)
                cerr << "Within PVertex::PVertex()"
                	 << "size: " << size() << endl;
        }
    
    // 扩展后
    PVertex*
    PVertex::PVertex(PVertex* this, bool __most_derived, float x, float y, float z)
    {
        // 条件式调用 virtual base constructor
        if (__most_derived != false)
            this->Point::Point(x, y);
        
        // 无条件调用上一层
        this->Vertex3d::Vertex3d(x, y, z);
        
        // 将相关的vptr初始化
        this->__vptr_PVertex = __vtbl_PVertex;
        this->__vptr_Point_PVertex = __vptr_Point_PVertex;
        
        // 程序员写的代码
        if (spyOn)
            cerr << "Within PVertex::PVertex()"
            	<< "size: " << size() << endl;
        
        // 传回被构造的对象
        return this;
    }
    

    现在我们再看一种情况:假设base中并没有调用virtual function

    // 假设我们新的定义
    Point::Point(float x, float y)}
    : _x(x), _y(y) { }
    Point3d::Point3d(float x, float y, float z)
    : Point(x, y), _z(z) { }
    

    在这种情况下,vptr并不需要在每个base constructor中设定,因为它并没有调用virtual function。所以我们可以将constrctor分裂为一个完整的object实例和一个subobject实例。在subobject中可以省略vptr的设定。

    下面是vptr被设定的两种情况:

    • 当一个完整的对象被构造起来时。
    • 当一个subobject constructor调用了一个virtual function时。

    最后,结论就是:在构造函数的成员初值化列表中调用虚函数是安全的,因为编译器会在执行成员初值化列表之前就将虚函数指针进行初始化;但是,从语义上看是不安全,因为调用的虚函数可能依赖于某些还未被初始化的成员变量。所以作者并不推荐在构造函数中使用虚函数。

定的两种情况:

  • 当一个完整的对象被构造起来时。
  • 当一个subobject constructor调用了一个virtual function时。

最后,结论就是:在构造函数的成员初值化列表中调用虚函数是安全的,因为编译器会在执行成员初值化列表之前就将虚函数指针进行初始化;但是,从语义上看是不安全,因为调用的虚函数可能依赖于某些还未被初始化的成员变量。所以作者并不推荐在构造函数中使用虚函数。

  • 7
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值