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

Why isn't sizeof for a struct equal to the sum of sizeof of each member?

提问人:Kevin 提问时间:9/23/2008 最后编辑:unalignedmemoryaccessKevin 更新时间:4/26/2022 访问量:289290

问:

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

C++ C 结构 体大小 C++-FAQ

评论

20赞 Richard Chambers 4/26/2013
请参阅此 C 常见问题解答,了解内存性能。c-faq.com/struct/align.esr.html
68赞 Elazar 9/3/2013
轶事:有一种实际的计算机病毒将其代码放在主机程序的结构填充中。
9赞 Omar and Lorraine 11/15/2016
@Elazar 这太令人印象深刻了!我从没想过可以将这么小的区域用于任何事情。中方能否提供更多细节?
3赞 hoodaticus 6/28/2017
@Wilson - 我敢肯定它涉及很多jmp。
10赞 EsmaeelE 12/9/2017
参见结构填充、填料失落的 C 结构填料艺术 Eric S. Raymond

答:

789赞 6 revs, 6 users 79%Kevin #1

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

  • 未对齐的访问可能是一个硬错误(通常)。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

评论

45赞 Serafina Brocious 9/23/2008
我想在这里做一个说明:大多数处理器会因为未对齐的内存访问而惩罚你(正如你提到的),但你不能忘记许多人完全不允许它。特别是大多数MIPS芯片,会在未对齐的访问上抛出异常。
42赞 Dark Shikari 9/23/2008
x86 芯片实际上相当独特,因为它们允许不对齐的访问,尽管会受到惩罚;AFAIK大多数芯片都会抛出异常,而不仅仅是少数。PowerPC是另一个常见的例子。
7赞 Mike Dimmick 9/23/2008
为未对齐的访问启用编译指示通常会导致代码在引发错位错误的处理器上膨胀,因为必须生成修复每个错位的代码。ARM 还会出现错位故障。
35赞 Lara Dougan 10/19/2008
未对齐的数据访问通常是 CISC 架构中的一个特性,大多数 RISC 架构(ARM、MIPS、PowerPC、Cell)不包括它。实际上,大多数芯片都不是桌面处理器,因为嵌入式芯片数量是按芯片数量来决定的,其中绝大多数是RISC架构。
7赞 8bittree 2/18/2017
@WayneO 填充量始终足以确保接下来的任何内容都根据其大小对齐。因此,在 中,后面有 2 个字节的填充,以确保 4 字节从 4 字节边界开始。在 中,后面有 1 个字节的填充,以确保 2 个字节从 2 个字节的边界开始。由于编译器无法知道内存中结构后面可能是什么(并且可能是许多不同的东西),因此它会为最坏的情况做好准备,并插入足够的填充以使结构成为 4 个字节的倍数。 需要 3 个字节才能达到 12,只需要 1 个字节才能达到 8。XshortintYcharshortXY
8赞 Orion Adrian 9/23/2008 #2

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

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

256赞 EmmEff 9/23/2008 #3

打包和字节对齐,如此处的 C 常见问题解答中所述:

这是为了对齐。许多处理器无法访问 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  |
+-------+-------+-------+-------+

评论

2赞 Lakshmi Sreekanth Chitla 12/26/2016
现在内存插槽 pad1、pad2 和 pad3 有什么用。
7赞 phuclv 3/2/2017
@YoYoYonnY这是不可能的。编译器不允许对结构成员进行重新排序,尽管 gcc 有一个实验性选项可以这样做
0赞 Balázs Börcsök 12/8/2019
@EmmEff这可能是错误的,但我不太明白:为什么数组中没有指针的内存插槽?
2赞 kbolino 3/31/2020
@BalázsBörcsök 这些是恒定大小的数组,因此它们的元素以固定的偏移量直接存储在结构中。编译器在编译时知道所有这些,因此指针是隐式的。例如,如果您有一个名为 then and 的此类型的结构变量(给定答案中所示的对齐方式)。仅当数组具有可变大小时(例如,已声明而不是 ),才存储指针,但随后必须将元素存储在其他位置。s&s.a == &s&s.d == &s + 12achar a[]char a[3]
1赞 puppydrum64 11/22/2022
@LakshmiSreekanthChitla 它们的存在只是为了占用空间。许多 CPU 体系结构(如 ARM)无法从不以 0、4、8 或 C 结尾的内存地址读取数据。因此,为了确保结构的每个成员都可以访问,这些空间被故意占用,以便下一个实际数据片段位于可以读取的地址。
19赞 Kyle Burton 9/23/2008 #4

