一、字节对齐规则
【规则一】数据成员对齐规则:变量只能存储在他的长度的整数倍地址上
结构(struct)(或联合(union))的数据成员,第一个数据成员放在 offset 为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
即以后每个数据成员放在 offset= \(min(手动设置对齐长度,当前数据成员长度)×正整数\)
【规则二】整体对齐规则:跟最大数据成员长度的整数倍对齐
在数据成员完成各自对齐之后,结构体(或联合体)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
即 size= \(min(手动设置对齐长度,最大数据成员长度)×正整数\)
【规则三】结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素长度的整数倍地址开始存储。
结构体成员从#pragma pack指定的数值和其内部最大元素长度之中较小值的整数倍地址开始存储。
即 offset= \(min(手动设置对齐长度,结构体成员内部最大数据成员长度,结构体成员手动设置对齐长度)×正整数\)
二、#pragma pack理解为“最高限速”
#pragma pack(n) // 表示它后面的代码都按照n个字节对齐
typedef struct st1 {
char v1;
int v2;
} st1;
#pragma pack() // 取消按照n个字节对齐,是对#pragma pack(n)的反向操作
n 的值是2的次幂;
这两行代码类似于开车时,走到某一段路的时候,发现一个限速60公里/h的指示牌,过了一段路之后,又会有解除限速的指示牌。
“限速”的意思就是实际对齐字节数要小于“限速”的值。
三、关于VS默认对齐长度
网上有一种说法,visual studio 中的默认对齐数=8,可通过#pragma pack(x)修改默认对齐数的大小。
我主要用的都是64位系统,看起来好像是对的。
接下来,用一段程序测试一下 #pragma pack(n),主要考虑了比默认对齐数8小的情况(手动设置对齐数4)以及比8大的情况(手动设置对齐数16):
#include "stdafx.h"
typedef struct A {
char v1;
double v2;
char v3;
} A_t;
#pragma pack(4)
typedef struct B {
char v1;
double v2;
char v3;
} B_t;
#pragma pack()
#pragma pack(16)
typedef struct C {
char v1;
double v2;
char v3;
} C_t;
#pragma pack()
int _tmain(int argc, _TCHAR* argv[])
{
printf("%lu\n", sizeof(A_t));
printf("%lu\n", sizeof(B_t));
printf("%lu\n", sizeof(C_t));
A_t a;
a.v1 = 0xa1;
a.v2 = 0;
a.v3 = 0xa3;
B_t b;
b.v1 = 0xb1;
b.v2 = 0;
b.v3 = 0xb3;
C_t c;
c.v1 = 0xc1;
c.v2 = 0;
c.v3 = 0xc3;
return 0;
}
输出结果为:
24
16
24
尽管我创建的是一个 Win32 程序,但是默认对齐数还是 8,至少在我64位Win10电脑上是这样的。
结构体 A_t a 对象结构如下图所示:
a.v1 从 offset=0 开始,占一个字节
a.v2 因为默认对齐数是8,基本类型double的长度也是8,按8字节对齐,offset必须是8的整数倍,所以选择从 offset=8 开始。【规则一】
a.v1 和 a.v2 之间(图上的灰色区域)则是对齐填充部分,因为 Windows 程序,默认是 0xcc 填充的。
a.v3 因为默认对齐数是8,基本类型char的长度是1,取较小值1,按1字节对齐,因此,在 a.v2 结束之后开始即可,所以选择 offset=16 开始,占一个字节。【规则一】
接着,结构体 A_t 的最大成员变量长度是8,默认对齐数也是8,整体按照8字节对齐,所以最终大小大于17且是8的整数倍,所以 sizeof(A_t)=24。【规则二】
结构体 B_t b 对象结构如下图所示:
b.v2 因为手动设置对齐数是4,基本类型double的长度是8,取较小值4,因此按4字节对齐,offset必须是4的整数倍,所以选择从 offset=4 开始。【规则一】
结构体 B_t 的最大成员变量长度是8,手动设置对齐数是4,整体按照4字节对齐。到 b.v3 结束位置,总共占了13字节,整体大小大于等于13且是4的倍数,因此 sizeof(B_t)=16【规则二】
结构体 C_t c 对象结构如下图所示:
其实和 A_t a 的对象结构差不多。
尽管设置了 #pragma pack(16) ,但实际上,无论规则一还是规则二中的对齐数,最终都取的是较小值 8。
因此,没有起到太大效果。
四、当最大成员变量长度小于8且没有手动设置对齐
例如:
typedef struct A {
char v1;
char v2;
char v3;
} A_t;
sizeof(A_t) 等于 3,对象结构如下图所示:
又例如:
typedef struct A {
char v1;
short v2;
char v3;
} A_t;
sizeof(A_t) 等于 6,对象结构如下图所示:
五、嵌套结构体
再来看一个嵌套结构体的程序:
#include "stdafx.h"
typedef struct A {
double v1;
char v2;
int v3;
} A_t;
typedef struct B {
char v1;
A_t v2;
double v3;
} B_t;
#pragma pack(2)
typedef struct C {
char v1;
A_t v2;
double v3;
} C_t;
#pragma pack()
int _tmain(int argc, _TCHAR* argv[])
{
printf("%lu\n", sizeof(A_t));
printf("%lu\n", sizeof(B_t));
printf("%lu\n", sizeof(C_t));
A_t a;
a.v1 = 0;
a.v2 = 0xa2;
a.v3 = 0xa3;
B_t b;
b.v1 = 0xb1;
b.v2 = a;
b.v3 = 0;
C_t c;
c.v1 = 0xc1;
c.v2 = a;
c.v3 = 0;
return 0;
}
输出结果为
16
32
26
结构体 A_t a 对象结构如下图所示:
B_t b 和 C_t c 在引用 A_t a 时,是保持结构不变的。
结构体 B_t b 对象结构如下图所示:
B_t 成员结构体 A_t 的内部最大数据成员 double 长度为8,默认对齐长度也为8,所以从8的整数倍地址开始存储成员结构体 b.v2, 即 offset=8。【规则三】
结构体 C_t c 对象结构如下图所示:
C_t 成员结构体 A_t 的内部最大数据成员 double 长度为8,手动设置对齐字节数为2,所以从2的整数倍地址开始存储成员结构体 c.v2,即 offset=2。【规则三】
但是,如果我们给 A_t 手动设置对齐数,则情况又要发生变化:
#pragma pack(4)
typedef struct A {
double v1;
char v2;
int v3;
} A_t;
#pragma pack()
结构体 A_t a 对象结构如下图所示:
内存中内容没有变化,现在整体按照手动设置对齐长度 4 来进行对齐。
结构体 B_t b 对象结构如下图所示:
成员结构体 A_t 的内部最大数据成员 double 长度为8,A_t 手动设置对齐字节数为 4,默认对齐字节数等于8,所以从4的整数倍开始存储成员结构体 b.v2,即 offset=4 【规则三】
b.v3 数据类型是double,长度为8,默认对齐字节数也等于8,因此,从8的整数倍开始存储 b.v3,即 offset=24,因此 offset:[20~23] 的部分是对齐填充区域。
结构体 C_t c 对象结构没有发生变化,因此不再赘述。