一个空class有一个隐藏的1byte大小,是被编译器安插进去的一个char,使得类的object有不同的内存地址。
empty virtual base class:提供一个virtual interface,没有定义任何数据。在其派生类中只有一份实力,不管被继承了几次。
1.Data Member 的绑定
总结:把所有的data members防止class声明的开头处,以确保正确的绑定。
对于成员函数本体的分析,会在整个class的声明都出现了才开始。故一个Inline member function内的一个data member的绑定操作会在class声明完成后才发生(避免了一个全局的一个class内部的名称冲突)
对于成员函数参数列表并不为真,其会在第一次遭遇时被适当的完成(即导致不明确行为)。
2.Data Member的布局
总结:nonstatic data members 各个access section 之间优先级相同,access section 内部按顺序排列,不要求连续
- nonstatic data members放置的是个别class 感兴趣的数据。对于继承而来的nonstatic data members也是如此。(但并未强制其间的排列顺序)
- static data members放置的是整个calss感兴趣的数据。放置在程序的一个global data segment中,永远只存在一份实例,不会影响个别的class object的大小。
对于nonstatic data members来说: - 同一个access section,members的排列只需要保证顺序与声明顺序一致即可,不要求连续。中间可能会填充一些其他东西,比如说边界调整,虚指针(一般来说位于最后,也可以放在任何位置)。
- 多个access section之间的data members可以自由排列,不必在乎其在class中的声明顺序。一般来说编译器将access section连锁在一起,按声明顺序成为一个连续区块。(access section的声明并不影响内存大小)。
3.Data Member 的存取
members的存取许可(即access section:private,public,protected)不会导致任何空间和时间上的额外负担。
static data members
- 位于程序的data segment中,只有一个实例,被视为gloabl变量(在class 生命范围内)。 通过成员运算符进行操作只是文法上的简单,实际上并不在class object中,存取并不需要通过class object。
- 每一次对其的参考引用都转换为对该唯一extern实例的直接参考操作。
- 通过指针和通过一个对象存取没有区别的唯一情况。
- 若取一个static data members的地址会得到一个指向其数据类型的指针,而非指向其class member的指针(并不包含在一个class object中)。
- 由于均放在global data segment中,多个class声明同名的变量时会导致名称冲突。对此编译器是暗中为每一个static data members编码。获得一个唯一的程序识别代码。
Nonstatic data members
- 存放在每一个class object中,除非经由显示的(explicit)或隐式的(implicit)class object,否则无法直接存取。
如果在members function中直接处理一个nonstatic data members就会发生implicit(经由this指针完成)。 - 编译器通过把class object的起始地址+datamember的偏移位置(offset)来对一个nonstatic data member进行存取操作。
- 使用指针来对nonstatic data members进行操作的话其执行效率在:struct member,class member,单一继承,多重继承都完全相同。如果为虚基类的成员,存取速度会慢。
- origin.x=0.0; pt->x=0.0;当origin为派生类,且x来自于其继承结构中的虚基类的成员时,二者存在较大差异。pt的具体指向必须延迟到执行期,经由一个额外的间接引导才能解决。如果为origin不存在这些问题,其offset的值即使为虚基类的在编译器也固定了。
4."继承“与data member
单一继承不含virtual functions
具体继承(相对于虚继承)不会增加空间和存取时间上的额外负担。
在使用的时候需要注意:
- 1.避免重复设计一些带有相同的函数,应当充分复用基类中的函数
- 2.class多层拆分有可能会为了”表现class体系的抽象化“而膨胀所需的空间。(边界整合)
类在继承后,原基类的存储空间和边界调整不变,本类的数据跟在边界调整之后,并不填充边界
原因:
在指向复制操作时,并不能区分边界和值。如果为填充模式的话,将一个基类的值赋值给一个派生类,那么派生类本身的值会被赋予边界值(不明确行为)。
单一继承并含virtual functions
为了支持虚函数所带来的代价:
- 1.导入一个虚表,用以存放声明的每一个虚函数的地址以及一个/两个slots。
- 2.在class object 中导入一个vptr提供执行其的连接。其派生类也会有其基类的vptr。(原因与上面相同)
- 3.加强construct,使其能够为vptr赋初值,让它指向类对应的虚表。
- 4.加强destructor,使之能够消除指向与class相关虚表的虚指针。(destructor的销毁顺序是反向的)
vptr在class object中的位置: - 1.在class object的尾部,可以保留base class C struct 的对象的布局,允许在C程序代码中运行。
- 2.在class object的首部,在多重继承下通过指向class members的指针调用virtual function带来帮助。但丧失了与C语言的兼容性。如果base class 没有虚函数但是派生类有,那么此时将派生类转换为基类需要编译器介入,调整地址。(由于首部vptr的缘故)
多重继承
多重继承的问题主要发生在dc和其第二或后继bc object之间的转换。
编译器一般按照继承的声明顺序排列他们在内存中的位置。
dc object将其赋值给:
- 其第一个bc的指针,情况与单一继承相同,代价仅是指针的地址指定操作而已。
- 第二个或后继的,需要进行地址修改,加上或减去(取决于内存栈的生长方向介于中间的bc的大小。
dc 指针将其赋值给后继的bc指针时:需要进行条件测试(避免指针所指对象大小为零即定义了指针未指向对象,然后继续进行地址加减调整运算)。
dc 引用不需要针对可能的0做防卫。因为引用不可能参考到无物。
取第二个或后继的bc data member需要额外成本吗?
不需要,因为members的位置在编译时期就固定了,存取只是简单的offset运算。
虚拟继承
class如果包含一个或多个虚基类,那么会被分割为两部分:一个不变区域和一个共享专区。
- 不变区域:不管后继如何衍化,总有固定的offset,这一部分数据可以被直接存取。
- 共享区域:位置会因为每次的派生操作而有变化,只可以间接存取。
问题:如何取得class的共享区域部分?
解决:
为每一个dc object安插一些指针,每个指针指向一个虚基类,若要存取继承得来的虚基类成员可以通过相关指针间接完成。
问题:
- 1.每个对象为其每个虚基类背负一个额外的指针。class object的负担随vbc个数变化
- 2.虚继承串链加长,导致间接存取层次增加。存取随虚拟派生的深度而变化。
问题2:
经过拷贝直接获取所有虚基类的指针,放到drived class object中。
问题1:
解决1:引入虚基类表(其中置有指向虚基类的指针),为拥有虚基类的class object安插一个指针,指向虚基类表。
解决2:在虚表中放入虚基类的Offset.(*将虚基类offset与虚函数条目混在一起)
一般而言,虚基类的运用:一个抽象的虚基类,没有任何data members。
5.对象成员的效率
程序效率要实地测试。优化操作不一定总能有效运行。
6.指向data members的指针
C++要去同一个access level中的members的排列顺序应与其声明顺序相同,但对于vptr的位置没有限制(但不是首就是尾)。
如何区分一个“没有指向任何data members”的指针和一个指向“第一个data members”的指针?(即均未实例化之前)
(如:float Point::*p1=0; float Point::*p2=&Point::x)
解决:每一个真正的member offset值都+1.
实例化之前:取class 的data member的地址->得到它在class 中的偏移值。
实例化后:取object的data member的地址->该member在内存中的真正地址。
(static 和nonstatic意义相同)
效率问题:
通过指针指向class的data members,相当于为data members提供了另一种存取方式(别名)。通过这个指针而非真正的data members存取会导致执行时间多出一倍不止。
指针指向object 的data members然后进行data members的存取同样也会导致执行时间加倍。
要把“指向member 的指针”绑定到class object上,需要额外的把offset-1.
虚继承:会妨碍优化的有效性。因为每一层虚继承会导入一个额外层次的间接性,会降低“把所有处理都半岛寄存器中执行”的优化能力。