ChatGPT解决这个技术问题 Extra ChatGPT

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

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

请参阅有关内存对齐的 C 常见问题解答。 c-faq.com/struct/align.esr.html
轶事:有一个实际的计算机病毒将其代码放在主机程序的结构填充中。
@Elazar 这令人印象深刻!我从来没有想过可以将这么小的区域用于任何事情。你能提供更多细节吗?
@Wilson - 我敢肯定它涉及很多 jmp。
请参阅结构 填充、包装The Lost Art of C Structure Packing Eric S. Raymond

6
6 revs, 6 users 79%

这是因为添加了填充以满足对齐约束。 Data structure alignment 影响程序的性能和正确性:

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


我想在这里做一个说明:大多数处理器会因未对齐的内存访问而惩罚您(正如您所提到的),但您不能忘记许多完全不允许它。特别是大多数 MIPS 芯片会在未对齐访问时抛出异常。
x86 芯片实际上是相当独特的,因为它们允许未对齐的访问,尽管会受到惩罚; AFAIK 大多数芯片都会抛出异常,而不仅仅是少数。 PowerPC 是另一个常见的例子。
为未对齐访问启用编译指示通常会导致您的代码在引发未对齐错误的处理器上膨胀,因为必须生成修复每个未对齐错误的代码。 ARM 还会引发未对齐错误。
未对齐的数据访问通常是 CISC 架构中的一个特性,大多数 RISC 架构不包括它(ARM、MIPS、PowerPC、Cell)。实际上,大多数芯片都不是台式机处理器,因为嵌入式芯片的数量是规则的,其中绝大多数是 RISC 架构。
@WayneO 填充量始终足以确保接下来的任何内容都根据其大小对齐。因此,在 X 中,在 short 之后有 2 个字节的填充,以确保 4 字节 int 从 4 字节边界开始。在 Y 中,在 char 之后有 1 个字节填充,以确保 2 个字节 short 从 2 个字节边界开始。由于编译器无法知道内存中的结构之后可能是什么(并且可能是许多不同的东西),它会为最坏的情况做准备并插入足够的填充以使结构成为 4 字节的倍数。 X 需要 3 个字节才能达到 12,Y 只需要 1 即可达到 8。
J
Jamal

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

是为了对齐。许多处理器无法访问 2 字节和 4 字节的数量(例如整数和长整数),如果它们被塞满的话。假设你有这样的结构: struct { char a[3];短整数 b;长整数 c;字符 d[3]; };现在,你可能认为应该可以像这样将这个结构打包到内存中:+-------+-------+--------+----- --+ |一个 |乙 | +-------+-------+-------+--------+ |乙 | c | +-------+-------+-------+--------+ | c | d | +-------+-------+-------+-------+ 但是如果编译器像这样安排它,它在处理器上会容易得多: +-------+-------+--------+ |一个 | +-------+-------+--------+ |乙 | +-------+-------+-------+--------+ | c | +-------+-------+-------+--------+ | d | +-------+-------+-------+ 在打包版本中,请注意您和我至少有点难以看到 b 和 c字段环绕?简而言之,处理器也很难。因此,大多数编译器会像这样填充结构(就像使用额外的、不可见的字段一样):+-------+-------+-------+----- --+ |一个 |垫1 | +-------+-------+-------+--------+ |乙 |垫2 | +-------+-------+-------+--------+ | c | +-------+-------+-------+--------+ | d |垫3 | +-------+-------+-------+--------+


现在内存插槽pad1、pad2和pad3有什么用。
@YoYoYonnY 那是不可能的。编译器是 not allowed to reorder struct members 虽然 gcc has an experimental option to do that
@EmmEff 这可能是错误的,但我不太明白:为什么数组中的指针没有内存插槽?
@BalázsBörcsök 这些是固定大小的数组,因此它们的元素以固定偏移量直接存储在结构中。编译器在编译时知道所有这些,因此指针是隐式的。例如,如果您有一个名为 s 的这种类型的结构变量,那么 &s.a == &s&s.d == &s + 12 (给定答案中显示的对齐方式)。仅当数组具有可变大小时才存储指针(例如,a 被声明为 char a[] 而不是 char a[3]),但元素必须存储在其他地方。
3
3 revs, 2 users 96%

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

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

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

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

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


