构造、析构、拷贝语义学(四)
对象复制语义学 Object Copy Semantics
当我们设计一个class,并以一个class object指定给另一个class object时,我们有三种选择:
- 什么都不做,因此得以实施默认行为
- 提供一个explicit copy assignment operator。
- 显式拒绝把一个class object指定给另一个 class object。
在这一节,我们需要验证 copy assignment operator的语意,以及它们如何被模塑出来。我们看看本节的例子
class Point {
public:
Point( float x = 0.0, float y = 0.0, float z = 0.0 );
protected:
float _x, _y;
};
对于上面的例子来说没什么必要禁止拷贝一个class object。那么编译器默认的行为是否足够呢?编译器默认是使用逐字进行拷贝的,但是上面的class中并没有指针或者应用这一类的东西,所以默认的拷贝行为也是足够的,而且编译器默认的行为不仅足够还有效率。只有在默认行为所导致的语意不安全时或不正确时,我们才需要设计一个copy assignment operator。
如果我们不对Point供应一个copy assignment operator,在以下情况,不会表现出bitwise copy语意:
- 当class内含一个member object,而其class有一个copy assignment operator时。
- 当一个class的base class有一个copy assignment operator时。
- 当一个class声明了任何virtual functions(我们一定不要拷贝右端class object的vptr地址,因为他可能是一个derived class object)时。
- 当class继承自一个virtual base class(不论此base class有没有copy operator)时。
C++standard上说,copy assignment operators并不表示bitwise copy semantic是nontrivial。实际上,只有nontrivial instance才会被合成出来。
所以,对于Point
这个类来说这样的赋值操作:
Point a, b;
a = b;
由bitwise copy完成,把Point b
拷贝到Point a
,期间并没有copy assignment operator被调用。从语意上或从效率上考虑,这都是我们需要注意的。注意,我们还是可能提供一个copy constructor,为的是把name return value(NRV)优化打开。copy constructor的出现不应该让我们以为也一定要提供一个copy assignment operator。
现在导入一个copy assignment operator,用以说明该在继承之下的行为:
inline Point& Point::operator=(const Point &p)
{
_x = p._x;
_y = p._y;
}
// 现在派生一个Point3d class
class Point3d : virtual public Point3d {
public:
Point3d( float x = 0.0, float y = 0.0, float z = 0.0 );
protected:
float _z;
}
如果在Point3d
中并没有声明一个copy assignment operator,编译器就必须显式合成一个,比如下面这样的
inline Point3d&::operator=(Point3d* const this, const Point3d &p)
{
// 调用 base class 的函数实例
this->Point::operator=(p);
_z = p._z;
return *this;
}
这里需要注意,由于Point3d
是Point
的一个虚拟派生类,所以,在同一个继承体系中,不止一个类对Point
进行了虚拟继承,这时候问题就出现了,跟我们之前的constructor一样,在调用派生类的时候,可能会调用多次基类的copy assignment operator。但是,copy assignment operator和constructor并不一样,所以这里作者给的一个建议就是:不要任何的virtual base class中声明数据。
析构语意学
如果class没有定义destructor,那么只有在class内含的member object(抑或是class自己的base class)拥有destructor的情况下,编译器才会自动合成一个出来。否则,destructor被视为不需要,也就不需要被合成。
当class内含member object 或者class的base class,它们中定义了destructor,那么编译器就会自动合成一个或者在我们定义的destructor中进行拓展,调用他们的destructor。一个由程序员定义的destructor被拓展的方式类似constructor被拓展的方式,但顺序相反:
- destructor的函数本体首先被执行
- 如果class拥有member class object,而后者拥有destructor,那么它们会以其声明顺序的相反顺序被调用
- 如果object内含一个vptr,现在被重新设定了指向了适当的base class的virtual table
- 如果由任何直接的nonvirtual base classes 拥有destructor,它们会以声明顺序的相反顺序被调用
- 如果有任何virtual base classes 拥有destructor,而目前讨论的这个class 是最尾端(most-derived)的class,那么他们会以原来的构造顺序的相反顺序被调用。
就像constructor一样,目前对于destructor的一种最佳的实现策略就是维护两份destructor实例:
- 一个complete object实例,总是设定好vptr,并调用virtual base class destructor。
- 一个base class subobject实例;除非在destructor函数中调用了一个virtual function,否则它绝不会调用virtual base class destructor并设定vptr。
一个object的生命结束于其destructor开始执行之时。由于每一个base class destructor 都轮番被调用,所以derived object实际上变成了一个完整的object。