这可能是由于字节对齐和填充,因此结构在您的平台上显示为偶数个字节(或字)。例如,在 Linux 上的 C 语言中,以下 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=4
twoInts=8
someBits=4

评论

6赞 dolmen 8/20/2013
“Linux 上使用 gcc 的 C 语言”不足以描述您的平台。对齐主要取决于 CPU 架构。
0赞 7/4/2018
-@Kyle伯顿。对不起,我不明白为什么结构“someBits”的大小等于 4,我期望 8 个字节,因为声明了 2 个整数 (2*sizeof(int)) = 8 个字节。谢谢
3赞 Kyle Burton 7/13/2018
嗨,@youpilat13,和实际上指定了 2 位和 6 位,而不是在这种情况下的完整 32 位整数。someBits.x,只有 2 位,只能存储 4 个可能的值:00、01、10 和 11(1、2、3 和 4)。这有意义吗?这是一篇关于该功能的文章:geeksforgeeks.org/bit-fields-c:2:6
35赞 3 revs, 2 users 96%INS #5

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

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

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

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

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

评论

7赞 Mr.Ree 12/8/2008
“充分的理由” 示例:在明天将展示的概念验证演示代码中,保持 32 位和 64 位系统之间的二进制兼容性(填充)一致。有时,必要性必须优先于适当性。
3赞 Blaisorblade 1/12/2009
一切都很好,除非你提到操作系统。这是CPU速度的问题,根本不涉及操作系统。
8赞 ceo 10/20/2009
另一个很好的理由是,如果你将数据流塞进一个结构体中,例如在解析网络协议时。
1赞 Blaisorblade 8/25/2013
@dolmen我刚才指出“Operatin系统更容易访问数据”是不正确的,因为操作系统不访问数据。
3赞 mvp 3/21/2021
最好使用 - MSVC、gcc 和 clang 支持它,这使您的代码更具可移植性#pragma pack(1)
5赞 JohnMcG 9/23/2008 #6

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

评论

11赞 Don Wakefield 10/18/2008
差一点。在典型的实现中,添加到结构中的是 vtable 指针
13赞 lkanab 5/31/2011 #7

另请参阅:

对于 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++ 中都没有成员订单保证。编译器可以(当然也会)将虚拟表指针和基结构的成员添加到结构中。即使标准也无法保证虚拟表的存在(没有指定虚拟机制的实现),因此可以得出结论,这种保证是不可能的。

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

评论

5赞 Ciro Santilli OurBigBook.com 5/4/2016
“我很确定成员订单在 C 中咕噜咕噜”。是的,C99 说:“在结构对象中,非位域成员和位域所在的单元具有按照声明顺序递增的地址。更多标准优点: stackoverflow.com/a/37032302/895245
11赞 sid1138 6/10/2015 #8

结构的尺寸大于其各部分的总和,因为所谓的填料。特定处理器具有它使用的首选数据大小。大多数现代处理器的首选大小为 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 1
struct MyStruct
{
    int a;
    char b;
    int c;
    short d;
} myData;

I = sizeof(myData);

我现在应该有 11 的长度。如果没有编译指示,我可能是 11 到 14 之间的任何值(对于某些系统,最多为 32 个),具体取决于编译器的默认打包。

评论

