5.1 "无继承" 情况下的对象构造

Point global;                   //1)
Point Foobar()
{
    Point local;                //2)
    Point *heap = new Point;    //3)
    *heap = local;              //4)
    //...stuff...
    delete heap;                //5)
    return local;               //6)
}

1), 2), 3) 为三种不同的对象产生方式: global内存配置;local 内存配置;heap 内存配置。4) 把一个object 指定给另一个, 6) 设定返回值, 7) 明确地以 delete 运算符删除 heap object.
一个 object 的生命, 是该 object 的一个属性, local object 的生命从定义开始, 到6)结束为止; global object 的生命和整个程序的生命相同; heap object 由 new 而生, 因 delete 而死.
下面是 Point 的第一次声明, 可以写成 C 程序, C++ standard 说这是一种 所谓的 Plain Old Data 声明形式:

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

如果用 C++ 来编译这段码, 会发生什么事? 观念上, 编译器会为 Point 声明一个 trivial default constructor, 一个 trival destructor, 一个 trivial copy constructor, 以及一个 trivial assignment operator. 但实际上, 编译器会分析这个声明, 并为它贴上 Plain Old Data 卷标.
当编译器遇到这样的定义:

Point global;

观念上 Point的 trivial constructor 和destructor 都会被产生并且调用, constructor 在程序起始处被调用 destructor 在程序的 exit() 处被调用(exit() 是系统产生, 放在 main() 结束之前). 然而, 事实上那些 trivial members 要不没定义要不就没被调用, 程序的行为亦如它在 C 中的行为表现一样.

当然, 还有一个小的例外, 就是: 在 C 中, global 被视为一个临时性的定义, 可以在程序中发生多次, 那些实例会被折叠起来, 只留下一个单独实体, 被放在程序的 data segment 中的一个特别保留给未初始化的 global objects 使用的空间. 由于历史原因的缘故, 这块空间被称为 BSS, 是 Block Started by Symbol的缩写。

C++ 并不支持临时性的定义, 这是因为 class 构造行为的隐含应用之故, 虽然公认这个语言可以判断一个 class objects 或是一个 Plain Old Data. 但似乎没有必要搞得那么复杂. 因此 global 在 C++ 中被视为完全定义(它会阻止第二个或更多个定义). C 与 C++ 的一个差异就在于, BSS data segment 在 C++ 中相当的不重要, C++ 所有全局变量都被当作初始化过的数据一样.

foobar() 中有一个 Point object local, 同样也是既没有被构造也没有被析构, 当然, 如果 Point object local 如果没有先经过初始化, 可能会成为一个潜在的 bug, 当你第一次使用它就需要初值的时候. 至于 heap object 的初始化操作:

Point *heap = new Point

会被转换为对 new 运算符的调用:

Point *heap = new( sizeof(Point) );

再次强调没有 default constructor 施行于 new 运算符所传回的 Point object 身上, 对于如下操作:

*heap = local;

如果 local 被适当的初始化过, 就没有问题, 但是事实上会产生编译警告如下:
warning, line 7 :local is used before being initialized.

观念上这样的操作会出发一个 trivial copy assignment operator 进行拷贝操作。实际上此 object 是一个 Plain Old Data, 所以赋值操作像 C 那样的纯粹搬移操作。
执行 delete 操作:

delete heap;
//会被转换为
__delete(heap);

观念上, 这样的操作会触发 Point的 trivial destructor. 但如我们所见, destructor 要么没被 产生(需要的时候才会产生) 要么就没被 调用(存在)。最后, 函数以传值的方式将 local 当作返回值传回, 这在观念上会触发 trivial copy constructor, 不过实际上 return 操作只是一个简单的 bitwise 操作, 因为对象是一个 Plain Old Data.

Plain Old Data 的含义:
POD类类型是指聚合类(aggregate classes, 即POD-struct types)与聚合union (POD-union types),且不具有下述成员:

  1. 指针到成员类型的非静态数据成员(包括数组)。
  2. 非POD类类型的非静态数据成员(包括数组)。
  3. 引用类型的(reference type)非静态数据成员。
  4. 用户定义的拷贝与赋值算子。
  5. 用户定义的析构函数。

