龙空技术网

C语言结构成员对齐、填充和数据打包

极客范 482

前言:

当前姐妹们对“c程序的基本组成单位是什么”大致比较注意,你们都需要了解一些“c程序的基本组成单位是什么”的相关资讯。那么小编同时在网络上收集了一些有关“c程序的基本组成单位是什么””的相关文章,希望看官们能喜欢,兄弟们快快来了解一下吧!

在C语言中,结构体被用作数据包。它们不提供数据封装或数据隐藏的功能。

在本文中,我们将讨论C语言中结构体填充的特性,以及数据对齐和结构体打包。

内存中的数据对齐

C语言中的每种数据类型都具有对齐要求(实际上是由处理器架构而不是语言规定的)。处理器的处理字长将与数据总线大小相同。在32位机器上,处理字长将为4字节。

历史上,内存是按字节寻址并按顺序排列的。如果将内存排列为单个一字节宽度的银行,处理器需要发出4个内存读取周期来获取一个整数。将整数的4个字节一次性读取可以更经济地使用内存周期。为了利用这样的优势,内存将被排列成4个银行的组合,如上图所示。

内存寻址仍然是顺序的。如果银行0占据地址X,银行1、银行2和银行3将位于地址(X + 1)、(X + 2)和(X + 3)。如果一个4字节的整数被分配在地址X上(X是4的倍数),处理器只需要一个内存周期就可以读取整个整数。然而,如果整数被分配在不是4的倍数的地址上,它将跨越两行银行,如下图所示。这样的整数需要两个内存读取周期来获取数据。

变量的数据对齐处理的是数据在这些银行中的存储方式。例如,在32位机器上,int 的自然对齐是4字节。当数据类型自然对齐时,CPU 以最少的读取周期获取它。

类似地,short int 的自然对齐是2字节。这意味着 short int 可以存储在银行0-银行1对或银行2-银行3对中。double 需要8字节,在内存银行中占据两行。任何对 double 的错误对齐都会导致需要超过两个读取周期来获取 double 数据。

请注意,在32位机器上,double 变量将被分配在8字节边界上,并且需要两个内存读取周期。在64位机器上,根据银行数量,double 变量将被分配在8字节边界上,并且只需要一个内存读取周期。

C中的结构填充

结构填充是在结构中添加一些空字节的内存,以使数据成员在内存中自然对齐。这样做是为了最小化 CPU 读取周期,以检索结构中的不同数据成员。

尝试计算以下结构的大小:

// structure Atypedef struct structa_tag {    char c;    short int s;} structa_t;  // structure Btypedef struct structb_tag {    short int s;    char c;    int i;} structb_t;  // structure Ctypedef struct structc_tag {    char c;    double d;    int s;} structc_t;  // structure Dtypedef struct structd_tag {    double d;    int s;    char c;} structd_t;

通过直接将所有成员的大小相加来计算每个结构的大小,我们得到:

结构 A 的大小 = (char + short int) 的大小 = 1 + 2 = 3。结构 B 的大小 = (short int + char + int) 的大小 = 2 + 1 + 4 = 7。结构 C 的大小 = (char + double + int) 的大小 = 1 + 8 + 4 = 13。结构 D 的大小 = (double + int + char) 的大小 = 8 + 4 + 1= 13。

现在让我们使用给定的 C 程序确认这些结构的大小:

// C Program to demonstrate the structure padding property#include <stdio.h>// Alignment requirements// (typical 32 bit machine)// char     1 byte// short int  2 bytes// int     4 bytes// double    8 bytes// structure Atypedef struct structa_tag {	char c;	short int s;} structa_t;// structure Btypedef struct structb_tag {	short int s;	char c;	int i;} structb_t;// structure Ctypedef struct structc_tag {	char c;	double d;	int s;} structc_t;// structure Dtypedef 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) = 4sizeof(structb_t) = 8sizeof(structc_t) = 24sizeof(structd_t) = 16

正如我们所看到的,结构的大小与我们计算的大小不同。

这是因为各种数据类型的对齐要求,结构的每个成员都应该自然对齐。结构的成员按顺序逐个分配,地址逐渐增加。

让我们分析一下上述程序中声明的每个结构。为方便起见,假设每个结构类型变量都在4字节边界上分配(假设为0x0000),即结构的基地址是4的倍数(不一定总是如此,请参阅structc_t的解释)。

