在 C 语言中,结构体被用作数据包。它们不提供任何数据封装或数据隐藏功能。在本文中,我们将讨论 C 中的结构填充属性以及数据对齐和结构打包。
内存中的数据对齐
C 语言中的每种数据类型都有对齐要求(实际上,这是由处理器架构而非语言强制要求的)。处理器的处理字长与数据总线大小相同。在 32 位机器上,处理字长为 4 字节。
历史上,内存是按字节寻址并按顺序排列的。如果将内存排列成一个单字节宽度的存储体,处理器需要发出 4 个内存读取周期来获取一个整数。在一个内存周期内读取一个整数的全部 4 个字节更为经济。为了利用这种优势,内存将被排列成一组 4 个存储体,如上图所示。
内存寻址仍然是连续的。如果存储体 0 占用地址 X,则存储体 1、存储体 2 和存储体 3 将分别位于 (X + 1)、(X + 2) 和 (X + 3) 地址。如果在 X 地址上分配了一个 4 字节的整数(X 是 4 的倍数),则处理器只需一个内存周期即可读取整个整数。然而,如果整数分配的地址不是 4 的倍数,则它会跨越存储体的两行,如下图所示。这样的整数需要两个内存读取周期才能获取数据。
变量的数据对齐与数据在这些存储体中的存储方式有关。例如,在 32 位计算机上, int
的自然对齐方式为 4 个字节。当数据类型自然对齐时,CPU 会以最少的读取周期获取该数据类型。
类似地,短整型的自然对齐方式是 2 个字节。这意味着短整型可以存储在存储体 0 和存储体 1 对中,或者存储体 2 和存储体 3 对中。双精度型需要 8 个字节,并在存储体中占据两行。任何双精度型的对齐错误都将导致需要超过两个读取周期才能获取双精度型数据。
请注意, 在 32 位机器上,双精度变量将分配在 8 字节边界上,并且需要两个内存读取周期。在 64 位机器上,根据存储体的数量,双精度变量将分配在 8 字节边界上,并且只需要一个内存读取周期。
C 语言中的结构填充
结构填充是指在结构中添加一些内存空字节,以使数据成员在内存中自然对齐。这样做是为了最大限度地减少 CPU 读取结构中不同数据成员所需的周期。
尝试计算以下结构的大小:
// structure A
typedef struct structa_tag {
char c;
short int s;
} structa_t;
// structure B
typedef struct structb_tag {
short int s;
char c;
int i;
} structb_t;
// structure C
typedef struct structc_tag {
char c;
double d;
int s;
} structc_t;
// structure D
typedef struct structd_tag {
double d;
int s;
char c;
} structd_t;
通过直接将所有成员的大小相加来计算每个结构的大小,我们得到:
结构 A 的大小3。
= (char + short int) 的大小 = 1 + 2 =
结构 B 的大小7。
= (short int + char + int) 的大小 = 2 + 1 + 4 =
结构 C 的大小13。
= (char + double + int) 的大小 = 1 + 8 + 4 =
结构 A 的大小13。
= (double + int + char) 的大小 = 8 + 4 + 1=
现在让我们使用给定的 C 程序确认这些结构的大小:
// C Program to demonstrate the structure padding property
// Alignment requirements
// (typical 32 bit machine)
// char 1 byte
// short int 2 bytes
// int 4 bytes
// double 8 bytes
// structure A
typedef struct structa_tag {
char c;
short int s;
} structa_t;
// structure B
typedef struct structb_tag {
short int s;
char c;
int i;
} structb_t;
// structure C
typedef struct structc_tag {
char c;
double d;
int s;
} structc_t;
// structure D
typedef struct structd_tag {
double d;
int s;
char c;
} structd_t;
int main()
{
printf("sizeof(structa_t) = %lu\\n", sizeof(structa_t));
printf("sizeof(structb_t) = %lu\\n", sizeof(structb_t));
printf("sizeof(structc_t) = %lu\\n", sizeof(structc_t));
printf("sizeof(structd_t) = %lu\\n", sizeof(structd_t));
return 0;
}
输出
sizeof(structa_t) = 4
sizeof(structb_t) = 8
sizeof(structc_t) = 24
sizeof(structd_t) = 16
我们可以看到,这些结构的尺寸与我们计算的尺寸不同。
这是因为各种数据类型都有对齐要求,结构体的每个成员都应该自然对齐。结构体成员的分配顺序是按升序进行的。
让我们分析一下上面程序中声明的每个结构体。为了方便起见,假设每个结构体类型变量都分配在 4 字节边界上(比如 0x0000
),也就是说,结构的基地址是 4 的倍数(但不一定总是如此,参见 structc_t
的解释)。
结构A
structa_t
的第一个元素是char
,它按单字节对齐,后面跟着*short int*
。 short int
按双字节对齐。如果 short int
元素紧跟在 char 元素之后分配,它将从奇数地址边界开始。编译器会在 char 之后插入一个填充字节,以确保 short int
的地址是 2 的倍数(即按双字节对齐)。structa_t
的总大小为:
sizeof(char) + 1 (填充) + sizeof(short),1 + 1 + 2 = 4 个字节。
结构B
*structb_t*
的第一个成员是短整型 (short int),后跟一个字符 (char)。由于字符可以位于任何字节边界,因此短整型 (short int) 和字符 (char) 之间不需要填充,因此它们总共占用 3 个字节。下一个成员是 int。如果立即分配 int,它将从奇数字节边界开始。我们需要在 char 成员后填充 1 个字节,以使下一个 int 成员的地址 4 字节对齐。总计:
structb_t需要2 + 1 + 1 (填充) + 4 = 8个字节。
结构 C – 每个结构也都有对齐要求
同样的分析,structc_t
需要 sizeof(char) + 7 个字节的填充 + sizeof(double) + sizeof(int) = 1 + 7 + 8 + 4 = 20 个字节。然而,sizeof(structc_t)
实际占用 24 个字节。这是因为,除了结构体成员之外,结构体变量也需要自然对齐。让我们通过一个例子来理解这一点。假设我们声明了一个 structc_t
数组,如下所示:
structc_t structc_array[3];
为了便于计算,假设structc_array
的基地址为 0x0000
。如果 structc_t
占用 20(0x14)个字节(正如我们计算的那样),则第二个 structc_t 数组元素(索引为 1)将位于 0x0000 + 0x0014 = 0x0014。它是数组索引为 1 的元素的起始地址。该 structc_t 的 double 成员将分配在 0x0014 + 0x1 + 0x7 = 0x001C(十进制 28)处,该地址不是 8 的倍数,并且与 double 的对齐要求冲突。正如我们在开头提到的,double 的对齐要求是 8 字节。
为了避免这种对齐错误,编译器为每个结构体引入了对齐要求。*该要求将与结构体中最大的成员对齐。在我们的例子中,structa_t 的对齐要求为 2,structb_t 的对齐要求为 4,structc_t 的对齐要求为 8。如果我们需要嵌套结构体,则*内部最大结构体的大小将与直接更大的结构体的对齐要求一致。
在上述程序的 structc_t 中,int 成员后面会填充 4 个字节,以使结构体大小为其对齐大小的倍数。因此,structc_t 的大小为 24 个字节。即使在数组中,它也能保证对齐正确。
结构D
类似地,结构D的大小为:
sizeof(double) + sizeof(int) + sizeof(char) + padding(3) = 8 + 4 + 1 + 3 = 16 字节
如何减少结构填充?
现在,填充显然是不可避免的。有一种方法可以最大限度地减少填充。程序员应该按大小的升序/降序声明结构体成员。例如,我们代码中的 structd_t 的大小为 16 字节,而 structc_t 的大小为 24 字节。
什么是结构填充?
大多数编译器都提供了非标准扩展来关闭默认填充,例如编译指示或命令行开关。更多详细信息,请参阅相应编译器的文档。
在GCC中我们可以使用如下代码进行结构打包:
或者
结构名称 {
...
}__attribute__((packed)) ;
// C Program to demonstrate the structure packing
#include <stdio.h>
#pragma pack(1)
// structure A
typedef struct structa_tag {
char c;
short int s;
} structa_t;
// structure B
typedef struct structb_tag {
short int s;
char c;
int i;
} structb_t;
// structure C
typedef struct structc_tag {
char c;
double d;
int s;
} structc_t;
// structure D
typedef struct structd_tag {
double d;
int s;
char c;
} structd_t;
int main()
{
printf("sizeof(structa_t) = %lu\\n", sizeof(structa_t));
printf("sizeof(structb_t) = %lu\\n", sizeof(structb_t));
printf("sizeof(structc_t) = %lu\\n", sizeof(structc_t));
printf("sizeof(structd_t) = %lu\\n", sizeof(structd_t));
return 0;
}
输出
sizeof(structa_t) = 3
sizeof(structb_t) = 7
sizeof(structc_t) = 13
sizeof(structd_t) = 13
void argument_alignment_check( char c1, char c2 )
{
// Considering downward stack
// (on upward stack the output will be negative)
printf("Displacement %d\\n", (int)&c2 - (int)&c1);
}
在 32 位机器上输出将为 4。这是因为出于对齐要求,每个字符占用 4 个字节。
如果我们尝试访问未对齐的数据会发生什么?
这取决于处理器架构。如果访问未对齐,处理器会自动发出足够的内存读取周期,并将数据正确地打包到数据总线上。这会降低性能。然而,少数处理器没有最后两条地址线,这意味着无法访问奇数字节边界。每次数据访问都必须正确对齐(4字节)。未对齐访问是此类处理器的一个关键异常。如果忽略该异常,读取的数据将不正确,结果也将不正确
有没有办法查询数据类型的对齐要求?
是的。编译器会针对此类需求提供非标准扩展。例如,Visual Studio 中的 __alignof() 函数可以帮助获取数据类型的对齐要求。详情请参阅 MSDN。
当在 32 位机器上每次读取 4 个字节的内存读取效率很高时,为什么双精度类型应该在 8 字节边界上对齐?
值得注意的是,大多数处理器都有一个数学协处理器,称为浮点单元(FPU)。代码中的任何浮点运算都将被转换成 FPU 指令。主处理器与浮点执行无关。所有这些都将在后台完成。按照标准,double 类型将占用 8 个字节。并且,在 FPU 中执行的每个浮点运算都将具有 64 位长度。即使是 float 类型在执行之前也会提升为 64 位。FPU寄存器的 64 位长度强制将 double 类型分配在 8 字节边界上。我假设(我没有具体的信息)在 FPU 操作的情况下,数据提取可能会有所不同,我的意思是数据总线,因为它转到 FPU。因此,对于 double 类型(预计在 8 字节边界上),地址解码将有所不同。这意味着浮点单元的地址解码电路将没有最后 3 个引脚。
转载来自:https://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/
#pragma pack
修改结构体的内存对齐
#pragma pack
的作用范围是:从指令出现的位置开始,影响它之后定义的结构体/联合体的内存对齐方式,直到:
再次调用
#pragma pack
进行更改,或者使用
#pragma pack(pop)
恢复之前保存的对齐设置
示例讲解:作用范围
// 从这里开始,结构体使用1字节对齐
struct A {
char a;
int b;
}; // 对齐为1,大小为5字节
struct B {
char x;
short y;
}; // 对齐为1,大小为3字节
// 修改为4字节对齐
struct C {
char m;
int n;
}; // 对齐为4,大小为8字节
// 恢复为默认对齐(编译器设定,通常为8)
struct D {
char p;
double q;
}; // 默认对齐,通常大小为16字节
用 push
/ pop
限定范围(推荐)
为了管理范围更清晰,建议使用 #pragma pack(push, n)
和 #pragma pack(pop)
:
// 保存当前对齐方式,并设置为1字节对齐
struct Packet {
char header;
int data;
}; // 1字节对齐,大小为5
// 恢复上一个对齐设置
你可以多层嵌套使用 push
,比如:
struct Outer {
char a;
int b;
};
struct Inner {
char x;
int y;
};
struct Outer2 {
char c;
double d;
};
总结:
指令 | 含义 |
---|---|
#pragma pack(n) | 设置对齐为 n 字节,从此位置往后生效 |
#pragma pack() | 恢复默认对齐(即编译器初始设定) |
#pragma pack(push, n) | 将当前对齐方式压栈并设置为 n 字节对齐 |
#pragma pack(pop) | 弹出最近一次 push |