为什么结构体的sizeof不等于每个成员的sizeof之和?

为什么sizeof运算符返回的结构大小比结构成员的总大小大?

268778 次浏览

这是因为添加了填充来满足对齐约束。数据结构对齐会影响程序的性能和正确性:

  • 错误对齐的访问可能是一个硬错误(通常为SIGBUS)。
  • 错误对齐的访问可能是一个软错误。
    • 要么在硬件中进行校正,以实现适度的性能下降。
    • 或者通过软件中的仿真来纠正严重的性能下降。
    • 此外,原子性和其他并发保证可能会被破坏,从而导致微妙的错误。

以下是使用x86处理器的典型设置的示例(均使用32位和64位模式):

struct X{short s; /* 2 bytes *//* 2 padding bytes */int   i; /* 4 bytes */char  c; /* 1 byte *//* 3 padding bytes */};
struct Y{int   i; /* 4 bytes */char  c; /* 1 byte *//* 1 padding byte */short s; /* 2 bytes */};
struct Z{int   i; /* 4 bytes */short s; /* 2 bytes */char  c; /* 1 byte *//* 1 padding byte */};
const int sizeX = sizeof(struct X); /* = 12 */const int sizeY = sizeof(struct Y); /* = 8 */const int sizeZ = sizeof(struct Z); /* = 8 */

可以通过按对齐方式对成员进行排序来最小化结构的大小(按大小排序足以满足基本类型的要求)(如上面示例中的结构Z)。

重要注意:C和C++标准都规定结构对齐是实现定义的。因此,每个编译器可能会选择以不同的方式对齐数据,导致数据布局不同且不兼容。因此,在处理不同编译器将使用的库时,了解编译器如何对齐数据很重要。一些编译器有命令行设置和/或特殊的#pragma语句来更改结构对齐设置。

如果您隐式或显式设置了结构的对齐方式,它可以这样做。对齐为4的结构将始终是4字节的倍数,即使其成员的大小不是4字节的倍数。

此外,一个库可能在x86下编译为32位整数,如果您手动执行此操作,您可能会在64位进程上比较其组件会给您不同的结果。

打包和字节对齐,如C FAQ这里中所述:

这是为了对齐。许多处理器无法访问2字节和4字节数量(例如整数和长整数),如果它们被填满什么都可以

假设你有这样的结构:

struct {char a[3];short int b;long int c;char d[3];};

现在,你可能会认为应该可以打包这个结构到内存中,如下所示:

+-------+-------+-------+-------+|           a           |   b   |+-------+-------+-------+-------+|   b   |           c           |+-------+-------+-------+-------+|   c   |           d           |+-------+-------+-------+-------+

但是如果编译器排列,它在处理器上要容易得多是这样的:

+-------+-------+-------+|           a           |+-------+-------+-------+|       b       |+-------+-------+-------+-------+|               c               |+-------+-------+-------+-------+|           d           |+-------+-------+-------+

在打包的版本中,注意到它至少有点难你和我来看看b和c字段是如何环绕的?简而言之,这对处理器来说也很困难。因此,大多数编译器都会结构(好像有额外的不可见字段)如下:

+-------+-------+-------+-------+|           a           | pad1  |+-------+-------+-------+-------+|       b       |     pad2      |+-------+-------+-------+-------+|               c               |+-------+-------+-------+-------+|           d           | pad3  |+-------+-------+-------+-------+

这可能是由于字节对齐和填充,因此结构在您的平台上出现偶数个字节(或单词)。例如在C onLinux中,以下3个结构:

#include "stdio.h"

struct oneInt {int x;};
struct twoInts {int x;int y;};
struct someBits {int x:2;int y:6;};

int main (int argc, char** argv) {printf("oneInt=%zu\n",sizeof(struct oneInt));printf("twoInts=%zu\n",sizeof(struct twoInts));printf("someBits=%zu\n",sizeof(struct someBits));return 0;}

成员的大小(以字节为单位)分别为4字节(32位)、8字节(2x 32位)和1字节(2+6位)。上述程序(在使用gcc的Linux)将大小打印为4、8和4-其中最后一个结构被填充,以便它是一个单词(在我的32位平台上为4 x 8位字节)。

