第五章:构造、析构、拷贝语意学
考虑下面这个abstract base class声明:
class Abstract_base{
public:
virtual ~Abstract_base() = 0;
virtual void interface() const =0;
virtual const char*
mumble() const {return_mumble;}
protected:
char* _mumble;
};
虽然这个class被设计为一个抽象的base class(其中有pure virtual function,使得Abstract_base不可能拥有实例),但它仍然需要一个显示的构造函数初始化其data member _mumble。如果没有这个操作,derived class的局部对象_ mumble将无法决定初值。
纯虚函数的存在
C++新手会惊讶的发现,一个人可以定义和调用一个纯虚函数;不过只能被静态的调用,不能经由虚拟机制调用。
例如:可以合法的写下下面的代码:
inline void
Abstract_base::interface1() const{
//...
}
inline Concrete_derived::interface1() const{
Abstract_base::interface1();//静态调用纯虚函数
//...
}
要不要定义和调用纯虚函数,由class设计者决定。唯一的例外就是pure virtual destructor:class设计者一定得定义它。因为每一个derived class destructor会在编译期间被编译器加以扩张,以静态调用的方式调用其“每一个 virtual base class”以及“上一层普通base class”的destructor。因此,只要却反任何一个base class destructor的定义,就会导致连接失败。
虚拟规格的存在:
如果把Abstract_base::mumble()设计为一个virtual function,那将是一个糟糕的选择,因为其函数定义内容并不与类型有关,因而几乎不会被后继的derived class改写。
虚拟规格中的const的存在:
声明一个函数为const,然后发现实际上其derived instance必须修改某一个data member,那么就建议一开始不要使用const。
重新考虑class的声明
class Abstract_base{
public:
virtual ~Abstract_base(); //不再是pure virtualfunction
virtual voidinterface1()= 0; //不再是const
const char*
mumble() const {return_mumble;} //不再是virtual
protected:
Abstract_base(char* pc = 0);//新增一个constructor以方便初始化_mumble
char* _mumble;
};
5.1“无继承”情况下的对象构造
1.Point global;
2.
3.Point foobar()
4.{
5. Point local;
6. Point* heap = new Point;
7. *heap = local;
8. //...
9. delete heap;
10. return local;
11.}
L1、L5、L6表现出三种不同对象的产生方式:global内存配置、local内存配置和heap内存配置。L7把一个class object指定给另一个,L10设定返回值,L9则显式的以delete运算符删除heap object。
Plain Ol’ Data声明
下面是Point的第一次声明:
typedef struct{
float x, y, z;
}Point;
这是一种所谓的Plain Ol’ Data(POD)声明,指的是含有数据成员类型如:基本数据类型、指针、union、数组、构造函数是 trivial 的 struct 或者 class。用来表明C++中与C相兼容的数据类型,可以按照C的方式来处理(运算、拷贝等)。非POD数据类型与C不兼容,只能按照C++特有的方式进行使用。
观念上,编译器会为Point声明一个trival default constructor、一个trivial destructor、一个trivial copy constructor以及一个trival copy assignment operator。但实际上这些trival member要不是没被定义就是没被调用,程序的行为一如它在C中一样,因为对象是一个Plain Ol’ Data。
抽象数据包类型:
以下是Point的第二次是声明,在public接口之下多了private数据,提供完整的封装性,但没有提供任何virtual function:
class Point{
public:
Point(float x = 0.0, floaty = 0.0, float z = 0.0)
:_x(x), _y(y),_z(z){}
private:
float _x, _y, _z;
};
这个封装的 class相对于上一个Point class大小没有改变,还是三个连续的float。
我们并没有为Point定义一个copy constructor或copy operator,因为默认的位语意(default bitwise semantic)已经足够。我们也不需要提供一个destructor,因为程序默认的内存管理方法也已经足够。
对于一个global实例:
Pointglobal; //实施Point::Point(0.0, 0.0, 0.0);
现在有了default constructor作用于其上。由于global被定义在全局范畴中,其初始化操作将延迟到程序启动时才开始。(6.1节对此有讨论)
对于local实例:
{
Pointlocal;
//…
}
现在编译器会为它加上default constructor的inline expansion:
{
Pointlocal;
local._x= 0.0; local._y = 0.0; local._y = 0.0;
//…
}
L6配置出一个heap Point object:
(6)Point* heap = new Point;
现在则被附加一个“对default Point constructor”的有条件调用操作:
//C++伪码
Point*heap = _new( sizeof( Point ) );
if (heap != 0)
heap->Point::Point();
然后才被编译器进行inline expansion操作。至于把heap指针指向local object:
(7) *heap= local;
则保持简单的为拷贝操作。以传值方式传回local object,情况也是一样:
(10)return local;
L9删除heap搜值对象:
(9)delete heap;
该操作不会导致destructor被调用,因为我们并没有显式地提供一个destructor函数实例。
观念是,我们的Point class有一个相关的default copy constructor、copy operator、和destructor。然而这里根本没有产生它们。
为继承做准备
第三个Point声明,将为“继承性质”以及某些操作的动态决议做准备。目前我们限制对z成员作存取操作:
class Point{
public:
Point(float x = 0.0, floaty = 0.0)
:_x(x), _y(y){}
virtual float z();
private:
float _x, _y;
};
因为virtual function的导入,会发生以下改变:
1.我们所定义的constructor被附加了一些代码,一边捡vptr初始化。这些代码必须被附加在任何base class constructor的调用之后,但必须在任何由使用者供应的代码之前。如,下面就是可能的附加结果:
Point*Point::Point(Point* this, float x, float y)
:_x(x), _y(y){
this->_vptr_Point = _vtbl_Point; //设定vptr
this->_x = x;
this->_y = y;
return this;
}
2.合成一个copy constructor和copy assignment operator,而且其操作不再是trival(但implicit destructor仍然是trival )。
//copyconstructor的内部合成
inlinePoint*
Point::Point(Point* this, const Point &rhs ){
this->_vptr_Point = _vtbl_Point;
//将rhs坐标中的连续位拷贝到this对象
return this;
}
L1的global初始化操作,L6的heap初始化操作以及L9的heap删除操作,都还是和稍早的Point版本相同,然而L7的memberwise赋值操作:
*heap =local;
很有可能触发copy assignment operator的合成,以及调用操作的一个inline expansion:以this取代heap,以rhs取代local。
在L10中,由于copy constructor的出现,foobar()很有可能被转化为下面这样:
//C++伪码
Pointfoobar(Point &_result){
Pointlocal;
local.Point::Point(0.0,0.0);
//heap的部分与前面相同…
_result.Point::Point(local); //copy constructor的应用
local.Point::~Point();
return;
}
如果支持named return value(NRV)优化,这个函数会进一步被转化为:
//C++伪码:foobar()的转化
Pointfoobar(Point& _result){
_result.Point::Point(0.0,0.0);
return;
}
一般而言,如果你的设计中,有许多函数都需要以传值方式传回一个local class object,例如像这样形式的一个算术运算:
Toperator+ (const T&, const T&){
T result;
//真正的工作在此
Return result;
}
那么提供一个copy constructor就比较合理。他的出现会促发NVR优化,经过NVR优化后的代码不会调用copy constructor。
5.2继承体系下的对象构造
constructor的调用可能内含大量的隐藏码,因为编译器会扩充每一个constructor,扩充程度视class T的继承体系而定。一般而言编译器所做的扩充操作大约如下:
1. 记录在member initialization list中的data member初始化操作会被放进constructor的函数本体,并以member的声明顺序为顺序。
2. 如果有一个member并没有出现在member initialization list之中,但它有一个default constructor,那么该default constructor必须被调用。
3. 在那之前,如果class object有virtual table pointer(s),它们必须被设定初值,指向适当的virtual tables。
4. 在那之前,所有上一层的base class constructor必须被调用,以base class的声明顺序为顺序(与member initialization list中的顺序没关联):
a) 如果base class被列于member initialization list中,那么任何显式指定的参数都应该传递进去。
b) 如果base class没有被列于member initialization list中,而它有default constructor(或default memberwise copy constructor),那么就调用。
c) 如果base class是多重继承下的第二或后继的base class,那么this指针必须有所调整。
5. 在那之前,所有的virtual base class constructor必须被调用,从左到右,从最深到最浅。(不论virtual base class constructor是否在上一次都必须被先调用)
a) 如果class被列于member initialization list中,那么如果有任何显式指定的参数,都应该传递过去。若没有列于list中,而class有一个default constructor,亦应该调用。
b) 此外,class中的每一个virtual base class subobject的偏移位置必须在执行期可被存取。
c) 如果class object是最底层(most-derived)的class,其constructors可能被调用;某些用以支持这一行为的机制必须被放进来.
在这一节,从“C++语言对class所保证的语意”这个角度,探讨constructors扩充的必要性。我们仍然以Point为例子,并为它增加一个copy constructor、一个copy operator、一个virtual destructor,如下所示:
class Point{
public:
Point(float x = 0.0, floaty = 0.0);
Point(const Point&);
Point& operator=(constPoint&);
virtual ~Point();
virtual float z(){return 0.0;}
//…
protected:
float _x, _y;
};
如果有class Line,由_begin和_end两个点构成:
class Line{
public:
Line(float x1 = 0.0, floaty1 = 0.0, float x2 = 0.0, float y2 = 0.0);
Line(const Point&, constPoint&);
draw();
private:
Point _begin, _end;
};
每一个explicit constructor都会扩充以调用其两个member class objects的constructor。如果我们定义constructor如下:
Line::Line(const Point&begin, const Point& end)
:_end(end),_begin(begin){}
它会被编译器扩充并转换为:
//C++伪码
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(本例为virtual),所以Line class的implicit copy constructor、copy operator和destructor都将有具体效用(nontrvial)。
当程序员写下:
Line a;
时,implicit Line destructor会被合成出来(如果Line派生自Point,那么合成出来的destructor将会是virtual。然而由于Line只是内含Pointobject而非继承自Point,所以被合成出来的destructor只是nonvirtual而已,但不是virtual)。其中,它的member class objects的destructor会被调用(以构造的相反顺序):
inline void Line::~Line(Line* this){
this->_end.Point::~Point();
this->_begin().Point::~Point();
}
虽然Point destructor是virtual,但其调用操作(在containing class destructor之中)会被静态地决议出来。
类似的道理,当一个程序员写下:
Line b =a;
时,implicit Line copy constructor会被合成出来,成为一个inline public member。
最后,当程序员写下:
a = b;
时,implicit copy assignment operator会被合成出来,成为一个inline public member。
虚拟继承:
考虑下面这个虚拟继承
class Point3d:public virtual Point{
public:
Point3d(float x=0.0,floaty=0.0,float z=3)
:Point(x,y),_z(z){}
Point3d(const Point3d& rhs)
:Point(rhs),_z(rhs._z){}
~Point3d();
Point3d& operator=(constPoint3d& );
virtual float z( ){return _z;}
protected:
float _z;
};
传统的constructor扩充现象并没有用,这是因为virtual base class的共享性之故。
试着想象一下三种类的派生情况:
ClassVertex : virtual public Point{…};
ClassVertex3d : public Point3d, public Vertex{…};
ClasspVertex : public Vertex 3d {…};
Vertex的constructor必须也调用Point的constructor。然而,当Point3d和Vertex同为Vertex的subobjects时,它们对Point constructor的调用操作一定不可以发生;取而代之的是,作为一个最底层的class, Vertex3d有责任将Point初始化。而更往后的继承,则由PVertex来负责完成被共享的Point subobject的构造。所以,constructor的函数本体因而必须条件式的测试传进来的参数,然后决定调用或不调用相关的virtual base class constructor。下面就是Point3d的constructor扩充内容:
//C++伪码:在virtual base class情况下的constructor扩充内容
Point3d*Point3d:(Point3d* this, bool _most_derived,
float x, float y, float z){
if(_most_derived != false)
this->Point::Point(x, y);
this->_vptr_Point3d = _vtbl_Point3d;
this->_vptr_Point3d_Point =_vtbl_Point3d_Point;
this->_z = rhs.z;
return this;
}
在更深层的继承情况下,例如Vertex3d,调用Point3d和Vertex的constructor时,总是会把它们的_most_derived参数设为false,于是就压制了两个constructors中对Point constructor的调用操作。
//C++伪码:
Vertex3d*Vertex3d::Vertex3d(Vertex3d* this, bool _most_derived,
float x, float y, float z){
if(_most_derived != false)
this->Point::Point(x, y);
this->Point3d::Point3d(false, x, y,z);
this->Vertex::Vertex(false, x, y);
//设定vptrs
//安插user code
return this;
}
当我们定义
Vertex3dcv;
时,Vertex constructor正确的调用Point constructor。Point3d和Vertex的constructor会做每一件该做的事情---对Point的调用操作除外。所以,“virtual base class constructor”的被调用有着明确的定义:只有当一个完整的class object被定义出来时,他才会被调用;如果object只是某个完整object的subobject,它就不会被调用。
某些新的编译器会把每一个constructor分裂为二,一个针对完整的object,另一个针对subobject。完整object版本无条件调用virtual base constructor,设定所有的vptr等。Subobject版本则不调用virtual base constructor,也不可能设定vptr。
Vptr初始化语意学
当我们定义一个PVertex object时,constructors的调用顺序是:
Point(x,y);
Point3d(x,y, z);
Vertex(x,y, z);
Vertex3d(x,y, z);
PVertex(x,y, z);
假设这个继承类体系中的每一个class都定义了一个virtual function size(),此函数负责传回class的大小。如果我们写:
PVertexpv;
Point3dp3d;
Point* pt= &pv;
那么这个调用操作:
pt->size();
将传回PVertex的大小,而:
pt =&p3d;
pt->size();
将传回Point3d的大小。
更进一步,我们假设这个继承体系中的每一个constructors内含一个调用操作,像这样:
Point3d::Point3d(floatx, float y, float z)
:_x(x), _y(y), _z(z){
if(spyOn){
cerr << “size ofPoint3d” << size() << endl;
}
}
当我们在定义PVertex的时候,在调用Point3d constructor时,会间接调用的是Point3d::size(),而不是PVertex::size()。这是因为当base class constructor执行时,derived实例还没有被构造起来。
总而言之,当派生类在构造(或析构)时,如果调用基类的构造函数(或析构函数),而基类的构造函数会调用虚函数,即便这个虚函数也在派生类中重新定义,此时调用的虚函数仍然是基类中的虚函数。
那么如何才能做到这一点?
根本的解决之道是:在执行一个constructor时,必须限制一组virtual function候选名单,也就是控制vptr的初始化和设定操作即可。
vptr初始化操作的规则:必须在base class constructor调用操作之后,但是在程序员供应的代码或是member initialization list中所列的members初始化操作之前!
所以constructor的执行算法通常如下:
1. 在derived class constructor中,所有的virtual base classes 以及上一层base class 的constructors会被调用
2. 上述完成后,对象的vptrs被初始化,指向相关的virtual tables.
3. 如果有member initialization list的话,将在constructor体内扩展开来。这必须在vptr被设定之后才做,以免有一个virtual member function在member initialization list中被调用。
4. 最后,执行程序员所提供的代码
例如:一直由程序员所定义的PVertex constructor:
PVertex::PVertex(floatx, float y, float z)
:_next(0),Vertex(x, y, z),Point(x,y){
if(spyOn)
cerr << “size ofPVertex” << size() << endl;
}
它很有可能被扩展为:
PVertex* PVertex::PVertex(PVertex* this, bool _most_derived,
float x, float y, float z){
if(_most_derived != false) //条件调用virtual base constructor
this->Point::Point(x, y);
this->Vertex3d::Vertex3d(x, y, z); //无条件调用上一层base
this->_vptr_PVertex =_vtbl_PVertex;//将相关的vptr初始化
this->_vptr_Point_PVertex =_verbl_Point_PVertex; //将相关的vptr初始化
if(spyOn) //程序员所写代码
cerr << “size ofPVertex” <<
<<(*this->_vptr_PVertex[3].faddr)(this) //经由虚拟机制调用
<< endl;
return this;
}
在class的constructor的member initialization list中调用该class的一个虚拟函数,从vptr的角度来看是安全的,这是因为vptr保证能够在member initialization list被扩展之前,由编译器正确设定好。但是在语意上可能是不安全的,因为函数本身可能还得依赖未被设立初值的members,所以这种做法不推荐。
5.3 对象复制语意学
当我们设计一个class,并以一个class object指定给另一个class object时,我们有三种选择:
1. 什么都不做,因此得以实施默认行为
2. 提供一个explicit copy assignment operator
3. 显式地拒绝一个class object指定给另一个class object
如果选择第三点,那么只要将copy assignment operator声明为private,并且不提供其定义即可。把它设定为private,我们就不在允许于任何地点(除了member function和该class的friend之中)做赋值操作。不提供其函数定义,则一旦某个member function或friend企图影响一份拷贝,程序在链接是失败。
对于第二点,只有在默认行为所导致的语意不安全或不正确时,我们才需要设计一个copy assignment operator。
一个class对于默认的copy assignment operator,在以下情况,不会表现出bitwise copy语意:
1. 当class内含一个member object,而其class有一个copy assignment operator时。
2. 当一个class的base class有一个copy assignment operator时。
3. 当一个class声明了任何virtual function(我们不一定要拷贝右端class object的vptr,因为它可能是一个derived class object)时。
4. 当class继承自一个virtual base class(不论此base class有没有copy operator)时。
在虚拟继承情况下,copy assignment opertator会遇到一个不可避免的问题, virtual base class subobject的复制行为会发生多次,与前面说到的在虚拟继承 情况下虚基类被构造多次是一个意思,不同的是在这里不能抑制非most-derived class 对virtual base class 的赋值行为。
5.4析构语意学
如果class没有定义destructor,那么只有在class内含的member object(抑或class自己的base class)拥有destructor的情况下,编译器才会自动合成一个出来。否则,destructor被视为不需要,也就不需要被合成。
如果一个派生类派生于两个基类,这两个基类一个有destructor(设计者定义或合成),另一个没有destructor,如果这个派生类没有显式定义destructor,那么编译器会为这个派生类合成一个destructor,它的唯一作用是调用那个有destructor的基类的destructor。
像constructor一样,目前对于destructor的一种最佳实现策略就是维护两份destructor实例:
1. 一个complete object实例,总是设定还vptrs,并调用virtual base class destructors
2. 一个base class subobject实例;除非在destructor函数调用一个virtual function,否则它绝不会调用base class destructor并设定vptr.
一个object的声明结束于destructor开始执行时,由于每一个base class destructor都轮番被调用,所以derived object实际上变成了一个完整的object。例如一个PVertex对象归还内存空间之前,会依次变成一个Vertex3d对象,一个Vertex对象,一个Point3d对象,最后变成一个Point对象。当我们的destructor中调用member function时,对象的蜕变会因为vptr的重新设定而受到影响。在程序施行destructors的真正语意将在第6章中详述。
一个由程序员定义的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,那么它们会以原来的构造顺序的相反顺序被调用。