【嵌入式编程】-C语言结构体成员对齐、填充和数据打包

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


在 C 语言中,结构用作数据包。它们不提供任何数据封装或数据隐藏功能。在本文中,我们将讨论 C 语言中结构体填充的属性以及数据对齐和结构打包。

1、内存中的数据对齐

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

在这里插入图片描述

从历史上看,内存是字节可寻址的,并按顺序排列。如果内存被安排为一个字节宽度的单个组,则处理器需要发出 4 个内存读取周期来获取整数。在一个内存周期内读取整数的所有 4 个字节更经济。为了利用这种优势,存储器将排列为一组 4 个组,如上图所示。

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

在这里插入图片描述

一个变量的数据对齐方式涉及到数据在这些存储区中的存储方式。例如,在 32 位计算机上,int 的自然对齐方式为 4 个字节。当数据类型自然对齐时,CPU 会在最短的读取周期内获取它。

同样,短 int 的自然对齐方式为 2 个字节。这意味着短 int 可以存储在 bank 0 – bank 1 对或 bank 2 – bank 3 对中。双精度需要 8 个字节,并在内存库中占据两行。任何 double 的未对齐都将强制两个以上的读取周期来获取 double 数据。

请注意,在 32 位计算机上,将在 8 字节边界上分配双精度变量,并且需要两个内存读取周期。在 64 位机器上,基于多个Bank,将在 8 字节边界上分配一个双精度变量,并且只需要一个内存读取周期。

2、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;

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

  • struct A 的大小 = (char + short int) 的大小 = 1 + 2 = 3。
  • struct B 的大小 = (short int + char + int) = 2 + 1 + 4 = 7 。
  • struct C 的大小 = (char + double + int) 的大小 = 1 + 8 + 4 = 13。
  • struct D 的大小 = (double + int + char) 的大小 = 8 + 4 + 1= 13。
// 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 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; 
}

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

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

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

structure A
structa_t第一个元素是 char,它对齐一个字节,然后是 short int。short int 是 2 个字节对齐。如果短 int 元素在 char 元素之后立即分配,它将从一个奇数地址边界开始。编译器将在 char 后插入一个填充字节,以确保短 int 的地址倍数为 2(即 2 个字节对齐)。structa_t的总大小将是:

sizeof(char) + 1 (padding) + sizeof(short), 1 + 1 + 2 = 4字节

structure B

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

2 + 1 + 1 (padding) + 4 = 8字节

structure 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 字节。即使在阵列中,它也能保证正确的对齐。

structure D

以类似的方式,结构 D 的大小为:

sizeof(double) + sizeof(int) + sizeof(char) + padding(3) = 8 + 4 + 1 + 3 = 16字节

3、如何减少结构填充

到现在为止,我们知道填充可能是不可避免的。有一种方法可以最小化填充。程序员应按大小递增/递减的顺序声明结构成员。我们的代码中structd_t给出了一个示例,其大小为 16 个字节,而不是 24 个字节的structc_t。

什么是结构体对齐?

有时,必须避免在结构成员之间填充字节。例如,读取 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 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

4、C语言中结构填充的常见问题解答

1)堆栈是否应用对齐?

是的。堆栈也是内存。系统程序员应使用正确对齐的内存地址加载堆栈指针。通常,处理器不会检查堆栈对齐,程序员有责任确保堆栈内存的正确对齐。任何错位都会导致运行时意外。
例如,如果处理器字长度为 32 位,则堆栈指针也应对齐为 4 个字节的倍数。

2)如果 char 数据放置在 bank 0 以外的 bank 中,则在读取内存时,它将被放置在错误的数据行上。处理器如何处理字符类型?

通常,处理器会根据指令识别数据类型(例如,ARM 处理器上的 LDRB)。根据存储的字节库,处理器将字节移位到最低有效数据线上。

3)当参数在堆栈上传递时,它们是否受到对齐的影响?

是的。编译器帮助程序员进行正确的对齐。例如,如果将 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)如果我们尝试访问未对齐的数据,会发生什么?

这取决于处理器体系结构。如果访问未对齐,处理器会自动发出足够的内存读取周期,并将数据正确打包到数据总线上。惩罚是针对绩效的。而很少有处理器没有最后两条地址线,这意味着无法访问奇数字节边界。每个数据访问都必须正确对齐(4 个字节)。未对准访问是此类处理器的一个关键例外。如果忽略异常,则读取的数据将不正确,因此结果将不正确。

5)有没有办法查询数据类型的对齐要求?

是的。编译器为此类需求提供非标准扩展。例如,Visual Studio 中的 __alignof() 有助于获取数据类型的对齐要求。有关详细信息,请阅读 MSDN。

6)当内存读取在 32 位计算机上一次读取 4 个字节时,为什么要在 8 字节边界上对齐双精度类型?

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

按照标准,double 类型将占用 8 个字节。而且,在 FPU 中执行的每个浮点运算都将是 64 位长度。甚至浮点类型也会在执行之前提升到 64 位。

FPU 寄存器的 64 位长度强制在 8 字节边界上分配 double 类型。假设在 FPU 操作的情况下,数据获取可能会有所不同,意思是数据总线,因为它进入 FPU。因此,对于双精度类型(预计位于 8 字节边界上),地址解码将有所不同。这意味着浮点单元的地址解码电路将没有最后 3 个引脚。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

视觉&物联智能

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值