oneInt=4twoInts=8someBits=4

如果您希望结构具有一定的大小,例如使用GCC,请使用__attribute__((packed))

在Windows上,当使用带有/Zp选择的cl.exe编译器时,您可以将对齐设置为一个字节。

通常,CPU更容易访问4(或8)倍数的数据,具体取决于平台和编译器。

所以这基本上是一个对齐的问题。

你需要有充分的理由来改变它。

除了其他答案之外,结构可以(但通常没有)具有虚函数,在这种情况下,结构的大小也将包括vtbl的空间。

另见:

对于Microsoft Visual C:

http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx

GCC声称与Microsoft的编译器兼容:

https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Structure_002dPacking-Pragmas.html

除了前面的答案之外,请注意,无论包装如何,编译器都可以(并且肯定会)将虚表指针和基结构的成员添加到结构中。即使是虚拟表的存在也不能由标准确保(没有指定虚拟机制实现),因此可以得出结论,这种保证是不可能的。

我很确定成员顺序在C中得到保证,但在编写跨平台或跨编译器程序时,我不会指望它。

由于所谓的打包,结构的大小大于其各部分的总和。特定的处理器有一个首选的数据大小。大多数现代处理器的首选大小是32位(4字节)。当数据位于这种边界上时访问内存比跨越该大小边界的东西更有效。

例如,考虑一个简单的结构:

struct myStruct{int a;char b;int c;} data;

如果机器是一台32位机器,数据在32位边界上对齐,我们会看到一个直接的问题(假设没有结构对齐)。在这个例子中,让我们假设结构数据从地址1024开始(0x400——注意最低的2位为零,因此数据与32位边界对齐)。对data. a的访问将正常工作,因为它从边界-0x400开始。对data. b的访问也将正常工作,因为它位于地址0x404——另一个32位边界。但是一个未对齐的结构将把data. c放在地址0x405。data. c的4个字节在0x405、0x406、0x407、0x408。在32位机器上,系统将在一个内存周期内读取data. c,但只能获得4个字节中的3个(第4个字节在下一个边界上)。因此,系统必须进行第二次内存访问才能获得第4个字节,

现在,如果编译器不是将data. c放在地址0x405,而是将结构填充3个字节并将data. c放在地址0x408,那么系统只需要1个周期来读取数据,将对该数据元素的访问时间缩短50%。填充交换内存效率以提高处理效率。鉴于计算机可以拥有大量内存(数千兆字节),编译器认为交换(速度超过大小)是合理的。

不幸的是,当你试图通过网络发送结构甚至将二进制数据写入二进制文件时,这个问题就成了一个杀手。在结构或类的元素之间插入的填充会破坏发送到文件或网络的数据。为了编写可移植代码(一个将去往多个不同编译器的代码),你可能必须单独访问结构的每个元素以确保正确的“打包”。

另一方面,不同的编译器有不同的能力来管理数据结构打包。例如,在Visual C/C++编译器支持#pragma pack命令。这将允许您调整数据打包和对齐。

例如:

#pragma pack 1struct MyStruct{int a;char b;int c;short d;} myData;
I = sizeof(myData);

我现在应该有11的长度。如果没有Pragma,我可以是11到14(对于某些系统,多达32),具体取决于编译器的默认打包。

C语言让编译器对结构元素在内存中的位置有一些自由:

  • 内存漏洞可能出现在任何两个组件之间以及最后一个组件之后这是由于目标计算机上某些类型的对象可能会受到寻址边界的限制
  • sizeof运算符结果中包含的“内存孔”大小。sizeof仅不包括灵活数组的大小,这在C/C++中可用
  • 该语言的一些实现允许您通过Pragma和编译器选项控制结构的内存布局

C语言为程序员提供了结构中元素布局的一些保证:

  • 编译器需要分配一系列组件增加内存地址
  • 第一组件的地址与结构的起始地址一致
  • 未命名的位字段可以包含在结构中以要求相邻元素的地址对齐

与元素对齐相关的问题:

  • 不同的计算机以不同的方式排列物体的边缘
  • 位域宽度的不同限制
  • 计算机在如何将字节存储在一个字上存在差异(Intel 80x86和Motorola 68000)

