深度探索c++对象模型大总结、中

--第五~八章

作者:July、吴黎明。

声明:版权所有,侵权必究。

二零一一年三月十八日。

本文接上一篇 c++对象模型大总结:第1-4章、对象初探与构造函数,而写。

第二部分

第五章、数据成员的布局

已知下面一组数据成员:
class Point3d{
public:
 //…
private:
 float x;
 static List<Point3d*> *freeList;
 float y;
 static const int chunkSize = 250;
 float z;
}
    非静态数据成员在class object中的排列顺序将和其被声明的顺序一样,任何中间介入的静态数据成员如freeList和chunkSize都不会被放进对象布局中。在上述例子中,每一个Point3d对象由三个float组成,次序是x、y、z。静态数据成员存放在程序的data segment中,和个别的class object无关。

C++标准要求,在同一个access section(也就是private、public、protected等区段)中,members的排列只须符合“较晚出现的members在class object中有较高的地址”这一条即可。也就是说,各个members并不一定得连续排列。什么东西可能会介于被声明的members之间呢?比如说members的边界调整时需要填充的一些字节等等。

同时,编译器还可能会合成一些内部使用的data members,以支持整个对象模型。vptr就是这样的东西,当前所有的编译器都把它安插在每一个“内含virtual function的class”的object内。vptr会被放在什么位置呢?传统上它被放在所有明确声明的members的最后,不过如今也有一些编译器把vptr放在class object的最前端。C++ standard允许编译器把这些内部产生出来的members自由放在任何位置上。

C++标准也允许编译器将多个access sections之中的data members自由排列,不必在乎它们出现在class声明中的次序。也就是说,下面这样的声明中:

class Point3d{
public:
 //…
private:
 float x;
 static List<Point3d*> *freeList;
private:
 float y;
 static const int chunkSize = 250;
private:
 float z;
}

其class object的大小和组成和我们先前声明的那个相同,但是members的排列次序则视编译器而定。编译器可以随意把y或z或其它什么东西放在第一个,不过大部分的编译器都没有这样做。当前各家编译器都是把一个以上的access sections连锁在一起,依照声明次序,成为一个连续的区块。access sections的多少,不会招来额外的负担。例如,在一个section中声明8个members,或是在8个sections中总共声明8个members,得到的object大小是一样的。

第六章、静态数据成员的存取

Static data members,按照其字面意思,被编译器提出到class之外,并被视为一个global变量(但只在class生命范围之内可见)。每一个member的存取许可(private或protected或public),以及与class的关联,并不会导致任何空间上或执行时间上的额外负担。
    每一个static data member只有一个实体,存放在程序的data segment之中。每次程序取用static data member,就会被内部转化为对该唯一的extern实体的直接操作:
      //origin.chunkSize = 250;
      Point3d::chunkSize = 250;
      //pt->chunkSize = 250;
      Point3d::chunkSize = 250;

从指令执行的观点来看,这是C++语言中“通过一个指针和通过一个对象来存取member,结论完全相同”的唯一一种情况。这是因为“经由member selection operators对一个static data member进行存取操作“只是语法上都一种便宜行事而已。member其实不在class object之中,因此存取static members并不需要通过class object。

如果chunkSize是从一个复杂继承关系中继承而来都member,又当如何呢?或许它是一个“virtual base class的virtual base class“(或其它同等复杂的继承结构)的member也说不定。即使这样的情况,也是无关紧要的,程序之中对于static members还是只有唯一的一个实体,而其存取路径依然是那么直接。

若取一个静态数据成员的地址,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针,因为static member并不内含在一个class object之中。例如:
      &Point3d::chunkSize;
会获得类型如下都内存地址:
      const int*

如果有两个classes,每一个都声明了一个static member freeList,那么当它们都被放在程序的data segment时,就会导致名称冲突。编译器的解决办法是暗中对每一个static data member编码(这种手法被称为:name-mangling),以获得一个独一无二的程序识别代码。

第七章、非静态数据成员的存取

非静态数据成员直接存放在每一个class object之中。除非经由明确的(explicit)或暗喻的(implicit)class object,没有办法直接存取它们。只要程序员在一个member function中直接处理一个nonstatic data member,所谓“implicit class object”就会发生。例如下面这段代码:

