《深度探索C++对象模型》读书笔记[第三章]

3 Data 语义学

对于空的 class,编译器会安插 1 byte的char,用以区分两个不同的object.
对象object的大小会受到三个因素的影响:

  1. 语言本身所造成的的额外负担(overhead)
    当语言支持 virtual base classes时,就会导致一些额外负担.在 derived class 中,这个额外负担反应在某种形式的指针身上,它或者指向virtual base classs subobject,或者指向一个相关表格;表格中存放的若不是 virtual base class subobject 的地址,就是其偏移量(offset)。
  2. 编译器UI与特殊情况所提供的的优化处理
    Virtual base class X subobject 的 1 byte 大小也出现在 class Y 和 Z身上.传统上它被放在 derived class 的固定(不变动)部分的尾端.某些编译器会对 empty virtual base class 提供特殊支持.即 一个 empty virtual base class 被视为 derived class object 最开头的一部分,也就是说它并没有花费任何的额外空间.
  3. Alignment的限制
    即常说的内存对齐.

一个 virtual base class subobject 只会在 derived class 中存在一份实体,不管它在 class 继承体系中出现了多少次.

3.1 Data Member 的绑定 (The Binding of a Data Member)

对 member functions 本身的分析,会直到整个 class 的声明都出现了才开始.因此,在一个 inline memberfunction 躯体之内的打一个 data member 绑定操作,会在整个 class 声明完成之后才发生.
但是,这对于 member function 的 argument list 并不为真. Argument list 中的名称还是会在他们第一次遭遇时被适当的决议(resolved)完成.

请始终把"nested type 声明"[嵌套类型] 放在class的起始处.[91页的这种状况经过现在的g++验证,不会报错,后面声明的typedef float length 定义会覆盖前面的length定义.所以这条建议是失效的.]

3.2 Data Member 的布局(Data Member Layout)

Nonstatic data members 在 class object 中的配列顺序将和其被声明的顺序一样,任何中间介入的 static data members 都不会被放进对象布局之中.
members 的排列只需符合"较晚出现的 members 在 class object 中有较高的地址"。各个members并不一定得连续排列,因为members的边界调整(alignment)可能就需要调补一些bytes.(内存对其)
编译器合成的vptr可以放在任何位置,C++标准不做要求,甚至可以放到程序员声明的 members 之间.
C++标准也允许多个 access sections 之中的 data members 自由排列,而不必在乎他们出现在 class 声明中的次序.

3.3 Data Member 的存取

问题: 如果有两个定义, originpt :

Point3d origin, *pt= &origin;

用它们来存取data members,像这样:

origin.x = 0.0
pt -> x = 0.0

通过 origin 存取和通过 pt 存取,有什么重大差异吗?

3.3.1 Static Data Members

Static Data Members 被编译器提出于 class 之外,并被视为一个 global 变量(但只在 class 声明范围之内可见)。
每一个 static data member 只有一个实体,存放在程序的 data segment 之中.
这是 C++ 语言中"通过一个指针和通过一个对象来存取member,结论完全相同"的唯一一种情况。

3.3.2 Nonstatic Data Members

欲对一个 nonstatic data member 进行存取操作,编译器需要把 class object 的起始地址加上 data member 的偏移量(offset). 每一个 nonstatic data member 的偏移量(offset) 在编译时期即可获知,甚至如果 member 属于一个 base class subobject(派生自单一或多重继承串链) 也是一样。因此,存取一个 nonstatic data member,其效率和存取一个 C strucct 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,存取速度会比较慢一点.

origin.x = 0.0
pt->x = 0.0

回到3.3的问题:"从 origin 存取"和"从pt存取"有什么重大的差异吗?答案是"当Point3d是一个 derived class,而在其集成结构中有一个 virtual base class,并且被存取的member(如本例的x)是一个从该 virtual base classs 继承而来的 member 时,就会存在重大的差异."。

原因是不能确定 pt 必然指向哪一种 class type(因此也就不能编译器确定这个member真正的offset位置),所以这个存取操作必须延迟至执行器,经由一个额外的简介导引,才能够解决.
但如果使用origin,其类型必然是 Point3d class, 而即使它继承自 virtual base class, members 的 offset 位置也在编译时期就确定了.

