码迷,mamicode.com
首页 > 编程语言 > 详细

C++对象模型——"无继承"情况下的对象构造(第五章)

时间:2015-08-13 22:30:11      阅读:151      评论:0      收藏:0      [点我收藏+]

标签:

5.2 继承体系下的对象构造

    当定义一个object如下:
T object;
    时,实际上会发生什么事情呢?如果T有一个constructor(不论是由user提供或是由编译器合成),它会被调用.这很明显,比较不明显的是,constructor的调用真正伴随了什么?
    constructor可能内带大量的隐藏码,因为编译器会扩充每一个constructor,扩充程度视 class T的继承体系而定.一般而言,编译器所做的扩充操作大约如下:
    1.记录在member initialization list中的data members初始化操作会被放进constructor的函数本身,并以members的声明顺序为顺序.
    2.如果有一个member并没有出现在member initialization list中,但它有一个default constructor,那么该default constructor必须被调用.
    3.在那之前,如果 class object有 virtual table pointers,它们必须被设定初值,指向适当的 virtual tables.
    4.在那之前,所有上一层的base class constructors必须被调用,以base class 的声明顺序为顺序(与member initialization list中的顺序没关联):

        如果base class 被列于member initialization list中,那么任何明确指定的参数都应该传递过去.
        如果base class 没有被列于member initialization list中,而它有default constructor(或default memberwise copy constructor),那么就调用它.
        如果base class 是多重继承下的第二或后继的base class,那么 this 指针必须有所调整.
    5.在那之前,所有 virtual base class constructor必须被调用,从左到右,从最深到最浅:
        如果 class 被列与member initialization list中,那么如果有任何明确指定的参数,都应该传递过去.若没有列于list中,而 class 有一个default constructor,也应该调用它.
        此外,class 中的每一个 virtual base class subobject的偏移量(offset)必须在执行期可内存取.
        如果 class object是最底层(most-derived)的 class,其constructors可能被调用;某些用以支持这个行为的机制必须被放进来.

    在这一节中,从"C++语言对classes所保证的语意"这个角度来探讨constructors扩充的必要性,再次以Point为例,并为它增加一个copy constructor,一个copy operator,一个 
virtual destructor如下:
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 destructor
	virtual float z() { return 0.0; }
protected:
	float _x, _y;
};
    在开始介绍Point的继承体系之前,先看看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.如果定义constructor如下:
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(本例为 virtual),所以Line class 的implicit copy constructor,copy operator和destructor都将有实际功能(nontrivial).
    当程序员写下:
Line a;
    时,implicit Line destructor会被合成出来(如果Line派生自Point,那么合成出来的destructor将会是 virtual.然而由于Line只是内带Point objects而非继承自Point,所以被合成出来的destructor只是nontrivial而已).在其中,它的member class objects的destructor会被调用(以其构造的相反顺序):
// C++伪代码:合成出来的Line destructor
inline void Line::~Line(Line *this) {
	this->_end.Point::~Point();
	this->_begin.Point::~Point();
}
  当然,如果Point destructor是 inline 函数,那么每一个调用操作会在调用地点被扩展开来.请注意,虽然Point destructor是 virtual,但其调用操作(在containing class destructor中)会被静态地决议出来(resolved statically).
    类似的道理,当一个程序员写下:
Line b = a;
    时,implicit Line copy constructor会被合成出来,成为一个 inline public member.
    最后,当程序员写下:
a = b;
    时,implicit copy assignment operator会被合成出来,成为一个 inline public member.
    在产生copy operator的时,需要使用如下的条件语句筛选:
if (this == &rhs)
	return *this;
  在一个由程序员供应的copy operator中忘记检查自我指派(赋值)操作是否失败,是新手极易陷入的一项错误,例如:
// 使用者供应的copy assignment operator
// 忘记提供一个自我拷贝时的筛选
String &String::operator=(const String &rhs) {
	// 这里需要筛选(在释放资源之前)
	delete []str;
	str = new char[strlen(rhs.str) + 1];
}
  这样一个警告信息是有帮助,"在一个copy operator中,面对自我拷贝缺乏一个筛选操作;但却有一个delete operator对应某个member操作".

虚拟继承 (virtual inheritance)

    考虑下面这个虚拟继承(继承自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;
};
  传统的"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;
}
  上面的Point3d constructor扩充内容有什么错误?
    试着想想以下三种类派生情况:
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有责任将Point初始化,而更往下的继承,则由PVertex(不再是Vertex3d)来负责完成"被共享的Point subobject"的构造.
    传统的策略如果要支持"ok,现在将virtual base clas初始化...oh,现在不需要...",会导致constructor中有更多的扩充内容,用以指示 virtual base class constructors应不应该被调用.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 = __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 classes
	// 设定__most_derived为false
	this->Point3d:::Point3d(false, x, y, z);
	this->Vertex::Vertex(false, x, y);
	// 设定vptrs
	// 插入user mode
	return this;
}
  这样的策略得以保持语意的正确无误.例如,当定义:
Point3d origin;
    时,Point3d constructor可以正确地调用其Point virtual base class subobject.而当定义:
Vertex3d cv;
    时,Vertex3d constructor正确地调用Point constructor.Point3d和Vertex的constructor会做每一件该做的事情——对Point的调用操作除外.

vtpr初始化语意学 (The Semantics of the vptr Initialization)

    当定义一个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的大小.
    更进一步,假设这个继承体系中的每一个constructors内带一个调用操作,像这样:
Point3d::Point3d(float x, float y, float z) : _x(x), _y(y), _z(z) {
	if (spyOn)
		cerr < "Within Point3d::Point3d()" << " size: " << size() << endl;
}
  当定义PVertex object时,前述的五个constructors会如何?每一次size()调用会被决议为PVertex::size()吗?或者每次调用会被决议为"当前正在执行的constructor所对应的class"的size()函数实体?
    C++语言规则指出,在Point3d constructor中调用的size()函数,必须比决议为Point3d::size()而不是PVertex:size.更一般的,在一个class的constructor或destructor中,经由构造中的对象来调用一个 virtual function,其函数实体应该是在此 class 中有作用的那个.由于各个constructors的调用顺序的缘故,上述情况是必要的.
    constructors的调用顺序是:由根源而末端,由内而外.当base class constructor执行时,derived实体还没有被构造处理.在PVertex constructor执行完毕之前,PVertex并不是一个完整的对象;Point3d constructor执行后,只有Point3d subobject构造完毕.
    这意味着,当每一个PVertex base class constructors被调用时,编译系统必须保证有适当的size()函数实体被调用,怎样保证这一点?
    如果调用操作限制必须在constructor或destructor中直接调用,那么答案十分显然:将每一个调用操作以静态方式决议,千万不要用到虚拟机制.只要是在Point3d constructor中,就明确地调用Point3d::size().
    然而如果size()中又调用一个 virtual function,会发生什么事情?这种情况下,这个调用也必须决议为Point3d的函数实体.而在其他情况下,这个调用是纯正的 virtual,必须经由虚拟机制来决定其归属.也就是说,虚拟机制本身必须知道是否这个调用源自于一个constructor中.
    另一个可以采取的方法是,在constructor(或destructor)内设立一个标志,指出以静态方式来决议,然后可以以标志值作为判断依据,产生条件式的调用操作.
    根本的解决之道是,在执行一个constructor时,必须限制一组 virtual functions候选名单.
    想一想,什么是决定一个 class 的 virtual functions名单的关键?答案是 virtual table.Virtual table如何被处理?答案是通过vptr.所以,为了控制一个 class 中有所作用的函数,编译系统只要简单地控制住vptr的初始化和设定操作即可.当然,设定vptr是编译器的责任,任何程序员不必操心此事.
    vptr初始化操作应该如何处理?本质而言,这需要视vptr在constructors中"应该在何时被初始化"而定.有三种选择:
    1.在任何操作之前.
    2.在base base class constructors调用之后,但在程序员供应的码或是在"member initialization list中所列的members初始化操作"之前.
    3.在每一件事情发生之后.
    答案是2.另外两种选择没有价值.策略2解决了"在class中限制一组virtual function名单"的问题.如果每一个constructor都一直等待直到其base class constructors执行完毕之后才设定其对象的vptr,那么每次它都能够调用正确的 virtual function实体.
     令每一个base class constructor设定其对象的vptr,使它指向相关的 virtual table之后,构造中的对象就可以严格而正确地变成"构造过程中所幻化出来的每一个class"的对象.也就是说,一个PVertex对象会先形成一个Point对象,Point3d对象,一个Vertex对象,一个Vertex3d对象,然而才成为一个PVertex对象,在每一个base class constructor中,对象可以与constructor‘s class的完整对象作比较.对于对象而言,"个体发生学"概括了"系统发生学".constructor的执行算法通常如下:
    1.在derived class constructor中,"所有virtual base classes"以及"上一层base class"的constructors会被调用.
    2.上述完成后,对象的vptr被初始化,指向相关的 virtual table.
    3.如果有member initialization list的话,将在constructors体内扩展开来.这必须在vptr被设定后才进行,以免有一个 virtual member function被调用.
    4.最后执行程序员所提供的代码.

    例如,已知这个程序员定义的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;
}
  它很可能被扩张为:
// C++伪代码:PVertex constructor的扩展结果
PVertex *PVertex::PVertex(PVertex *this, bool __most_derived, float x, float y, float z) {
	// 条件式地调用virtual base constructor
	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;
	// 程序员所写的代码
	if (spyOn)
		cerr << "Within PVertex::PVertex() " << "size: " << (*this->__vptr_PVertex[3].faddr)(this) << endl;
	// 传回被构造的对象
	return this;
}
  这就完美地解决了所说的有关限制虚拟机制的问题,但是,这真是一个完美的解答?假设Point Constructor定义为:
Point::Point(flaot x, float y) : _x(x), _y(y)
{}
  Point3d constructor定义为:
Point3d::Point3d(float x, float y, float z) : Point(x, y), _z(z)
{}
  更进一步,假设Vertex和Vertex3d constructor有类似的定义.是否能够看出解决办法并不完美?
    下面是vptr必须被设定的两种情况:
    1.当一个完整的对象被构造起来时.如果声明一个Point对象,Point constructor必须设定其vptr.
    2.当一个subobject constructor调用了一个 virtual function(不论是直接调用或间接调用)时.
    当声明一个PVertex对象,然后由于对其base class constructors的最新定义,其vptr将不再需要在每一个base class constructor中被设定.解决之道是把constructor分裂为一个完整的object实体和一个subobject实体.在subobject实体中,vptr的设定可以忽略.
    知道了这些,就能够回答下面的问题:在 class 的constructor的member initialization list中调用该 class 的一个虚拟函数,安全吗?就实际而言,将该函数运行于其class‘s data member的初始化行动中,总是安全的.这是因为,vptr保证能够在member initialization被扩展之前,由编译器正确设定好.但在语意上这可能是不安全的,因为函数本身可能还得依赖未被设立初值的members,所以并不推荐这种做法.然而,从vptr的整体角度来看,这是安全的.
     何时需要供应参数给一个base class constructor?这种情况下在"class的constructor的member initialization list中"调用该 class 的虚拟函数,仍然安全吗?不!此时vptr若不是尚未设定好,就是被设定指向错误的 class.更进一步地,该函数所存取的任何class‘s data members一定还没有被初始化.

版权声明:本文为博主原创文章,未经博主允许不得转载。

C++对象模型——"无继承"情况下的对象构造(第五章)

标签:

原文地址:http://blog.csdn.net/yiranant/article/details/47617795

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!