Point3d Point3d::translate( const Point3d &pt ){
 x += pt.x;
 y += pt.y;
 z += pt.z;
}

表面上所看到的对于x、y、z的直接存取,事实上是经由一个“implicit class object“(由this指针表达)来完成,事实上这个函数的参数为:

//member function的内部转化
Point3d Point3d::translate( Point3d * const this, const Point3d &pt ){
 this->x += pt.x;
 this->y += pt.y;
 this->z += pt.z;
}

欲对一个非静态数据成员进行存取操作,编译器需要把class object的起始地址加上data member的偏移量。比如:
origin._y = 0.0;
地址&origin._y将等于:
&origin + (&Point3d::_y - 1);

要注意这里都有减1的操作。指向数据成员的指针,其offset值总是被加上1,这样可以使编译系统区分出“没有指向任何数据成员的指针”和“指向第一个数据成员的指针”这两种情况。
每一个非静态数据成员的偏移量(offset)在编译时期即可获知,甚至如果member属于一个base class subobject也是一样,因此,存取一个非静态数据成员,其效率和存取一个C struct member或一个nonderived class的member也是一样的。

现在我们看看虚拟继承。虚拟继承将为“经由base class subobject“存取class members导入一层新的间接性,譬如:

Point3d *pt3d;
Pt3d->_x = 0.0;

其执行效率在_x是一个struct member、一个class member、单一继承、多重继承的情况下都完全相同。但如果_x是一个virtual base class的member,存取速度会慢一些。

第八章、“继承“与数据成员

只要继承不要多态(Inheritance without Polymorphism)
假设有如下三个类及其继承关系:

class Concreate1{
public:
 //...
private:
 int val;
 char bit1;
};
class Concreate2 : public Concrete1{
public:
 //...
private:
 char bit2;
}
class Concreate3 : public Concrete2{
public:
 //...
private:
 char bit3;
}

Concreate1、Concreate2、Concreate3的对象布局情况:

C++语言保证”出现在派生类中的base class subobject有其完整原样性“。Concrete1内含两个members:val和bit1,加起来5bytes。而一个Concreate1 object实际上用掉8bytes,包括填充用的3bytes,以使object能够符合一部机器的word边界。一般而言,边界调整(alignment)是由处理器来决定的。
    然而,Concreate2的bit2实际上却是被放在填补空间所用的3bytes之后,于是其大小变成12bytes,而不是8bytes,其中6bytes浪费在填补空间上。相同的道理使得Concreate3 object的大小是16bytes,其中9bytes用于填补空间。

加上多态(adding Polymorphism)
    假设我们要处理一个坐标点,而不打算在乎它是一个Point2d或Point3d实例,那么我们需要在继承关系中提供一个virtual function接口:

class Point2d{
public:
 Point2d(float x=0.0, float y=0.0) : _x(x),_y(y){};
 virtual float z(){return 0.0;}
 virtual void z(float){}
 operate+=(const Point2d &rhs){
 _x += rhs.x();
 _y += rhs.y();
 }
protected:
 float _x;
 float _y;
}

class Point3d : public Point2d{
public:
 Point3d(float x=0.0, float y=0.0, float z=0.0) : Point2d(x,y),_z(z){};
 virtual float z(){ return _z; }
 virtual void z(float newZ){ _z = newZ; }
 operate+=(const Point2d &rhs){
 Point2d::operator+=(rhs);
 _z += rhs.z();
 }
protected:
 float _z;
}

只有当我们以多态的方式来处理2d或3d坐标点时,在设计之中导入一个virtual接口才显得合理。也就是说,写下这样的代码:

void foo( Point2d &p1, Point2d &p2 ){
 //…
 P1 += p2;
 //…
}

其中p1和p2可能是2d也可能是3d坐标点,这并不是以前任何设计所能支持的。这样的弹性,当然正是面向对象程序设计的中心。同时,支持这样的弹性,也给我们的Point2d class带来空间和存取时间的额外负担:
导入一个和Point2d有关的virtual table,用来存放它所声明的每一个virtual functions的地址。
在每一个class object中导入一个vptr,提供执行期的链接,使每一个object能够找到相应的virtual table。

加强constructor,使它能够为vptr设定初值,让它指向class所对应的virtual table。这可能意味着在derived class和每一个base class的constructor中,重新设定vptr的值。其情况视编译器的优化的积极性而定。

