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


697

为什么sizeof操作员返回的结构尺寸大于结构成员的总尺寸?


14
请参阅此C常见问题解答中有关内存分配的信息。c-faq.com/struct/align.esr.html
理查德·钱伯斯

48
轶事:有一种实际的计算机病毒,它会将其代码放入宿主程序的struct padding中。
Elazar 2013年

4
@Elazar令人印象深刻!我从来没有想到过可以将如此小的区域用于任何事情。您是否可以提供更多详细信息?
威尔逊

1
@Wilson-我确定它涉及很多jmp。
hoodaticus

4
请参见结构填充,打包C结构打包的失落艺术Eric S. Raymond
EsmaeelE

Answers:


649

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

  • 对齐错误的访问可能是一个硬错误(通常是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语句来更改结构对齐设置。


38
我想在这里做个说明:大多数处理器都会因未对齐的内存访问而对您造成不利影响(如您所提到的那样),但是您不能忘记有很多完全不允许这样做。特别是大多数MIPS芯片在未对齐访问时将引发异常。
科迪·布罗维奇

35
实际上,x86芯片非常独特,因为它们允许不对齐的访问,尽管受到了惩罚。AFAIK 大多数芯片都会抛出异常,而不仅仅是少数。PowerPC是另一个常见示例。
Dark Shikari

6
在未对齐访问的处理器上启用未对齐访问的编译指示通常会导致代码尺寸膨胀,因为必须生成用于解决所有未对齐问题的代码。ARM还引发未对齐错误。
Mike Dimmick

5
@Dark-完全同意。但是大多数台式机处理器都是x86 / x64,因此大多数芯片不会发出数据对齐错误;)
Aaron

27
未对齐的数据访问通常是CISC体系结构中的一项功能,大多数RISC体系结构均不包含此功能(ARM,MIPS,PowerPC,Cell)。实际上,对于大多数芯片而言,嵌入式规则并不是大多数台式机处理器,而其中大多数是RISC架构。
Lara Dougan

191

包装和字节对齐,如在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  |
+-------+-------+-------+-------+

1
现在,内存插槽pad1,pad2和pad3的用途是什么。
Lakshmi Sreekanth Chitla,2016年

7
@YoYoYonnY这是不可能的。尽管gcc有实验性的选择,不允许编译器对结构成员进行重新排序
phuclv

@EmmEff这可能是错误的,但我不太明白:为什么数组中的指针没有内存插槽?
巴拉兹Börcsök

1
@BalázsBörcsök这些是恒定大小的数组,因此它们的元素以固定的偏移量直接存储在结构中。编译器在编译时就知道了这一切,因此指针是隐式的。例如,如果您有一个名为sthen &s.a == &s和的这种类型的结构变量&s.d == &s + 12(给出了答案中显示的对齐方式)。仅当数组具有可变大小(例如,a声明为char a[]而不是char a[3])时才存储指针,但随后必须将元素存储在其他位置。
kbolino

27

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

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

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

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

您需要有充分的理由进行更改。


5
“充分的理由”示例:在概念验证演示代码中,针对复杂结构在32位和64位系统之间保持二进制兼容性(填充)一致,该演示代码将在明天展示。有时,必须优先于礼节。
Ree先生

2
一切正常,除非您提到操作系统。这是CPU速度的问题,根本不涉及OS。
Blaisorblade

3
另一个很好的理由是,例如,在解析网络协议时,是否将数据流填充到结构中。
首席执行官2009年

1
@dolmen我刚刚指出,“ Operatin系统更容易访问数据”是不正确的,因为OS无法访问数据。
Blaisorblade

1
@dolmen实际上,应该谈论ABI(应用程序二进制接口)。默认对齐方式(如果您未在源代码中进行更改,则使用该对齐方式)取决于ABI,并且许多操作系统支持多个ABI(例如32位和64位,或者用于来自不同OS的二进制文件,或者用于不同的编译方式)。同一操作系统的相同二进制文件)。OTOH,哪种对齐方式在性能上是方便的,这取决于CPU-使用32位还是64位模式都可以以相同的方式访问内存(我无法评论实模式,但是如今看来与性能无关。)IIRC奔腾开始偏爱8字节对齐。
Blaisorblade

15