结构 A

structa_t 的第一个元素是 char,占用一个字节对齐,然后是 short int。short int 是2字节对齐。如果 short int 元素直接跟在 char 元素之后分配,它将以奇数地址边界开始。编译器会在 char 之后插入一个填充字节,以确保 short int 具有2的倍数地址(即2字节对齐)。structa_t 的总大小将为,

sizeof(char) + 1 (padding) + sizeof(short), 1 + 1 + 2 = 4 bytes.

结构 B

structb_t 的第一个成员是 short int,后跟 char。由于 char 可以位于任何字节边界,short int 和 char 之间不需要填充,总共占用 3 个字节。下一个成员是 int。如果 int 立即分配,它将以奇数字节边界开始。我们需要在 char 成员后面填充 1 字节,使下一个 int 成员的地址对齐到 4 字节。总共,

the structb_t requires , 2 + 1 + 1 (padding) + 4 = 8 bytes.

结构 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 bytes
如何减少结构填充?

到目前为止,可能已经清楚填充是不可避免的。有一种方法可以最小化填充。程序员应该按照大小的递增/递减顺序声明结构成员。我们代码中给出的 structd_t 就是一个例子,其大小为 16 字节,而不是 structc_t 的 24 字节。

什么是结构体打包?

有时候必须避免结构成员之间的填充字节。例如,读取 ELF 文件头、BMP 文件头或 JPEG 文件头的内容。我们需要定义一个与头部布局类似的结构,并进行映射。然而,在访问这些成员时应该小心。通常,逐字节读取是避免不对齐异常的选项,但性能会受到影响。

大多数编译器提供了非标准扩展来关闭默认的填充,比如编译指示或命令行开关。请查阅相应编译器的文档以获取更多详细信息。

在 GCC 中,我们可以使用以下代码进行结构体打包:

#pragma pack(1)

或者

struct name {    ...}__attribute__((packed));

例子

// C Program to demonstrate the structure packing#include <stdio.h>#pragma pack(1)// structure Atypedef struct structa_tag {	char c;	short int s;} structa_t;// structure Btypedef struct structb_tag {	short int s;	char c;	int i;} structb_t;// structure Ctypedef struct structc_tag {	char c;	double d;	int s;} structc_t;// structure Dtypedef 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) = 3sizeof(structb_t) = 7sizeof(structc_t) = 13sizeof(structd_t) = 13
C中结构填充的常见问题解答对栈应用对齐吗?

是的。栈也是内存。系统程序员应该将栈指针加载为适当对齐的内存地址。通常,处理器不会检查栈的对齐,程序员有责任确保栈内存的正确对齐。任何不对齐都会导致运行时错误。

例如,如果处理器的字长为32位,则栈指针也应该对齐为4字节的倍数。如果 char 数据放在非 0 号银行,那么在内存读取时它将被放置在错误的数据线上。处理器如何处理 char 类型?

通常,处理器会根据指令(例如 ARM 处理器上的 LDRB 指令)识别数据类型。根据存储的银行,处理器将字节移动到最低有效数据线上。当参数通过栈传递时,它们是否受到对齐的限制?

是的。编译器会帮助程序员进行正确的对齐。例如,如果将一个16位值推送到32位宽的栈上,该值会自动使用零填充到32位。请考虑以下程序。

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字节为单位进行内存读取效率较高,为什么 double 类型需要在8字节边界上对齐?

需要注意的是,大多数处理器都会有一个数学协处理器,称为浮点运算单元(FPU)。代码中的任何浮点操作都将被转换为 FPU 指令。主处理器与浮点执行无关。所有这些都在幕后完成。

根据标准,double 类型将占据8个字节。而且,FPU 中执行的每个浮点操作都是64位长的。即使是 float 类型在执行之前也会提升为64位。

FPU 寄存器的64位长度要求 double 类型在8字节边界上进行分配。我假设(我没有确凿的信息)在 FPU 操作中,数据获取可能会有所不同,我的意思是数据总线因为要传递给 FPU。因此,对于期望位于8字节边界上的 double 类型,地址解码将有所不同。这意味着浮点运算单元的地址解码电路将没有最后3个引脚。

标签: #c程序的基本组成单位是什么 #c语言中设两 #c语言右对齐输出 #c语言有哪几种数据存储类型 #c语言优势