结构体数组 = 一排"结构体",等于一张表格。 数组每一格装的是一整条记录(姓名+年龄+成绩),所以它天然就是"成绩表 / 通讯录 / 商品清单"的写法: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](选哪一行),再 .成员(选哪一列)。
cls[i].name✅cls.name[i]❌(cls是数组,没有.name这种东西)
四、内存:一整排扁平的格子
别看写法是"两维"的,内存里它仍是"紧挨着排的一长条",只不过每格 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"在数组里同样生效)。
七、常见误区
- "结构体数组和二维数组差不多,选哪个都行"——看列的类型:列的类型都相同(比如全是成绩)用二维数组就行;列的类型不同、还各有含义(姓名 / 年龄 / 成绩)就用结构体数组。后者更安全(下标天然对齐)。
- "
cls[i].name可以写成cls.name[i]"——不行。顺序是先下标、后成员。 - "
cls[3].name是最后一个学生"——越界。Student cls[3]的合法下标是0~2(7.1.2 的老规矩,越界不报错)。 - "
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])`。- ☐ 8.4.1 结构体数组的声明与引用(下一节)
相关笔记
- 使用结构体指针变量做函数参数 —— 上一节:传地址(含实例03)
- 结构体数组的声明与引用 —— 下一节:声明与遍历
- 二维数组 —— 另一种"表格"
- 结构体的大小 —— 每格 32 字节是怎么来的
- 🔷 C++基础 · 索引 —— 全课路线图