一、前言
结构体字节对齐属于老生常谈的问题,看似简单,却很容易忘记。而且因为结构体使用的普遍性,使得字节对齐也成为了一个不得不谈的话题。
二、什么是结构体字节对齐
- 假设现在有一个结构体如下,问你它的一个对象占用几个字节?
struct A{
int a;
char b;
short c;
};
- 关于每个变量所占的字节数,笔者环境下的值列举如下:
bool | char | short | int | long | float | long int | long long | double | long double |
---|---|---|---|---|---|---|---|---|---|
1 | 1 | 2 | 4 | 4 | 4 | 4 | 8 | 8 | 16 |
- 我们知道这三个变量所占字节数分别为4,1,2,那么应该占用7个字节。
- 但实际上,它占用8个字节,这就是结构体的字节对齐机制。
注:笔者环境(IDE:DEV-C 5.10,编译器:GCC4.8.1,64位)下,默认对齐字节数为16,后文有验证。其他环境或有不同
三、对齐规则相关定义
-
系统默认对齐字节数:系统默认对齐的字节数
-
(变量)自身对齐字节数:变量自身
sizeof()
得到的大小 -
(变量)自身有效对齐字节数:min(自身对齐字节数,系统默认对齐字节数)
-
(结构体)最大对齐字节数:max(结构体中所有变量的自身有效对齐字节数)
如果上述定义觉得复杂,可以记下下面这句话:
- 结构体中每个变量需在不超过系统默认字节对齐数的条件下满足自身对齐的要求,且整个结构体也需满足对齐规则。
关于对齐规则,记住下面的条件就好了。
- 条件①:结构体当前大小%当前变量
自身有效对齐字节数
=0,如果无法对齐,则持续填充字节直至对齐。 - 条件②:结构体的总大小%结构体
最大对齐字节数
=0,如果无法对齐,则持续填充字节直至对齐。
四、简单结构体变量对齐
实例1:
struct A{
int a;
char b;
short c;
};
-
答案如下:
-
解析如下:
当前变量 | 自身对齐字节数 | 系统默认字节对齐数 | 自身有效对齐字节数 | 对齐前所占总字节数 | 字节对齐需填充字节数 | 对齐后所占总字节数 |
---|---|---|---|---|---|---|
a | 4 | 16 | min(4,16)=4 | 0 | 0 | 4 |
b | 1 | 16 | min(1,16)=1 | 4 | 0 | 5 |
c | 2 | 16 | min(2,16)=2 | 5 | 1 | 8 |
1、当前结构体大小为0,当前int变量自身有效对齐数为4,满足条件①0%4=0,结构体大小0+4=4;
2、第二个char变量自身有效对齐数为1,当前结构体大小为4。满足条件①4%1=0,结构体大小4+1=5;
3、第三个short变量自身有效对齐数为2,当前结构体大小为5,不满足条件①5%2=0,故填充一个字节。现在6%2=0,满足条件,结构体大小6+2=8;
4、结构体总字节大小为8=(4+1+1+2),满足条件②8%4=0
5、得出结果结构体A占8字节;
- 存储结构大致示意如下:
|int|int|int|int|char|XXX|short|short|
如果觉得不好理解,可以理解成如下的形式,这样也更能体现“对齐”的含义:
int | int | int | int |
---|---|---|---|
char | XXX | short | short |
实例2:
struct B{
char b;
int a;
short c;
};
- 答案如下:
- 解析如下:
当前变量 | 自身对齐字节数 | 系统默认字节对齐数 | 自身有效对齐字节数 | 对齐前所占总字节数 | 字节对齐需填充字节数 | 对齐后所占总字节数 |
---|---|---|---|---|---|---|
a | 1 | 16 | min(1,16)=1 | 0 | 0 | 1 |
b | 4 | 16 | min(4,16)=4 | 1 | 3 | 8 |
c | 2 | 16 | min(2,16)=2 | 8 | 0 | 10 |
1、第一个char变量满足条件①0%1=0,结构体大小0+1=1;
2、第二个int不满足条件①1%4=0,故填充3个空字节。现在4%4=0满足条件,当前结构体大小4+4=8;
3、第三个short满足条件8%2=0,当前结构体大小8+2=10;
4、结构体总字节大小为10(1+3+4+2)不满足条件②10%4=0,故添加两个空字节,现在12%4=0满足条件②;
5、得出结构体B占12字节;
- 存储结构大致如下:
|char| XXX | XXX | XXX |int| int | int | int |short| short| short| short|
char | XXX | XXX | XXX |
---|---|---|---|
int | int | int | int |
short | short | XXX | XXX |
综合以上两个例子,我们发现对于如上两个成员变量类型、数目相同,但定义先后位置的不同的结构体,实际占用内存空间却相差4。
五、含有数组的结构体字节对齐
对于含有数组的情况,我们不用考虑的过于复杂,把它当做连续的相同类型变量即可。
实例3:
struct C{
char a;
short b;
char c;
int d;
char e[3];
};
答案是16
- 解析如下:
1、第一个char满足条件①0%1=0;
2、第二个short变量不满足条件①1%2=0,故填充一个空字节。现在满足条件①2%2=0;
3、第三个char变量满足条件①4%1=0;
4、第四个int变量不满足条件①5%4=0,最少需填充3个字节,填充后满足条件①8%4=0;
5、第五个e数组,每个数据都是char类型,把他当作三个char变量即可。三次均满足条件①12%1=0、13%1=0、14%1=0;
5、最后结构体总字节数为15,不满足条件②,填充一个空字节后16%4=0满足条件②;
6、最后得出结果结构体C占16字节,存储结构大致如下:
注:实际上,对于数组元素,只会存在第一个元素不满足对齐条件的情况发生。因为当第一个元素对齐后,后续元素显然也是对齐的。
- 存储结构大致如下:
| char | XXX | short | short | char | XXX | XXX | XXX | int | int | int| int| char[0]| char[1]| char[2]| XXX |
或
char | XXX | short | short |
---|---|---|---|
char | XXX | XXX | XXX |
int | int | int | int |
char[0] | char[1] | char[2] | XXX |
实例4:
struct D{
int a[2];
short b[3];
char c[3];
double d[2];
};
大家可以先自己口算一下需要用几个字节
- 存储结构大致的示意图如下:
int[0] | int[0] | int[0] | int[0] | int[1] | int[1] | int[1] | int[1] |
---|---|---|---|---|---|---|---|
short[0] | short[0] | short[1] | short[1] | short[2] | short[2] | char[0] | char[1] |
char[2] | XXX | XXX | XXX | XXX | XXX | XXX | XXX |
double[0] | double[0] | double[0] | double[0] | double[0] | double[0] | double[0] | double[0] |
double[1] | double[1] | double[1] | double[1] | double[1] | double[1] | double[1] | double[1] |
算对了吗?答案是40字节。
实例5:
struct C {
char a;
char b[3];
char c;
};
注意,结构体最大对齐字节数为1
所以答案是5,所有变量对齐后,结构体本身并不需要再次填充字节对齐了。
- 存储结构大致的示意图如下:
| char | char [0] | char [1]| char [2]|char |
六、改变系统默认值后的结构体字节对齐
- 通过
# pragma pack()
命令可以指定默认对齐字节数,可选参数有1/2/4/8/16
,不带参或参数非以上值时,将恢复默认值。 - 所以经常这一对代码经常成对出现,保证只有自己使用,防止对之后结构体的字节对齐产生影响。
实例6:
#pragma pack(2)
struct C{
char b;
int a;
short c;
};
#pragma pack() // 取消指定对齐,恢复缺省对齐
- 答案如下:
答案:结构体C占8个字节
- 解析如下:
注意:由于#pragma pack(2)命令的存在,系统默认对齐字节数为2,最大对齐字节数根据公式max(1,2,2)=2;
1、char:0%1=0;
2、int:1%2!=0,添加一个空字节,2%2=0满足条件①。但注意到int仍然还有2个字节没有对齐,所以继续,4%2=0满足条件①;
3、short:6%2=0;
4、得出结构体C占8个字节,存储结构大致如下:
|char| XXX | int | int |int| int | short | short |
或:
| char| XXX |
| int | int |
| int | int |
|short|short|
实例7:
#pragma pack(8)
struct D{
char b;
short a;
char c;
};
#pragma pack()
- 答案如下:
答案:结构体D占6个字节
-
解析如下:
注意:由于#pragma pack(8)命令的存在,系统默认对齐字节数为8,最大对齐字节数根据公式max(1,2,1)=2;
1、char:0%1=0
2、short:1%2!=0,添加一个空字节,2%2=0满足条件①。
3、char:4%1=0;
4、结构体总字节数5,5%2!=0,故添加一个空字节,6%2=0;
5、得出结构体C占6个字节 -
存储结构大致如下:
char | XXX |
---|---|
short | short |
char | XXX |
七、结构体对象充当成员变量的结构体字节对齐
-
其实对于这种情况,我们目前的项目中也经常出现。
-
虽然计算起来看似有些麻烦。但实际上,只需要要把结构体对象也当做一个变量处理即可。
-
而且,结构体本身的功能也是因为系统内置的变量类型不能满足我们的需要,所以需要一个自定义的”变量”而已。
注意:结构体对象在作为另一结构体的成员变量时,其内部成员变量已经被对齐,故变量的自身有效对齐数已经变为结构体最大对齐字节数。
实例8:
struct B {
char b[3];
};
struct C {
char a;
B b;
char c;
};
思考一下,本例和实例5有些相似。
- 答案如下:
结构体C占用5字节
- 存储结构大致的示意图如下:
| char | char(B) [0] | char(B)[1] | char(B)[2] | char |
看到这,是不是觉得简单,那就看看下面这个例子
实例9:
struct B{
char b;
int a;
short c;
};
// sizeof(struct B)=12
struct C{
short a;
B b;
float c;
};
- 答案如下:
结构体C占用20字节
- 存储结构大致的示意图如下:
short | short | XXX | XXX |
---|---|---|---|
char(B) | XXX | XXX | XXX |
int(B) | int(B) | int(B) | int(B) |
short(B) | short(B) | XXX | XXX |
float | float | float | float |
再来尝试下最后一个例子
实例10:
struct B{
char b;
char c;
int a;
};
struct C{
char a;
B b;
};
- 答案如下:
答案是12,相信你已经算对了,但存储结构是否和你想象的一致呢?
- 存储结构大致的示意图如下:
|char|XXX | XXX | XXX |
|char(B)|char(B)|XXX| XXX |
| int(B)| int(B)| int(B)| int(B)|
如果我们将对齐结构体B按照实际对齐的4字节进行拆分,就会拆成两段。那么实际上,你可以把结构体B当做由两个4字节的“变量”组成。
我们验证一番,可以发现对于结构体C中char变量的存放确实如上
八、关于系统默认字节对齐数的验证
- vs2013可以在属性页中对字节对齐数进行设置,相当于自动为你添加了
#pragma pack()
现在,我们做一个小小的验证,去验证我们的环境下系统默认对齐字节数为16。
struct Test{
char a;
long double b;
};
对于上面这个例子,我们可以熟练的算出对于系统对齐字节数为1、2、4、8、16时的结果分别是17、18、20、24、32
而对于上述例子的测试结果如下:
答案是32个字节,所以我们根据排除法得到笔者环境下系统默认字节对齐数为16。
九、结构体字节对齐的意义
- 字节对齐很简单,但我们了解了规则后,更应该了解为什么要这样设计。
- 首先,我们需要明确无论数据是否对齐,大多数计算机还是能够正确工作,但在有些处理器中,如果存在未对齐的数据,可能无法运行。
- 我们假设计算机总是从内存中取8个字节,如果一个double数据的地址对齐成8的倍数,那么一次内存操作就可以读或者写。
- 但是如果这个double数据的地址没有对齐,数据就可能被放在两个8字节块中,那么我们可能需要执行两次内存访问,才能读写完成。
- 显然在这样的情况下,是低效的,所以需要字节对齐来提高内存读写性能。①
十、关于字节对齐的一些思考
- 问:结构体到底该怎么写?如何保证字节对齐?
- 答:根据前面的一些例子,相信大家也已经发现,其实只要按照字节大小的顺序排列就可以,无论从大到小,或从小到大。
之后,考虑到目前我们所使用的结构体成员变量大多是int与char[]类型。且int类型更多,更通用,所以为了易于扩展,也为了适配大多数人的习惯在结构体末尾扩展,将int类型放在末尾会更好一些。
如果无法在多人协同开发的过程中保证结构体变量大小的有序性,那么一种更加显式的方法是将相同类型的变量放在一起。这样也可以更很好的规避空间浪费的情况。 - 问:既然为了节省空间,为什么不直接使用1字节对齐的方式呢?这样就不会存在空间浪费的情况。
- 答:设置为1字节对齐,确实会节省空间,但这样对存取效率会有影响。
正如在最后一个例子中所抽象出的“变量”概念,1字节对齐造成的后果就是“变量”变多,那么在内存寻址方面的次数就会大大增加,耗时也会增加,这显然这不是我们想要的。
而且我们对速度的要求也往往大过内存,所以牺牲部分内存换取速度也是很值得的。