构造、析构、拷贝语义学(二)
上一期我们对一个 class 的定义进行了优化,并讨论了三种变量的初始化情况还有显式处置列表(explicit initialization list)。那现在我们就接着来看看,成员函数的继承把。
-
为继承做准备
老样子,我们先给处我们的例子。
class Point { public: Point( float x = 0.0, float y = 0.0 ) : _x(x), _y(y) // 注意了,这个是初值化列表,跟我们上节课讲的显式初值化列表完全不同 {} // 没有定义析构函数,拷贝构造函数,或者拷贝运算符 virtual float z(); protected: float _x, _y; };
由于引入了 virtual function,编译器会对我们的代码进行拓展。
-
我们所定义的 constructor 被附加上一些代码,以便讲 vptr 初始化。这些代码必须被附在任何 base class constructor 的调用之后,但必须在任何由使用者供应的代码之前。
Point* Point::Point(Point *this, float x, float y) : _x(x), _y(y) { // 设定 object 的 virtual table pointer this->__vptr_Point = __vtbl_Point; // 拓展 member initialization list this->_x = x; this->_y = y; // 传回this对象 return this; }
-
合成一个 copy constructor 和一个 copy assignment operator,而且其操作不再是 trivial(但是implicit destructor 仍然是 trivial)。如果一个 Point object 被初始化或以一个 derived class object赋值,那么以位为基础的操作可能对 vptr 带来非法的设定。
编译器在优化状态下可能会把object的连续内容拷贝到另一个object身上,而不会实现一个精确地“以成员为基础”的赋值操作。C++ standard 要求编译器尽量延迟 nontrivial members 的实际合成过程,直到真正遇到其使用场景为止。
还是之前的例子
Point global; Point foobar() { Point local; Point *heap = new Point; *heap = local; delete heap; return local; } // 我们看到 *heap = local 这个操作 // 这个操作就有可能会触发一个 copy assignment operator // 的合成,及其调用操作的一个inline expansion (行内扩张) // 以this取代heap,以rhs取代local // 我们在看到return local 这个操作 // 编译器可能通过优化会将其转换成 void foobar(Point &__result) { Point local; local.Point::Point(0.0, 0.0); // 非重点 Point *heap = new Point; *heap = local; // 合成一个 copy constructor __result.Point::Point(local); local.Point::~Point(); return; } // 如果支持NRV优化的化 void foobar( Point &__result ) { __result.Point::Point( 0.0, 0.0 ); return; }
一般而言,如果一个类中有比较多的函数返回一个 local class object,那么为这个类设计一个拷贝构造函数是有必要的。
-
继承体系下的对象构造
一般而言,编译器会对类的每一个构造函数进行扩张,扩张的程度由类的继承体系而定。编译器所做的扩充操作如下:
- 记录在 member initialization list 中的 data members 初始化操作会被放进构造函数的函数本体,并以 data member 的声明顺序为顺序。
- 如果有一个 member 并没有出现在 member initialization list 之中,但它有一个 default constructor,那么该 default constructor 必须被调用。
- 在这之前,如果class object有virtual table pointers,它们必须被设定初值,指向适当的virtual table
- 在这之前,所有上一层的 base class constructors 必须被调用,以base class 的声明顺序为顺序
- 如果base class 都被列在 member initialization list 中,显式指定的参数都应该传过去。
- 如果 base class 没有被列于 member initialization list 中,而它有 default constructor (或 data memberwise copy constructor),那么就调用它。
- 如果base class 是多重继承下的第二个或后继的class,那么this指针需要被调整
- 在这之前,所有的virtual base class constructors 必须被调用,从左到右,从最深到最浅:
- 如果class 被列于 member initialization list 中,那么如果有任何显式指定的参数,都应该传递过去。如果没有列于list 中,而class有一个default constructor,也应该调用它。
- 此外,class中的每一个 virtual base class subobject 的偏移位置必须在执行期可被存取
- 如果class object是最底层的class,其constructor可能被调用:某些用以支持这一行为的机制必须被放进来。
现在给出这个小节的核心用例
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 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();
};
// 我们看看第二个构造函数的声明
Line::Line(const Point& begin, const Point& end)
: _end(end), _begin(begin)
{}
// 它会被编译器扩充为
Line*
Line::Line(Line* this, const Point& beign, const Point& end)
{
// 因为Point中生成了拷贝构造和赋值运算符
// 所以编译器直接使用我们定义的函数,无需额外合成
this->_begin = begin;
this->_end = end;
return this;
}
// 当我们执行这样的操作
Line a;
// 由于在Line中并未显式定义构造函数
// 所以编译器会帮我们合成一个
void inline
Line::~Line(Line *this)
{
// 调用的顺序和析构的顺序相反的,调用栈了解一下
this->_end.Point:~:Point()l
this->_begin.Point::~Point();
}
Line b = a;
// 如果是这样的化,编译器就会为我们
// 合成一个拷贝构造函数
Line c;
c = a;
// 如果是这种,编译器就会为我们合成一个
// 赋值运算符
我们在编写拷贝构造函数和赋值运算符的时候,还需要注意一点:我们需要防止自身拷贝。
所以,在书上,作者大大建议我们在进行拷贝之前加上这样一句话
if (this == &rhs) return *this;
能够避免一个多余的无效操作,也能避免在拷贝的时候将原来的对象释放,造成悬空引用。