C/C++内存对齐
字节对齐
成员变量的位置(偏移量)必须是其宽度的整数倍;整个结构体的宽度取决于其成员变量宽度的最大值。
什么是字节对齐,这里不多说。为什么要字节对齐,大概是能够提高CPU存储变量的速度,这里也不多说。但是可以肯定的是,字节对齐是编译器的行为,凭这一点就决定了我们需要掌握它。
首先,需要先定义两个概念:
- 变量的偏移量:所谓变量的偏移量其实是成员变量的起始地址相对于结构体的起始地址的偏移量。决定了结构体的内部细节。
- 结构体的字节边界数:即该结构体中最大的变量类型所占用的字节数。决定了结构体的整体宽度。
编译器决定结构体所占用的内存大小也是基于上面两个概念,有了下面两个规则:
- 成员变量的地址偏移量必须为该变量类型的大小的整数倍。
- 最终形成的结构体的大小必须为该结构体的字节边界数的整数倍。
查看下面这个结构体,其sizeof(struct point)会是多大呢
1 | struct point |
答案是16。x占8个字节,这时偏移量为0,0是8的倍数,满足条件可以放;y占1个字节,这时偏移量为9,9是1的倍数,满足条件可以放;z占4个字节,这时偏移量为9,9不是4的倍数,不满足条件,此时z不能直接放,需要在y后面填充3个无效字节,使得偏移量变为12,这时满足条件可以放z。此时,结构体的大小为8 + 1 + 3 + 4 = 16,且字节边界数的大小是double的大小8,也满足。所以最终结构体的大小就是16
再看看下面这个结构体的大小会是多少,只是上面的结构体中的两个成员变量变换了一下顺序而已。
1 | struct point |
答案是24。同样来分析一下。y占1个字节,这时其偏移量为0,满足条件;x占8个字节,这时其偏移量为1,不满足条件,所以需要在y后面补足7个无效字节,这时其偏移量为8,满足条件;z占4个字节,这时其偏移量为16,满足条件。这时结构体的大小为1 + 7 + 8 + 4 = 20个字节,但是结构体的字节边界数8,20不是8的倍数,还需要在z的后面补足4个字节。所以,最终结构体的大小为24。
讲到这里,用上面的方法解释基本类型组成的结构体还是可以的。但是涉及到结构的成员变量有结构体呢,上述的方法又有些行不通了。这说明上面的解释还不够笼统和精确(上面讲的当废话好了。学到后面,需要提出一个新的概念:成员变量的对齐参数。这个概念的使用还是会需要用到上面说的偏移量,但是字节边界数可能就不需要了。
会什么要强调成员变量,因为这个对齐参数是作用于成员变量的。
结构体中的每个成员变量会有其对齐参数(成员变量的宽度),而这个对齐参数来源于:默认对齐参数 和 指定对齐参数。成员变量的对齐参数需要从这两个参数中选择,选择的方式也很简单:哪个小选哪个。一般来说,基本变量类型的默认对齐参数就是其大小,而结构体变量类型的默认对齐参数则是从其所有成员变量的对齐参数中选择最大的那一个;指定对齐参数会在后面再解释。
从这个新的概念出发,结构体的构造有了下面新的规则:
- 从结构体的局部性来看,成员变量的地址偏移量必须是其对齐参数的整数倍,不够的话补足无效字节。决定了结构体的内部细节。
- 从结构体的整体性来看,最终结构体的大小必须是所有成员变量的对齐参数的整数倍,或者说最小公因数,不够的话在结尾补足无效字节。决定了结构体的整体宽度。
分析一下下面的结构体
1 | struct point |
先看一下struct point,对x来说,其对齐参数为sizeof(char) = 1,此时偏移量为0,偏移量是对齐参数的整数倍;对y来说,其对齐参数为8,此时偏移量为1,不满足条件,先在x后面补足7个字节使得y的偏移量为8,这样y的偏移量就是其对齐参数的整数倍了;对z来说,其对齐参数为4,此时偏移量为16,满足条件,直接放。此时结构体的大小为1 + 7 + 8 + 4 = 20个字节,不是所有对齐参数(x 1,y 8,z 4)的最小公倍数,所以还需再补足4个字节。由此,最终结构体的大小为24。
同时,还能得出struct point的默认对齐参数为其所有成员变量的对齐参数的最大值,也就是y的对齐参数8。
再看一下struct line,对a来说,其对齐参数为sizeof(int) = 4,此时偏移量为0,满足条件;对p来说,其对齐参数为8,此时的偏移量为4不满足条件,再补足4个字节,偏移量变成8。于是,结构体的大小为4 + 4 + 24 = 32,也同时满足了最小公倍数的条件。所以,最终结构体的大小为32了。
上面的例子中还没有添加指定对齐参数,而添加指定对齐参数的方法就是添加预处理指令#pragma pack(n),有了这条指令后,下面代码的指定对齐参数都为n
1 |
|
前面说过,一个成员变量的对齐参数为min{默认对齐参数, 指定对齐参数}
这样的话,对x来说,其默认对齐参数为1而指定对齐参数为4,选择小的那个,x的对齐参数为1。对y来说,默认对齐参数为8,指定对齐参数为4,所以选择指定对齐参数4为其对齐参数。而此时偏移量为1,先在x后面补足3个字节,让y的偏移量变为4。z的默认对齐参数为4,指定对齐参数也为4,对齐参数选哪个都行反正为4,此时偏移量为12,也满足条件,放上就行。此时结构体的大小为1 + 3 + 8 + 4 = 16,满足公倍数条件(x 的对齐参数为 1,y 的对齐参数为 4,z 的对齐参数为 4)。最终结构体的大小就为16了。
同时也可以得出struct point的默认对齐参数为4,所有成员变量的对齐参数中最大的那个。
1 |
|
然后基于此分析一下struct line。
a的默认对齐参数为1,指定对齐参数为4,所以其对齐参数为1,此时偏移量为0,满足条件;p的默认对齐参数为4,指定对齐参数为4,所以对齐参数为4,偏移量为1,不满足条件,先在a后面补足3个字节然后放入。b的默认对齐参数为8,指定对齐参数为4,所以对齐参数为4,偏移量为20,满足对齐参数是偏移量整数倍的条件,所以可以放入不用补足字节
此时,结构体的大小为1 + 3 + 16 + 8 = 28个字节,所有对齐参数为1 4 4,也满足公倍数条件,不用在结尾补足字节,所以可以确定最终结构体大小为24
