一、字节对齐规则
【规则一】数据成员对齐规则:变量只能从他的长度的整数倍地址开始存储
第一个数据成员放在 offset 为 0的地方,以后每个数据成员的对齐按照操作系统的基本字节单位(32位操作系统为4,64位操作系统为8)和这个数据成员自身长度中,比较小的那个进行。
即以后每个数据成员放在 offset= \(min(操作系统的基本字节单位,当前数据成员长度)×正整数\)
【规则二】整体对齐规则:跟最大数据成员长度的整数倍对齐
在数据成员完成各自对齐之后,结构体(或联合体)本身也要进行对齐。
所有结构体成员的字节长度 没有超出(<=) 操作系统的基本字节单位(32位操作系统为4,64位操作系统为8),按照结构体中字节数最大的变量长度来对齐
结构体中某个成员的字节长度 超出(>) 操作系统基本字节单位,按照系统基本字节单位来对齐
【规则三】结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素长度的整数倍地址开始存储。
从操作系统的基本字节单位(32位操作系统为4,64位操作系统为8)和其内部最大元素长度两者中取较小值,从该值的整数倍地址开始存储。
即 offset= \(min(操作系统的基本字节单位,结构体成员内部最大数据成员长度)×正整数\)
特别地,如果结构体成员指定了__attribute__((aligned(n)))
,那么从n的整数倍地址开始存储。(见“七、嵌套结构体”的例子)
二、attribute((aligned(n)))
__attribute__((aligned(n)))
中,n
的有效参数为2的幂值,32位最大为\(2^{32}\),64位为 \(2^{64}\),这个时候编译器会将让n
与默认的对齐字节数进行比较,取较大值为对齐字节数,与#pragma pack(n)
恰好相反。
它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各数据成员也要n字节对齐)
三、attribute((packed)) 取消编译时对齐优化
__attribute__((packed))
为取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,也就是采用1字节对齐。
四、attribute()在结构体类型中的使用方法
__attribute__()
的位置比较灵活
定义结构体时不对类型重命名,即不使用 typedef
时:
struct mystruct
{
/*成员变量定义*/
}__attribute__() /*(可同时在这定义变量)*/;
struct __attribute__() mystruct
{
/*成员变量定义*/
}/*(可同时在这定义变量)*/;
定义结构体同时对类型进行重命名,即使用 typedef
时:
typedef struct mystruct
{
/*成员变量定义*/
}__attribute__() mystruct_t;
typedef struct __attribute__() mystruct
{
/*成员变量定义*/
} mystruct_t;
五、测试代码
#include <stdio.h>
typedef struct A {
int v1;
double v2;
char v3;
} A_t;
typedef struct B {
int v1;
double v2;
char v3;
}__attribute__((aligned(4))) B_t;
typedef struct __attribute__((aligned(16))) C {
int v1;
double v2;
char v3;
} C_t;
typedef struct __attribute__((packed)) D {
int v1;
double v2;
char v3;
} D_t;
int main() {
printf("%d\n", (int)sizeof(A_t));
printf("%d\n", (int)sizeof(B_t));
printf("%d\n", (int)sizeof(C_t));
printf("%d\n", (int)sizeof(D_t));
A_t a;
a.v1 = 0x11;
a.v2 = 0x1;
a.v3 = 0xa;
B_t b;
b.v1 = 0x22;
b.v2 = 0x2;
b.v3 = 0xb;
C_t c;
c.v1 = 0x33;
c.v2 = 0x3;
c.v3 = 0xc;
D_t d;
d.v1 = 0x44;
d.v2 = 0x4;
d.v3 = 0xd;
return 0;
}
执行编译指令:
geekziyu@geekziyu-ubuntu-1:~/CLionProjects/c-helloworld$ gcc -m32 main.c -o main.32.o
geekziyu@geekziyu-ubuntu-1:~/CLionProjects/c-helloworld$ gcc main.c -o main.o
执行结果如下:
geekziyu@geekziyu-ubuntu-1:~/CLionProjects/c-helloworld$ ./main.o
24
24
32
13
geekziyu@geekziyu-ubuntu-1:~/CLionProjects/c-helloworld$ ./main.32.o
16
16
16
13
5.1 64位程序
结构体 A_t a 对象结构如下图所示:
a.v1 从偏移地址0开始存储,int占4个字节 【规则一】
a.v2 是double类型,长度为8个字节,64位程序默认对齐数也是8个字节,因此从8的整数倍偏移地址开始存储,即 offset=8【规则一】
a.v3 是char类型,长度为1个字节,64位程序默认对齐数是8个字节,因此可以从 offset=9 开始存储 【规则一】
接着,当前结构体 A_t a 整体 应该按照64位默认对齐数8B来对齐,即整体大小是8的整数倍
现在已经有17个字节了,因此,通过对齐填充扩展到size=24 【规则二】
0x3ff0000000000000 等于double型的1
结构体 B_t b 对象结构如下图所示:
__attribute__((aligned(4)))
并 不 要求结构体数据成员对齐;
0x4000000000000000 等于double型的2
结构体 C_t c 对象结构如下图所示:
0x4008000000000000 等于double型的3
结构体 D_t d 对象结构如下图所示:
按一个字节对齐是最简单,结构也是最紧凑的,没有对齐填充的部分。
0x4010000000000000 等于double型的4
5.2 32位程序
结构体 A_t a 对象结构如下图所示:
a.v2 是double类型,长度为8个字节,32位程序默认对齐数是4个字节,取较小值4B,因此从4的整数倍偏移地址开始存储,即 offset=4【规则一】
整体是按4B还是8B对齐暂时不好判断,需要增加实验。
结构体 B_t b 对象结构如下图所示:
整体是按4B还是8B对齐暂时不好判断,需要增加实验。
结构体 C_t c 对象结构如下图所示:
整体是按16B还是8B对齐暂时不好判断,需要增加实验。
结构体 D_t d 对象结构取消了对齐,因此结构和64位程序一样。
六、分析与验证
6.1 规则一与操作系统的基本字节单位有关
例6.1-1:把以下程序编译为64位程序并运行:
typedef struct A {
char v1;
int v2;
} A_t;
int main() {
A_t a;
a.v1 = 0x11;
a.v2 = 0x22;
return 0;
}
预期结果分析:
结构体成员变量 i 长度为 4B,编译为64位程序,则基本字节单位为 8B,取较小值 4B,因此它从偏移地址 4 开始存储,即 offset=4
根据 GDB 分析后,该结构体在栈上的存储如下图所示:
例6.1-2:把以下程序编译为32位程序并运行:
#include <stdio.h>
typedef struct A {
char v1;
double v2;
} A_t;
int main() {
printf("%d\n", (int) sizeof(A_t)); // 输出结果为12
A_t a;
a.v1 = 0x11;
a.v2 = 0x2;
return 0;
}
预期结果分析:
结构体成员变量 v2 程度是 8B,编译为64位程序,则基本字节单位为 4B,取较小值 4B,因此它从偏移地址 4 开始存储,即 offset=4
根据 GDB 分析后,该结构体在栈上的存储如下图所示:
6.2 规则一与__attribute__((aligned(n)))无关
上一节6.1的结构体都加上 __attribute__((aligned(n)))
,第二个成员的起始偏移地址 offset 也不会发生变化
6.3 规则二不是32位系统就按照4字节对齐,64位系统就按照8字节对齐
typedef struct A {
char v1;
char v2;
char v3;
} A_t;
上述程序在32位和64位操作系统下,sizeof(A_t)都等于3。而并非32位系统下等于4,64位系统下等于8
这个例子就说明:如果 所有结构体成员的长度 没有超出(<=) 操作系统的基本字节单位(32位操作系统为4,64位操作系统为8),按照结构体中字节数最大的变量长度来对齐
6.4 规则二并不总是按最大成员长度对齐
typedef struct A {
char v1;
double v2;
} A_t;
上述程序在32位操作系统下,sizeof(A_t) 等于12。正好是32位系统基本字节单位4B的整数倍,而非最大数据成员长度8B的整数倍16。
这个例子就说明:结构体中某个成员的字节长度 超出(>) 操作系统基本字节单位,按照系统基本字节单位来对齐。
6.5 规则二与__attribute__((aligned(n)))的关联
我在结构体内就放一个成员变量,然后在32位和64位上测试 sizeof() 的结果填写到单元格中,以下是汇总表格:
结构体内成员变量类型 | __attribute__((aligned(n))) |
32位 | 64位 | 备注 |
---|---|---|---|---|
char | 不设置 | 1 | 1 | 32位和64位的sizeof的结果都可以证明 对齐字节数是从 操作系统基本字节单元 和 最大数据成员长度 中选取较小值 |
char | n=4 | 4 | 4 | |
char | n=8 | 8 | 8 | |
char | n=16 | 16 | 16 | |
char | n=32 | 32 | 32 | |
int | 不设置 | 4 | 4 | |
int | n=4 | 4 | 4 | 64位的sizeof的结果相对比较明显地证明 对齐字节数是从 操作系统基本字节单元 和 最大数据成员长度 中选取较小值 |
int | n=8 | 8 | 8 | |
int | n=16 | 16 | 16 | |
int | n=32 | 32 | 32 |
当你没有设置 __attribute__((aligned(n)))
时,对齐字节数是从 操作系统基本字节单元 和 最大数据成员长度 中选取较小值;
但是,当你设置了 __attribute__((aligned(n)))
之后,整体就得按 n 来对齐。
七、嵌套结构体
在64位操作系统下编译运行以下程序:
#include <stdio.h>
typedef short t;
typedef struct A {
t v1;
char v2;
} A_t;
typedef struct B {
char v1;
A_t v2;
} B_t;
typedef struct __attribute__((aligned(32)))C {
char v1;
A_t v2;
} C_t;
int main() {
printf("%d\n", (int)sizeof(A_t));
printf("%d\n", (int)sizeof(B_t));
printf("%d\n", (int)sizeof(C_t));
A_t a;
a.v1 = 0x1;
a.v2 = 0x11;
B_t b;
b.v1 = 0x22;
b.v2 = a;
C_t c;
c.v1 = 0x33;
c.v2 = a;
return 0;
}
结构体 A_t a 的内存结构如下图:
结构体 B_t b 的内存结构如下图:
结构体 C_t c 的内存结构如下图:
B_t b 和 C_t c 第二个成员结构体变量,都是从 2B 的整数倍地址开始的,2B 是 操作系统基本字节单元8B 和结构体 A_t 中的最大成员长度2B 中的较小值。
B_t b 和 C_t c 分布基本相同,差别是 C_t c 的对齐填充更多。
如果把结构体 A 改为整体 8B 对齐:
typedef struct __attribute__((aligned(8))) A {
t v1;
char v2;
} A_t;
结构体 A_t a 的内存结构如下图:
结构体 B_t b 的内存结构如下图:
结构体 C_t c 的内存结构如下图:
B_t b 和 C_t c 第二个成员结构体变量,按照 8B 的整数倍地址开始,显然是因为
__attribute__((aligned(8)))
指定的 8B。
为了验证__attribute__((aligned(8)))
的“绝对成立性”,我又去编译了32位程序,结果还是从 8B 的整数倍地址开始!
B_t b 和 C_t c 分布基本相同,差别是 C_t c 的对齐填充更多。