为什么sizeof
运算符返回的结构大小大于结构成员的总大小?
12 回答
这是因为添加了填充以满足对齐约束。数据结构对齐会影响程序的性能和正确性:
- 未对齐的访问可能是一个硬错误(通常
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
语句来更改结构对齐设置。
打包和字节对齐,如此处 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 | +-------+-------+-------+-------+
如果您希望结构在 GCC 中具有一定的大小,例如使用__attribute__((packed))
.
在 Windows 上,当使用带有/Zp 选项的 cl.exe 编译器时,您可以将对齐设置为一个字节。
通常 CPU 更容易访问 4(或 8)的倍数的数据,这取决于平台和编译器。
所以基本上是对齐的问题。
你需要有充分的理由来改变它。
这可能是由于字节对齐和填充导致结构在您的平台上出现偶数个字节(或字)。例如在 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
也可以看看:
对于 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中保证了成员顺序,但是在编写跨平台或跨编译器程序时,我不会指望它。
由于所谓的包装,结构的大小大于其各部分的总和。一个特定的处理器有一个可以使用的首选数据大小。大多数现代处理器的首选大小,如果是 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 个),这取决于编译器的默认打包。
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 大小:
2 当应用于一个类时,结果是该类的对象中的字节数,包括将该类型的对象放入数组中所需的任何填充。
9.2 班级成员:
指向标准布局结构对象的指针,使用 reinterpret_cast 进行适当转换,指向其初始成员(或者如果该成员是位字段,则指向它所在的单元),反之亦然。[注意:因此,标准布局结构对象中可能存在未命名的填充,但不是在其开头,这是实现适当对齐所必需的。——尾注]
我只知道足够的 C++ 来理解注释:-)
如果您已隐式或显式设置结构的对齐方式,则可以这样做。对齐 4 的结构将始终是 4 字节的倍数,即使其成员的大小不是 4 字节的倍数。
此外,可以使用 32 位整数在 x86 下编译库,如果您手动执行此操作,您可能会在 64 位进程上比较它的组件会给您带来不同的结果。
C 语言让编译器对内存中结构元素的位置有一些自由:
- 内存孔可能出现在任何两个组件之间,以及最后一个组件之后。这是由于目标计算机上某些类型的对象可能会受到寻址边界的限制。
- “内存孔”大小包含在 sizeof 运算符的结果中。sizeof 仅不包括灵活数组的大小,在 C/C++ 中可用
- 该语言的一些实现允许您通过编译指示和编译器选项控制结构的内存布局
C语言为结构中元素布局的程序员提供了一些保证:
- 编译器需要分配一系列组件,增加内存地址
- 第一个组件的地址与结构的起始地址一致
- 未命名的位字段可能包含在结构中,以与相邻元素的所需地址对齐
与元素对齐有关的问题:
- 不同的计算机以不同的方式排列物体的边缘
- 位域宽度的不同限制
- 计算机在如何将字节存储在一个字中有所不同(英特尔 80x86 和摩托罗拉 68000)
对齐的工作原理:
- 结构占用的体积计算为此类结构阵列的对齐单个元素的大小。结构应该结束,以便下一个结构的第一个元素不违反对齐要求
ps 更多详细信息可在此处获得:“Samuel P.Harbison,Guy L.Steele CA 参考,(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:对齐很重要。
除了其他答案之外,结构可以(但通常没有)具有虚函数,在这种情况下,结构的大小还将包括 vtbl 的空间。
在有关内存对齐和结构填充/打包的其他解释清楚的答案中,我通过仔细阅读在问题本身中发现了一些东西。
“为什么
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
.
这也是关于结构填充的第二个最受欢迎的问题的对象,这里: