🔷 第八章 结构体与共用体
📄 一句话概括

用 sizeof(结构体名) 能问出它占多少字节——但答案往往比"成员大小相加"更大,因为里面藏着看不见的空白。这空白叫内存对齐填充,由两条规则决定:① 每个成员必须从"自己大小的整数倍"位置开始;② 整个结构体的大小必须是"最宽成员"的整数倍。把成员按大小从大到小排列,往往能省下几个字节。

📝 本节和前面几节的关系

8.1.1~8.1.4 我们一路都在说"结构体是一块连续内存"。这一节真正掀开这块内存看排布。它把好几个旧知识点串了起来:2.4 的"类型决定占多大"、3.1.7 的 sizeof、7.1.1 的"sizeof(a)/sizeof(a[0]) 数格子"、7.4.3 的"指针 +1 跳几个字节"——指针跳多远,就是靠这一节讲的"结构体大小"算出来的(8.4.2 会用到)。

一、先看一个"对不上"的例子

struct A {
    char c;         // 1 字节
    int  i;         // 4 字节
};

cout << sizeof(A) << endl;      // 输出 8,不是 5

1 + 4 = 5,可 sizeof 说 8。多出来的 3 个字节去哪了?

它们被"垫"进去了——不是你的数据,是为了让 i 能站在一个"整齐"的位置上而空出来的空白。

二、两条规则(背下来就不怕算错)

规则 内容 后果
规则 1(成员各自对齐) 每个成员的起始位置必须是它自身大小的整数倍 不够就在它前面垫空白
规则 2(整体对齐) 结构体总大小必须是最宽成员大小的整数倍 不够就在末尾垫空白

"垫空白"就是浪费,但这是 C++ 的默认行为,编译器不会为省这点空间去牺牲速度(原因见第五节)。

三、逐字节排队演算

例 1:struct A { char c; int i; };

位置 放什么 说明
0 c 1 字节
1~3 空白(padding) i 是 int,要求起点是 4 的倍数 → 下一站是 4
4~7 i 4 字节

总用量 8,是 4 的倍数 → sizeof(A) = 8 ✅

例 2:struct B { char c; short s; int i; };

位置 放什么
0 c
1 空白(s 要求 2 的倍数)
2~3 s
4~7 i(4 的倍数,正好)

总用量 8 → sizeof(B) = 8 ✅(同样是 8,但只浪费了 1 个字节)

例 3:struct C { char c; int i; short s; };

位置 放什么
0 c
1~3 空白(i 要 4 的倍数)
4~7 i
8~9 s
10~11 末尾空白(总用量 10,但整体要凑成 4 的倍数 → 12)

sizeof(C) = 12 ✅——同样的三个成员,只因为换了顺序,就从 8 变成 12。

四、所以:换顺序能省空间

把例 3 改成"从大到小"排:

struct C1 { char c; int i; short s;  };     // 12 字节
struct C2 { int i; short s; char c;  };     // 8  字节
位置 C2 里放什么
0~3 i
4~5 s
6 c
7 末尾空白(凑成 4 的倍数)

省下 4 个字节。一个结构体省 4 字节不起眼,但 8.4 的"结构体数组"里乘以 1000 个元素,就是 4000 字节。

实用口诀:成员按大小从大到小排(或用 char 收尾),填充最少。 教材里的 Student 之所以排成 char[20] → int → double,也是这个道理(具体见下)。

五、为什么要有对齐?

因为 CPU 不是按字节读内存的,而是按"字长"整块读的(32 位机器一次读 4 字节,64 位读 8 字节)。

// 假设 i 从位置 2 开始(未对齐)
// 位置 2~5 是一个 int
// CPU 读一次只能覆盖 0~3 或 4~7 —— 两次都装不下它
// → 必须读两次(0~3 和 4~7),再各自切出 2 个字节拼起来

对齐 = 让每个成员都完整地落在某一次"整块读取"里,一次就拿到。这是拿内存空间换 CPU 时间——而内存便宜、时间贵,所以 C++ 默认选择对齐。

六、三个常见大小的快速估算

