前言
本章主要介绍了数据成员的存储与处理。
一、对象的内存布局
1、空类也有 1Byte 的大小,因为这样才能使得这个 class 的 objects在内存中有独一无二的地址。
2、一个对象的内存布局大小:
-
其nonstatic data member的总和大小;
-
任何由于位对齐所需要的填补上去的空间,字节对齐一方面是为了不同硬件平台间的兼容,因为各个硬件平台对存储空间的处理不尽相同;另一方面是为了使bus的”运输量“达到最高效率,因为bus是以字节块为单位运送数据的,字节对齐可以避免一个数据被放到2个字节块中,从而需要多执行一次内存访问。
-
加上了为了支持virtual机制而引起的额外负担。
另外,编译器对于特殊情况提供一定的优化处理,virtual base class subobject的1byte在虚基类为empty virtual base class时会被优化掉,因为既然有了member,就不需要原本为了占位而安插的一个char。
3、nonstatic data members的数据被存放在每个类实例之中,而static data members的数据被存放在程序的一个global data segment中,永远只存在一份实例,即使该类没有任何实例,它也已经存在,但是template class的static data members要到实例化时才定义(注意是定义,不是声明)。
二、Data Member的绑定
1、对member functions本身的分析会直到整个class的声明都出现了才开始。所以在一个inline member function躯体之内的一个data member绑定操作,会在整个class声明完成之后才发生。也就是说,class的member functions可以引用声明在后面的成员。
2、但是,member function的argument list中的名称还是会在它们第一次遭遇时被适当地决议(resolved)完成。因此,类中的 typedef 的影响会受到函数与 typedef 的先后顺序的影响。
所以,对于typedef仍然需要防御性的程序风格:始终把nested type声明(即typedef)放在class起始处!
三、Data Member的布局
1、members的排列只需要符合“较晚出现的members在class object中有较高的地址”这一条即可。
2、传统上,vptr被安放在所有被明确声明的member的最后,不过也有些编译器把vptr放在最前面(MSVC++就是把vptr放在最前面,而GCC是把vptr放在最后面)。
四、Data Mmber的存取
1、经由对象和指针存取数据成员有什么不同?
经由对象存取在编译期就确定存取的member的offset位置,而经由指针存取需要等到执行期通过一个额外的间接引导才能正确存取member。但是对于经由一个对象来存取和由一个指针来存取一个静态的member来说,是完全一样的,都会被编译器所扩展。
2、经由一个函数调用的结果来存取静态成员,C++标准要求编译器必须对这个函数进行求值,虽然这个求值的结果并无用处。
foo().static_member = 100;
//foo()返回一个类型为X的对象,含有一个static_member,foo()其实可以不用求值而直接访
//问这个静态成员,但是C++标准保证了foo()会被求值,可能的代码扩展为:
(void) foo();
X::static_member = 100;
3、若取一个static data member的地址,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针。
4、编译器通过name-mangling手法(对每一个static data member进行编码)解决不同类中静态成员的名称冲突。
5、对一个nonstatic data member进行存取操作,编译器会进行如下扩展:
origin.y_ = 10;
//扩展为=>
&origin + (&Point3d::y_ - 1) //&Point3d::y_其实是相对于object初始地址的偏移量,如果data member放在第一个,则偏移量为0
注意其中的-1操作:指向data member的指针,其offset值总是被加上1。这样可以使编译系统区分出“一个指针data member的指针,用以指向class的第一个member”和“一个指向data member的指针,但是没有指向任何member”两种情况(成员指针也需要有个表示NULL的方式,0相当于用来表示NULL了,其它的就都要加上1了)。
五、继承与Data Member
1、派生类的成员和基类成员的排列次序并未在C++ Standard中强制指定;理论上编译器可以自由安排, 对于大部分的编译器实现来说,都是把基类成员放在前面,但是 virtual base class 除外(一般而言,任何一条规则一旦碰到virtual base class就没辙了)。这种安排下,有了派生类的指针,要获得基类的指针就不必要计算偏移量了。
2、C++语言保证“出现在derived class中的base class subobject有其完整原样性”,也就是说字节填充部分也都会继承下来。
3、单一继承并含有虚拟函数时的内存布局
vptr放在class object的哪里更好呢?
(1)放在class object的尾端,可以保留base class C struct的对象布局,因而允许在C程序代码中使用;
(2)放在class object的前端,对于“在多重继承下,通过指向class member的指针调用virtual function”可以节省一个v_offset字段。代价就是丧失了与C语言的兼容性。
4、多重继承时的内存布局
在多重继承的派生体系中,将派生类的地址转换为第1基类是成本与单继承是相同的,只需要改换地址的解释方式而已;而对于转换为非第1基类的情况,则需要对地址进行一定的offset操作才行,加上(或减去,如果downcast的话)介于中间的base class subobject(s)大小。
C++ Standard 并未明确 base classes 的特定排列次序,但是目前的编译器都是按照声明的次序来安放他们的。(有一个优化:如果第1基类没有vtable而后继基类有,则把多个base classes的顺序调换,这样可以在derived class object中少产生一个vptr)。
多重继承中,可能会有多个vptr指针,视其继承体系而定:派生类中vptr的数目最等于所有基类的vptr数目的总和。
5、虚拟继承:虚拟继承把一个类切割为 2 部分:一个不变局部和一个共享局部。
不变区域中的数据,不管后继如何衍化,总是拥有固定的offset(从object的开头算起),所以这部分数据可以被直接存取。
这个共享局部必须通过编译器安插的一些指针指向virtual base classobject来间接的存取,这样才能够实现共享。目前有3种主流策略:
(1)在每个derived class object中安插一些指针,每个指针指向一个virtual base class;
(2)在每个derived class object中安插一个指针,每个指针指向virtual base class table,而真正的virtual base class指针或者offset存放于该表格中。
(3)将virtual base class offset和virtual function entries混杂在一起,通过正值索引到vitual functions,负值索引到virtual base class offsets。
6、一般而言: virtual base classes 最有效的一种运用形式就是:一个抽象的 virtual base class ,没有任何 data members。
7、普通封装不会带来任何执行期的成本,编译器可以轻松优化掉普通封装带来的任何成本。但是一旦涉及到虚拟继承,效率就会大幅降低,在有n层的虚拟继承体系中,普通的访问就要经过n次间接,普通访问的成本就变为了n倍。再次表示,C++中的额外成本基本都是由于virtual机制引起的。
六、指向Data Members的指针
1、指向Data Members的指针内部实际保存的是这个data member相对于对象起始地址的偏移地址(offset)(但需要另外加1以区分空指针,前面有讲过了)!
2、在多重继承之下,若要将第二个(或后继)base class的指针,和一个“与derived class object绑定”的member结合起来,那么将会因为需要加入offset值而变得相当复杂。
3、使用指向Data Members的指针时也不会损失效率,成本与直接存取相同。而虚拟继承的引入妨碍了优化的有效性,因为每一层虚拟继承都导入一个额外层次的间接性,而间接性会降低“把所有的处理都搬移到寄存器中执行”的优化能力。