C语言中的结构体内存布局

102

我有C#编程背景。像C这样的低级语言对我来说还是新手。

在C#中,struct的内存布局默认由编译器进行排列。编译器可以隐式地重新排序数据字段或在字段之间填充额外的位。因此,我必须指定一些特殊的属性来覆盖此行为以获得精确的布局。

据我所知,C不会默认重新排序或对齐struct的内存布局。但是,我听说有一个很难找到的小例外。

C的内存布局行为是什么?应该重新排序/对齐什么和不应该?

3个回答

146

这是实现特定的,但在实践中规则是(如果没有#pragma pack或类似的内容):

  • 结构体成员按照它们声明的顺序存储。(这是C99标准要求的,前面提到过。)
  • 必要时,在结构体成员之间添加填充,以确保后一个成员使用正确的对齐方式。
  • 每个原始类型T需要sizeof(T)字节的对齐。

因此,给定以下结构体:

struct ST
{
   char ch1;
   short s;
   char ch2;
   long long ll;
   int i;
};
  • ch1位于偏移量0
  • 插入填充字节以对齐...
  • s位于偏移量2
  • ch2位于偏移量4,在s之后立即出现
  • 插入3个填充字节以对齐...
  • ll位于偏移量8
  • i位于偏移量16,紧随ll之后
  • 在结构体末尾添加4个填充字节,以使整个结构体成为8字节的倍数。我在64位系统上进行了检查:32位系统可以允许结构体具有4字节对齐。

因此,sizeof(ST)为24。

通过重新排列成员以避免填充,可以将其减小到16字节:

struct ST
{
   long long ll; // @ 0
   int i;        // @ 8
   short s;      // @ 12
   char ch1;     // @ 14
   char ch2;     // @ 15
} ST;

3
如果必要,会在...之前或之后添加填充内容。最好在你的示例中添加一个最终的char成员。 - Deduplicator
12
原始类型不一定需要对齐 sizeof(T) 字节。例如,在常见的32位架构上,double 是8字节,但通常只需要4字节对齐。此外,结构体末尾的填充仅填充到最宽的结构体成员的对齐方式。例如,一个由3个char变量组成的结构体可能没有填充。 - Matt
2
@dan04,按sizeof(T)的降序布局结构体是否是一个好的实践?这样做会有什么不利影响吗? - RohitMat
第一个成员难道不保证有零偏移量,这意味着它之前不能有任何填充吗? - Some programmer dude

121

在C语言中,编译器允许为每种基本类型指定一些对齐方式。通常情况下,对齐方式是类型大小。但这完全取决于实现。

填充字节会被引入以便每个对象都能得到适当的对齐。重新排序是不被允许的。

可能每个现代编译器都实现了#pragma pack,它允许控制填充并且将其留给程序员来遵守ABI(尽管严格来说它是非标准的)。

C99 §6.7.2.1:

12 结构体或联合体对象的每个非位域成员都以实现定义的方式对齐到适当的类型。

13 在结构体对象内,非位域成员和位域所驻留的单元的地址按照它们声明的顺序增加。指向结构体对象的指针经过适当转换后,指向它的初始成员(如果该成员是位域,则指向所驻留的单元),反之亦然。结构体对象内可能存在未命名的填充,但不能在其开始处出现。


1
一些编译器(例如GCC)实现了与#pragma pack相同的效果,但对语义具有更细粒度的控制。 - Chris Lutz
2
C11还有_Alignas - idmean

14
你可以先阅读数据结构对齐维基百科文章,以更好地理解数据对齐。
来自维基百科文章

数据对齐是指将数据放置在内存偏移量等于某个字大小的倍数上,这会增加系统的性能,因为CPU处理内存的方式。为了对齐数据,可能需要在上一个数据结构的末尾和下一个数据结构的开始之间插入一些无意义的字节,这就是数据结构填充。

来自GCC文档6.54.8结构体打包指令
为了与 Microsoft Windows 编译器兼容,GCC支持一组 #pragma 指令,可更改后续定义的结构体(除零宽度位域以外)、联合体和类成员的最大对齐方式。下面的 n 值必须始终是 2 的幂次方,并指定新的字节对齐方式。
  1. #pragma pack(n) 仅设置新的对齐方式。
  2. #pragma pack() 将对齐方式设置为编译开始时有效的对齐方式(也可以使用命令行选项 -fpack-struct[=] 参见 Code Gen Options)。
  3. #pragma pack(push[,n]) 将当前对齐设置推送到内部堆栈上,然后可选择设置新的对齐方式。
  4. #pragma pack(pop) 将对齐设置恢复为在内部堆栈顶部保存的设置(并删除该堆栈条目)。请注意,#pragma pack([n]) 不影响此内部堆栈;因此可以有 #pragma pack(push) 后跟多个 #pragma pack(n) 实例,最后由一个 #pragma pack(pop) 完成。
一些目标(例如i386和powerpc)支持 ms_struct #pragma,它将结构体布局为文档中记录的 __attribute__ ((ms_struct))
  1. #pragma ms_struct on 打开声明的结构体的布局。
  2. #pragma ms_struct off 关闭声明的结构体的布局。
  3. #pragma ms_struct reset 返回默认布局。

谢谢关心。我按照你的指导修改了问题。 - eonil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接