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

853

sizeof运算符为什么返回的结构体大小比结构体成员变量的总和要大?


20
请参考这篇关于内存对齐的C语言常见问题解答(C FAQ)文章:http://c-faq.com/struct/align.esr.html - Richard Chambers
68
趣闻:曾经有一种真正的计算机病毒,将其代码放在主程序中的结构填充部分。 - Elazar
9
@Elazar 这非常令人印象深刻!我从未想过可以在这么小的区域内使用任何东西。您能否提供更多细节? - Героям слава
3
@Wilson - 我确定它涉及了很多jmp指令。 - hoodaticus
10
请见结构体中的 填充(padding) 和 ***紧凑(packing)***:C语言结构体紧凑技巧,引自 Eric S. Raymond失落的 C 结构体紧凑技巧 - EsmaeelE
显示剩余6条评论
13个回答

789

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

  • 未对齐的访问可能是一个严重的错误 (通常是 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
我在这里想要做个注记:大多数处理器会因为未对齐内存访问而对你进行惩罚(就像你提到的那样),但你不能忘记很多处理器完全禁止它。特别是大多数MIPS芯片将在未对齐访问时抛出异常。 - Serafina Brocious
42
x86芯片的独特之处在于它们允许进行不对齐的访问,尽管会受到惩罚;据我所知,大多数芯片都会抛出异常,而不仅仅是一小部分。PowerPC是另一个常见的例子。 - Dark Shikari
7
启用未对齐访问的编译指示通常会导致代码膨胀,对于那些可能引发不对齐故障的处理器,每次出现不对齐都需要生成修复代码。ARM 处理器也会引发不对齐故障。 - Mike Dimmick
35
未对齐的数据访问通常是CISC架构中的一种特性,而大多数RISC架构不包括它(ARM、MIPS、PowerPC、Cell)。实际上,大多数芯片都不是台式机处理器,对于由大量芯片控制的嵌入式系统而言,绝大多数都是RISC架构。 - Lara Dougan
7
填充量始终足够,以确保下一个元素根据其大小对齐。因此,在“X”中,“short”后面有2个字节的填充,以确保4字节的“int”从4字节边界开始。在“Y”中,“char”后面有1个字节的填充,以确保2字节的“short”从2字节边界开始。由于编译器无法知道内存中结构体之后可能是什么(可能是许多不同的内容),因此它做最坏的打算并插入足够的填充使结构体成为4字节的倍数。“X”需要3个字节才能达到12,“Y”只需要1个字节就可以达到8。 - 8bittree
显示剩余13条评论

256

打包和字节对齐,就像在C语言FAQ这里所描述的:

It's for alignment. Many processors can't access 2- and 4-byte quantities (e.g. ints and long ints) if they're crammed in every-which-way.

Suppose you have this structure:

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

Now, you might think that it ought to be possible to pack this structure into memory like this:

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

But it's much, much easier on the processor if the compiler arranges it like this:

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

In the packed version, notice how it's at least a little bit hard for you and me to see how the b and c fields wrap around? In a nutshell, it's hard for the processor, too. Therefore, most compilers will pad the structure (as if with extra, invisible fields) like this:

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

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

35
如果你想在 GCC 中使结构体拥有特定的大小,例如使用 __attribute__((packed))

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

通常,CPU 更容易访问与平台和编译器相关的多个 4(或 8)字节的数据。

因此,基本上这是一个对齐的问题。

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


7
“好的理由”,例如:“为了明天展示的概念验证演示代码中的复杂结构在32位和64位系统之间保持二进制兼容性(填充)一致。” 有时候需要优先考虑必要性而非适当性。 - Mr.Ree
3
除了提到操作系统之外,一切都很好。这对于CPU速度是一个问题,操作系统根本没有涉及。 - Blaisorblade
8
另一个很好的理由是,如果你正在将数据流填充到结构体中,例如解析网络协议时。 - ceo
1
@dolmen实际上应该讨论ABI(应用程序二进制接口)。默认对齐方式(如果您在源代码中没有更改)取决于ABI,许多操作系统支持多个ABI(例如32位和64位,或来自不同操作系统的二进制文件,或使用不同的方式为相同的操作系统编译相同的二进制文件)。另一方面,什么样的对齐方式从性能上来说更方便,这取决于CPU - 无论您是在32位还是64位模式下访问内存(我无法评论实模式,但现在似乎几乎没有性能问题)。我记得奔腾开始更喜欢8字节对齐。 - Blaisorblade
3
最好使用 #pragma pack(1) - 它被 MSVC、gcc 和 clang 支持,这使得你的代码更具可移植性。 - mvp
显示剩余5条评论

19

这可能是由于字节对齐和填充导致的,使得结构在您的平台上输出为偶数个字节(或单词)。例如,在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字节(2×32位)和1字节(2+6位)。在Linux上使用gcc编译的上述程序将这些大小打印为4、8和4 - 最后一个结构被填充,使其成为一个单词(在我的32位平台上,一个单词是4个8位字节)。

oneInt=4
twoInts=8
someBits=4

6
“在Linux上使用gcc编译C程序”并不足以描述您的平台。对齐方式大多取决于CPU架构。 - dolmen
请原谅,我不明白为什么“someBits”结构的大小等于4,我期望有2个整数声明(2*sizeof(int))= 8字节,因此应该有8个字节。谢谢。 - user1773603
3
嗨@youpilat13,:2:6实际上在这种情况下指定的是2位和6位,而不是完整的32位整数。someBits.x只有2位,因此只能存储4个可能的值:00、01、10和11(1、2、3和4)。这有意义吗?这是一篇关于该功能的文章:https://www.geeksforgeeks.org/bit-fields-c/ - Kyle Burton

13

参见:

对于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
我相当确定在 C 语言中保证了成员顺序。是的,C99 规定:“在结构体对象内,非位域成员和位域所在单元的地址按照它们声明的顺序递增。” 更多标准规范请参见:https://dev59.com/jnVD5IYBdhLWcg3wAWoO#37032302 - Ciro Santilli OurBigBook.com
5
在C ++中有一些顺序保证:“在没有中间访问说明符声明的(非联合)类的非静态数据成员被分配,以便后面的成员在类对象内具有更高的地址” - jfs

11

结构体的大小大于其各部分之和,这是因为所谓的打包。特定处理器有首选的数据大小。大多数现代处理器的首选大小为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,但只能获得其中的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)。


