为什么 sizeof(结构体) 不等于各字段 sizeof 之和?全面解析

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

这怎么可能?sizeof 悖论介绍

\n\n

想象一下:你在 C++ 中创建了一个简单的结构体,包含两个字段——char(1 字节)和 int(4 字节)。逻辑上,你可能会认为它的大小是 5 字节。你编写代码,调用 sizeof,然后……得到了 8 字节。“胡说!”你想。“编译器疯了吗?”

\n\n

这个问题是 Stack Overflow 上最热门的问题之一(超过 20 万次浏览)。它并非关于编译器错误。而是关于处理器和内存实际上在底层是如何工作的。在本文中,我们将深入探讨 C++ 中的结构体对齐、填充(padding) 的概念,并学会预测结构体的实际大小。最重要的是,我们将了解在真正需要时如何控制它。

\n\n

为什么 1 + 4 ≠ 5?物理过程

\n\n

现代处理器并非逐字节读取内存。它们以“字”为单位工作——即 4、8 或 16 字节的块(取决于架构)。如果一个 4 字节的整数(int)位于一个不是 4 的倍数的地址上,处理器将需要 两次内存访问 而不是一次来读取这个整数,然后从两个片段中“拼凑”它。这非常慢。

\n\n

为了避免这种情况,编译器在结构体的字段之间插入“空”字节(填充)。它将每个字段对齐到其大小倍数的地址上。这被称为 自然对齐。

\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\n
    \n
  • 规则 1:字段顺序保持不变。 编译器不会在内存中重新排列字段(尽管某些编译器在优化时可能会这样做,但标准不保证这一点)。
  • \n
  • 规则 2:每个字段都对齐到其大小倍数的地址上。 char(1 字节)可以放在任何地方。short(2 字节)放在偶数地址上。int(4 字节)放在 4 的倍数地址上。double(8 字节)放在 8 的倍数地址上。
  • \n
  • 规则 3:整个结构体的大小必须是最大字段对齐值的倍数。 这是为了确保结构体数组能够正常工作。如果最大的变量是 double(8 字节),那么结构体的大小将是 8 的倍数。
  • \n
\n\n

让我们分析一个更复杂的例子:

\n\n
struct 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

博客

书籍推荐