第5章 构造、析构、拷贝语义学2: 继承情况下的对象构造
重点:构造函数调用顺序
当我们定义一个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中面对自我拷贝设计一个自我过滤操作
一、虚拟继承
重点:虚拟继承下constructor如何调用
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;
}
当我们定义
Vertex3d cv;
Vertext3d 的constructor 正确的调用 Pointer constructor。
virtual base class constructor被调用有着明确的定义,只有当一个完整的class object被定义出来(例如 Point3d origin)时,它才会被调用,如果object只是某个完整的object的subobject,他就不会被调用
还可以把constructor一分为2,一个针对一个完整的object,一个针对subobject,完整的object无条件调用virtual base constructor,设定vptrs,subobject不调用virtual base constructor,也可能不设定vptrs
二、Vptr初始化语义学
重点:Vptr在那一步做的
定义一个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;
}
编译器如何保证适当的size()被正确的调用? 如果调用操作必须在 constructor 或 destructor 中, 那么答案十分明显:将每一个调用操作以静态决议,千万不要用虚拟机制
vptr的初始化操作:在base class constructor调用操作之后,但是在程序员供应的代码或”member initialization list中所有列的members初始化操作”之前。如果每一个constructor 都一直等待其 base class construtors 执行完毕之后才设定其对象的vptr,那么每次都能够调用正确的 virtual function 实例。
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提供参数时,在class的constructor的成员初值列中调用该class的一个虚函数这就是不安全的,此时vptr要么尚未被设定好,要么指向错误的class。该函数存取任何class's data members一定还没有初始化。(子类初始化列表->初始化父类,子类初始化列表含有虚函数的情况)
参考 构造、析构、拷贝语意学 - tianzeng - 博客园