这篇文章讨论了结构填充的后果,但并没有回答这个问题。 - Keith Thompson
“... because of what is called packing…” - 我认为你的意思是“填充(padding)”而不是“打包(packing)”。“现代大多数处理器首选的大小是32位(4字节)”- 这有点过于简化了。通常支持8、16、32和64位大小;每个大小通常都有自己的对齐方式。我不确定你的答案是否提供了任何新信息,这些信息不在已接受的答案中。 - Keith Thompson
2
当我说打包时,我指的是编译器如何将数据打包到结构中(它可以通过填充小项来实现打包,但不需要填充,但它总是会打包)。至于大小 - 我谈论的是系统架构,而不是系统将支持的数据访问(这与底层总线架构有很大不同)。至于你的最后评论,我给出了一个权衡(速度与大小)的简化和扩展解释 - 这是一个重要的编程问题。我还描述了一种解决问题的方法 - 这不在被接受的答案中。 - sid1138
在这个上下文中,“Packing”通常指比默认情况下更紧密地分配成员,例如使用#pragma pack。如果成员按照它们的默认对齐方式进行分配,我通常会说该结构被打包。 - Keith Thompson
1
打包是一个有点多义的术语。它指的是如何将结构元素放入内存中,类似于将物体装入箱子(搬家时的打包)。它还意味着在没有填充的情况下将元素放入内存中(有点像“紧密打包”的简写)。然后,在#pragma pack命令中,这个词也有命令版本的含义。 - sid1138

10

C99 N1256标准草案

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

6.5.3.4 sizeof运算符:

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

6.7.2.1 结构体和联合体说明符:

13 ... 结构体对象内部可能存在未命名的填充,但不在其开头。

并且:

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

新的C99 灵活数组成员特性 (struct S {int is[];};) 也可能会影响填充:

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

Annex J 可移植性问题 再次强调:

以下内容是未指定的: ...

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

C++11 N3337标准草案

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

5.3.3 Sizeof:

2 当应用于类时,结果是该类对象中的字节数,包括为将该类型的对象放置在数组中而需要的任何填充。

9.2 类成员:

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

我只知道足够的C++,可以理解这个注释 :-)


8

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

此外,某个库可能是在使用32位整型的x86下编译的,而您可能在64位进程中比较其组件,如果您手动完成此操作,则会得到不同的结果。


7

C语言在内存中的结构元素的位置方面留给编译器一些自由度:

  • 任何两个组件之间以及最后一个组件之后都可能出现内存空洞(memory holes),这是因为目标计算机上某些类型的对象可以受到寻址边界的限制。
  • 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)"


7

这个想法是为了提高速度和缓存效果,操作数应该从与它们自然大小对齐的地址中读取。为了实现这一点,编译器会填充结构成员,以便下一个成员或下一个结构体能够对齐。

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的一部分,并表示最小对齐方式,最终将均匀填满缓存行。

简而言之:对齐很重要。


ISO C的处理方式是,alignof(int)是程序中任何可能存在的int的最小对齐方式,包括在struct内部。每种类型的最小对齐方式由实现定义,这并不总是它的大小(例如,在某些32位平台上,alignof(long long) == 4)。这就是ISO C允许实现适应不同CPU要求的方式。通常的选择是将类型对齐到其大小或寄存器宽度(如果更小),但alignof(int)==1是您可以考虑用于x86的一种情况。 - Peter Cordes
整个结构体的对齐方式至少是任何成员的 alignof() 的最大值。(packed 结构体是一种扩展,它改变了规则;它们允许您创建一个未对齐的 int,因此只能通过结构体安全地访问,而不能取指向它的指针。请参见 为什么对 mmap'ed 内存的未对齐访问有时会在 AMD64 上导致 segfault? 以获取关于未对齐指针的示例,即使在编译非 SIMD 加载不需要对齐的 x86 时,也是 C 未定义行为。) - Peter Cordes

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