提问人:Kevin 提问时间:9/23/2008 最后编辑:unalignedmemoryaccessKevin 更新时间:4/26/2022 访问量:289290
为什么结构体的 sizeof 不等于每个成员的 sizeof 之和?
Why isn't sizeof for a struct equal to the sum of sizeof of each member?
答:
这是因为添加了填充以满足对齐约束。数据结构一致性会影响程序的性能和正确性:
- 未对齐的访问可能是一个硬错误(通常)。
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
评论
X
short
int
Y
char
short
X
Y
如果您隐式或显式设置了结构的对齐方式,则可以这样做。对齐 4 的结构将始终是 4 个字节的倍数,即使其成员的大小不是 4 个字节的倍数。
此外,一个库可以在 x86 下使用 32 位整数进行编译,如果您手动执行此操作,您可能会在 64 位进程上比较其组件,这会给您带来不同的结果。
打包和字节对齐,如此处的 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 | +-------+-------+-------+-------+
评论
s
&s.a == &s
&s.d == &s + 12
a
char a[]
char a[3]
这可能是由于字节对齐和填充,因此结构在您的平台上显示为偶数个字节(或字)。例如,在 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
评论
:2
:6
例如,如果您希望结构在 GCC 中具有一定的大小,请使用 __attribute__((packed))。
在 Windows 上,当使用带有 /Zp 选项的 cl.exe 编译器时,可以将对齐方式设置为一个字节。
通常,CPU 更容易访问是 4(或 8)的倍数的数据,具体取决于平台和编译器。
所以这基本上是一个对齐问题。
你需要有充分的理由来改变它。
评论
#pragma pack(1)
除了其他答案之外,结构体可以(但通常没有)具有虚拟函数,在这种情况下,结构体的大小也将包括 vtbl 的空间。
评论
另请参阅:
对于 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 语言中可以保证成员顺序,但在编写跨平台或跨编译程序时,我不会指望它。
评论
结构的尺寸大于其各部分的总和,因为所谓的填料。特定处理器具有它使用的首选数据大小。大多数现代处理器的首选大小为 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 个),具体取决于编译器的默认打包。
评论
#pragma pack
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)”
这个想法是,出于速度和缓存的考虑,操作数应该从与其自然大小对齐的地址中读取。为此,编译器填充结构成员,以便对齐以下成员或以下结构。
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:对齐很重要。
评论
alignof(int)
int
struct
alignof(long long) == 4
alignof(int)==1
alignof()
packed
int
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++ 来理解注释:-)
在关于内存对齐和结构填充/打包的其他解释良好的答案中,通过仔细阅读,我在问题本身中发现了一些东西。
"为什么结构体的
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) == 2
sizeof(int) == 4
a
b
c
sizeof(bar) == 8
这也是关于结构填充的第二个最热门问题的对象,这里是:
评论
上面给出了很多信息(解释)。
而且,我只想分享一些方法来解决这个问题。
您可以通过添加编译指示包来避免它
#pragma pack(push, 1)
// your structure
#pragma pack(pop)
评论