术语聚合是指任何的数组或者类,且不具有下述特征:

  1. 用户定义的构造函数。
  2. 私有或保护的非静态数据成员。
  3. 基类。
  4. 虚函数。
    可见,POD类类型就是指class、struct、union,且不具有用户定义的构造函数、析构函数、拷贝算子、赋值算子;不具有继承关系,因此没有基类;不具有虚函数,所以就没有虚表;非静态数据成员没有私有或保护属性的、没有引用类型的、没有非POD类类型的(即嵌套类都必须是POD)、没有指针到成员类型的(因为这个类型内含了this指针)。

抽象数据类型

以下是 Point 的第二次声明, 在 public 接口下多了 private 数据, 提供完整的封装性, 但没有提供任何的 virtual function:

class Point
{
public:
    Point(float x = 0.0, float y = 0.0, float z = 0.0):_x(x), _y(y), _z(z){ }
    //no copy constructor, copy operator or destructor defined
private:
    float _x, _y, _z;
};

这个经过封装的 Point class, 其大小并没有改变, 还是三个连续的 float, 是的, 不论是 public, private 存取层, 还是 member function 的声明, 都不会占用对象额外的空间.
并没有为 Point 定义一个 copy constructor 或 copy operator, 因为默认的位语意(default bitwise semantics) 已经足够, 我们也不需要提供一个 destructor, 因为程序默认的内存管理方法已足够。
对于 Point global, 现在有了 default constructor 作用于其上, 由于 global 被定义在全局范围中, 其初始化操作将延迟到程序激活时才开始。
如果要对 class 中的所有成员都设定常量初值, 那么给予一个 explicit initialization 比较高效, 甚至在local scope 中也是如此, 举个例子:

void mumble()
{
    Point local1 = {1.0, 1.0, 1.0};
    Point local2;

    //相当于一个 inline expansion
    //explicit initialization 会快一点
    local2._x = 1.0;
    local2._y = 1.0;
    local2._z = 1.0;
}

local1 的初始化操作比 local2 的要高效。** 因为当函数的 activation record 被放进堆栈时, 上述 initialization 中的常量就被放入 local1 内存中了**.
Explicit initialization list 带来三个缺点:

  1. 只有当 class members 都是 publics 时, 此法才奏效.
  2. 只能指定常量, 因为它们在编译时期就可以被评估求值.
  3. 由于编译器没有自动执行, 所以初始化行为的失败可能性会比较高

效率:一些特殊情况, 比如想把一堆常量颠倒给程序, 那么 explicit initialization list 的效率就比 inline constructor 的效率好得多, 特别是对一个全局对象而言。
在编译器层面, 会有一个优化机制用来识别 inline constructors(如果简单的提供一个 member-bymember 的常量指定操作)。编译器会抽取出那些值, 并且对待它们和 explicit initialization list 供应的一样, 而不会把 constructor 扩展成为一系列的 assignment 指令。
于是,local Point object 的定义: 见书上

{
    Point local;
    //...
}
//附上 default Point constructor 的 inline expansion 之后
{
    //inline expansion of default constructor
    Point local;
    local._x = 0.0;
    local._y = 0.0;
    local._z = 0.0;
    //...
}

对于一个 heap Point object, 现在附加一个对 default Point constructor 的有条件的调用操作:

Point *heap = __new( sizeof(Point) );
if(heap != 0){ heap->Point::Point(); }

然后才又被编译器进行 inline expansion 操作. 至于把 heap 指针指向 local object:
*heap = local;
则保持着简单的 bitwise copy 操作.
以传值的方式来传回 local object 也是一样.
后来删除 delete heap 操作并不会导致 destructor 被调用, 因为并没有明确的提供一个 destructor 函数实体(new不会被销毁).
观念上, Point class 有一个相关的 default copy constructor, copy operator 和 destructor, 但是他们都是 trivial, 所以编译器就根本没产生它们.

为继承做准备

第三个 Point 声明, 将为继承性质以及某些操作的动态决议做准备, 现在限制对_z 的存取操作:

