构造、析构、拷贝语意学
对于abstract base class(抽象基类),class中的data member应该被初始化,并且只在constructor中或者在其他member functions中指定初值。
c++不支持“临时性定义”,因为class构造函数的隐式应用原因。global对象在c++中被视为完全定义(它会阻止第二个或更多的定义)。c和c++差别:BSS data segment在c++中相对的不重要。c++的所有全局对象都以被“初始化过的数据”来对待。
纯虚函数的存在
纯虚函数的意义在于只定义一个可继承的接口,并没有实现(虚函数意义在于定义一个可继承的接口并且有一份缺省实现,普通的成员函数意义在于定义一个可继承的接口并且有一份强制实现)
纯虚函数不能经由虚拟机制调用,但是可以被静态地调用(invoked statically, AbsractClassName::pureFunction() )
对于pure virtual destructor,一定要定义它,因为每一个derived class destructor会被编译器加以扩张,以静态调用的方式调用其”每一个virtual base class”以及”上一层base class”的destructor,因此只要缺乏何一个base class destructor的定义,就会导致链接失败。
编译器不会合成一个pure virtual destructor的函数定义。
一个较好的替代方案就是:不要把virtual destructor声明成pure
虚拟规格的存在
一个函数几乎不会被后继的derived class改写,而且是inline函数,将其改写成virtual function是一个糟糕的选择。
一般而言,把所有的成员都声明为虚函数,然后再经过编译器优化把非必要的virtual invocation去除,并不是好的设计理念。
虚拟规格中const的存在
不使用const
重新考虑class的声明
原声明
class Abstract_base { public: virtual ~Abstract_base() = 0; virtual void interface() const = 0; virtual const char* mumble() const { return _mumble; } private: char* _mumble; }
改写后
class Abstract_base { public: virtual ~Abstract_base();//不再是纯虚函数 virtual void interface() = 0;//不再是const const char* //不再是虚函数 mumble() const { return _mumble; } private: Abstact_base(char *pc = 0); //新增一个带有唯一参数的constructor char* _mumble; }
一、“无继承”情况下的对象构造
Point global; Point foobar() { Point local; Point *heap = new Point; *heap = local; delete heap; return local; }
三种对象产生方式:global内存配置,local内存配置和heap内存配置。
Point定义如下:
typedef struct { float x,y,z; }Point;
POD类型:plain Old Data,c风格的struct结构体定义的数据结构
以C++来编译这段代码的话,编译器会为Point声明:trival默认构造函数,trival析构函数,trival拷贝构造函数,trival拷贝赋值运算符。
第一行代码Point global;,会在程序起始时调用Point的constructor,然后在系统产生的exit()之前调用destructor。而事实上,trivial members要不是没被定义,就是没被调用。
在C语言中,global被视为一个“临时性对象”,没有显式初始化,被存放在data segment中的BSS区段(Block Start By Symbol,放置没有初始化的全局变量),但是在C++中,全局对象都是被以“初始过的数据”来对待,因此置于data segment。
第三行代码Point local;这个局部变量没有经过初始化,可能成为一个潜在的问题。
第四行代码Point *heap = new Point;声明一个堆上的对象,其中new运算符会被转化为:
Point *heap = __new(sizeof(Point));
此时并没有default constructor施行于*heap object。
第五行代码heap = local;由于local没有初始化,因此会产生编译警告”local is used before being initaalized”。
接着delete heap;会被转化为__delete(heap);这样会触发heap的trival destructor。
最后函数已传值的方式将local当作返回值传回,这样会触发trival copy constructor,不过由于该对象是个POD类型,所以return操作只是一个简单的bitwise copy。
抽象数据类型
将Point不在声明为POD,提供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) { } private: float _x, _y, _z; };
对于global实例:
Point global;
现在有了默认构造函数作用在其身上,由于global定义在全局范畴,其初始化操作会延迟到程序启动(startup)时才开始。(统一构造一个_main()函数,该函数内调用所有global对象的默认构造函数)。
void mumble { Point local1 = {1.0, 1.0, 1.0}; Point local2; local2._x = 1.0; //_x必须是public,且必须指定常量 local2._y = 1.0; local2._z = 1.0; }
local1的初始化比local2的初始化有效率,因为当函数activation record被放进程序堆栈时,上述initialization list中的常量就可以被放进local1内存中。
但是explicit initialization list的三个缺点
- 只有当class members都是public时,才奏效。
- 只能指定常量,因为它们在编译时期就可以被评估求值。
- 由于编译器没有自动施行,所以初始化失败可能性变高。
Point *heap = new Point; //被附加default Point constructor后 Point *heap = __new(sizeof(Point)); if(heap != 0) heap->Point:Point():
delete heap;并不会导致destructor被调用,因为我们并没有显示提供一个destructor函数实例。
为继承做准备
class Point { public: Point(float x = 0.0, float y = 0.0) : _x(x), _y(y) { } virtual float z(); private: float _x, _y; };
所定义的constructor被附加了一些代码,增加了虚函数之后,用来初始化vptr
Point::Point(Point *this, float x, float y) : _x(x), _y(y) { this->__vptr_Point = __vbtl_Point;//设定虚表 this->_x = x; //扩展初值列表 this->_y = y; return this;//返回this }
合成一个copy constructor和一个copy assignment operator,其操作是nontrival,但是implicit destructor仍然是trival。如果以一个基类对象被初始化或以一个派生类对象赋值,且是以位运算为基础,那么vptr的设定有可能是非法的。
inline Point* Point::Point(Point *this, const Point& rhs) { this->__vptr_Point = __vbtl_Point; //将rhs坐标的连续位拷贝到this对象 return this; }
如果在程序设计中许多函数都需要提供以传值方式传回一个local class object,那么提供一个copy constructor就比较合理,因为这会产生named return value(NRV)优化。
二、继承体系下的对象构造
当我们定义一个object如:T object;
如果T有一个constructor(不管是trival还是nontrival),它都会被调用。
- 记录在member initialization list中的data members初始化操作放在constructor的函数体内,并且以members的声明顺序为顺序
- 如果一个member没有出现在members initialization list中,但是它有一个默认构造函数,那么这个默认构造函数必须被调用
- 在那之前,如果class object有virtual table pointer(s),它们必须被设定初始值,指向适当的virtual table
- 在那之前,所有上一层的base class constructor必须被调用,以base class 的声明顺序为顺序(与members initialization list中的顺序没关联)
- 如果base class被列于members initialization list中,那么任何显示指定的参数都应该传递过去
- 如果base class没有被列于member initialization list中,而他有default constructor(或default memberwise copy constructor),那么就调用之
- 如果base class是多重继承下的第二或者后继的base class,那么this指针应该有所调整
- 在那之前,所有virtual base class constructor必须被调用,从左往右,从最深到最浅
- 如果class位member initialization list,那么有任何显示指定的参数都应该传递过去;若没有位于list,而class含有一个默认构造函数,也应该调用。
- class中的每一个virtual base class subobject的偏移量(offset)必须在执行期可存取
- 如果class object是最底层(most-derived)的class,其constructors可能被调用;某些用以支持这一行为的机制必须被放进来
//以point为例 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; } private: float _x, _y; }; //以Point为基础的类Lines的扩张过程 class Lines { Point _begin, _end; public: Lines(float = 0.0, float = 0.0, float = 0.0, float = 0.0); Lines(const Point&, const Point&); draw(); }; //来看第二个构造函数的定义与内部转化 Lines::Lines(const Point& begin, const Point& end) : _begin(begin), _end(end){} //定义 //下面是内部转化,将初值列中的成员构造放入函数体,调用这些成员的构造函数 Lines* Lines::Lines(Lines *this, const Point& begin, const Point& end) { this->_begin.Point::Point(begin); this->_end.Point::Point(end); return this; } //我们写下 Lines a; //合成的destructor在内部可能如下,如果line派生于point那么合成的destructor将会是virtual //然而由于line是内含point object而不是继承,那么合成出来的destructor是nonvirtual;如果point destructor是inline,那么每一个调用点会被扩张 inline void Lines::~Lines(Lines *this) // { this->_begin.Point::~Point(); this->_end.Point::~Point(); } Lines b = a; //这个时候调用合成的拷贝构造函数,合成的拷贝构造在内部可能如下 inline Lines& Lines::Lines(const Lines& rhs) { if(*this = rsh) return *this; //证同测试,或者可以采用copy and swap,具体见effective C++ //还要注意深拷贝和浅拷贝 this->_begin.Point::Point(rhs._begin); this->_end.Point::Point(rhs._end); return *this; }
应该在copy operator中检查自我指派是否失败。
在copy operator中面对自我拷贝设计一个自我过滤操作
虚拟继承
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; } private: float _z; }
传统的constructor扩张并没有用,因为virtual base class的共享性的原因。试想以下继承关系
class Vertex : virtual public Point { }; class Vertex3d : public Point3d, public Vertex { }; class PVertex : public Vertex3d { };
当point3d和vertex同为vertex3d的subobject时,对point constructor的调用操作一定不可以发生,作为一个最底层的class pvertex,有责任将point初始化
constructor的函数本体因而必须条件式地侧测试传进来的参数,然后决定调用或不调用相关的virtual base class constructor。
Point3d的constructor的扩充如下
//c++伪码 Point3d* Point3d::Point3d(Point3d* this, bool __most_derived, float x = 0.0, float y = 0.0, float z = 0.0) { if(__most_derived != false) this->Point::Point(); //虚拟继承下两个虚表的设定 this->__vptr_Point3d = __vtbl_Point3d; this->__vptr_Point3d__Point = __vtbl_Point3d__Point; this->z = rhs.z; return this; }
而并非如下传统的扩张
//c++伪码 Point3d* Point3d::Point3d(Point3d* this, float x = 0.0, float y = 0.0, float z = 0.0) { this->Point::Point(); this->__vptr_Point3d = __vtbl_Point3d; this->__vptr_Point3d__Point = __vtbl_Point3d__Point; this->z = rhs.z; return this; }
两种扩张的不同之处在于参数__most_derived,在更加深层次的继承情况下,例如Vextex3d,调用Point3d和Vertex的constructor时,总会将该参数设置为false,于是就压制了两个constructors对Point constructor的调用操作。 例如:
//c++伪码 Vextex3d* Vextex3d::Vextex3d(Vextex3d* this, bool __most_derived, float x = 0.0, float y = 0.0, float z = 0.0) { if(__most_derived != false) this->Point::Point(); //设定__most_derived为false this->Point3d::Point3d(false, x, y, z); this->Vertex::Vertex(false, x, y); //设定vptrs return this; }
virtual base class constructor被调用有着明确的定义,只有当一个完整的class object被定义出来(例如origin)时,它才会被调用,如果object只是某个完整的object的subobject,他就不会被调用
还可以把constructor一分为2,一个针对一个完整的object,一个针对subobject,完整的object无条件调用virtual base constructor,设定vptrs,subobject不调用virtual base constructor,也可能不设定vptrs
vptrs初始化语意学
定义一个PVertex object时,constructor的调用顺序是
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 constructor
PVertex::Pvertex(float x, float y, float z) : _next(0), Vertex3d(x, y, z), Point(x, y) { if(spyOn) cerr << "Within Pvertex::PVertex()" << "size: " << size() << endl; }
vptr的初始化操作:在base class constructor调用操作之后,但是在程序员供应的代码或”member initialization list中所有列的members初始化操作”之前。
constructor的执行算法通常如下:
- 在derived class constructor中,所有的virtual base class以及上一层的base class的constructor会被调用。
- 上述完成之后,对象的vptrs被初始化,指向相关的虚表。
- 如果有member initialization list的话,将在constructor的函数体内展开,这必须是在vptr设定之后才做的,以免有一个virtual member function被调用。
- 最后执行程序员的代码。
之前的PVertex constructor可能被扩张成
PVertex* PVertex::Pvertex(PVertex*this, bool __most_derived, float x, float y, float z) { //有条件调用virtual base class constructor if(__most_derived != false) this->Point::Point(); //无条件调用上一层base class constructor this->Vertex3d::Vertex3d(x, y, z); //设定vptr this->__vptr_PVertex = __vtbl_PVertex; this->__vptr_Point3d__PVertex = __vtbl_Point3d__PVertex; //执行程序员的代码 if(spyOn) cerr << "Within Pvertex::PVertex()" << "size: " //虚拟机制调用 << (*this->__vptr_PVertex[3].faddr)(this) << endl; return this; }
下面是vptr必须被设定的两种情况:
- 当一个完整的对象被析构起来时,如果我们声明一个Point对象,则Point constructor必须设定其vptr。
- 当一个subobject constructor调用了一个virtual function时(不论是直接调用还是间接调用)。
在class的constructor的member initialization list中调用该class的一个虚函数,这是安全的。因为vptr的设定总保证在member initialization list扩展之前就被设定好;但是这在语义上时不安全的,因为函数本身可能依赖未被设定初值的成员。
当需要为base class constructor提供参数时(英文原文为:What about when providing an argument for a base class constructor?),在class的constructor的成员初值列中调用该class的一个虚函数这就是不安全的,此时vptr要么尚未被设定好,要么指向错误的class。该函数存取任何class's data members一定还没有初始化。
三、对象复制语意学
当我们指定一个class object给另一个class object时,通常有三种选择:
- 什么都不做,实施默认的行为。
- 提供一个explicit copy assignment operator。
- 显式地拒绝指定一个class object给另一个class object,声明为private(并且此时不同函数的定义,一旦某个member function或friend企图影响一份拷贝,程序链接时就会失败)。
利用Point class来帮助讨论:
class Point { public: Point(float x = 0.0, float y = 0.0); private: float _x, _y; };
如果不对class point提供copy assignment operator,光是依赖默认的memberwise copy,编译器不会产生出一个实例,因为此class已经有了bitwise copy语意,所以implicit copy assignment operator被视为毫无用处,根本不会合成出来。
一个class对于默认的copy assignment operator,在以下情况下,不会表现出bitwise copy语意
- 当class内含一个class object,而其class有个copy assignment operator时
- 当一个class的base class有个copy assignment operator是
- 当一个class声明了任何virtual function(一定不要拷贝右端class object的vptr地址,因为他可能是一个derived class object)时
- 当一个class继承自一个virtual base class(不论base class有没有copy operator)时
如果没有为Point3d设定一个copy assignment operator,编译器就会为其合成一个:
inline Point3d& Point3d::operator=(point3d* this, const Point3d& p) { this->Point::operator=(p); //base class operator= _z = p._z; //memberwise copy return *this; }
copy assignment operator缺乏一个member assignment list,类似于成员初值列,比如
inline Point3d& Point3d::operator=(const Point3d& p) : Point(p), _z(p._z) { } //这是不支持的,只能写成上面合成的形式
缺少member assignment list,编译器一般就没有办法压抑上层base class的copy operator被调用。还是考虑之前的继承体系,类Vertex虚拟自Point,并且从Point3d和Vertex派生出Vertex3d。则copy operator如下:
inline Vertex& Vertex operator=(const Vertex& v) { this->Point::operator=(v); _next = v._next; return *this; } inline Vertex3d& Vertex operator=(const Vertex3d& v) { this->Point::operator=(v); this->Point3d::operator=(v); this->Vertex::operator=(v); return *this; }
编译器如何在Point3d和Vertex的copy assignment operator压制Point的copy assignment operator呢?constructor中的解决办法是添加一个额外的参数__most_derived。解决办法是:为copy assignment operator提供分发函数(split functions)以支持这个class称为most-derived class或者成为中间的base class。
尽可能不要允许一个virtual base class的拷贝操作,更进一步,不要在任何virtual base class中声明数据
五、析构语意学
析构函数也是根据编译器的需要才会合成出来,两种情况:
- class中有某个object拥有析构函数;
- 继承自某个base class,该base class含有析构函数。
定义了constructor后不一定要定义destructor,决定class是否需要destructor是程序层面的事。
与构造函数相比,即使拥有虚函数或者虚拟继承,不满足上述两个条件,编译器是不会合成析构函数的。在继承体系中,由我们定义的destructor的扩展方式和constructor类似,只是顺序相反,顺序如下:
- destructor的函数本体首先被执行
- 如果class拥有member class object,且该class含有destructor,那么它们会以声明顺序相反的顺序依次被调用。
- 如果object内含一个vptr,现在被重新被设定,指向适当的base class的virtual table
- 如果有任何直接的(上一层)nonvirtual base classes拥有destructor,那么它们会以声明顺序相反的顺序依次被调用。
- 如果有任何virtual base classes拥有destructor,而目前讨论这个class是最尾端(most-derived)的class,那么它们会以原来构造顺序相反的顺序依次被调用。
一个object的声明结束于destructor开始执行时,由于每个base class destructor都被调用,所以derived object实际上变成了一个完成的object。例如一个pvertex对象归还其内存空间之前,会依次变成一个vertex3d,vertex,point3d,最后成为一个point对象。当在destructor中调用member function时,对象的蜕变会因为vptr的重新设定(在每一个destructor中,程序员所提供的代码前)而受到影响。