5.1 纯虚拟函数的存在
我们可以定义和调用一个纯虚函数,不过只能被静态调用,不能由虚拟机制调用:
inline void Abstract_base::interface() const
{}
inline void Concrete_derived::interface() const
{
Abstract_base::interface();
}
PS:类中的pure virtual destructor必须要定义,原因是每一个派生类的析构函数会被编译器加以扩展,以静态调用方式调用其”每一个虚基类“以及上一层基类的析构函数。如果缺少任何一个基类的析构函数,会链接失败。建议还是不要把虚析构函数声明为pure。
5.2 虚拟规格的存在
通常不建议把所有的成员函数都声明为虚函数,就算编译器会进行优化而把非必要的虚函数声明去除。
5.3 虚拟规格中const存在
建议不在虚函数中使用const,否则带来的麻烦有基类声明为const,可能派生类中需要修改某些成员变量的值等。
5.4 重新考虑class的声明
5.5 ”无继承“情况下的对象构造
typedef struct
{
float x,y,z;
}Point;
这种声明被称作Plain OL‘ Data声明形式,编译器在遇到这种声明时会为其贴上这个标签。
5.5.1 抽象数据类型
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;
}
void mumble()
{
Point local1 = {1.0,1.0,1.0};
Point local2;
//inline expansion
local2._x = 1.0;
local2._y = 1.0;
local2._z = 1.0;
}
local1操作会快于local2,因为explicit initialization list更快。
但是explicit initialization list也有一定的缺点:
1.只有当class members都是public时,此法才奏效
2.只能指定常量,因为它们在编译期就可以被评估求值
3.初始化行为失败的可能性会高一些
5.5.2 为继承做准备
class Point
{
public:
Point(float x = 0.0, float y = 0.0)
:_x(x),_y(y){}
virtual float z();
private:
float _x, _y;
}
当引入vptr后class Point对象的新产生的内容:
1.构造函数增加了一些内容,用来初始化vptr。这些内容附加在基类构造函数调用之后,explicit user code之前。具体的转化过程如下:
Point* Point::Point(Point *this,float x, float y)
:_x(x),_y(y)
{
//设定object的vptr
this->__vptr_Point = __vtbl__Point;
this->_x = x;
this->_y = y;
return this;
}
2.合成一个复制构造函数,赋值操作符函数。
inline Point* Point::Point(Point *this,const Point &rhs)
{
/设定object的vptr
this->__vptr_Point = __vtbl__Point;
//将rhs坐标连续位拷贝到this对象
return this;
}
5.6 继承体系下的对象构造
Object T;
当我们定义了上面的变量时,当调用构造函数时,编译器内部会进行大量的扩充操作,主要有:
1.记录在成员初始化列表中的成员变量初始化操作会放进构造函数本身
2.如果有一个成员没有出现在初始化列表中,但它有自己的构造函数,那么它的构造函数也要被调用。
3.在那之前,如果类对象存在虚函数机制,那么vptr必须被设定初值
4.在那之前,所有上一层的基类构造函数要被调用,以基类的声明顺序为顺序。
4.1 如果基类被列于初始化列表中,那么任何明确指定的参数都应传递进去
4.2 如果基类没有出现在初始化列表中,但它自身有构造函数,那么要被调用
4.3 如果基类是多重继承下的第二或后继的基类,那么this指针需要调整
5.在那之前,所有的虚基类构造函数要被调用,从左到右,从深到浅
5.1 如果该类被列于初始化列表中,那么任何明确指定的参数都应传递进去,如果没有,但它自身有构造函数,那么要被调用
5.2 类中的每一个virtual base class subobject的偏移量必须在执行期可被存取
5.3 如果类对象是最底层的class,其构造函数可能被调用,某些用以支持这个行为的机制必须被放进来
下面我们举个例子来说明下继承体系下的类对象构造过程:
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&):_end(end), _begin(begin);
draw();
}
Line的构造函数会被扩充来调用两个member class object的构造函数,在编译器内部会转化为:
Line* Line::Line(Line *this,const Point& begin, const Point& end)
{
this->_begin.Point::Point(begin);
this->_end.Point::Point(end);
return this;
}
析构函数同理,只是与构造的顺序相反。
5.6.1 虚拟继承
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 Vertex3d {};
在这个例子中,我们分别砍下Point3d和Vertex3d在虚基类继承情况下的构造函数调用情况:
Point3d* 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* Vertex3d::Vertex3d(Vertex3d *this, bool __most_derived, float x, float y, float z)
{
if(__most_derived != false)
this->Point::Point(x,y);
//调用上一层base classes
//设定__most_derived为false
this->Point3d::Point3d(false,x,y,z);
this->Vertex::Vertex(false,x,y);
//设定vptrs
//安插user code
return this;
}
5.6.2 vptr初始化语义学
vptr的初始化时机:在基类构造函数调用之后,在程序员供应的代码和初始化成员列表中成员初始化操作之前。例如:
PVertex::PVertex(float x,float y,float z)
:_next(0),Vertex3d(x,y,z),Point(x,y)
{
....//程序员所写的代码
}
//编译器内部扩展
PVertex* PVertex::PVertex(PVertex* this,bool __most_derived,float x,float y,float z)
{
//条件式的调用虚基类构造函数
if(__most_derived != false)
this->Point::Point(x,y);
//无条件的调用上一层base
this->Vertex3d::Vertex3d(x,y,z);
//将相关的vptr初始化
this->__vptr_PVertex = __vtbl__PVertex;
this->__vptr_Point__PVertex = __vtbl_Point_PVertex;
//程序员所写代码
...
return this;
}
PS:在class的构造函数内的初始化成员列表中调用该class的一个虚函数,这样是安全的吗?
理论上来说这是安全的,因为vptr总是在初始化成员列表操作之前就已完成,但是在语义上可能是不安全的,因为函数本身可能还依赖未被设立初值的成员。
5.7 对象复制语义学
如果复制构造函数、赋值操作函数表现为bitwise copy,那么编译器就不会生成一个默认的函数实体。
在下面几种情况下,不会表现出bitwise copy语义:
1.当类中带有一个member class object,而该class有它自己的赋值操作符
2.当class的基类有赋值操作符
3.class继承自一个虚基类(不论它有没有赋值操作符)
4.class有声明虚函数
PS:赋值操作符缺乏一个member assignment list,因此不能够这样写:
inline Point3d&
Point3d::operator=(const Point3d &p3d)
:Point(p3d),z(p3d._z){}
5.8 对象的功能
5.9 解构语义学
如果class内含的member class object(或其base class)含有析构函数时,编译器才会合成出一个析构函数,否则其他情况下都没有必要合成它。
那么为什么当一个class中的基类或类成员含有析构函数时要合成呢?原因是我们希望基类的析构函数被调用来完成必要的操作,如果想被调用,那么该class的析构函数里就要调用基类的,因此它的析构函数也是必要的。
在编译器中析构函数的扩展过程:
1.析构函数本身现在执行,那么vptr会在程序员所写代码之前被重设
2.如果class有member class object且后者有析构函数,那么内部的class会以声明顺序相反的顺序调用析构函数
3.如果object内带一个vptr,那么首先重设相关的虚函数表
4.如果有任何直接的non-virtual base class有析构函数,那么会以声明顺序相反的顺序调用析构函数
5.如果有任何的虚基类有析构函数,且当前讨论的这个class是最尾端的class,那么会以声明顺序相反的顺序调用析构函数