0赞 Keith Thompson 6/10/2015
这讨论了结构填充的后果,但它没有回答这个问题。
0赞 Keith Thompson 6/13/2015
"。因为所谓的包装。...--我想你的意思是“填充”。“大多数现代处理器的首选大小是 32 位(4 字节)”——这有点过于简单化了。通常支持 8、16、32 和 64 位的大小;通常,每种尺寸都有自己的对齐方式。而且我不确定您的答案是否添加了任何已接受答案中尚未包含的新信息。
2赞 sid1138 6/13/2015
当我说打包时,我的意思是编译器如何将数据打包到结构中(它可以通过填充小项目来实现,但它不需要填充,但它总是打包)。至于大小 - 我说的是系统架构,而不是系统将支持什么数据访问(这与底层总线架构有很大不同)。至于你的最后评论,我对权衡的一个方面(速度与大小)进行了简化和扩展的解释——一个主要的编程问题。我还描述了一种解决问题的方法 - 这不在公认的答案中。
0赞 Keith Thompson 6/13/2015
在此上下文中,“打包”通常是指比默认成员更紧密地分配成员,如 .如果成员是按其默认对齐方式分配的,我通常会说结构没有打包。#pragma pack
1赞 sid1138 6/14/2015
包装是一个超载的术语。这意味着你如何将结构元素放入内存中。类似于将物体放入盒子(移动包装)的含义。这也意味着将元素放入内存中,没有填充(有点像“紧密包装”的简写)。然后是 #pragma pack 命令中单词的命令版本。
7赞 Konstantin Burlachenko 7/29/2015 #9

C 语言为编译器提供了一些关于内存中结构元素位置的自由:

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

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

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

与元素对齐相关的问题:

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

对齐的工作原理:

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

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

7赞 DigitalRoss 2/24/2016 #10

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

struct pixel {
    unsigned char red;   // 0
    unsigned char green; // 1
    unsigned 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 的一部分,表示最终将均匀填满缓存行的最小对齐。

TL;DR:对齐很重要。

评论

0赞 Peter Cordes 3/16/2023
从形式上讲,ISO C 处理此问题的方式是程序中可能存在的任何内容的最小对齐方式,包括 .由实现来定义每种类型的最小对齐方式,这并不总是其大小(例如 在某些 32 位平台上。这就是 ISO C 允许实现适应不同 CPU 要求的方式。常见的选择是将类型与其大小对齐,或者与寄存器宽度(如果较小)对齐,但您可以考虑将其用于 x86。alignof(int)intstructalignof(long long) == 4alignof(int)==1
0赞 Peter Cordes 3/16/2023
整个结构的对齐方式至少是任何成员的最大值。( structs 是一个扩展,它改变了规则;它们允许你创建一个未对齐的结构,所以只有通过结构体访问才是安全的,而不是获取指向它的指针。请参阅为什么在 AMD64 上对 mmap 内存的未对齐访问有时会出现段错误?,了解未对齐指针的示例,即使在针对 x86 进行编译时,非 SIMD 负载也不需要对齐。alignof()packedint
10赞 Ciro Santilli OurBigBook.com 5/4/2016 #11

C99 N1256标准草案

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

6.5.3.4 算子的大小

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

6.7.2.1 结构和联合说明符

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

和:

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

新的 C99 灵活阵列成员功能 () 也可能影响填充:struct S {int is[];};

16 作为特殊情况,具有多个命名成员的结构的最后一个元素可能 数组类型不完整;这称为灵活数组成员。在大多数情况下, 灵活数组成员将被忽略。特别是,结构的大小好像 省略了灵活的数组成员,只是它可能具有比 省略意味着。

附件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++ 来理解注释:-)

4赞 RobertS supports Monica Cellio 6/29/2020 #12

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

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

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

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

应用于结构对象的操作数的结果可以等于分别应用于每个成员的总和。它不必更大/不同。sizeofsizeof

如果没有填充的原因,则不会填充内存。


如果结构仅包含相同类型的成员,则最一种实现:

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

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

例如,这里也是如此:

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

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

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

评论

1赞 philipxy 10/12/2020
“如果没有填充的理由,就不会填充内存。”这是无益和误导的。语言有一个定义,这不是基于它。它属于典型/假设实现的部分。(你有)。然后是重言式。(我意识到这可能是修辞性的。
3赞 Zrn-dev 4/26/2022 #13

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

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

您可以通过添加编译指示包来避免它

#pragma pack(push, 1)

// your structure

#pragma pack(pop) 

评论

0赞 Arian Sakhaei 10/4/2023
完美的答案是没有得到足够的信任!TNX很多。
0赞 stackoverblown 11/13/2023
问题不在于如何避免它。那么,我们为什么要给予它信任呢?