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

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一定还没有被初始化.

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

时间: 2024-10-10 20:32:36

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

&quot;无继承&quot; 情况下的对象构造

考虑以下代码: Point global; //1) Point Foobar() { Point local; //2) Point *heap = new Point; //3) *heap = local; //4) //...stuff... delete heap; //5) return local; //6) } 1), 2), 3) 为三种不同的对象产生方式: global内存配置, local 内存配置和 heap 内存配置. 4) 把一个object 指定给另一个, 6) 设

15.5——继承情况下的类作用域

继承情况下的类作用域: 在继承的情况下,派生类的作用域嵌套在基类作用域的下. 先在派生类的作用域范围内查找,要是没找到,接着在外围的基类作用域中查找. 1. 名字查找在编译时发生 (1)对象,引用或指针的静态类型决定了其所能作用的成员,即使是当动态类型和静态类型可能不一样时也满足 (2)例如使用基类的指针就不能去访问派生类的成员. 2. 名字冲突与继承 (1)当基类和派生类的成员同名时,基类的成员在直接访问时将被屏蔽. (2)可以采用域作用符::来访问被屏蔽的成员. (3)最好不要有基类和派生类

C++ Primer 学习笔记_69_面向对象编程 --继承情况下的类作用域

面向对象编程 --继承情况下的类作用域 引言: 在继承情况下,派生类的作用域嵌套在基类作用域中:如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义. 正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好像这些成员是派生类成员一样: Bulk_item bulk; cout << bulk.book() << endl; 名字book的使用将这样确定[先派生->后基类]: 1)bulk是Bulk_item类对象,在Bulk_item类中查找,找不到名

无网络情况下rpm方式安装

记录一下最近遇到的坑: 在无网络的情况下,rpm方式安装linux软件,遇到报nokey的错误,下载了好几个RPM-GPG-KEY都没有用,最后使用rpm -ivh *.rpm --force --nodeps命令解决问题. 下次好好研究下rpm命令 顺便记录下搜狐强大的mirror:http://mirrors.sohu.com/

迁移/home目录至新硬盘分区总结--无备份情况下

搞了一天,终于成功迁移.由于一开始就没备份过程实在很曲折. 希望本篇对那些没有备份习惯的朋友们有所帮助. 准备工作: sudo vim /etc/fstab 在文件中加入: /dev/sdb8       /home            ext4    user,rw 0       2 这里其实有问题的,后面会提到 一个新的linux分区,这里我的新分区是sdb8,个人不尽相同.我用的是ext4格式,注意要新的,不要有坏块,最好重新格式化下.我就是因为没格式化,吃过亏,logo界面就提示挂载

无归档情况下使用BBED处理ORA-01113错误

在丢失归档情况下,恢复时常会遇到ora-01113错误,以下实验模拟表空间offline,然后在丢失归档文件的情况下使用BBED修改文件头信息,最后恢复数据文件: 数据库版本: SQL> select * from v$version; BANNER -------------------------------------------------------------------------------- Oracle Database 11g Enterprise Edition Rele

C++中虚函数的理解,以及简单继承情况下的虚函数的表!

面向对象的三大特征=封装性+继承性+多态性 封装=将客观事物抽象成类,每个类对自身的数据和方法实行权限的控制 继承=实现继承+可视继承+接口继承 多态=将父类对象设置成为和一个或者更多它的子对象相等的技术, 用子类对象给父类对象赋值之后, 父类对象就可以根据当前赋值给它的子对象的特性一不同的方式运作 C++的空类有哪些成员函数 1.缺省构造函数 2.缺省拷贝构造函数 3.缺省析构函数 4.缺省赋值运算符 5.缺省取址运算符 6.缺省取址运算符const PS:只有当实际使用的时候才会去使用这些类

无备份情况下回复undo表空间

UNDO表空间存储着DML操作数据块的前镜像数据,在数据回滚,一致性读,闪回操作,实例恢复的时候都可能用到UNDO表空间中的数据.如果在生产过程中丢失或破坏了UNDO表空间,可能导致某些事务无法回滚,数据库无法恢复到一致性的状态,Oracle实例可能宕机,之后实例无法正常启动:如果有多个UNDO表空间数据文件,丢失其中一个数据文件数据库实例可能不会导致实例宕机,数据库无法干净的关闭(只能SHUTDOWN ABORT),数据库实例能正常的重启,但所有未回滚的数据块依然无法处理,尝试新建UNDO表空

无网络情况下 如何安装GCC

在有网络的情况下安装gcc只需一条指令:yum install gcc  那么在没有网络的情况下该如何安装gcc呢?虽然没有网络,但是我想你应该有安装光盘或者ISO镜像了,如果这些也没有的话,那就. 假设你有这些吧,我们只需利用安装光盘或ISO镜像来挂载一个本地yum源,利用这个来安装gcc. 一:挂载yum源,我这里用的是ISO镜像     在终端输入指令:mount -o loop xxxx.iso  /media/Centos/ 说明:xxxx.iso   是你的镜像文件名(注意路径正确)