这怎么可能?sizeof 悖论介绍
\n\n想象一下:你在 C++ 中创建了一个简单的结构体,包含两个字段——char(1 字节)和 int(4 字节)。逻辑上,你可能会认为它的大小是 5 字节。你编写代码,调用 sizeof,然后……得到了 8 字节。“胡说!”你想。“编译器疯了吗?”
这个问题是 Stack Overflow 上最热门的问题之一(超过 20 万次浏览)。它并非关于编译器错误。而是关于处理器和内存实际上在底层是如何工作的。在本文中,我们将深入探讨 C++ 中的结构体对齐、填充(padding) 的概念,并学会预测结构体的实际大小。最重要的是,我们将了解在真正需要时如何控制它。
\n\n为什么 1 + 4 ≠ 5?物理过程
\n\n现代处理器并非逐字节读取内存。它们以“字”为单位工作——即 4、8 或 16 字节的块(取决于架构)。如果一个 4 字节的整数(int)位于一个不是 4 的倍数的地址上,处理器将需要 两次内存访问 而不是一次来读取这个整数,然后从两个片段中“拼凑”它。这非常慢。
为了避免这种情况,编译器在结构体的字段之间插入“空”字节(填充)。它将每个字段对齐到其大小倍数的地址上。这被称为 自然对齐。
\n\n// 一个让所有人都惊讶的简单例子\n#include <iostream>\n\nstruct Example {\n char c; // 1 字节\n int i; // 4 字节\n};\n\nint main() {\n std::cout << "char 的大小: " << sizeof(char) << std::endl; // 1\n std::cout << "int 的大小: " << sizeof(int) << std::endl; // 4\n std::cout << "结构体的大小: " << sizeof(Example) << std::endl; // 8 (!)\n return 0;\n}\n\n这在内存中是什么样子?c 占用字节 0。字节 1、2、3 是 填充。而 i 从字节 4 开始。总计:1 (c) + 3 (空) + 4 (i) = 8 字节。
对齐规则:编译器如何做决定
\n\n编译器遵循简单但严格的规则。了解这些规则,你将能够在不运行程序的情况下预测结构体的大小。
\n\n- \n
- 规则 1:字段顺序保持不变。 编译器不会在内存中重新排列字段(尽管某些编译器在优化时可能会这样做,但标准不保证这一点)。 \n
- 规则 2:每个字段都对齐到其大小倍数的地址上。
char(1 字节)可以放在任何地方。short(2 字节)放在偶数地址上。int(4 字节)放在 4 的倍数地址上。double(8 字节)放在 8 的倍数地址上。 \n - 规则 3:整个结构体的大小必须是最大字段对齐值的倍数。 这是为了确保结构体数组能够正常工作。如果最大的变量是
double(8 字节),那么结构体的大小将是 8 的倍数。 \n
让我们分析一个更复杂的例子:
\n\nstruct Complex {\n char a; // 1 字节\n int b; // 4 字节\n short c; // 2 字节\n char d; // 1 字节\n};\n// sizeof 会是多少?我们一起算。\n\n逐步分析:
\n- \n
a(char) — 字节 0。 \n b(int) — 必须位于 4 的倍数地址上。下一个这样的地址是 4。因此,填充:字节 1、2、3。 b 之后总计:8 字节 (0-7)。 \n c(short) — 必须位于偶数地址上。当前位置是 8(偶数)。很好。 c 之后总计:10 字节 (0-9)。 \n d(char) — 可以放在任何地方。放在字节 10。 d 之后总计:11 字节 (0-10)。 \n