Class Data
1 |
|
为什么?既然是空的类,那么更应该是0才对
然而输出的时候,A=1,B=8,C=8
不难理解,B和C存在vtpr
,因此会占用4byte,而A并不是空,它存在隐藏的1byte
C++中一个类的实例需要得到区分,那么就需要类里面可以装点东西,例如一个空类,然后声明3个实例,有点编译器就在类中插入一个char
以区分不同的实例
由于这个原因,在B和C上也会存在一个char
,这样一来,B=C=5byte
为了更有效的存储,B和C会受到 Memory Alignment (内存对齐)的限制,从而被拓展为8byte
但是如果是那种对Empty Virtual Base Class
进行特别优化的编译器,那么额外的那个char将会被拿去,只用vptr标示,这样B和C都是8byte
The Binding Of Data Member
在早期的C++编译器中,会出现class
中的member function
引用global object
而不是data member
的情况
1 | extern float x; |
因为当时对data member
的绑定是遇到就开始的
虽然现在已经改为整个class
声明结束才开始,但是对于typedef
仍需要防御型风格
1 | typedef int length; |
Data Member Layout
c++要求,同一个access section
中(private
, public
, protected
区段)中,member
的排列要符合较晚出现的members
要在class object
中有较高的位置
也就是说,各个member
不一定连续,原因是Memory Alignment时产生的byte
此外,编译器还会插入一些内部生成的data members
,例如vptr
,不过由于c++对内部的data members
的位置持放任态度,因此甚至可能将其放置在程序员声明对data members
之间
Data Member的存取
static Data Member
这是一个被编译器提出到class
之外的member
正因如此,每一个static data member
只有一个实例,存放在程序的data segment
中
其存取并不需要通过object
1 | class Test { |
即便static data member
是通过层层继承过来的,仍可以使用以上方法进行存取
Nonstatic Data Member
nonstatic data member
存储在class object
之中,只有借助显式或者隐式的class object
才能存取它们
1 | class Test { |
那为什么non-static data member
的存取需要借助class object
?
因为欲对一个non-static data member
进行存取,编译器需要吧class object
的起始地址加上data member
的偏移地址
1 | object.data = 0; |
此时,由于offset
的值在编译期便可得知,所以这时non-static data member
的存取效率和struct
是一样的
但是,如果加上虚拟继承呢?
如果我们要存取的non-static data member
是从virtual base class
继承过来的,那么存取速度就会变慢
如下
1 | Point3d origin; |
当x
是从一个virtual base class
中继承过来的,那么两种方法则会有很大差异
origin
,我们知道它的起始地址就是Point3d
的开头,那么,对x
的offset
也会在编译期确定下来
单pt
则不同,我们不知道它指向的类型是Point3d
还是Point3d 的 Base Class
,这样一来,offset
也无法确定,所以存取操作必须延迟到执行期
继承与Data Member
derived class member
和base class member
的位置理论上可以由编译器自由安排,但是在大部分的编译器中,base class member
会首先出现,前提是不存在virtual base class
本节将讨论
- 单一继承
- 单一继承+virtual function
- 多重继承
- 虚拟继承
单一继承
单一继承并不会增加空间和存取时间上的额外负担
但是,如果是糟糕的设计,那么将会因为内存对齐膨胀所需空间,例如,把一个class分解为多层
由于C++保证,出现在derived class中的base class具有完整的原样性,也就是说,在base class中为内存对齐安插的空间不变
假设base class的member所占空间为1,derived class的member所占空间为3,对齐为4
那么布局如下所示
1 | 对齐4 |
可以看出,derived class 所占空间是 1+(3)+3+(1),而不是1+3
为什么会这样?因为如果不这样的话,就会在进行某些操作时破坏整个类的空间
1 | class1 *p1, *p3; |
加上多态
支持多态性,势必会带来空间和存取时间上的负担
- 导入virtual table,存放每一个virtual function所带来的负担
- 导入vptr,提供vtbl地址所带来的负担
- 在constructor中初始化vptr所带来的负担
- 在deconstructor中,重设vptr
为什么要重设vptr?
1 | class A { |
p所指空间存在两种成分,A和B,p中只有一个vptr,p通过vptr调用virtual函数
为了使得在A和B的构造函数能调用正确的get(),我们可以控制vptr的指向达到这个目的
vptr的初始化在于base class
构造之后,在member
初始化之前
1 | B() { |
对于vptr在布局中所处的位置,c++标准并没有规定,具体位置看不同厂商的编译器的实现
该讨论没有意义
多重继承
在多重继承下,一个 derived class 内含 n-1 个额外的 virtual tables , n 表示其上一层 base classes 的个数;针对每一个 virtual tables, derived 对象中有对应的 vptr
1 | class A1; |
将c转为b
1 | b = c ? //判断是否为空 |
不过对于A2和B的排列顺序并没有具体规定,上面的代码只是为了说明多重继承中的转化需要经过运算
虚拟继承
如果
1 | class B : public A1; |
B也继承与A1,那么在布局中就会含有两份A1,语言层面的解决方法就是使用虚拟继承
为了实现虚拟继承,会将A1分成两部分
- 不变区域
- 共享区域
共享区域会因为操作有所变化,而不变区域则不会
一般的布局是先安排好不变区域,然后再建立共享区域,那么如何存取共享部分?
cfront
会在每一个derived class object
中安插指针,指向virtual base class
1 | class A1 {int x;}; |
如果在B中有如下操作
1 | x++; |
如果要实现转化
1 | A1 *a1; |
这个模型主要有两个问题
- 对于每一个
virtual base class
,derived class
必须有一个指向virtual base class
的指针,增加了空间成本 - 如果继承链加长,通过指针间接存取导致时间成本增加
对于第一个问题,有两种解决方法(注意⚠️,这不是标准)
- 可以设置
virtual base class table
- 可以在
virtual table
中存放virtual base class
的offset