📄 一句话概括
第八章讲的是"怎么把数据组织成有意义的一块"。三件工具:结构体(把不同类型并排打包成一条记录)、共用体(让几个成员重叠用同一块内存)、枚举(给一组整数起名字)。它们共同的关键词是"打包"与"命名"——这正是从"写代码"走向"建模型"的第一步:先能描述"一个学生由什么组成",才谈得上管理"一群学生"。
📝 本节和前面几节的关系
这一章把前七章的"零件"组装成了"模型":类型(第二章)、表达式与转换(第三章)、分支与循环(第四、五章)、函数(第六章)、数组与指针(第七章)——全部用上了。而下一章要在这个地基上盖更高一层:类。结构体就是"类的雏形"——它已经有"把数据打包"的能力,只差"把操作数据的方法也打包进去"和"把数据藏起来"(封装),那就是 class。
一、本章知识地图
| 块 | 小节 | 一句话要点 |
|---|---|---|
| 结构体基础 | 8.1 / 8.1.1 / 8.1.2 / 8.1.3 / 8.1.4 / 8.1.5 | struct 名 { 成员 };——末尾分号别漏;定义只是画图纸、声明变量才占内存;成员访问用 .;同类型可整体赋值 s2 = s1;;嵌套要一层层点(s.birth.y);大小 ≠ 成员相加(对齐填充) |
| 重命名类型 | 8.2 | typedef 原类型 新名字;——新名在最后;它是编译器处理的类型别名,和 #define 的文本替换完全不同(PChar a, b; 两个都是指针) |
| 结构体与函数 | 8.3 / 8.3.1 / 8.3.2 | 三种交法:传值(复制、改不动)/ 传指针(p->x + &stu,改原件、省拷贝)/ 传引用(s.x + stu,最干净)。只读大对象首选 const T & |
| 结构体数组 | 8.4 / 8.4.1 / 8.4.2 | 一张表格,cls[i].成员 两段式访问;下标天然对齐;排序要交换整条记录;数组名=首地址,p++ 跳 sizeof(Student);传参长度必须另传 |
| 共用体 | 8.5 / 8.5.1 / 8.5.2 / 8.5.3 | 成员共用一块内存(地址相同);大小取最大(再对齐);一次只有一个有效——要安全就配 tag 标签 |
| 枚举 | 8.6 / 8.6.1 / 8.6.2 / 8.6.3 | 给整数起名字,默认 0 起自动递增;不能用 int 直接赋值、不能 cin;枚举值共享命名空间(不同枚举别重名);配 switch 最漂亮 |
二、必须记住的七个结论
- 结构体定义只是"画图纸",声明变量才分配内存。定义末尾的分号是硬伤级别的坑(8.1.1)。
- 访问成员:变量用
.,指针用->。p->x≡(*p).x;不能写*p.x(.优先级高于*)。 sizeof(结构体)≠ 成员大小相加。两条对齐规则:① 成员起点是自身大小的整数倍;② 总大小是最宽成员的整数倍。按大小从大到小排最省空间(8.1.5)。- 三种交法各有位置:只读小对象用传值;要改原件用指针或引用;只读大对象用
const T &。结构体越大,"复制"越贵。 - 结构体数组就是一张表:
cls[i].成员;排序交换的是整条记录(不是某个字段);p++在结构体指针上是跳sizeof(结构体)字节;传函数时长度必须另传(数组退化成指针)。 - 共用体取最大、只留一个:成员地址相同,后写的覆盖先写的;读之前必须知道当前有效的是谁(配
tag)。和结构体的区别是"重叠 vs 并排"。 - 枚举是"起了名字的整数":默认 0 起、可自定义;能比较、能算术(结果转
int)、能当数组下标、最适合配switch;不能直接赋int、不能cin。
三、动手 Demo:两关自测
第一关(写程序):一个简易成绩管理系统
要求:用结构体数组存 5 个学生(姓名、年龄、成绩),实现四个功能——
- 用指针遍历打印全班(
const Student *p做形参); - 算出平均分和最高分(最高分要能说出是谁);
- 按成绩从高到低排序(交换整条记录);
- 用指针修改某个学生的成绩(写
void setScore(Student *p, double v))。
#include <iostream>
#include <cstring>
using namespace std;
struct Student {
char name[20];
int age;
double score;
};
// ① 指针遍历打印(只读 → const 指针)
void printAll(const Student *p, int n) {
for (int i = 0; i < n; i++)
cout << " " << (p + i)->name << "\t" << (p + i)->age
<< "\t" << (p + i)->score << endl;
}
// ② 平均分
double average(const Student *p, int n) {
double sum = 0;
for (int i = 0; i < n; i++) sum += (p + i)->score;
return sum / n;
}
// ③ 返回最高分那条记录的地址(安全:指向调用方的数组)
const Student *best(const Student *p, int n) {
const Student *m = p;
for (int i = 1; i < n; i++)
if ((p + i)->score > m->score) m = p + i;
return m;
}
// ④ 用指针改成绩
void setScore(Student *p, double v) {
p->score = v;
}
int main() {
Student cls[5] = {
{"张三", 18, 92.5},
{"李四", 19, 85.0},
{"王五", 20, 78.5},
{"赵六", 18, 58.0},
{"孙七", 19, 66.5}
};
int n = sizeof(cls) / sizeof(cls[0]);
cout << "=== 原始名单 ===" << endl;
printAll(cls, n);
cout << "平均分 = " << average(cls, n) << endl;
const Student *top = best(cls, n);
cout << "最高分:" << top->name << " " << top->score
<< "(在第 " << (top - cls) << " 格)" << endl; // 指针相减算下标
cout << "=== 给赵六改成 70 ===" << endl;
setScore(&cls[3], 70); // 传地址
printAll(cls, n);
cout << "=== 按成绩从高到低排序 ===" << endl;
for (int i = 0; i < n - 1; i++)
for (int j = 0; j < n - 1 - i; j++)
if (cls[j].score < cls[j+1].score) {
Student t = cls[j]; cls[j] = cls[j+1]; cls[j+1] = t;
}
printAll(cls, n);
cout << "sizeof(Student) = " << sizeof(Student)
<< ",整排 = " << sizeof(cls) << " 字节" << endl;
return 0;
}
看到什么算成功:
=== 原始名单 ===
张三 18 92.5
李四 19 85
王五 20 78.5
赵六 18 58
孙七 19 66.5
平均分 = 76.1
最高分:张三 92.5(在第 0 格)
=== 给赵六改成 70 ===
张三 18 92.5
李四 19 85
王五 20 78.5
赵六 18 70
孙七 19 66.5
=== 按成绩从高到低排序 ===
张三 18 92.5
李四 19 85
王五 20 78.5
赵六 18 70
孙七 19 66.5
sizeof(Student) = 32,整排 = 160 字节
检查清单:
- ① 只读函数形参用了 const Student *(7.8.2 的好习惯);
- ② 返回的是数组内部的地址(安全),调用方用 top - cls 算下标(7.4.3);
- ③ 排序交换的是整条记录(Student t = cls[j];),不是只换分数;
- ④ setScore(&cls[3], 70) 传的是地址,所以原件真的改了。
第二关(找错):八处问题
下面这段有八处问题,找出来并说明原因:
struct Point { int x; int y } // ①
struct Student {
char name[20];
int age;
};
void init(Student s) { // ②
s.name = "张三"; // ③
s.age = 18;
}
void print(Student *p) {
cout << p.name << endl; // ④
}
int main() {
Student a = {"张三", 18};
Student b = a;
init(a);
print(&a);
Student cls[3] = {"张三", 18, "李四", 19, "王五", 20}; // ⑤
cout << cls[3].name << endl; // ⑥
cout << sizeof(Student) << endl; // ⑦:以为等于 20 + 4
union U { int i; double d; };
U u;
u.i = 100;
u.d = 2.5;
cout << u.i << endl; // ⑧
return 0;
}
答案与理由:
| # | 问题 | 原因与修法 |
|---|---|---|
| ① | 结构体定义末尾漏分号 | struct Point { int x; int y };(8.1.1)。漏了会连累后面一整段报错 |
| ② | init(Student s) 是传值 |
函数里怎么改都改不到原件(8.3.1)。改成 void init(Student &s) 或 Student *p 配 &a |
| ③ | s.name = "张三"; |
name 是字符数组,不能用 =(7.3、8.1.3)。改成 strcpy(s.name, "张三");(并且要 #include <cstring>) |
| ④ | p.name |
p 是指针,必须用 ->:p->name(8.3.2)。. 是给变量用的 |
| ⑤ | 平铺初始化,没写内层花括号 | 应该写 {{"张三",18},{"李四",19},{"王五",20}}(8.4.1)。平铺写法位置全靠数,极易错 |
| ⑥ | cls[3] 越界 |
3 个元素的合法下标是 0~2。越界不报错,会踩坏相邻内存(7.1.2、8.4.1) |
| ⑦ | 以为 sizeof(Student) = 20 + 4 = 24 |
错。要考虑对齐填充(8.1.5)。实际值以机器实测为准(本例常见为 24,但不能靠相加去猜) |
| ⑧ | 读错共用体成员 | 写了 u.d 之后 u.i 已经失效(8.5.2)。要么只读"最后写的那个",要么配 tag 标签 |
30 秒实验:把上面八处一一改对,跑一遍。特别试一下 ⑥:把 cls[3].name 改成 cls[2].name——程序从"可能崩"变成"正常",但你不会看到任何警告。"不报错的错"才是真危险,这是第八章最该记住的一句话。
四、常见误区(本章汇总)
- "结构体就是几个变量放一起"——它的价值在于"一条记录变成一个可整体搬运的东西":能整体赋值、能进函数、能进数组、能被指针指着。这是"数据建模"的起点(下一章的类就长在它身上)。
- "结构体大小就是成员相加"——不是,还有对齐填充(8.1.5)。这也是笔试高频题。
- "
->和.差不多,随便用"——看左边是什么:变量用.、指针用->。这是本章一半编译错误的来源。 - "共用体只是省内存的结构体"——它是"一次只能放一个"的容器,成员互斥。当"省空间的结构体"用,必然读到垃圾。
- "枚举是字符串"——枚举值是整数,名字只活在源码里(8.6)。所以
cout出来是数字,也没法cin。 - "结构体和指针/引用无关"——恰恰相反:本章所有"能改原件"的场景(8.3.2、8.4.2)都靠第七章的指针和引用。第七章是地基,第八章是盖在上面的第一层楼。
自测八问(点开对答案)
1. 定义结构体时最容易漏什么?——末尾的**分号**。 2. `p->x` 展开成"解引用 + 点"怎么写?为什么不能写 `*p.x`?——`(*p).x`;因为 `.` 优先级高于 `*`,`*p.x` 会被当成 `*(p.x)`。 3. `sizeof(Student)` 为什么可能大于成员相加?——**内存对齐**:成员起点要满足自身对齐要求,总大小要是最宽成员的整数倍,所以中间和末尾都可能垫空白。 4. 三种传参各适合什么场合?——传值(只读、小结构体)、传指针(要改、或可能为空)、传引用(要改、写法最干净);**只读大对象首选 `const T &`**。 5. 结构体数组按成绩排序时,`Student t = cls[j];` 这句为什么必要?——因为姓名、年龄、成绩是**同一条记录**,必须整条交换,否则名字和分数会错配。 6. 共用体的三条特点是什么?——① 成员共用同一块内存(地址相同);② 一次只有一个有效(后写覆盖先写);③ 大小取最大成员(再对齐)。 7. 什么时候用 `union` 而不是 `struct`?——数据**互斥、同一时刻只用其中一个**时(协议字段、类型可变的记录、省内存场合)。 8. 枚举值能不能直接参与比较和算术?有什么限制?——能比较(六个关系运算符都行);算术的结果是 **`int`**,要存回枚举变量必须**强转**,而且**强转可能越界**,转之前先判断范围。- ☑ 第九章 面向对象编程基础 —— 已完结:小结(换一副眼镜看程序:对象=属性+方法)
相关笔记
- 结构体 —— 本章起点:把一条记录打包
- 使用结构体指针变量做函数参数 —— 最实用的"改原件"
- 使用指针访问结构体数组 —— 表格 + 指针
- 共用体的特点 —— 并排 vs 重叠
- 枚举类型的运算 —— 枚举 + switch
- 小结 —— 上一章知识地图
- 小结 —— 下一章知识地图:从"打包数据"迈向"面向对象"
- 🔷 C++基础 · 索引 —— 全课路线图