对齐如何工作:

  • 结构占用的体积计算为此类结构数组中对齐的单个元素的大小。结构应该结束,使下一个结构的第一个元素不违反对齐的要求

p. s更详细的信息可在此处获得:“Samuel P. Harbison,Guy L. Steele C A Reference,(5.6.2-5.6.7)”

这个想法是出于速度和缓存考虑,应该从与其自然大小对齐的地址读取操作数。为了实现这一点,编译器会填充结构成员,以便以下成员或以下结构将对齐。

struct pixel {unsigned char red;   // 0unsigned char green; // 1unsigned int alpha;  // 4 (gotta skip to an aligned offset)unsigned char blue;  // 8 (then skip 9 10 11)};
// next offset: 12

x86架构一直能够获取未对齐的地址。然而,它更慢,当未对齐重叠两个不同的缓存线时,当对齐的访问只会逐出一个缓存线时,它会逐出两个缓存线。

一些架构实际上必须捕获未对齐的读写,以及ARM架构的早期版本(演变成今天所有移动CPU的版本)……好吧,他们实际上只是为这些返回了坏数据。(他们忽略了低阶位。)

最后,请注意缓存行可以任意大,编译器不会尝试猜测这些行或进行空间与速度的权衡。相反,对齐决策是ABI的一部分,代表最终将均匀填满缓存行的最小对齐。

太长别读:对齐很重要。

C99 N1256标准草案

http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf

6.5.3.4sizeof运算符

3当应用于具有结构或联合类型的操作数时,结果是这样一个对象中的总字节数,包括内部和尾随填充。

6.7.2.1结构和联合说明符

13…可能有未命名的在结构对象中填充,但不是在其开头。

和:

15结构或联合的末尾可能有未命名的填充。

新的C99柔性阵列成员特征struct S {int is[];};)也可能影响填充:

16作为特例,具有多个命名成员的结构的最后一个元素可能具有不完整的数组类型;这称为灵活数组成员。在大多数情况下,柔性数组成员被忽略。特别是,结构的大小就像省略了柔性数组成员,只是它可能具有比#36825;的暗示

附件J可移植性问题重申:

以下未指定:…

  • 在结构或联合中存储值时填充字节的值(6.2.6.1)

C++11 N3337标准草案

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

5.3.3尺寸

2应用时对于一个类,结果是该类的对象中的字节数,包括所需的任何填充将该类型的对象放入数组中。

9.2类成员

指向标准布局结构对象的指针,使用reinterpret_cast适当转换,指向其初始成员(或者如果该成员是位域,则为其所在的单元),反之亦然。因此,标准布局结构对象中可能存在未命名的填充,但不是在其开始时,必要时实现适当的对齐。-结束注释]

我只知道足够的C++来理解笔记:-)

在关于内存对齐和结构填充/打包的其他解释充分的答案中,通过仔细阅读,我在问题本身中发现了一些东西。

"为什么结构的#0不等于每个成员的#0之和?"

为什么#0运算符返回的结构大小比结构成员的总大小大”?

这两个问题都暗示了一些明显的错误。至少在一个通用的、非示例的集中视图中,这就是这里的情况。

应用于结构对象可以sizeof操作数的结果等于单独应用于每个成员的sizeof的总和。它没有必须更大/不同。

如果没有填充的理由,则不会填充内存。


大多数实现,如果结构仅包含相同类型的成员:

struct foo {int a;int b;int c;} bar;

假设sizeof(int) == 4,结构bar的大小将等于所有成员的大小之和,sizeof(bar) == 12。这里没有填充。

例如,这里也是如此:

struct foo {short int a;short int b;int c;} bar;

假设sizeof(short int) == 2sizeof(int) == 4ab的分配字节之和等于c的分配字节,最大的成员,并且一切都完全对齐。因此,sizeof(bar) == 8

这也是关于结构填充的第二个最受欢迎的问题的对象,这里:

上面给出了很多信息(解释)。

并且,我只是想分享一些方法来解决这个问题。

您可以通过添加Pragma包来避免它

#pragma pack(push, 1)
// your structure
#pragma pack(pop)