3.4 "继承" 与 Data Member

在C++集成模型中,一个 derived class object 所表现出来的东西,是其自己的members加上其 base class(es) members 的总和。至于 derived class members 和 base classes members 的排列次序并未在 C++ Standard中强制指定;理论上编译器可以自由安排之.在大部分编译实现上, base class members 总是先出现,但属于 virtual base class 的除外(一般而言,任何一条规则一旦碰上 virtual base class 就没辙儿).

问题:一个class同时包含x,y,z和"提供两层或三层继承结构,每一层(代表一个维度)是一个 class,派生自较低维层次"有什么不同?下面各小结将讨论涵盖"单一继承且不含 virtual functions"、"单一继承并含 virtual functions"、"多重继承"、"虚拟继承"等四种情况。

3.4.1 只要继承不要多态(Inheritance without Polymorphism)

C++语言保证"出现在 derived class 中的 base class subobject 有其完整原样性"
继承层数增加,会导致因为内存边界调整而增加的padding.

3.4.2 加上多态(Adding Polymorphism)

多态增加的空间和存取时间的额外负担[这几点是本书中被从多个角度中解读的知识点了]:
I. 导入一个和Point2d有关的virtual table,用来存放它所声明的每一个 virtual functions 的地址。
II. 在每一个 class object 中导入一个 vptr, 提供执行期的链接,使每一个 object 能够找到相应的 virtual table.
III.加强 constructor,使它能够为 vptr 设定初值,让它指向 class 所对应的 virtual table.这可能意味着在 derived class 和每一个 base class 的 constructor 中,重新设定 vptr 的值.
IV.加强 destructor,使它能够抹消 "指向 class 之相关 virtual table" 的 vptr。destructor的调用次序是反向的:从 derived class 到 base class。
单一继承并含虚函数情况下的数据布局:

float_x
float_y
__vptr__Point2d
class Point2d p2d;
float_x
float_y
__vptr__Point2d
float_z
class Point3d:
public Point2d p3d;

3.4.3 多重继承(Multiple Inheritance)

多重继承的问题主要发生于 derived class objects 和其第二或后继的 base class objects 之间的转换.
对于一个多重派生对象,将其地址指定给"最左端 base class 的指针",情况和单一继承时相同,因为二者都指向相同的起始地址,需付出的成本只有地址的指定操作. 至于第二个或后继的 base class 的地址指定操作,则需要将地址修改过: 加上(或减去,如果downcast 的话) 介于中间的 base class subobject(s) 大小.

3.4.4 虚拟继承(Virtual Inheritance)

要在编译器中支持虚拟继承,实现技术的挑战在于,要找到一个足够有效的方法,将istream和ostream各自维护的一个 ios subobject,折叠成为一个由 iostream 维护的单一 ios subobject,并且还可以保存 base class 和 derived class 的指针(以及 references) 之间的多态指定操作(polymorphism assignments)。

一般的实现方法如下所述.Class 如果内含一个或多个 virtual base class subobjects,像istream 那样,将被分割为两部分:一个不变局部和一个共享局部.不变局部中的数据,不管后续如何衍化,总是拥有固定的offset(从object的开头算起),所以这一部分数据可以被直接存取.至于共享局部,所变现的就是 virtual base class subobject。这一部分的数据,其位置会因为每次的派生操作而有变化,所以他们只可以被简介存取.各家编译器实现技术之间的差异就在于简介存取的方法不同.

一般布局策略是先安排好 derived class 的不变部分,然后再建立其共享部分.然而,这中间存在着一个问题:如何能够存取 class 的共享部分呢?cfront 编译器会在每一个 derived class object 中安插一些指针,每个指针指向一个 virtual base class.要存取继承得来的virtual base class members,可以使用相关指针间接完成.

一般而言, virtual base class 最有效的一种运用形式就是: 一个抽象的 virtual base class,没有任何 data members.

3.6 指向Data Members的指针

取一个 nonstatic data member 的地址(&Point3d:😒),将会得到它在class 中的offset.取一个绑定于真正 class object 身上的data member 的地址(& origin.z),将会得到该member 在内存中的真正地址.

posted @ 2022-02-17 12:16  liyakai  阅读(38)  评论(0编辑  收藏  举报