当前位置: 首页 > article >正文

第5章 构造、析构、拷贝语义学2: 继承情况下的对象构造

重点:构造函数调用顺序

当我们定义一个object如:T object;如果T有一个constructor(不管是trival还是nontrival),它都会被调用。

  1. 记录在member initialization list中的data members初始化操作放在constructor的函数体内,并且以members的声明顺序为顺序

  2. 如果一个member没有出现在members initialization list中,但是它有一个默认构造函数,那么这个默认构造函数必须被调用

  3. 在那之前,如果class object有virtual table pointer(s),它们必须被设定初始值,指向适当的virtual table

  4. 在那之前,所有上一层的base class constructor必须被调用,以base class 的声明顺序为顺序(与members initialization list中的顺序没关联)

    • 如果base class被列于members initialization list中,那么任何显示指定的参数都应该传递过去

    • 如果base class没有被列于member initialization list中,而他有default constructor(或default memberwise copy constructor),那么就调用之

    • 如果base class是多重继承下的第二或者后继的base class,那么this指针应该有所调整

  5. 在那之前,所有virtual base class constructor必须被调用,从左往右,从最深到最浅

    1. 如果class位member initialization list,那么有任何显示指定的参数都应该传递过去;若没有位于list,而class含有一个默认构造函数,也应该调用。

    2. class中的每一个virtual base class subobject的偏移量(offset)必须在执行期可存取

    3. 如果class object是最底层(most-derived)的class,其constructors可能被调用;某些用以支持这一行为的机制必须被放进来

//以point为例
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; }
private:
    float _x, _y;
};

//以Point为基础的类Lines的扩张过程
class Lines {
    Point _begin, _end;
public:
    Lines(float = 0.0, float = 0.0, float = 0.0, float = 0.0);
    Lines(const Point&, const Point&);
    draw();
};

//来看第二个构造函数的定义与内部转化
Lines::Lines(const Point& begin, const Point& end) :
    _begin(begin), _end(end){} //定义
//下面是内部转化,将初值列中的成员构造放入函数体,调用这些成员的构造函数
Lines* Lines::Lines(Lines *this, const Point& begin, const Point& end) {
    this->_begin.Point::Point(begin);
    this->_end.Point::Point(end);
    return this;
}

//我们写下
Lines a;
//合成的destructor在内部可能如下,
//如果line派生于point那么合成的destructor将会是virtual
//然而由于line是内含point object而不是继承,
//   那么合成出来的destructor是nonvirtual;
//   如果point destructor是inline,那么每一个调用点会被扩张
inline void
Lines::~Lines(Lines *this) //
{
    this->_begin.Point::~Point();
    this->_end.Point::~Point();
}

Lines b = a;
//这个时候调用合成的拷贝构造函数,合成的拷贝构造在内部可能如下
inline Lines&
Lines::Lines(const Lines& rhs) {
    if(*this = rsh) return *this;   
    //证同测试,或者可以采用copy and swap,具体见effective C++
    //还要注意深拷贝和浅拷贝
    this->_begin.Point::Point(rhs._begin);
    this->_end.Point::Point(rhs._end);
    return *this;
}

应该在copy operator中检查自我指派是否失败。

在copy operator中面对自我拷贝设计一个自我过滤操作

一、虚拟继承

重点:虚拟继承下constructor如何调用

class Point3d : public virtual 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; }
private:
    float _z;
}

 传统的constructor扩张并没有用,因为virtual base class的共享性的原因。试想以下继承关系

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

 

当point3d和vertex同为vertex3d的subobject时,对point constructor的调用操作一定不可以发生,作为一个最底层的class pvertex,有责任将point初始化

constructor的函数本体因而必须条件式地侧测试传进来的参数,然后决定调用或不调用相关的virtual base class constructor。

Point3d的constructor的扩充如下

//c++伪码
Point3d*
Point3d::Point3d(Point3d* this, bool __most_derived,
                float x = 0.0, float y = 0.0, float z = 0.0) {
    if(__most_derived != false) this->Point::Point();
    //虚拟继承下两个虚表的设定
    this->__vptr_Point3d = __vtbl_Point3d;
    this->__vptr_Point3d__Point = __vtbl_Point3d__Point;
    this->z = rhs.z;
    return this;
}

 而并非如下传统的扩张

//c++伪码
Point3d*
Point3d::Point3d(Point3d* this,
                float x = 0.0, float y = 0.0, float z = 0.0) {
    this->Point::Point();
    this->__vptr_Point3d = __vtbl_Point3d;
    this->__vptr_Point3d__Point = __vtbl_Point3d__Point;
    this->z = rhs.z;
    return this;
}

两种扩张的不同之处在于参数__most_derived,在更加深层次的继承情况下,例如Vextex3d,调用Point3d和Vertex的constructor时,总会将该参数设置为false,于是就压制了两个constructors对Point constructor的调用操作。 例如:

