"无继承" 情况下的对象构造
考虑以下代码:
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 运算符删除 ehap object.
一个 object 的生命, 是该 object 的一个属性, local object 的生命从定义开始, 到作用于结束为止; 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的缩写, 是 IBM 704 assmbler 的一个 pseudo-op.
C++ 并不支持临时性的定义, 这是因为 class 构造行为的隐含应用之故, 虽然大家公认这个语言可以判断一个 class objects 或是一个 Plain Old Data. 但似乎没有必要搞得那么复杂. 因此 global 在 C++ 中被视为完全定义(它会组阻止第二个或更多个定义). C 与 C++ 的一个差异就在于, BSS data segment 在 C++ 中相当的不重要, C++ 所有全局变量都被当作初始化过的数据一样.
Foobar() 中有一个 Point object lcal, 同样也是既没有被构造也没有被解构, 当然, 如果 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, copyoperator //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 Member() { 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 函数实体.
观念上, 我们的 Point class 有一个相关的 default copy constructor, copy operator 和 destructor, 但是他们都是 trivial, 所以编译器就根本没产生它们.