这可能是由于字节对齐和填充所致,因此该结构在您的平台上显示为偶数个字节(或字)。例如,在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

4
“使用gcc在Linux上的C”不足以描述您的平台。对齐方式主要取决于CPU架构。
2013年

-@凯尔·伯顿(Kyle Burton)。对不起,我不明白为什么结构“ someBits”的大小等于4,我期望8个字节,因为声明了2个整数(2 * sizeof(int))= 8个字节。谢谢
youpilat13 '18

1
@ youpilat13,您好,:2:6实际上指定了2位和6位,在这种情况下不是完整的32位整数。someBits.x仅2位,只能存储4个可能的值:00、01、10和11(1、2、3和4)。这有意义吗?以下是有关此功能的文章:geeksforgeeks.org/bit-fields-c
凯尔·伯顿

11

也可以看看:

对于Microsoft Visual C:

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

和GCC声称与Microsoft的编译器兼容。

http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html

除了前面的答案外,请注意,无论包装如何,C ++中都没有member-order-guarantee。编译器可以(当然可以)将虚拟表指针和基本结构的成员添加到该结构中。该标准甚至无法确保虚拟表的存在(未指定虚拟机制的实现),因此可以得出这样的保证是根本不可能的。

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


4
“我很确定成员顺序在C语言中很重要”。是的,C99说:“在结构对象中,非位字段成员和位字段所在的单元的地址按照声明的顺序增加。” :更多标准善良的一面stackoverflow.com/a/37032302/895245
西罗桑蒂利冠状病毒审查六四事件法轮功


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%填充将内存效率交换为处理效率。鉴于计算机可以拥有大量的内存(许多GB),因此编译器认为交换(速度超过大小)是一个合理的选择。

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

另一方面,不同的编译器具有管理数据结构打包的不同能力。例如,在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个),具体取决于编译器的默认打包方式。


这讨论了结构填充的后果,但没有回答这个问题。
基思·汤普森

...因为所谓的打包。...-我认为您的意思是“填充”。“ 大多数现代处理器的首选大小(如果32位(4字节)) ”-有点过分简化了。通常大小8,16,32和64位的支持,往往是每个尺寸都有自己的排列和我不知道你的答案补充说,已经不是在接受的答案的任何新信息。
基思·汤普森

1
当我说打包时,我指的是编译器如何将数据打包到结构中(它可以通过填充小项目来实现,但是不需要填充,但它总是打包)。至于大小-我说的是系统架构,而不是系统将支持数据访问的方式(这与基础总线架构有所不同)。至于您的最后评论,我对权衡的一个方面(速度与大小)(一个主要的编程问题)进行了简化和扩展的解释。我还描述了一种解决问题的方法-不在可接受的答案中。
sid1138

在这种情况下,“打包”通常是指比默认情况更紧密地分配成员,例如#pragma pack。如果按默认对齐方式分配成员,我通常会说该结构打包。
基思·汤普森

包装是一个超载术语。这意味着您如何将结构元素放入内存。类似于将对象放入盒子(移动包装)的意思。这也意味着无需填充即可将元素放入内存中(“紧紧包装”的一种矮手)。然后在#pragma pack命令中有该单词的命令版本。
sid1138

5

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

另外,库可能会在x86下使用32位int进行编译,并且您可能在64位进程中比较其组件,如果您手动执行此操作,则会得到不同的结果。


5

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作为特殊情况,结构中具有多个命名成员的最后一个元素可能具有不完整的数组类型;这称为灵活数组成员。在大多数情况下,柔性数组成员将被忽略。特别地,该结构的尺寸就好像省略了柔性阵列构件,除了其可能具有比省略所暗示的更多的尾随填充。

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


4

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


8
不完全的。在典型的实现中,添加到该结构的是vtable 指针
唐·韦克菲尔德

3

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

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

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

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

与元素对齐有关的问题:

  • 不同的计算机以不同的方式排列对象的边缘
  • 位域宽度的不同限制
  • 计算机在字节中存储字节的方式有所不同(Intel 80x86和Motorola 68000)

对齐方式如何工作:

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

ps此处有更多详细信息:“ Samuel P.Harbison,Guy L.Steele CA参考,(5.6.2-5.6.7)”


2

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

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:对齐很重要。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.