加强destructor,使它能够抹消“指向class之相关virtual table“的vptr。要知道,vptr很可能已经在derived class destructor中被设定为derived class的virtual table地址。记住,desturctor的调用次序是反向的:从derived class到base class。

下图显示了Point2d和Point3d加上了virtual function之后的继承布局。此图把vptr放在base class的尾端:

多重继承(Multiple Inheritance)
    多重继承不像单一继承,不容易模塑出其模型。多重继承的复杂度在于derived class和其上一个base class乃至于上上一个base class…之间的“非自然“关系。例如,考虑下面这个多重继承所获得的class Vertex3d:

class Point2d{
public:
 //...
protected:
 float _x;
 float _y;
}

class Point3d : public Point2d{
public:
 //...
ptotected:
 float _z;
}

class Vertex{
public:
 //...
protected:
 Vertex *next;
}

class Vertex3d : public Point3d, public Vertex{
public:
 //...
protected:
 float mumble;
}

至此,Point2d、Point3d、Vertex、Vertex3d的继承关系如下图所示:

多重继承的主要问题发生于derived class objects和其第二或后继的base class objects之间的转换。对于一个多重派生对象,将其地址指定给“最左端base class的指针”,情况将和单一继承时相同,因为二者都指向相同的起始地址。至于第二个或后继的base class的地址指定操作,则需要对地址进行调整:加上(或减去,如果downcast的话)介于中间的base class subobject大小,例如:

Vertex3d v3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;

那么下面这个指定操作:
       pv = &v3d;
需要这样的内部转化:
//虚拟C++码
      pv = (Vertex *)( ((char *)&v3d) + sizeof( Point3d ) )

而下面都指定操作:
      p2d = &v3d;
      p3d = &v3d;
都只需要简单地拷贝其地址就可以了。下面是该多重继承的数据布局示意图:

虚拟继承(Virtual Inheritance)
    下图是Point2d、Point3d、Vertex、Vertex3d的继承体系:

class Point2d{
public:
 //...
protected:
 float _x;
 float _y;
};

class Vertex : public virtual Point2d{
public:
 //...
protected:
 Vertex *next;
};

class Point3d : public virtual Point2d{
public:
 //...
protected:
 float _z;
};

class Vertex3d: public Vertex, public Point3d{
public:
 //...
protected:
 float mumble;
};

在存取派生类的共有的虚拟基类的时候,cfront编译器会在每一个派生类对象中安插一些指针,每个指针指向一个virtual base class。要存取继承得来的virtual base class members,可以使用相关指针间接完成。

上面这种实现方式的一个缺点是:每一个对象必须针对每一个virtual base class背负一个额外的指针。然而理想情况下我们希望class object有固定的负担,不因为其virtual base classes的数目而有所变化。该如何解决这个问题呢?virtual table offset strategy采用了另外一种实现策略:在virtual function table中放置virtual base class的offset(而不是地址),将virtual base class offset和virtual function entries混在一起。virtual function table可经由正值或负值来索引:如果是正值,很显然就索引到virtual functions;如果是负值,则索引到virtual base class offsets。

一般而言,虚基类最有效的一种运用形式就是:一个抽象的虚基类,其中没有任何数据成员。
参考资料:
《深度探索C++对象模型》

版权归本人、吴黎明、CSDN共同所有,任何人,任何网站,在未经本人书面许可的情况下,严禁转载。
否则,尽我一切,永久追究法律责任的权利。July、二零一一年三月十八日声明。

转载于:https://www.cnblogs.com/v-July-v/archive/2011/03/18/2009184.html