class Point
{
public:
    Point(float x = 0.0, float y = 0.0, float z = 0.0):_x(x), _y(y){ }

    //no destructor, copy constructor or copy operator defined...

    virtual float Z();
    //...
protected:
    float _x, _y;
}

上没有定义一个 copy constructor, copy operator, destructor。所有的 members 都以数值来储存, 因此在程序层面的默认语意之下行为良好。有人可能会说, virtual function 的引入应该总是附带着一个virtual destructor 的声明, 在本例中那么做对我们无好处.
virtual functions 的引入使每一个 Point object 拥有一个 virtual table pointer. 这个指针提供给我们 virtual 接口的弹性, 代价是每一个 object 需要额外的一个 word 空间. 如果要表现一个复杂的几何形状, 有着 60 个 NURB 表面, 每个表面有512个控制点, 那么每个 object 多负担 4 个 bytes 将导致大约 200000 bytes 的额外负担. 这可能有意义, 也可能没意义, 这取决于多态设计的实际效益.
除了每一个 class object 多负担一个 vptr 之外, virtual fuinction 的引入也使编译器对我们的 Point class 起扩展作用:

1.constructor 被附加了一些码, 以便将 vptr 初始化, 这些码必须被附加在任何 base class constructors 的调用之后, user code 之前:

Point* Point::Point(Point *this, float x, float y):_x(x), _y(y)
{
    //初始化 vptr
    this->__vptr_Point = __vtbl__Point;

    // 扩展 member initialization list
    this->_x = x;
    this->_y = y;

    return this;
}
  1. 必须合成 copy constructor 和 copy assignment operator, 因为其操作不再是 trivial(被需要)(但 destructor 依然是 trivial)。 如果一个 Point object 被初始化或以一个 derived class object 赋值, bitwise 的操作可能给 vptr 带来非法设定:
//编译器合成的 copy constructor
inline Point* Point::Point(Point *this, const Point &rhs)
{
    this->__vptr_Point = __vtbl_Point;
    //将 rhs 坐标中的连续位拷贝到 this 对象,
    //或是经由 member assignment 提供一个member......

    return this;
}

编译器在优化状态下可能会把 object 的连续内容拷贝到另一个 object 身上, 而不会实现一个精确的 memberwise 的赋值操作。C++ standard 要求编译器尽量延迟 nontrivial members 的实际合成操作, 直到真正遇到其适用场合为止.
比如之前的 Foobar():

Point global;
Point foobar()
{
    Point local;
    Point *heap = new Point;
    *heap = local;
    /...stuff...
    delete heap;
    return local;
}

前面的操作都与之前分析的相同, 一直到 *heap = local 的 memberwise 赋值操作才很有可能触发 copy assignment operator 的合成, 及其调用操作的一个 inline expansion: 相当于前文中的代码现在以 heap 取代 this, 以 local 取代 rhs。
另外一个变化就是 return local; 这一行, 由于 copy constructor的出现, foobar() 很有可能被转化为下面这样的形式:

Point foobar(Point &__result)
{
    Point local;
    local.Point::Point(0.0, 0.0);
     Point *heap = new Point;
    *heap = local;
    __result.Point::Point(local);

    // local 对象的 destructor 将在这里执行
    // 调用 Point 定义的 destructor
    // local.Point::~Point();
	delete heap;
    return;
}

如果支持 NRV 优化, 这个函数会进一步转化为:

Point Foobar(Point &__result)
{
    __result.Point::Point(0.0, 0.0);
    //heap 部分与前文相同
    return;
}

一般而言, 如果设计之中有许多函数都需要以传值的方式传回一个 local class object, 例如像如下形式的一个算术运算:

T Operator+(const T&, const T&)
{
    T result;
    //...这里才是真正的工作
    return result;
}

那么提供一个 copy constructor 就比较合理, 即使 default memberwise 的语意已经足够. 它的出现会触发 NRV 优化, 就像之前显示的那样, NRV 优化之后不再需要调用 copy constructor, 因为结果已被直接置于将被传回的 object 体内了。

posted @ 2021-04-22 17:41  点|滴  阅读(10)  评论(0)    收藏  举报