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++对象模型——继承体系下的对象构造(第五章)的更多相关文章

  1. C++对象模型——&quot;无继承&quot;情况下的对象构造(第五章)

    5.1 "无继承"情况下的对象构造 考虑以下这个程序片段: 1 Point global; 2 3 Point foobar() 4 { 5 Point local; 6 Poin ...

  2. 理解SQL Server中的权限体系(下)----安全对象和权限

    原文:http://www.cnblogs.com/CareySon/archive/2012/04/12/SQL-Security-SecurableAndPermission.html 在开始阅读 ...

  3. Python基础总结之初步认识---class类的继承(下)。第十五天开始(新手可相互督促

    年薪百万的步伐慢了两天hhhh严格意义是三天.最近买了新的玩具,在家玩玩玩!~~~~ 今天开始正式认识类的继承.类的继承是怎么继承呢?看下代码: class Animal(object): #父类 d ...

  4. 【C++对象模型】第五章 构造、解构、拷贝 语意学

    1.构造语义学 C++的构造函数可能内带大量的隐藏码,因为编译器会扩充每一个构造函数,扩充程度视 class 的继承体系而定.一般而言编译器所做的扩充操作大约如下: 所有虚基类成员构造函数必须被调用, ...

  5. Java中对象构造

    构造函数 作用:在构造对象的同时初始化对象.java强制要求对象 诞生同时被初始化,保证数据安全. 调用过程和机制:①申请内存,②执行构造函数的函数体,③返回对象的引用. 特点:与类同名,无返回类型, ...

  6. 浅谈javascript继承体系

    最近做web项目,接触了jquery等框架,虽然使用方便,但是还是想学习下Javascript,今天分享下最近对js原型继承的理解,不足之处欢迎指正. 一.构造器的原型属性与原型对象 刚接触js时通常 ...

  7. java基础课程笔记 static 主函数 静态工具类 classpath java文档注释 静态代码块 对象初始化过程 设计模式 继承 子父类中的函数 继承中的构造函数 对象转型 多态 封装 抽象类 final 接口 包 jar包

    Static那些事儿 Static关键字 被static修饰的变量成为静态变量(类变量) 作用:是一个修饰符,用于修饰成员(成员变量,成员方法) 1.被static修饰后的成员变量只有一份 2.当成员 ...

  8. 从基层容器类看万变不离其宗的JAVA继承体系

    以容器类为例子,可以观一叶而知秋,看看以前的前辈们是如何处理各种面向对象思想下的继承体系的.读的源代码越多,就越要总结这个继承关系否则读的多也忘得快. 首先摆上一张图片: 看到这张图很多人就慌了,难道 ...

  9. 对象&内置对象& 对象构造 &JSON&__proto__和prototype

    原型是一个对象,其他对象可以通过它实现属性继承 原型链:每个对象都会在其内部初始化一个属性,就是__proto__,当我们访问一个对象的属性 时,如果这个对象内部不存在这个属性,那么他就会去__pro ...

随机推荐

  1. Java运行报错问题——Picked up JAVA_TOOL_OPTIONS: -agentlib:jvmhook

    http://blog.csdn.net/xifeijian/article/details/8830933 上述这个朋友博文提醒,可能是因为其他软件添加了JAVA_HOME的路径造成冲突.但他支持删 ...

  2. 全3D模具设计自动化解決方案

  3. sql server truncate table 删除表数据限制条件

    truncate 注释 注释TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同:二者均删除表中的全部行.但 TRUNCATE TABLE 比 DELETE 速度快 ...

  4. Shell script之How to write

    Write shell script: 1) Editor like vi or mcedi 2) Set execute permission for your script chmod  perm ...

  5. vps 虚拟服务器 教程 ( Virtual Private Server 虚拟专用服务器 )

    VPS是虚拟服务器的意思.他是通过软件在独立服务器上划分出来的一部分资源.从而虚拟出一个服务器.他拥有独立的IP.独立的操作系统.以及用户名和密码.在功能和使用方法上与服务器一模一样.用户也可以根据自 ...

  6. Codeforces_776_C_(思维)(前缀和)

    C. Molly's Chemicals time limit per test 2.5 seconds memory limit per test 512 megabytes input stand ...

  7. CAD利用Select2得到所有实体(网页版)

    主要用到函数说明: IMxDrawSelectionSet::Select2 构造选择集.详细说明如下: 参数 说明 [in] MCAD_McSelect Mode 构造选择集方式 [in] VARI ...

  8. Functional language 函数

    一.什么是函数式语言?       函数式语言一类程序设计语言,是一种非冯·诺伊曼式的程序设计语言.函数式语言主要成分是原始函数.定义函数和函数型.这种语言具有较强的组织数据结构的能力,可以把某一数据 ...

  9. java计算两地距离(公里)

    //目标经度,目标纬度,自己经度,自己纬度 public static double getDistance(double lon1, double lat1, double lon2, double ...

  10. SIGFPE能处理上下溢吗?

    SIGFPE可以报告算术异常.包括上下溢.除零等. C标准库说: 各种实现报告的异常类型区别也很大,但几乎没有实现会报告整型溢出. 真的吗?我测试了一下,发现在GCC上,整型.浮点上下溢都不会报告,但 ...