构造、解构、拷贝语意学(Semantics of Constuction,Destruction,and Copy)
假设有以下的代码:
class Abstract_base
{
public:
virtual ~Abstract_base()=0;//pure virtual function
virtual void interface() const=0;
virtual const char*
mumble() const{return _mumble;}
protected:
char *_mumble;
};
因为该class被设计为一个抽象的base class(因为有pure virtual function,使得Abstract_base不能拥有实体),但这个类仍然需要一个明确的构造函数来初始化它的成员变量:_mumble。如果没有初始化操作,那么这个base class的derived class中,作为局部性对象的_mumble将不能决定它自己的初值。即使,我们想要Abastract_base的derived class来提供_mumbel的初值,那么我们必须提供一个带有唯一参数的protected constructor:
Abstract_base::
Abstract_base(char *mumble_value=0):_mumble(mumble_value)
{ }
一般来说,class的data member应该被初始化,并且只在constructor中或是在class的其他member functions 中指定初值。其他任何操作都将破坏封装性质。
纯虚函数的存在
在base class中,我们是可以为一个pure virtual functions 进行定义的,要不要定义全有class设计者自己决定。
唯一的例外就是pure virtual destructor,class设计者一定要定义它。因为每一个derived class destructor会被编译器加以扩展,以静态调用的调用方式调用其“每一个virtual base class”以及“上一层base class”的destructor。因此,只要缺乏任何一个base class destructor的定义,就会导致链接失败。
c++语言保证的一个前提就是:继承体系中的每一个class object 的destructor都会被调用。
一个比较好的方案就是,不要把virtual destructor声明为pure。
虚拟规格的存在
==如果一个函数不会对之后的derived class造成影响,那么这个函数就不应该设值为virtual ==。
一般而言,把所有的成员函数都声明为virtual function ,然后再靠编译器的优化操作把非必要的virtual function去除,并不是好的设计观念。
虚拟规格中const的存在
决定一个virtual function是否需要const,当我们真正面对一个abstract base class时,不容易做决定。因为这个决定意外着假设subclass实体可能被无穷次数地使用。不把函数声明为const,意味着该函数不能够获得一个const reference或const pointer。但声明一个函数为const时,之后可能会发现实际上其derived instance必须修改某一个data member,所以,简单点,不在用const就是。
重新考虑class的声明
class Abstarct_base
{
public:
virtual ~Abstract_base() {} //不再是pure virtual
virtual void interface() = 0; //不再是const
const char* mumble() const { return _mumble; }//不再是virtual
protected:
Abstract_base(char *pc = 0) :_mumble(pc) {}
char *_mumble;
}
5.1“无继承”情况下的对象构造
(1) Point global;
(2)
(3) Point foobar()
(4) {
(5) Point local;
(6) Point *heap=new Point();
(7) *heap=local;
(8) //..stuff....
(9) delete heap;
(10) return local;
(11) }
L1,L5,L6表现出不同的对象产生方式:global(全局)内存配置,local(局部)内存配置和heap(堆)内存配置。
一个对象(object)的生存周期,是该Object的一个执行属性。local object的生命从L5的定义开始,到L10未知。global object的生命和整个程序的生命相同。heap object的生命从它被new 运算符配置出来开始,直到被delete运算符摧毁为止。
c++Standard有一种Plain old Data的声明形式:
typedef struct
{
float x,y,z;
}Point;
当编译器遇到这种情况时,会为它贴上一个Plain Old Data卷标:然后他们会与在C中的表现一样。
再次强调的是,没有default constructor施行于new运算符所传回的Point object身上。L7对此object有一个赋值操作,如果local曾被适当初始化过,一切就没有问题。
(7) *heap=local;
因为object是一个Plain Old Data,所以赋值操作只会向C这样的纯粹位搬移操作。
同样delete也是同样的结果。
抽象数据类型
以下是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 ,copy operator
// or destructor defined...
//......
private:
float _x,_y,_z;
};
我们没有为Point定义一个copy constructor或copy operator,因为默认的位语意已经足够,同时也不需要提供一个destructor,因为程序默认的内存管理方法也已经足够。
为继承做准备
第三个Point声明,将为“继承性质”以及某些操作的动态决议做准备,当前我们限制对z成员进行存取操作:
class Point
{
public:
Point(float x=0.0,float y=0.0):_x(x),_y(y){}
// no destructor,copy constructor ,or
// copy operator defiend
virtual float z();
//....
protectd:
float _x,_y;
};
在这里并没有定义copy constructor、copy operator、destructor。这个类中的所有members都以数值来储存,因此在程序层面的默认语意之下,运行良好。
virtual functions的引入促使每一个Point object拥有一个virtual table pointer。这个指针提供给我们virtual接口的弹性。
除了每一个class object 多负担一个vptr之外,virtual functions的引入也引发编译器对于Point class产生膨胀作用:
- 我们所定义的constructor被附加了一些代码,以便将vptr初始化,这些代码必须被附加在任何base class constructors的调用之后,但必须在任何使用者编写的代码之前。
//c++ 伪代码 :内部膨胀
Point *
Point::Point(Point* this,float x,float y):_x(x),_y(y)
{
//设定object的virtual table pointer(vptr)
this->__vptr_Point=__vtbl__Point;
//扩展member initialization list
this->_x=x;
this->_y=y;
//传回this对象
return this;
}
- 合成一个copy constructor和一个 copy assignment operator,而且其操作不再是trivial。如果一个Point object 被初始化或以一个derived class object赋值。那么以位基础的操作(bitwise)可能给vptr带来非法设定。
//c++ 伪代码
// copy constructor 的内部合成
inline Point*
Point::Point(Point *this,const Point &rhs)
{
//设定object的virtual table pointer(vptr)
this->__vptr_Point=__vtbl__Point;
//将rhs 坐标中的连续位拷贝到this对象
//或是经由member assignment 提供一个member
return this;
}
编译器在优化状态下可能会把object的连续内容拷贝到另一个object身上,而不会精确地“以成员为基础(memberwise)” 的赋值操作。
如果我们设计的函数中有许多函数都是需要以传值方式(by value)传回一个local class object。那么提供一个copy constructor 就比较合理—即使default memberwise语意已经足够。它的出现可以出发NRV优化。NRV优化后将不需要copy constructor,因为运算结果已经将直接置于“将被传回的object”体内了。
5.2 继承体系下的对象构造
当我们定义object如下: T object;时,会发生什么事呢?
如果T有一个constructor(不论是user提供或是由编译器合成的),它都会被调用。那么constructor被调用时,会发生什么呢? Constructor内带有大量的隐藏码,因为编译器会扩充每一个constructor,扩充的程度视class T的继承体系而定。
一般而言编译器所做的扩充操作大约如下:
- 1、记录在member initialization list中的data member初始化操作会被放进constructor的函数本身,并以members的声明顺序为顺序。
- 2、如果有一个member并没有出现在member initialization list之中,但它有一个default constructor,那么该default constructor必须被调用。
- 3、在那之前,如果class object有virtual table pointers,它们必须被设定初值,指向适当的virtual tables.
- 4、在那之前,所有上一层的base class constructor 必须被调用,以base class的声明顺序为顺序(与member initialization list中的顺序没关联):
- a、如果base calss 被列于 member initialization list中,那么任何明确指定的参数都应该被传递过去。
- b、如果base class没有被列于member initialization list中,而它有default constructor(或default memberwise copy constructor),那么就调用它。
- c、如果base class 是多重继承下的第二或后继的base class,那么this指针必须有所调整。
- 5、在那之前,所有的virutal base class constructors必须被调用,从左到右,从最深到最浅。
- a、如果class被列于member initialization list中,那么如果有任何明确指定的参数,都应该传递过去。若没有列于List之中,而class由一个default constructor,也应该调用它。
- b、此外,class中的每一个virtual base class subobject的偏移量(offset)必须在执行可被存取。
- c、如果class object是最底层(most-derived)的class,其constructors可能被调用;某些用以支持这个行为的机制必须被放进来。
再次扩充Point:
class Point
{
public:
Point(float x=0.0,float y=0.0);
Point(const Point&); //copy constructor
Point& operator=(const Point&); //copy constructor
virtual ~Point(); //virtual destructor
virtual float z(){return 0.0;}
protected:
float _x,_y;
};
在声明一个Line class,它由_begin和_end两个点组成:
class Line
{
Point _begin,_end;
public:
Line(float =0.0,float =0.0,float =0.0,float =0.0);
Line(const Point& ,const Point&);
draw();
//........
};
每一个explicit constructor 都会被扩充以调用其他两个member class objects的constructors。如果我们定义constructors定义如下:
Line::Line(const Point &begin,const Point &end)
:_end(end),_begin(begin){}
它会被编译器扩充并转换为:
// c++ 伪代码:Line constructor的扩充
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 consturctor 、copy operator和destructor都将有实际功能(nontrivial):
虚拟继承
考虑下面这个虚拟继承,继承自Point
class Point3d :Public virtual Point
{
public:
Point3d(float x=0.0,float y=0.0,float z=0.0)
:Point(x,y),_z(z){}
Point3d(const Point3d& rhs)
:Point(rhs),_z(rhs._z){}
~Point3d();
Point3d& operator=(const Point3d& );
virtual float z() {return _z;}
protected:
float _z;
};
试想,如果有下面三种类派生情况:
class Vertex: virtual public Point{.........};
class Vertex3d: public Point3d,public Vertex{......};
class PVertex : public Vectext3d{........};
下面就是Point3d中正确地constructor扩充内容:
//c++伪代码
//在virtual base class情况下的constructor扩充内容
Point3d*
Point3d::Point3d{Point3d *this,bool __moset_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++伪代码
//在virtual base class情况下的constructor扩充内容
Vertex3d*
Vertex3d::Vertex3d(Vertex3d *this,bool __most_derived,float x,float y,float z)
{
if(__most_derived!=false)
this->Point::Point(x,y);
//调用上一层 base class
//设定 __most_derived 为false
this->Point3d::Point3d(false,x,y,z);
this->Vertex::Vertex(false,x,y);
//设定vptrs
//安插user code
return this;
}
这样的策略可以保持语意的正确无误,当我们定义
Point3d origin;
时,Point3d constructor可以正确地调用其Point virtual base class subobject。而当我们定义:
Vertex3d cv;
Vertex3d constructor正确地调用Point constructor。Point3d和Vertex的constructor会做每一件该做的事情—除了对Point的调用操作。
在这种状态下,“virtual base class constructors的被调用”有着明确的定义:只有当一个完整的class object被定义出来时,它才会被调用;如果object只是某个完整object的subobject(???),它就不会被调用。
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的大小。如果我们写:
PVertex pv;
Point3d p3d;
Point *pt=&pv;
那么调用操作:
pt->size();
将传回PVertex的大小。而
pt=&p3d;
pt->size();
将传回Point3d的大小。
c++中constructor的调用顺序是:由根源到末端,由内而外。当base class constructor执行时,derived 实体还没有被构造出来。在PVertex constructor执行完毕之前,PVertex并不是一个完整的对象;Point3d constructor执行之后,只有Point3d subobject构造完毕。
virtual table 是决定一个class的virtual functions名单的关键,通过vptr可以处理Virtual table。为了控制class中有所作用的函数,编译系统只要简单地控制住vptr的初始化和设定操作即可。
vptr初始化操作应该如何处理呢?在 base class constructors调用操作之后,但在其他程序或是==“member initialization list 中所列的members初始化操作”之前==。
如果每一个constructor都一直等待到其base class constructor执行完毕之后才设定其对象的vptr,那么每次它都能够调用正确地virtual function实体。
令每一个base class constructor设定其对象的vptr,使它指向相关的virtual table之后,构造中的对象就可以严格而正确地变成“构造过程中所幻化出来的每一个class”的对象。也就是说,一个PVertex对象会先形成一个Point对象,一个Point3d对象、一个Vertex对象、一个Vertex3d对象,然后才是一个PVertex对象。
constructor的执行算法通常如下:
- 1、在derived class constructor 中,“所有virtual base classes”及“上一层base class”的constructors会被调用。
- 2、上述完成后,对象的vptr(s)被初始化,指向相关的virtual table(s).
- 3、如果有member initialization list 的话,将在constructor体内扩展开来。这必须在vptr被设定之后才进行,以免有一个virtual member function被调用。
- 4、最后,执行我们写的其他代码。
下面有两种vptr必须被设定的情况:
- 1、当一个完整的对象被构造起来时,如果我们声明一个Point对象,Point construtor必须设定其Vptr.(????)
- 2、当一个subobject constructor调用一个virtual function(不论是直接调用或间接调用)。