继承体系下的对象构造
继承前的准备
前一篇博客的三个 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 defiend... virtual float Z(); //... protected: float _x, _y; }
我并没有定义一个 copy constructor, copy operator, destructor. 咱们所有的 members 都以数值来储存, 因此在程序层面的默认语意之下行为良好. 有人可能会说, virtual fuinction 的引入应该总是附带着一个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; }
2. 必须合成 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 = __vbl_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); //heap 部分相同 __result.Point::Point(local); //随后 lcoal 对象的 destructor 将在这里执行 //local.Point::~Point(); 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 体内了.
现在进入主题, 当我们定义一个 object 如下:
T object;
时, 实际会发生什么事呢?如果 T 有一个 constructor, 它会被调用. 这很明显, 而不明显的是, constructor 的调用真正伴随了什么?
Constructor 可能内带大量的隐藏码, 因为编译器会扩充每一个 constructor, 扩充程度视 class T 的继承体系而定. 一般而言编译器所做的扩充操作大约如下:
1. 记录在 member initialization list 中的 data members 初始化操作会被放进 constructor 的函数本身, 并以 members 的声明顺序作为顺序.
2. 如果有一个 member 并没有出现在 member initialization list 之中, 但它有一个 default constructor, 那么该 default constructor 必须被调用.
3. 在那之前, 如果 class object 有 virtual table pointers 它们必须被设定初值, 指向适当的 virtual tables.
4. 在那之前, 所有上一层的 base class constructors 必须被调用, 以 base class 的声明顺序为顺序 (这个与 member initialization list 中的顺序没关系):
4.1 如果 base class 被列于 member initialization list 中, 那么任何明确指定 的参数都应该被传递过去.
4.2 如果 base class 没有列于 member initialization list 中, 而它有 default constructor(哪怕是 default memberwise copy constructor), 调用之.
4.3 如果 base class 是多重继承下的第二或后继的 base class , 那么 this 指针 必须有所调整
5. 在那之前, 所有 virtual base ckass constructors 必须被调用, 由最深到最浅:
5.1 如果 class 被列于 member initialization list 中, 那么如果有任何明确指定 的参数, 都应该传递过去, 若没有列于 list 之中但是有一个 default constructors 的, 也该调用之.
5.2 此外, class 中的每一个 virtual base class subobject 的偏移量(offset) 必须在执行期可被存取.
5.3如果 class object 是最底层的 class, 其 constructors 可能被调用, 某些用以支持这个行为的机制必须被放进来.
在这个部分中, 我想从 C++ 语言对 classes 所保证的语意这个角度来探讨 constructors 扩充的必要性. 我再次以 Point 为例, 并为它增加一个 copy constructor, 一个 copy operator, 一个 virtual destructor 如下:
class Point { public: Point(float x = 0.0, float y = 0.0); Point(const Point&); Point& operator=(const Point&); virtual ~Point(); virtual float Z(); protected: float _x, _y; }
在分析以这个 class 为 base 的继承体系之前, 先看这么个类:
class Line { Point _begin, _end; public: Line(float = 0.0. float = 0.0, float = 0.0, float = 0.0); Line(const Point&, const Point&); void Draw(); //... };
这个类中每个 explicit constructor 都会被扩充, 以调用其两个 member class objects 的 constructors. 如果我们定义 constructor 如下:
Line::Line(const Poit &begin, const Point &end) :_end(end), _begin(begin){} //它会被扩充为 Line* Line::Line(Line *this, const Point &begin, const Point &end) { this->_begin.Point::Point(begin); this->_end.Point::Point(end); return this; }
由于 Point 声明了一个 copy constructor, 一个copy operator, 以及一个 destructor, 所以 Line class 的 implicit copy constructor. copy operator 和 destructor 都是 non-trivial.
当程序员写下 Line a; 时, implicit Line destructor 就会被合成出来(如果 Line 派生出 Point, 那么合成出的 destructor 将会是 virtual, 然由于 Line 只是 has Point objects 而非继承自 Point, 所以被合成出的 destructor 只是 nontrivial 而已, 并不是说立刻也把 Point 的 destructor 也合成出来). 在其中, 它的 memberclas objects 的会被调用:
inline void Line::~Line(Line *this) { this->_begtin.Point::~Point(); this->_end.::Point::~Point(); }
当然, 如果 Point destructor 是 inline 函数, 那么每一个调用操作会在调用地点被扩展开来. 请注意, 虽然 Point destructor 是 virtual , 但其调用操作会被静态的决议出来.
类似的, 当写下 Line b = a; 时, implicit copy assignment operator 会被合成出来, 成为一个 inline public member.
Lippman 在改写 cfront 时, 注意到在产生 copy operator 的时候, 并没有用如下的条件语句筛选:
if(this == &rhs)returnj *this; 于是 cfront 会做多余的拷贝操作: Line *p1 = &a; Line *p2 = &b; *p1 = *p2;
并且并不是 cfront 才如此, Borland 也缺少这项, 因此大部分编译器都难以保证不是如此. 上述的重复操作虽然安全但是累赘, 因为并没有任何有关资源释放的行动. 一个有程序员供应的 copy operator 中忘记检查自我指派操作是否失败, 是新手极易陷入的问题(其实在他的另一本书 C++ primer 5th 中反复的提到了这个问题, 因此新手就是犯了这个错误也是没仔细看书):
String& String::operator=(const String &rhs) { delete []str; str = new char[strlen(rhs) + 1 ]; }