c/c++对象模型大总结:第5-8章、数据成员的存取与布局相关推荐

  1. C++【对象模型】 | 【05】类与类之间各种关系下对数据成员的存取、绑定、布局

    文章目录 索引 1.类继承造成的负担 2.data member 3.data member的绑定 4.data member的布局 5.data member的存取 静态数据成员 非静态数据成员 6 ...

  2. c++对象模型大总结:第1-4章、对象初探与构造函数

    深度探索c++对象模型大总结.上 --第一~四章 作者:July.吴黎明. 声明:版权所有,侵权必究. 二零一一年三月十七日. 说明: 本份资料主要是参考侯捷先生译的<深度探索c++对象模型&g ...

  3. 按一行一行的方法将一个文本文件复制到另一个文件中_大文件上的结构化数据计算示例...

    [摘要] 本文分析大文件计算的实现原理,如过滤.聚合计算.添加计算列.排序.分组聚合.topN 等,以及利用并行计算来提高计算速度,并用 esProc SPL 举例说明如何用简洁的脚本实现大文件计算. ...

  4. Python金融大数据分析——第五章数据可视化(1)二维绘图

    目录 第五章 数据可视化 5.1 二维绘图 5.1.1 一维数据集 5.1.2 二维数据集 5.1.3绘制其他图表 5.1.3.1绘制散点图 5.1.3.2 直方图 5.1.3.3 箱型图 第五章 数 ...

  5. 《Spark快速大数据分析》—— 第三章 RDD编程

    本文转自博客园xingoo的博客,原文链接:<Spark快速大数据分析>-- 第三章 RDD编程,如需转载请自行联系原博主.

  6. 《大数据之路:阿里巴巴大数据实践》-第3篇 数据管理篇 -第15章 数据质量

    <大数据之路:阿里巴巴大数据实践>系列丛书  第1章 总述 第1篇 数据技术篇  第2章 日志釆集  第3章 数据同步  第4章 离线数据开发  第5章 实时技术  第6章 数据服务  第 ...

  7. 获取大麦网孟鹤堂演出数据并播报和在右下角弹窗提示

    #!/usr/bin/env python # coding=utf-8#!/usr/bin/env python # coding=utf-8 # 获取大麦网孟鹤堂演出数据并播报和在右下角弹窗提示i ...

  8. 大数据环境下互联网行业数据仓库/数据平台的架构之漫谈

    导读: 整体架构 数据采集 数据存储与分析 数据共享 数据应用 实时计算 任务调度与监控 元数据管理 总结 一直想整理一下这块内容,既然是漫谈,就想起什么说什么吧.我一直是在互联网行业,就以互联网行业 ...

  9. 第一章 数据与大数据

    大数据导论 昨天出去玩了回来之后实在是太困太困了,早上看了一些内容,在晚上的时候电脑都准备打开来写写,但是就完全睁不开眼睛了,今天来补一下.这两次看的都还是一些概念性的问题,没有关于技术的,实在是有点 ...

最新文章

  1. JAVA 创建线程池
  2. C,C++,C#的点评
  3. ubuntu14.04下安装cudnn5.1.3,opencv3.0,编译caffe及配置matlab和python接口过程记录
  4. mysql 修改单表导入大小_MySQL更改大库大表存储引擎方案
  5. 地方商城门户网页模板-商城模板
  6. 容器编排技术 -- 从零开始k8s
  7. 让你的创业失败的18个昏招 都归结到这里
  8. [转]sql server性能分析--检测数据库阻塞语句
  9. 由一个DAOHelper类引发的思考
  10. LaTeX常用符号与语法
  11. Phaserjs基础教程第二节:加载图片、文字和动画
  12. Web前端开发规范手册 1
  13. 自定义bt服务器,[教程]Aria2自动更新BT Tracker服务器列表的方法
  14. 经验系列之java拦截器获取POST入参导致@RequestBody参数丢失问题解决
  15. c语言整形数组存放字符串,用一维字符数组存放字符串
  16. 生如夏花之绚烂,死如秋叶之静美---也传奇
  17. 【科技检索报告】基于大规模浮动车GPS数据的实时地图匹配方法
  18. Android程序员该如何进阶?,2021Android面经
  19. win10+macOS双系统时间不同步解决方案
  20. 五年级上册计算机教学工作计划,小学五年级上册信息技术教学工作计划(精选5篇)...

热门文章

  1. 稳扎稳打Silverlight(13) - 2.0交互之鼠标事件和键盘事件
  2. R之ddlpy函数学习[转载]
  3. Guice系列之用户指南(五)
  4. iOS开发网络篇—使用ASI框架进行文件下载
  5. display vs visibility
  6. release和retain还有多少人在用
  7. Zabbix配置详解
  8. iptables 过滤条件(Matches)
  9. CF1131E String Multiplication(???)
  10. Keras Theano 输出中间层结果