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

结构体数组 = 一排"结构体",等于一张表格。 数组每一格装的是一整条记录(姓名+年龄+成绩),所以它天然就是"成绩表 / 通讯录 / 商品清单"的写法:Student cls[3] 就是"3 个学生"。访问要两段式:先用下标选行 cls[i],再用点选列 .name——cls[i].name。

📝 本节和前面几节的关系

这是 7.1(数组)和 8.1(结构体)的第一次合体。回想 7.2 的二维数组:它也能存表格,但每一格只能是同一种类型(比如全是 double 成绩),要存姓名就得再开一个 char 数组——两个数组靠"下标 i"手动对齐,非常容易错位。结构体数组把这一排"姓名数组 / 年龄数组 / 成绩数组"合并成一排"学生",下标天然对齐,这是它最大的价值。

一、二维数组的老问题

假设要存 3 个学生的姓名、年龄、成绩。用 7.2 的二维数组,只能这样:

char   names[3][20] = {"张三", "李四", "王五"};
int    ages[3]      = {18, 19, 20};
double scores[3]    = {92.5, 85.0, 78.5};

能用,但暗藏危险:

scores[1] = 90;         // 想改"李四"的成绩
// 但如果有人手滑写成 ages[1] = 90; 呢?—— 编译能过,数据错了
names[2] 和 scores[2]  是不是同一个人? 全靠你脑子记着"下标 2 对应王五"

三个问题:① 三个数组必须同步维护(插一个学生要改三处);② 下标对齐靠人,编译器帮不上忙;③ 想"整体交换两个学生",得交换三个数组的元素。

二、结构体数组:一条记录 = 一格

Student cls[3] = {
    {"张三", 18, 92.5},         // 第 0 格:一整条记录
    {"李四", 19, 85.0},         // 第 1 格
    {"王五", 20, 78.5}          // 第 2 格
};

读成大白话:Student cls[3] = "订一排 3 格的柜子,每一格都装一个完整的学生"。

现在: - 下标天然对齐:cls[1] 就是李四的全部信息,不会串行; - 整体可搬运:cls[0] = cls[1]; 就能把李四整条记录复制到第 0 格; - 可以整体交换:Student t = cls[0]; cls[0] = cls[1]; cls[1] = t;(排序的基础动作)。

它就是一张表格:

下标 name age score
0 张三 18 92.5
1 李四 19 85.0
2 王五 20 78.5

二维数组也是表格——区别在于:结构体数组的"列"可以各是不同类型、还各有名字。

三、访问:两段式,先选行再选列

cls[1].name        // 第 1 个学生的姓名
cls[1].age         // 第 1 个学生的年龄
cls[i].score       // 第 i 个学生的成绩(循环里最常用)

顺序不能反:先 [i](选哪一行),再 .成员(选哪一列)。

四、内存:一整排扁平的格子

别看写法是"两维"的,内存里它仍是"紧挨着排的一长条",只不过每格 32 字节(8.1.5 算过的 sizeof(Student)):

[ cls[0] 32字节 ][ cls[1] 32字节 ][ cls[2] 32字节 ]
 ↑偏移 0          ↑偏移 32         ↑偏移 64

所以 cls[i] 的地址偏移 = i × sizeof(Student)——这正是 7.4.3"指针 +1 跳一个元素"在结构体数组上的体现,也是 8.4.2 用指针遍历的根据。

五、本节两篇的分工

小节 内容
8.4.1 结构体数组的声明与引用 怎么声明、怎么初始化、怎么用下标遍历
8.4.2 使用指针访问结构体数组 用指针 + -> 遍历(含实例04)

六、动手 Demo:先感受"下标天然对齐"的好

#include <iostream>
#include <cstring>
using namespace std;

struct Student {
    char   name[20];
    int    age;
    double score;
};

int main() {
    Student cls[3] = {
        {"张三", 18, 92.5},
        {"李四", 19, 85.0},
        {"王五", 20, 78.5}
    };

    // ① 打印整张表
    cout << "姓名\t年龄\t成绩" << endl;
    for (int i = 0; i < 3; i++)
        cout << cls[i].name << "\t" << cls[i].age << "\t" << cls[i].score << endl;

    // ② 只改一个人的成绩,不会串到别人身上
    cls[1].score = 90.0;
    cout << "李四改分后:" << cls[1].name << " " << cls[1].score << endl;

    // ③ 整体交换两条记录(排序的基础)
    Student t  = cls[0];
    cls[0]     = cls[1];
    cls[1]     = t;
    cout << "交换后第 0 格是:" << cls[0].name << endl;

    cout << "整排占 " << sizeof(cls) << " 字节 = 3 × " << sizeof(Student) << endl;
    return 0;
}

看到什么算成功:

姓名  年龄  成绩
张三  18  92.5
李四  19  85
王五  20  78.5
李四改分后:李四 90
交换后第 0 格是:李四
整排占 96 字节 = 3 × 32

两个看点: - cls[1].score = 90 只改了李四的成绩,张三、王五毫发无伤——这就是"下标天然对齐"的价值(对比第一节那三个平行数组的隐患); - 整排 96 字节 = 3 × 32——结构体数组的总内存 = 元素个数 × sizeof(Student),不能用"成员相加 × 3"去估(8.1.5)。

30 秒实验:在 cls 的初始化里,把李四那行只写姓名——{"李四"},其他几行照旧。运行后你会看到李四的年龄和成绩都是 0(8.1.3 的"填不满补 0"在数组里同样生效)。

七、常见误区

  1. "结构体数组和二维数组差不多,选哪个都行"——看列的类型:列的类型都相同(比如全是成绩)用二维数组就行;列的类型不同、还各有含义(姓名 / 年龄 / 成绩)就用结构体数组。后者更安全(下标天然对齐)。
  2. "cls[i].name 可以写成 cls.name[i]"——不行。顺序是先下标、后成员。
  3. "cls[3].name 是最后一个学生"——越界。Student cls[3] 的合法下标是 0~2(7.1.2 的老规矩,越界不报错)。
  4. "sizeof(cls) 是元素个数"——是总字节数(96)。元素个数要写 sizeof(cls) / sizeof(cls[0])(7.1.1 的老技巧),这里是 96 / 32 = 3。
自测三问(点开对答案) 1. `Student cls[3]` 里 `cls[1].name` 是什么含义?——第 1 格(第 2 个学生)这一整条记录里的 `name` 成员。 2. 结构体数组比"几个平行的数组"好在哪?——① 下标天然对齐,不会串行;② 一条记录可以整体赋值、整体交换;③ 增删要改的地方集中在一处。 3. `Student cls[5]` 一共占多少字节(`sizeof(Student)` = 32)?元素个数怎么算?——占 160 字节;个数用 `sizeof(cls) / sizeof(cls[0])`。

相关笔记