目录
当我们定义一个object如下:
T object;
在这里,不明显的是constructor的调用真正伴随了什么?Constructor可能含有大量的隐藏代码,因为每个编译器会扩充每一个constructor,扩充的程度视class T的继承体系而定。
这一节中,我要从“C++语言对classes所保证的语意”这个角度,探讨constructor扩充的必要性,如下所示的代码。
class Point
{
public:
Point(float x = 0.0, float y = 0.0);
Point(const Point&); //译注:copy constructor
Point& operator=(const Point& ); //译注:copy assignment operator
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&);
draw();
//...
};
每一个explicit constructor都会被扩充以调用其两个member class objects的constructors。如果我们定义constructor如下:
Line::Line(const Point&begin, const Point &end)
:_end(end),_begin(begin)
{}
它会被编译器扩充并转换为:
Line *
Line::Line(Line *this,
const Point&begin, const Point &end)
{
this->_begin.Point::Point(begin);
this->_end.Point::Point(end);
return this;
}
当程序员写下:Line a;
Implicit Line destructor会被合成出来,其中它的member class objects的destructors会被调用(以其构造的相反顺序):
//C++伪代码:合成出来的Line destructor
inline void
Line::~Line(Line *this)
{
this->_end.Point::~Point();
this->_begin.Point::~Point();
}
类似的道理,当程序员写下:Line b = a;
时,implicit Line copy constructor operator 会被合成出来,成为一个inline public member。
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;
};
传统的“constructor扩充现象”并没有用,这是因为virtual base class的“共享性”之故:
//C++伪代码
//不合法的constructor扩充内容
Point3d*
Point3d::Point3d(Point3d *this,
float x,float y,float z)
{
this->Point::Point(x,y);
this->__vptr_Point3d = __vtbl_Point3d;
this->__vptr_Point3d_Point = __vtbl_Point3d_Point;
this->_z = rhs._z;
return this;
}
上面的扩充有啥问题么?
试想以下的三种类的派生情况:
class Vertex : virtual public Point {...};
class Vertex3d : public Point3d,public Vertex {...};
class PVertex : public Vertex3d {...};
Vertex的constructor必须也调用Point的constructor。然而,当Point3d和Vertex同为Vertex3d的subobjects时,它们对Point constructor的调用操作一定不可以发生;取而代之的是,作为一个最底层的class,Vertex3d有责任将Point3d初始化。而更加往下的继承,则由PVertex来负责完成“被共享之Point subobject”的构造。
那么以上的问题怎么解决呢?constructor的函数体因而必须条件式地测试传进入的参数,然后决定调用或者不调用相关的virtual base class constructors。下面是Point3d的constructor扩充内容。
//C++伪代码
//在virtual base class情况下的constructor扩充内容
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 = __vptr_Point3d;
this->__vptr_Point3d_Point = __vptr_Point3d_Point;
this->_z = rhs._z;
return this;
}
同样的道理,Vertex的constructor也类似同样的处理策略,代码如下。
//C++伪代码
//在virtual base class情况下的constructor扩充内容
Vertex *
Vertex::Vertex(Vertex *this,bool __most_derived,
float x,float y,float z)
{
if(__most_derived != false)
{
this->Point::Point(x,y);
}
this->__vptr_Vertex = __vptr_Vertex;
this->__vptr_Vertex_Point = __vptr_Vertex_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 classes
//设定__most_derived为false
this->Point3d::Point3d(false, x, y, z);
this->Vertex::Vertex(false, x, y, z);
//设定vptrs
//安插user code
return this;
}
2、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 functions size(),此函数负责传回class的大小。如果我们写:
PVertex pv;
Point3d p3d;
Point *pt = &pv;
那么这个调用操作:
pt->size();
将传回PVertex的大小,而:
pt = &p3d;
pt->size();
将传回Point3d的大小。
进一步,我们假设这个继承体系中每一个constructor内含一个调用操作,像这样子:
Point3d::Point3d(float x,float y, float z)
{
if(spyOn)
{
cerr << "Within Point3d::Point3d()"
<<"size: "<<size()<<endl;
}
}
在Point3d constructor中调用的size()函数,必须决议成Point3d::size()而不是PVertex::size()。
想一想,什么决定一个class的virtual functions名单的关键?答案是virtual table。Virtual table如何被处理?答案是通过vptr。所以为了控制一个class中所作用的函数,编译系统只要简单的控制住vptr的初始化和设定操作即可。