结构体 成员相加 实际 为什么
struct A { char c; int i; }; 5 8 i 前垫 3
struct C { char c; int i; short s; }; 7 12 i 前垫 3 + 末尾垫 2
struct Student { char name[20]; int age; double score; }; 32 32 20(age 起点 20,是 4 的倍数 ✓);24(score 起点 24,是 8 的倍数 ✓);总 32,是 8 的倍数 ✓——正好,一个空白都不多

数组作成员时按"元素类型"对齐:char name[20] 按 char 的规则(对齐 1)——它只看元素是 char,不看有 20 个。这是最容易算错的一点。

七、动手 Demo:把每个结构体的真实大小打印出来

#include <iostream>
using namespace std;

struct A { char c; int  i; };           // 1 + 4
struct B { char c; short s; int i; };   // 1 + 2 + 4
struct C { char c; int  i; short s; };  // 1 + 4 + 2
struct D { int  i; short s; char c; };  // 4 + 2 + 1(从大到小)
struct Student {
    char   name[20];
    int    age;
    double score;
};

int main() {
    cout << "A(char + int)          = " << sizeof(A)  << "  (相加 5)"  << endl;
    cout << "B(char + short + int)  = " << sizeof(B)  << "  (相加 7)"  << endl;
    cout << "C(char + int + short)  = " << sizeof(C)  << "  (相加 7)"  << endl;
    cout << "D(int + short + char)  = " << sizeof(D)  << "  (相加 7)"  << endl;
    cout << "Student                = " << sizeof(Student) << endl;
    return 0;
}

看到什么算成功:

A(char + int)          = 8   (相加 5)
B(char + short + int)  = 8   (相加 7)
C(char + int + short)  = 12  (相加 7)
D(int + short + char)  = 8   (相加 7)
Student                = 32

三个看点: - A 相加 5、实际 8——垫了 3 个空白; - B 和 C 成员完全一样(只是个顺序不同),一个 8 一个 12——顺序真的影响大小; - D 就是 C 从大到小重排,立刻省下 4 字节。

30 秒实验:在 C 后面加一句 char d;,猜猜变成多少,再跑一遍验证。你会发现它可能一点都不变(14 → 凑到 16 之前,d 正好填进了原来的末尾空白里)——"加了个成员,大小没变" 是对齐最反直觉的一幕。

八、这件事在实际中有什么用

  1. 算结构体数组的总内存:sizeof(Student) * 1000 才是真占用,不能拿"成员相加 × 1000"估(8.4)。
  2. 指针跳动的单位:p + 1 在 Student *p 上跳的是 sizeof(Student) 字节(7.4.3 的规则)——跳过 32 字节,不是跳过 5+4+8。
  3. 文件读写/网络传输:结构体的字节数和字段位置必须双方约定一致(这也是为什么"别把结构体直接写进文件再换编译器读")。
  4. 笔试面试高频题:sizeof 算结构体大小,几乎必考。

九、常见误区

  1. "结构体大小 = 成员大小相加"——错的,还要加上对齐填充。这是本节最核心的一条。
  2. "char name[20] 按 20 对齐"——按元素类型对齐,char 就是 1。看元素类型,不看元素个数。
  3. "成员顺序随便,反正编译器会优化"——编译器不会替你重排成员(语言规定成员顺序就是声明顺序)。想省空间只能自己排。
  4. "填充字节里是 0"——不保证。跟未初始化变量一样,里面的内容是垃圾值(有的调试器会刷成特定值,别依赖)。所以不要整体比较内存来判断两个结构体是否相等,要逐成员比较。
  5. "那用 #pragma pack(1) 压掉填充就好了"——它能取消填充(笔试和嵌入式场合常见),代价是访问变慢、某些平台直接崩。学习阶段不用碰它,知道这回事就行。
自测三问(点开对答案) 1. `struct A { char c; int i; };` 的 `sizeof` 是多少?为什么不是 5?——8。因为 `int` 必须从 4 的倍数位置开始,`c` 占 0 之后要垫 3 个字节到位置 4。 2. 对齐的两条规则是什么?——① 每个成员的起始位置是**自身大小**的整数倍;② 结构体总大小是**最宽成员大小**的整数倍。 3. `struct C1 { char c; int i; short s; };` 和 `struct C2 { int i; short s; char c; };` 谁更小?差多少?——`C1` 是 12,`C2` 是 8,**差 4 字节**。按大小从大到小排更省。

相关笔记