//c++伪码
Vextex3d*
Vextex3d::Vextex3d(Vextex3d* this, bool __most_derived,
                float x = 0.0, float y = 0.0, float z = 0.0) {
    if(__most_derived != false) this->Point::Point();
    //设定__most_derived为false
    this->Point3d::Point3d(false, x, y, z);
    this->Vertex::Vertex(false, x, y);
    //设定vptrs
    return this;
}

当我们定义

Vertex3d cv;

 Vertext3d 的constructor 正确的调用 Pointer constructor。

virtual base class constructor被调用有着明确的定义,只有当一个完整的class object被定义出来(例如 Point3d origin)时,它才会被调用,如果object只是某个完整的object的subobject,他就不会被调用

还可以把constructor一分为2,一个针对一个完整的object,一个针对subobject,完整的object无条件调用virtual base constructor,设定vptrs,subobject不调用virtual base constructor,也可能不设定vptrs

二、Vptr初始化语义学

重点:Vptr在那一步做的

定义一个PVertex object时,constructor的调用顺序是

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

假设每个class都定义了一个virtual function size();返回该class的大小。我们来看看定义的PVertex constructor

PVertex::Pvertex(float x, float y, float z)
    : _next(0), Vertex3d(x, y, z), Point(x, y) {
    if(spyOn)
        cerr << "Within Pvertex::PVertex()"
             << "size: " << size() << endl;
}

编译器如何保证适当的size()被正确的调用? 如果调用操作必须在 constructor 或 destructor 中, 那么答案十分明显:将每一个调用操作以静态决议,千万不要用虚拟机制

vptr的初始化操作:在base class constructor调用操作之后,但是在程序员供应的代码或”member initialization list中所有列的members初始化操作”之前。如果每一个constructor 都一直等待其 base class construtors 执行完毕之后才设定其对象的vptr,那么每次都能够调用正确的 virtual function 实例。

constructor的执行算法通常如下:

  1. 在derived class constructor中,所有的virtual base class以及上一层的base class的constructor会被调用。

  2. 上述完成之后,对象的vptrs被初始化,指向相关的虚表。

  3. 如果有member initialization list的话,将在constructor的函数体内展开,这必须是在vptr设定之后才做的,以免有一个virtual member function被调用。

  4. 最后执行程序员的代码。

之前的PVertex constructor可能被扩张成

PVertex*
PVertex::Pvertex(PVertex*this, bool __most_derived,
            float x, float y, float z) {
    //有条件调用virtual base class constructor
    if(__most_derived != false) this->Point::Point();
    //无条件调用上一层base class constructor
    this->Vertex3d::Vertex3d(x, y, z);
    //设定vptr
    this->__vptr_PVertex = __vtbl_PVertex;
    this->__vptr_Point3d__PVertex = __vtbl_Point3d__PVertex;
    //执行程序员的代码
    if(spyOn)
        cerr << "Within Pvertex::PVertex()"
             << "size: "
             //虚拟机制调用
             << (*this->__vptr_PVertex[3].faddr)(this)
             << endl;
    return this;
}

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

  1. 当一个完整的对象被析构起来时,如果我们声明一个Point对象,则Point constructor必须设定其vptr。

  2. 当一个subobject constructor调用了一个virtual function时(不论是直接调用还是间接调用)。

在class的 constructor 的 member initialization list中调用该class的一个虚函数,这是安全的。因为vptr的设定总保证在member initialization list扩展之前就被设定好;但是这在语义上时不安全的,因为函数本身可能依赖未被设定初值的成员。

当需要为base class constructor提供参数时,在class的constructor的成员初值列中调用该class的一个虚函数这就是不安全的,此时vptr要么尚未被设定好,要么指向错误的class。该函数存取任何class's data members一定还没有初始化。(子类初始化列表->初始化父类,子类初始化列表含有虚函数的情况)

参考 构造、析构、拷贝语意学 - tianzeng - 博客园


http://www.kler.cn/a/588309.html

相关文章:

  • ETIMEDOUT 网络超时问题
  • Webpack总结
  • PowerBI数据建模基础操作1:数据关系(基数、双向筛选、常规关系、有限关系)与星型架构(维度表、事实表)
  • ArcGis使用-对轨迹起点终点的网格化编号
  • React(二):JSX语法解析+综合案例
  • 单片机自学指南
  • Java 绘制图形验证码
  • 【数据库相关】mysql数据库巡检
  • Adobe Premiere Pro的简单音频调节
  • 介绍如何使用YOLOv8模型进行基于深度学习的吸烟行为检测
  • Java集合简单理解
  • 快速导出接口设计表——基于DOMParser的Swagger接口详情半自动化提取方法
  • 物联网(Internet of Things,IoT)的核心概念
  • 【机器学习】主成分分析法求数据前n个主成分
  • 基于javaweb的SpringBoot精美物流管理系统设计与实现(源码+文档+部署讲解)
  • 地基Prompt提示常用方式
  • DQN 玩 2048 实战|第二期!设计 ε 贪心策略神经网络,简单训练一下吧!
  • 【SegRNN 源码理解】验证集和测试集
  • 【C语言】函数和数组实践与应用:开发简单的扫雷游戏
  • 【Linux文件IO】系统IO中API描述和基本使用