“很好的理由”示例:在明天展示的概念验证演示代码中为复杂结构保持 32 位和 64 位系统之间的二进制兼容性(填充)一致。有时必要性必须优先于适当性。
一切都很好,除非你提到操作系统。这是CPU速度的问题,根本不涉及操作系统。
另一个很好的理由是,如果您将数据流填充到结构中,例如在解析网络协议时。
@dolmen我刚刚指出“操作系统更容易访问数据”是不正确的,因为操作系统不访问数据。
最好使用 #pragma pack(1) - 它受 MSVC、gcc 和 clang 支持,这使您的代码更具可移植性
K
Kyle Burton

这可能是由于字节对齐和填充导致结构在您的平台上出现偶数个字节(或字)。例如在 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

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

也可以看看:

对于 Microsoft Visual C:

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

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

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

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

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


“我很确定会员订单在 C 中是咕噜咕噜的”。是的,C99 说:“在结构对象中,非位域成员和位域所在的单元的地址按照声明的顺序增加。”更多标准优点:stackoverflow.com/a/37032302/895245
s
sid1138

由于所谓的包装,结构的大小大于其各部分的总和。一个特定的处理器有一个可以使用的首选数据大小。大多数现代处理器的首选大小,如果是 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 位的大小;通常每种大小都有自己的对齐方式。而且我不确定您的答案是否添加了任何尚未包含在已接受答案中的新信息。
当我说打包时,我的意思是编译器如何将数据打包到一个结构中(它可以通过填充小项来做到这一点,但它不需要填充,但它总是打包)。至于大小——我说的是系统架构,而不是系统将支持什么数据访问(这与底层总线架构有很大不同)。至于您的最终评论,我对权衡的一个方面(速度与大小)进行了简化和扩展的解释 - 一个主要的编程问题。我还描述了一种解决问题的方法 - 这不在接受的答案中。
在这种情况下,“打包”通常是指比默认值更紧密地分配成员,如 #pragma pack。如果成员是按照默认对齐方式分配的,我通常会说该结构是not 打包的。
包装是一个超载的术语。这意味着您如何将结构元素放入内存中。类似于将物品放入盒子中的意思(打包移动)。这也意味着将元素放入内存中而没有填充(“紧密包装”的一种简写形式)。然后是#pragma pack 命令中单词的命令版本。
C
Community

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 flexible array member 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++ 来理解注释:-)


O
Orion Adrian

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

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


K
Konstantin Burlachenko

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

内存孔可能出现在任何两个组件之间,以及最后一个组件之后。这是由于目标计算机上某些类型的对象可能会受到寻址边界的限制。

“内存孔”大小包含在 sizeof 运算符的结果中。 sizeof 仅不包括灵活数组的大小,在 C/C++ 中可用

该语言的一些实现允许您通过编译指示和编译器选项控制结构的内存布局

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

编译器需要分配一系列组件,增加内存地址

第一个组件的地址与结构的起始地址一致

未命名的位字段可能包含在结构中,以与相邻元素的所需地址对齐

与元素对齐有关的问题:

不同的计算机以不同的方式排列物体的边缘

位域宽度的不同限制

计算机在如何将字节存储在一个字中有所不同(英特尔 80x86 和摩托罗拉 68000)

对齐的工作原理:

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

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


D
DigitalRoss

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

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


J
JohnMcG

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


不完全的。在典型的实现中,添加到结构中的是一个 vtable 指针。
R
RobertS supports Monica Cellio

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

“为什么结构的 sizeof 不等于每个成员的 sizeof 之和?” “为什么 sizeof 运算符返回的结构大小大于结构成员的总大小”?

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

应用于结构对象的 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

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

C 结构中的内存对齐


“如果没有填充的原因,则不会填充内存。”这是无益且具有误导性的。该语言有一个定义,这不是基于它。它属于关于典型/假设实现的部分。 (你有)。然后是重言式。 (我意识到这可能是修辞。)
Z
Zarina Abdibaitova

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

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

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

#pragma pack(push, 1)

// your structure

#pragma pack(pop)