《深度探索C++对象模型》读书笔记[第三章]
3 Data 语义学
对于空的 class,编译器会安插 1 byte的char,用以区分两个不同的object.
对象object的大小会受到三个因素的影响:
- 语言本身所造成的的额外负担(overhead)
当语言支持 virtual base classes时,就会导致一些额外负担.在 derived class 中,这个额外负担反应在某种形式的指针身上,它或者指向virtual base classs subobject,或者指向一个相关表格;表格中存放的若不是 virtual base class subobject 的地址,就是其偏移量(offset)。 - 编译器UI与特殊情况所提供的的优化处理
Virtual base class X subobject 的 1 byte 大小也出现在 class Y 和 Z身上.传统上它被放在 derived class 的固定(不变动)部分的尾端.某些编译器会对 empty virtual base class 提供特殊支持.即 一个 empty virtual base class 被视为 derived class object 最开头的一部分,也就是说它并没有花费任何的额外空间. - 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 的存取
问题: 如果有两个定义, origin
和 pt
:
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 在内存中的真正地址.