为什么structof sizeof不等于每个成员的sizeof总和?

Kev*_*vin 644 c c++ struct c++-faq sizeof

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

Kev*_*vin 614

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

  • 错误对齐访问可能是一个很难的错误(通常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 */
Run Code Online (Sandbox Code Playgroud)

可以通过对齐对成员进行排序来最小化结构的大小(按基本类型中的大小排序)(Z如上例中的结构).

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

  • 我想在这里做一个说明:大多数处理器会因为未对齐的内存访问而惩罚你(正如你所提到的),但你不能忘记许多处理器完全不允许它.特别是大多数MIPS芯片会在未对齐访问时抛出异常. (37认同)
  • x86芯片实际上是相当独特的,因为它们允许不对齐访问,尽管受到了惩罚; AFAIK*大多数*芯片将抛出异常,而不仅仅是少数.PowerPC是另一个常见的例子. (34认同)
  • 未对齐数据访问通常是CISC体系结构中的一项功能,大多数RISC体系结构都不包含它(ARM,MIPS,PowerPC,Cell).实际上,*大多数*芯片不是台式机处理器,因为芯片数量的嵌入式规则,其中绝大多数是RISC架构. (26认同)
  • 为未对齐访问启用编译指示通常会导致代码在引发错位错误的处理器上出现大小,因为必须生成修复每个错位的代码.ARM也会引发错位故障. (5认同)
  • @Dark - 完全同意.但*大多数*台式机处理器是x86/x64,所以*大多数*芯片不会发出数据对齐错误;) (5认同)
  • @Kerrek SB:该标准保证任何结构的对齐,无论使用的类型如何.但是对于大小为1字节的char,它无法进行未对齐.因此,如果所有struct成员都是char而没有任何PADDING,则标准保证对齐. (3认同)
  • @WayneO填充量总是足以确保接下来的任何内容根据其大小对齐.因此,在`X`中,在'short`之后有2个字节的填充,以确保4字节`int`在4字节边界上开始.在`Y`中,`char`之后有1个字节填充,以确保2字节`short`在2字节边界上开始.由于编译器无法知道内存中的结构后面可能是什么(并且可能有许多不同的东西),因此它会为最坏的情况做好准备并插入足够的填充以使结构成为4个字节的倍数.`X`需要3个字节才能达到12,`Y`只需要1个8. (3认同)
  • "x86芯片具有对未对齐访问的硬件支持"True."x86芯片不会发出数据对齐错误"错误.它取决于指令,特别是SSE指令往往会在未对准时出错(除了特殊的未对齐变化). (3认同)
  • 未对齐的访问陷阱(或肯定曾经)用于函数式语言实现中,用于对值进行标记,以便他们的垃圾收集器可以知道他们正在查看的任意内存是什么。总而言之,这是一个非常聪明的 hack(根据 Kernighan 的格言,我太聪明了,无法在我的代码中使用)。 (2认同)
  • 要迂腐,如果所有struct成员都是char类型,标准是否保证对齐? (2认同)
  • 为什么第一个 `char` 有 3 个字节的填充,而接下来的 2 个只有 1 个字节? (2认同)

Emm*_*Eff 170

包装和字节对齐,如在C FAQ描述在这里:

这是为了对齐.许多处理器如果以各种方式填充,则无法访问2字节和4字节数量(例如,整数和长整数).

假设你有这个结构:

struct {
    char a[3];
    short int b;
    long int c;
    char d[3];
};
Run Code Online (Sandbox Code Playgroud)

现在,您可能认为应该可以将此结构打包到内存中,如下所示:

+-------+-------+-------+-------+
|           a           |   b   |
+-------+-------+-------+-------+
|   b   |           c           |
+-------+-------+-------+-------+
|   c   |           d           |
+-------+-------+-------+-------+
Run Code Online (Sandbox Code Playgroud)

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

+-------+-------+-------+
|           a           |
+-------+-------+-------+
|       b       |
+-------+-------+-------+-------+
|               c               |
+-------+-------+-------+-------+
|           d           |
+-------+-------+-------+
Run Code Online (Sandbox Code Playgroud)

在打包版本中,请注意你和我看到b和c字段如何环绕至少有点困难?简而言之,处理器也很难.因此,大多数编译器将填充结构(就好像有额外的,不可见的字段),如下所示:

+-------+-------+-------+-------+
|           a           | pad1  |
+-------+-------+-------+-------+
|       b       |     pad2      |
+-------+-------+-------+-------+
|               c               |
+-------+-------+-------+-------+
|           d           | pad3  |
+-------+-------+-------+-------+
Run Code Online (Sandbox Code Playgroud)

  • @YoYoYonnY那是不可能的.编译器[不允许对结构成员进行重新排序](http://stackoverflow.com/q/9486364/995714)虽然[gcc有一个实验性选项可以做到](http://stackoverflow.com/q/14671253/ 995714) (7认同)
  • 现在内存槽pad1、pad2、pad3有什么用。 (2认同)
  • @BalázsBörcsök 这些是恒定大小的数组,因此它们的元素以固定偏移量直接存储在结构中。编译器在编译时知道所有这些,因此指针是隐式的。例如,如果您有一个名为“s”的这种类型的结构变量,则“&s.a == &s”和“&s.d == &s + 12”(给出答案中显示的对齐方式)。仅当数组具有可变大小时才存储指针(例如,将“a”声明为“char a[]”而不是“char a[3]”),但随后元素必须存储在其他位置。 (2认同)

INS*_*INS 23

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

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

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

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

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

  • "很好的理由"示例:在明天展示的概念验证演示代码中,为32位和64位系统之间的二进制兼容性(填充)保持一致.有时必须优先于适当. (5认同)
  • 另一个很好的理由是,如果您将数据流填充到结构中,例如在解析网络协议时. (3认同)
  • 除非你提到操作系统,否则一切都很好.这是CPU速度的问题,操作系统根本不涉及. (2认同)
  • 最好使用 `#pragma pack(1)` - 它受 MSVC、gcc 和 clang 支持,这使您的代码更加可移植 (2认同)

Kyl*_*ton 13

这可能是由于字节对齐和填充,因此结构在平台上产生偶数个字节(或字).例如,在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;
}
Run Code Online (Sandbox Code Playgroud)

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

oneInt=4
twoInts=8
someBits=4
Run Code Online (Sandbox Code Playgroud)

  • "使用gcc在Linux上使用C"并不足以描述您的平台.对齐主要取决于CPU架构. (4认同)
  • @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/ (3认同)

lka*_*nab 9

也可以看看:

对于Microsoft Visual C:

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

和GCC声称与微软的编译器兼容:

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

除了之前的答案,请注意,无论包装如何,C++中都没有成员订单保证.编译器可以(当然也可以)将虚拟表指针和基础结构的成员添加到结构中.标准不能确保虚拟表的存在(未指定虚拟机制实现),因此可以得出结论,这种保证是不可能的.

我非常确定C语言中的成员顺序是有保证的,但在编写跨平台或交叉编译程序时我不会指望它.

  • "我很确定会员订单在C中哼了一声".是的,C99说:"在一个结构对象中,非位字段成员和位字段所在的单位的地址会按声明的顺序增加." 更加标准的优点:http://stackoverflow.com/a/37032302/895245 (4认同)
  • 在C++中有一些顺序保证:[*"在没有插入访问说明符的情况下声明的(非联合)类的非静态数据成员被分配,以便后面的成员在类对象"*]中具有更高的地址(http:/ /stackoverflow.com/questions/6730664/why-doesnt-c-make-the-structure-tighter#comment7974233_6730704) (3认同)

bru*_*iuz 7

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

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

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

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

与元素对齐相关的问题:

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

对齐的工作原理:

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

ps 更详细的信息可在此处找到:“Samuel P.Harbison,Guy L.Steele CA 参考,(5.6.2 - 5.6.7)”


Dig*_*oss 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
Run Code Online (Sandbox Code Playgroud)

x86 架构始终能够获取未对齐的地址。然而,它的速度较慢,并且当未对齐重叠两个不同的高速缓存行时,当对齐访问只会逐出一个高速缓存行时,它会逐出两个高速缓存行。

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

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

TL;DR:对齐很重要。


sid*_*138 6

由于所谓的包装,结构的尺寸大于其部件的总和.特定处理器具有与其一起使用的优选数据大小.大多数现代处理器的首选大小,如果是32位(4字节).当数据处于这种边界时访问存储器比跨越该大小边界的事物更有效.

例如.考虑一下简单的结构:

struct myStruct
{
   int a;
   char b;
   int c;
} data;
Run Code Online (Sandbox Code Playgroud)

如果机器是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);
Run Code Online (Sandbox Code Playgroud)

我现在应该有11的长度.没有编译指示,我可以是11到14之间的任何东西(对于某些系统,多达32个),具体取决于编译器的默认打包.

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

Ori*_*ian 5

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

也可以在x86下使用32位整数编译库,如果你手动执行此操作,可能会在64位进程上比较它的组件会产生不同的结果.


Joh*_*McG 5

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

  • 不完全的。在典型的实现中,添加到结构中的是 vtable *指针*。 (11认同)

Cir*_*四事件 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 灵活数组成员feature(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++来理解笔记:-)


归档时间:

查看次数:

180403 次

最近记录:

6 年,11 月 前