教材《C程序设计》第五版(谭浩强)第9章,书页 293–330,共 38 页|建议 2h
课前一句话
前 8 课我们用的都是"系统发给我们的类型":int 装整数、float 装小数、char 数组装字符串——一个变量只能装一样东西。可现实中的一份"学生档案"是混装的:学号是整数、姓名是字符串、成绩是小数,它们必须待在一起,而且一删全删、一存全存。这一课就学一件事:自己设计一种"表格"类型,把不同类型的数据打包成一个整体。学完你能写出「结构体 + 数组 + 指针 + 函数」四件套组合的完整程序,也能看懂链表是怎么把一张张表串起来的。
一、先补地基:本章要解决的问题
1. 先说一个具体的麻烦
要存一个学生的信息:学号 10010、姓名 Li Fang、性别 M、年龄 18、成绩 87.5、地址 Beijing。
用前 8 课的工具,你只能这么干:
int num; // 学号
char name[20]; // 姓名
char sex; // 性别
int age; // 年龄
float score; // 成绩
char addr[30]; // 地址六个变量,各在各的抽屉里,互不相识。
- 程序里看不出这六个变量是"同一个人"的;
- 学生变成 3 个,就要写 18 个变量,名字变成
student1_num、student1_name、student2_num…… - 想把"这个学生的信息"传给一个函数,要传 6 个参数;
- 想按成绩排序,交换两个学生,要写 6 条交换语句,少写一条就错位。
2. 那用数组行不行?
不行。一个数组只能装同一类型的数据。
int a[6] 能装学号和年龄,装不下 "Li Fang";char b[6][30] 能装字符串,装不下 87.5 这个小数。这是数组的死穴:数组元素必须同类型。
3. 于是 C 给了你一张"空白表格"
C 语言允许你自己造一种新类型,它的每个"格子"可以是不一样的类型。这种类型叫结构体(structure),其他语言里叫"记录(record)"。
本章从头到尾就一个比喻:表格。
| 比喻里的东西 | C 语言里的东西 | 说明 |
|---|---|---|
| 一张空白表格的格式设计(只画了格子,没印表、没填字) | 结构体类型 struct Student | 只是个"模型/图纸",不占内存 |
| 按格式印出来、填好字、放进抽屉的一张表 | 结构体变量 student1 | 系统给它分配内存 |
| 表上的一个个格子(学号格、姓名格…) | 成员(member)num、name… | 每个格子有自己的类型 |
| 一叠按顺序排好的表 | 结构体数组 leader[3] | 每个元素是一整张表 |
| 一张便条,上面写着"那份表在第几个抽屉" | 结构体指针 struct Student *p | 存的是地址 |
| 打开某张表的某个格子 | student1.num(成员运算符 .) | |
| 顺着便条找到表,再打开格子 | p->num(指向运算符 ->) | |
| 每张表最后一格写着"下一张表在哪个抽屉" | 链表 | 最后一格写 NULL 表示到头 |
| 一个格子用铅笔反复擦写,同一时刻只能有一个内容 | 共用体 union | 几张嘴抢一个碗 |
| 表格上只有几个勾选框,只能勾一个 | 枚举 enum | 值只能是列出来的那几个 |
| 给这套表格格式起个外号 | typedef | 起别名,不造新类型 |
这张表建议你多看两眼,后面每一节都是它的一行。
二、知识点讲解
9.1 定义和使用结构体变量
9.1.1 自己建立结构体类型
一句话定义:结构体类型就是你亲手设计的一张"表格格式",它规定了这张表有哪几个格子、每个格子装什么类型的数据。
拆名字:
struct= structure(结构)的缩写。structure 拆开是struct(建造)+ure(名词后缀)→ "建造出来的东西" → 结构。Student是结构体名,也叫结构体标记(structure tag,tag = 标签)。名字由你起。struct Student合起来才是类型名,struct不能省。
生活比喻:你去打印店,先在电脑上设计一张"学生登记表"的版式:第一格写学号、第二格写姓名……。这份版式设计就是结构体类型。版式设计本身不是一张表,你不能在版式上写字。
怎么用(一般形式):
struct 结构体名
{ 成员表列 };本例:
struct Student
{ int num; // 学号为整型
char name[20]; // 姓名为字符串
char sex; // 性别为字符型
int age; // 年龄为整型
float score; // 成绩为实型
char addr[30]; // 地址为字符串
}; // 注意最后有一个分号逐行看:
- 第 1 行
struct Student:声明"我要设计一种叫 Student 的表格"。 - 第 2–7 行:花括号里是成员表列(member list,也叫域表 field list),每行格式都是
类型名 成员名;。 - 第 8 行:
};这个分号必须有——初学者第一大坑。
两个重要说明(教材原文):
- 结构体类型不止一种。你还可以造
struct Teacher、struct Worker、struct Date,各含不同成员。 - 成员本身可以是另一个结构体类型。比如先造一个"日期表",再把它当成学生表里的一格:
struct Date // 先声明一个结构体类型 struct Date
{ int month; // 月
int day; // 日
int year; // 年
};
struct Student // 再声明一个结构体类型 struct Student
{ int num;
char name[20];
char sex;
int age;
struct Date birthday; // 成员 birthday 属于 struct Date 类型
char addr[30];
};比喻:学生表里有一格叫"出生日期",而这一格本身又是一张小三格的表(年/月/日)。表格套表格。
关于占多少内存考点:
struct Student(6 个成员那版)各成员长度加起来 4 + 20 + 1 + 4 + 4 + 30 = 63 字节。但在 Visual C++ 里用 sizeof(struct Student) 量出来是 64。为什么?因为计算机按"字"(4 字节)管理内存,剩下的 1 个字节不会接着塞下一个数据,而是从下一个"字"开始,于是补齐到 4 的倍数。
易错 结构体变量的长度不一定等于各成员长度之和,别自己拿加法算,用 sizeof 量。
考点 结构体名、共用体名、枚举名的首字母习惯用大写(Student、Data、Color),这只是习惯,不是语法规定。
9.1.2 定义结构体类型变量
一句话定义:类型只是版式设计,要真正能装数据,得"按版式印表"——也就是定义结构体变量。
生活比喻:版式设计好了,但你手里还没有一张能填字的表。印表并放进抽屉,才叫定义变量。系统只为"表"分配抽屉(内存),不为"版式"分配。
教材给了 3 种定义方法。
方法 1:先声明类型,再定义变量(最常用、最推荐)
struct Student student1, student2;形式和 int a, b; 一模一样:前面是类型名,后面是变量名。
方法 2:声明类型的同时定义变量
struct Student
{ int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
} student1, student2; // 花括号后直接跟变量名方法 3:不指定类型名,直接定义变量
struct
{ int num;
char name[20];
...
} student1, student2; // 连 Student 这个名字都不起这叫无名结构体类型,因为没名字,以后再也无法用它定义别的变量,用得不多。
三种方法对比考点:
| 写法 | 优点 | 缺点 | 使用建议 | |
|---|---|---|---|---|
| 方法1 | 先 struct Student{...}; 再 struct Student a,b; | 类型与变量分离,随时可加变量,程序结构清晰 | 多写一行 | 大程序首选 |
| 方法2 | struct Student{...} a,b; | 一眼看到结构,写小程序方便 | 类型和变量搅在一起,不便维护 | 小练习可用 |
| 方法3 | struct {...} a,b; | 省一个名字 | 类型没名字,无法复用 | 基本不用 |
三条说明(教材原文,全是考点):
- 结构体类型和结构体变量是两回事。只能对变量赋值、存取、运算,不能对类型赋值、存取、运算。编译时对类型不分配空间,只对变量分配。
- 比喻:你可以在表上写字,不能在"版式设计图"上写字。
- 成员名可以和程序里的变量名相同,二者互不干扰。你完全可以另外定义一个
int num;,它和struct Student里的num是两回事。 - 成员可以单独使用,它的作用和地位相当于一个普通变量。
student1.num就是一个 int,能加减、能输入输出。
易错 最典型的三连错:
struct Student; // ❌ 光写这一句不算定义完,没有成员表
student1.num = 101; // ❌ 没定义 student1
Student s; // ❌ 类型名是 struct Student,不能只写 Student(除非用了 typedef)9.1.3 结构体变量的初始化和引用
一句话定义:定义变量时可以顺便填表(初始化);填好之后,用「变量名.成员名」打开某个格子读写。
1) 初始化
struct Student a = {10101, "Li Lin", 'M', "123 Beijing Road"};花括号里的常量按成员顺序一一对应赋值:10101→num,"Li Lin"→name,'M'→sex,"123 Beijing Road"→addr。
注意:是给结构体变量初始化,不是给结构体类型初始化。类型是版式,版式上不能填字。
C99 还允许只给某一个成员初始化:
struct Student b = {.name = "Zhang Fang"}; // 成员名前的 . 是成员运算符.name 隐含代表"变量 b 中的成员 b.name"。其余没指定的成员:数值型自动为 0,字符型为 '\0',指针型为 NULL。
考点 这个"点+成员名"的写法是 C99 新增的指定初始化,考试里常拿来考"其余成员被初始化成什么"。
2) 引用成员
引用方式就一条公式:
结构体变量名.成员名student1.num 表示 student1 这张表的"学号格"。
关于 . 运算符考点:"." 叫成员运算符,它在所有运算符中优先级最高。所以 student1.num 可以整体看成一个普通变量:
student1.num = 10010; // 把 10010 写进 student1 的 num 格
student2.score = student1.score;
sum = student1.score + student2.score;
student1.age++; // 等价于 (student1.age)++,不是先给 age 自加3) 嵌套结构体的引用考点
如果成员本身又是结构体,就要一级一级往下找,直到最低一级:
student1.num // 变量 student1 中的成员 num
student1.birthday.month // 变量 student1 中成员 birthday 中的成员 month不能用 student1.birthday 去访问——因为 birthday 本身是一整个结构体,不是最终数据。
4) 同类结构体变量可以整体赋值考点
student1 = student2; // 前提:两者类型相同这是整张表复印一份,不是把两张表钉在一起。
5) 不能整体输入输出【高频易错】
printf("%s\n", student1); // ❌ 错!不能输出整个结构体
scanf("%d,%s,%c", &student1); // ❌ 错!不能整体读入只能一个成员一个成员地输入输出。原因:系统不知道你想按什么格式输出哪一个格子。
6) 可以引用成员的地址,也可以引用整个变量的地址
scanf("%d", &student1.num); // 输入 student1.num 的值
printf("%p", &student1); // 输出 student1 的起始地址结构体变量的地址主要用作函数参数,把整张表的地址传过去。
9.2 使用结构体数组
一句话定义:结构体数组就是一叠表,每个数组元素是一整张结构体类型的表。
生活比喻:一个班 30 人,就是 30 张同样格式的表,按学号排好放进一个文件柜。柜子的第 0 格、第 1 格……各是一张完整的表。
为什么需要:一个结构体变量装一个人的数据;10 个人就要用数组——这就是结构体数组。它和以前数值型数组的区别只在于:每个元素本身又是一张表。
9.2.1 定义结构体数组
一般形式(两种写法):
// 写法①:声明类型的同时定义数组
struct 结构体名
{ 成员表列 } 数组名[数组长度];
// 写法②:先声明类型,再定义数组(推荐)
struct Person leader[3]; // leader 是结构体数组名初始化:在数组后面加 = {初值表列};,每个元素用一对花括号括起来最清楚:
struct Person leader[3] = {"Li", 0, "Zhang", 0, "Sun", 0};
// 更清楚的写法:
struct Person leader[3] = {{"Li", 0}, {"Zhang", 0}, {"Sun", 0}};怎么引用:数组名[下标].成员名
leader[0].name // 第 0 张表的姓名格
leader[j].count++ // 第 j 张表的票数加 1注意 leader[j].count++ 里,. 的优先级高于 ++,所以它等价于 (leader[j].count)++,是给那个格子加 1。
9.2.2 结构体数组的应用举例
结构体数组最常见的用法就是:按某个成员排序。排序时交换的不是单个数字,而是整张表——所以要一个同类型的临时变量来周转。这一点在例 9.4 里会看到。
9.3 结构体指针
一句话定义:结构体指针就是存放结构体变量起始地址的指针变量。它不装表,它装"表在哪个抽屉"的便条。
生活比喻:你不必把整张表复印给别人,只需递一张便条:"表在 3 号柜第 2 层"。这张便条就是结构体指针。
关键前提考点:指针变量的基类型必须与结构体变量的类型相同。
struct Student *pt; // pt 可以指向 struct Student 类型的变量或数组元素9.3.1 指向结构体变量的指针
三种等价的访问写法【必考,必背】:
设 p 指向结构体变量 stu,下面三种写法完全等价:
| 写法 | 含义 | 说明 |
|---|---|---|
stu.成员名 | 直接用变量名访问 | 例如 stu.num |
(*p).成员名 | 先 *p 得到那张表,再取成员 | 例如 (*p).num,括号不能省 |
p->成员名 | 顺着便条找到表,再取成员 | 例如 p->num,最常用 |
-> 怎么读:叫指向运算符,外形是个箭头,读作"p 指向的结构的成员"。
【易错·高频】 *p.num 是错的!因为 . 优先级高于 *,*p.num 等价于 *(p.num)——而 p 根本没有 num 这个成员。必须写 (*p).num(加括号)或直接 p->num。
9.3.2 指向结构体数组的指针
指针可以指向结构体数组的元素。这里有个极其重要的性质考点:
p++不是加 1 个字节,而是加"一个结构体元素的长度"。
例:struct Student 一个元素理论上 29 字节(4+20+1+4),VC++ 实际分配 32 字节。所以 p++ 让 p 的地址值增加 32。指针的加法永远是"按它所指类型的大小"跳。
(++p)->num 与 (p++)->num 的区别【高频考点】:
(++p)->num // 先 p 自加 1(指向下一个元素),再取 num
(p++)->num // 先取当前 p 的 num,然后 p 自加 1记忆法:++ 在前,先动指针再取值;++ 在后,先取值再动指针。
易错 别把成员的地址赋给结构体指针:
p = stu[0].name; // ❌ 类型不匹配,name 是 char*,p 是 struct Student*
p = (struct Student *)stu[0].name; // 语法上能过,但语义很怪,不要这么写9.3.3 用结构体变量和结构体变量的指针作函数参数
把结构体的值传给函数,有 3 种方法【考点,常出对比题】:
| 方式 | 实参 | 形参 | 传递内容 | 特点 |
|---|---|---|---|---|
| ① 用成员作参数 | stu[i].num | 普通变量 | 一个成员的值 | 就是普通值传递,类型要一致 |
| ② 用结构体变量作参数 | stu[i] | 同类型结构体变量 | 整张表全部内容按顺序复制 | 值传递;空间时间开销大;函数内改了形参传不回主调函数 |
| ③ 用指向结构体的指针作参数 | p 或 &stu[i] | 结构体指针 | 地址 | 开销小;函数内改的就是原来的表;最常用 |
比喻:
- ① 是"我只告诉你学号是多少";
- ② 是"我把整张表复印一份给你"——你在我复印的那份上改,我原件没变;
- ③ 是"我把抽屉钥匙给你"——你改的就是原件。
*9.4 用指针处理链表【选学,了解即可】
这一节教材打了星号。非计算机专业了解核心概念即可,能看懂"链表长什么样"就够了,不必背建表代码。
一句话定义:链表是把一批动态分配的结点,用指针一个扣一个串起来的结构。
为什么需要:数组长度必须事先定死。一个班 100 人、另一个班 30 人,用同一个数组就得开 100 个,浪费 70 个。链表要一个开一个,不多不少。
生活比喻(接本课的"表格"):一叠表,每张表的最后一格不填数据,改写"下一张表在哪个抽屉"。你要找第 3 张表,必须先找到第 1 张,顺着它最后一格的地址找到第 2 张,再找到第 3 张。中间断一环,后面全找不到。
三个术语考点:
| 术语 | 含义 |
|---|---|
| 头指针 head | 存放第 1 个结点地址的指针。没有它,整个链表都访问不到 |
| 结点(node) | 链表中的每个元素,包含两部分:数据域(用户要用的数据)+ 指针域(下一个结点的地址) |
| 表尾 | 最后一个结点,它的指针域放 NULL(空地址),表示"到此结束" |
结点怎么定义(结构体套自己的指针):
struct Student
{ long num;
float score;
struct Student *next; // next 是指针变量,指向 struct Student 类型
};这叫自引用结构:结构体里的一个成员,指向它自己这种类型。
静态链表 vs 动态链表:
| 静态链表 | 动态链表 | |
|---|---|---|
| 结点怎么来 | 程序中直接定义 struct Student a,b,c; | 运行时用 malloc 临时开辟 |
| 能否释放 | 不能 | 能用 free 释放 |
| 用途 | 理解链表原理 | 实际使用 |
建立动态链表的三件套(了解即可):
malloc(LEN)开辟一个结点,LEN定义为sizeof(struct Student);p2->next = p1;把新结点挂到表尾;- 结束时
p2->next = NULL;封尾。
教材例 9.9(creat 建表)、例 9.10(print 输出)就是干这个的,本课在第四节习题 6~12 里会用到它的简化版。
*9.5 共用体类型选学
一句话定义:共用体(union)让几个不同类型的变量共用同一段内存,同一时刻只有一个成员有效。
拆名字:union = 联合、合并。国内有些书直译成"联合",谭浩强认为译成"共用体"更贴切——几个变量共用一个内存区。
生活比喻:一个格子,用铅笔写。你今天写"整数 97",明天擦掉写字符 'a'——格子里永远只有一样东西。而结构体是一排格子,各写各的、互不影响。
怎么用:
union Data
{ int i;
char ch;
float f;
} a, b, c; // 声明类型的同时定义变量内存怎么分【必考】:
| 结构体 struct | 共用体 union | |
|---|---|---|
| 占用长度 | 各成员长度之和(还要考虑对齐) | 最长的成员的长度 |
| 成员之间 | 各有各的内存,互不干扰 | 共用同一段内存,互相覆盖 |
| 同一时刻 | 所有成员同时有效 | 只有一个成员有效(最后一次赋值的那个) |
| 关键字 | struct | union |
上例中 a 占 4 字节(因为 float 最长,4 字节),而不是 4+1+4=9 字节。
三条最要命的特点考点:
- 同一时刻只能存一个成员。写了
a.i = 97;,此时a.ch、a.f都是"97 这个二进制串的另一种解释":
printf("%d", a.i); // 输出 97
printf("%c", a.ch); // 输出字符 'a'(97 正好是 'a' 的 ASCII 码)
printf("%f", a.f); // 输出 0.000000(按浮点规则解释这串二进制)- 初始化只能有一个常量:
union Data a = {16}; // ✅ 只给第 1 个成员
union Data a = {16, 'a', 1.5}; // ❌ 不能初始化 3 个,它们共用一段内存- 起作用的是最后一次赋值的成员。依次执行
a.i=97; a.ch='a'; a.f=1.5; a.i=40;之后,格子里是 40,原来的都被覆盖。此时用%c输出a.ch得到的是字符'('(40 是它的 ASCII 码),不是'a'。
其他要点:
- 只能引用成员,不能引用共用体变量整体:
printf("%d", a);❌ - 共用体变量和它各成员的地址相同:
&a.i、&a.ch、&a.f、&a是同一个值 - C99 允许同类型的共用体变量互相赋值;C99 也允许共用体变量作函数参数(旧 C 不允许,只能用指针)
- 共用体可以出现在结构体里,结构体也可以出现在共用体里
什么时候用:同一段空间要"一物多用"时。比如一张人员表,学生那行第 5 格填"班级号"(整数),教师那行第 5 格填"职务"(字符串)——同一格,两种含义,正好用共用体(教材例 9.11)。
9.6 使用枚举类型
一句话定义:一个变量只有几种可能的值时,把这几种值一个个列出来,这种类型叫枚举类型。
拆名字:enum = enumeration 的缩写。e-(出) + numer(数) + -ation(名词后缀) → 把可能的数一个个点出来 → 枚举。
生活比喻:表格上原本是空格,你把它改成五个勾选框:红、黄、蓝、白、黑。只能勾一个,想勾"紫色"?没这个框。
怎么用:
enum Weekday {sun, mon, tue, wed, thu, fri, sat};
enum Weekday workday, weekend; // 枚举类型 枚举变量花括号里的 sun, mon, ... 叫枚举元素或枚举常量,是你自己起的名字。
枚举变量的值只能取列出来的那几个:
workday = mon; // ✅ 正确,mon 是指定的枚举常量之一
workday = sun; // ✅ 正确
workday = monday; // ❌ 错,monday 不是指定的枚举常量之一四条核心规则【全是考点】:
- 枚举元素按常量处理,不能对它们赋值。
sun = 0;❌ - 每个枚举元素代表一个整数,默认从 0 开始。上例中 sun=0, mon=1, tue=2, ... sat=6。 所以
workday = mon;等价于workday = 1;,printf("%d", workday);输出 1。 - 可以人为指定数值,后面的元素在它基础上依次加 1:
enum Weekday {sun = 7, mon = 1, tue, wed, thu, fri, sat};
// sun=7, mon=1, tue=2, wed=3, thu=4, fri=5, sat=6- 枚举元素可以比较大小,规则是按它们代表的整数比。默认规则下
mon > sun,sat > fri。
易错 枚举常量不是字符串。printf("%s", red); 是错的——red 是个整数 0,不是字符串 "red"。想输出单词,得用 switch 一个个判断再打印(例 9.12 就是这么干的)。
考点 C99 把枚举类型也归入整型数据,是"用户自己定义的整数类型"。
*9.7 用 typedef 声明新类型名选学
一句话定义:typedef 给已经存在的类型起一个新名字(别名)。
拆名字:typedef = type(类型)+ def(define,定义)→ 类型定义。
生活比喻:这套"日期表格"格式,每次都要写 struct Date 太啰嗦,你跟同事约定:"以后说 Date 就是指这套格式。"——只是起了个外号,表格格式一点没变。
最核心的一句:typedef 只是起别名,没有创造新类型。
四步法(教材给的通用套路,背这个就够):
- 先按定义变量的方法写出定义体,例如
int i; - 把变量名换成你想用的新类型名,例如把
i换成Count→int Count - 在最前面加
typedef→typedef int Count; - 以后就能用新名字定义变量:
Count i, j;(等价于int i, j;)
四个常见用法:
typedef int Integer; // ① 简单类型起别名
typedef struct
{ int month; int day; int year; } Date; // ② 结构体类型起别名
typedef int Num[100]; // ③ 数组类型起别名:Num a; 等于 int a[100];
typedef char *String; // ④ 指针类型起别名:String p, s[10];注意:用了 typedef struct {...} Date; 之后,定义变量写 Date birthday;,不要再写 struct Date birthday;——因为这里根本没给结构体起 Date 这个 tag,Date 只是 typedef 名。
typedef 与 #define 的区别【考点,常考】:
typedef int Count; | #define Count int | |
|---|---|---|
| 处理阶段 | 编译阶段 | 预编译阶段(预处理) |
| 本质 | 给类型起别名,编译器真正认识这个类型名 | 简单的字符串替换 |
| 复杂类型 | 能处理数组、指针、结构体等复杂类型 | 只能做机械替换,容易出错 |
| 作用域 | 遵守作用域规则 | 从定义处到文件末尾一律替换 |
三、现场写代码
本章例题多,这里挑 10 个最典型的。例 9.9、9.10(动态链表 creat/print)放在第四节习题 6~9 里讲,效果一样。
例 9.1 把一个学生的信息放进结构体变量并输出
题目:把一个学生的信息(学号、姓名、性别、住址)放在一个结构体变量中,然后输出。
思路:自己建一个结构体类型 → 用它定义变量并顺便初始化 → 用 printf 逐个成员输出。
#include <stdio.h>
int main()
{ struct Student
{ long int num;
char name[20];
char sex;
char addr[20];
} a = {10101, "Li Lin", 'M', "123 Beijing Road"}; // 定义结构体变量 a 并初始化
printf("NO.:%ld\nname:%s\nsex:%c\naddress:%s\n",
a.num, a.name, a.sex, a.addr);
return 0;
}逐行看:
struct Student { ... } a = {...};:一边声明类型,一边定义变量a,一边按顺序把 4 个值填进 4 个格子。a = {10101, "Li Lin", 'M', "123 Beijing Road"}:注意'M'是单引号(字符),"Li Lin"是双引号(字符串),别写反。a.num/a.name/a.sex/a.addr:用「变量名.成员名」逐个取出来。%ld对应long int,%s对应字符数组,%c对应 char。
运行结果:
NO.:10101
name:Li Lin
sex:M
address:123 Beijing Road例 9.2 输入两个学生,输出成绩高的那个
题目:输入两个学生的学号、姓名和成绩,输出成绩较高的学生的学号、姓名和成绩。
思路:定义两个同类型结构体变量 → 分别读入 3 个成员 → 比较 score 成员 → 输出对应的那张表。
#include <stdio.h>
int main()
{ struct Student
{ int num;
char name[20];
float score;
} student1, student2;
scanf("%d%s%f", &student1.num, student1.name, &student1.score);
scanf("%d%s%f", &student2.num, student2.name, &student2.score);
printf("The higher score is:\n");
if (student1.score > student2.score)
printf("%d %s %6.2f\n", student1.num, student1.name, student1.score);
else if (student1.score < student2.score)
printf("%d %s %6.2f\n", student2.num, student2.name, student2.score);
else
{ printf("%d %s %6.2f\n", student1.num, student1.name, student1.score);
printf("%d %s %6.2f\n", student2.num, student2.name, student2.score);
}
return 0;
}逐行看:
- 两次
scanf:必须一个一个成员读,不能整体读&student1。 &student1.num要加 &(普通变量);student1.name不加 &(数组名本身就是地址)。这是本章最常考的一个细节。- 三个分支:>、<、==(相等时两人都输出)。
这就是结构体的好处:一旦确定"学生 1 成绩高",输出他的全部信息就是顺手的事,因为 3 项数据本来捆在一起。
例 9.3 统计 3 个候选人的得票
题目:有 3 个候选人,每个选民只能投一人,编程序统计选票,先输入被选人名字,最后输出各人得票结果(共 10 票)。
思路:用结构体数组存 3 个候选人(姓名 + 票数,票数先置 0)→ 循环 10 次读入姓名 → 和 3 个候选人的 name 逐一 strcmp → 相同就给那个元素的 count 加 1。
#include <string.h>
#include <stdio.h>
struct Person
{ char name[20]; // 候选人姓名
int count; // 候选人得票数
} leader[3] = {"Li", 0, "Zhang", 0, "Sun", 0}; // 定义结构体数组并初始化
int main()
{ int i, j;
char leader_name[20]; // 定义字符数组
for (i = 1; i <= 10; i++)
{ scanf("%s", leader_name); // 输入所选候选人姓名
for (j = 0; j < 3; j++)
if (strcmp(leader_name, leader[j].name) == 0)
leader[j].count++;
}
printf("\nResult:\n");
for (i = 0; i < 3; i++)
printf("%5s:%d\n", leader[i].name, leader[i].count);
return 0;
}逐行看:
leader[3] = {"Li",0,"Zhang",0,"Sun",0}:3 个元素,每个元素 2 个格子,按顺序填。char leader_name[20]:用来装输入的那张票的临时字符数组。strcmp(a, b) == 0:字符串比较不能用==,必须用strcmp,返回 0 表示相等。leader[j].count++:因为.优先级高于++,等价于(leader[j].count)++。- 第一个
for是 10 次投票,第二个for是 3 个候选人,两个循环不能搞混。
运行结果(输入 10 个名字后):
Result:
Li:4
Zhang:2
Sun:3例 9.4 按成绩高低输出学生信息(结构体数组排序)
题目:有 n 个学生的信息(学号、姓名、成绩),按成绩高低顺序输出。
思路:结构体数组 + 选择排序。不同的是:交换的是整张表,所以要一个 struct Student 类型的临时变量 temp。
#include <stdio.h>
struct Student
{ int num;
char name[20];
float score;
};
int main()
{ struct Student stu[5] = {{10101, "Zhang", 78}, {10103, "Wang", 98.5},
{10106, "Li", 86}, {10108, "Ling", 73.5},
{10110, "Sun", 100}}; // 定义结构体数组并初始化
struct Student temp; // 交换时的临时变量
const int n = 5; // 定义常变量
int i, j, k;
printf("The order is:\n");
for (i = 0; i < n - 1; i++)
{ k = i;
for (j = i + 1; j < n; j++)
if (stu[j].score > stu[k].score) k = j; // 进行成绩的比较
temp = stu[k]; stu[k] = stu[i]; stu[i] = temp; // stu[k] 和 stu[i] 元素互换
}
for (i = 0; i < n; i++)
printf("%6d %8s %6.2f\n", stu[i].num, stu[i].name, stu[i].score);
printf("\n");
return 0;
}逐行看:
const int n = 5;:常变量,运行期间不能改。学生变成 30 人只改这一行。- 外层
for走n-1趟;k = i先假设第 i 个最大;内层for在剩下的里面找真正最大的下标存进k。 temp = stu[k]; stu[k] = stu[i]; stu[i] = temp;:三只杯子换水,一次换掉整张表的所有成员,不用逐个成员换。这就是结构体相比普通变量的最大便利。temp必须是struct Student类型——只有同类型结构体变量才能互相赋值。
运行结果:
The order is:
10110 Sun 100.00
10103 Wang 98.50
10106 Li 86.00
10101 Zhang 78.00
10108 Ling 73.50例 9.5 通过指向结构体变量的指针输出成员
题目:通过指向结构体变量的指针变量输出结构体变量中成员的信息。
#include <stdio.h>
#include <string.h>
int main()
{ struct Student
{ long num;
char name[20];
char sex;
float score;
};
struct Student stu1; // 定义 struct Student 类型的变量 stu1
struct Student *p; // 定义指向 struct Student 类型数据的指针变量 p
p = &stu1; // p 指向 stu1
stu1.num = 10101; // 对结构体变量的成员赋值
strcpy(stu1.name, "Li Lin"); // 用字符串复制函数给 stu1.name 赋值
stu1.sex = 'M';
stu1.score = 89.5;
printf("No.%ld\nname:%s\nsex:%c\nscore:%5.1f\n",
stu1.num, stu1.name, stu1.sex, stu1.score);
printf("\nNo.%ld\nname:%s\nsex:%c\nscore:%5.1f\n",
(*p).num, (*p).name, (*p).sex, (*p).score);
return 0;
}逐行看:
p = &stu1;:把 stu1 的起始地址(也就是第一格 num 的地址)存进 p。strcpy(stu1.name, "Li Lin");:字符数组不能用stu1.name = "Li Lin";赋值,必须用strcpy。- 第一个
printf用stu1.xxx,第二个用(*p).xxx,输出完全一样。 (*p)括号不能省:*p.num会被理解成*(p.num),编译报错。
运行结果:两段输出一模一样。
例 9.6 用指针变量遍历结构体数组
题目:有 3 个学生的信息放在结构体数组中,要求输出全部学生信息。
#include <stdio.h>
struct Student
{ int num;
char name[20];
char sex;
int age;
};
struct Student stu[3] = {{10101, "Li Lin", 'M', 18},
{10102, "Zhang Fang", 'M', 19},
{10104, "Wang Min", 'F', 20}};
int main()
{ struct Student *p;
printf(" No. Name sex age\n");
for (p = stu; p < stu + 3; p++)
printf("%5d %-20s %2c %4d\n", p->num, p->name, p->sex, p->age);
return 0;
}逐行看:
p = stu;:数组名stu就是首元素地址,等价于p = &stu[0];p < stu + 3:3 个元素,走到stu+3就停(越界前一格)。p++:加一个元素的大小(本例 VC++ 下是 32 字节),不是加 1。p->num:等价于(*p).num。
运行结果:
No. Name sex age
10101 Li Lin M 18
10102 Zhang Fang M 19
10104 Wang Min F 20例 9.7 用结构体指针作函数参数(三个函数配合)
题目:有 n 个学生,每人有学号、姓名和 3 门课成绩。输出平均成绩最高的学生的信息。
思路:功能函数化,分 3 个函数:input 输入并算平均分,max 找平均分最高的元素,print 输出。主函数用指针把数组传给它们。
#include <stdio.h>
#define N 3
struct Student
{ int num;
char name[20];
float score[3];
float aver;
};
int main()
{ void input(struct Student stu[]); // 函数声明
struct Student max(struct Student stu[]); // 函数声明
void print(struct Student stu); // 函数声明
struct Student stu[N], *p = stu; // 定义结构体数组和指针
input(p); // 调用 input 函数
print(max(p)); // 用 max 的返回值作 print 的实参
return 0;
}
void input(struct Student stu[]) // 定义 input 函数
{ int i;
printf("请输入各学生的信息:学号、姓名、3门课成绩:\n");
for (i = 0; i < N; i++)
{ scanf("%d %s %f %f %f", &stu[i].num, stu[i].name,
&stu[i].score[0], &stu[i].score[1], &stu[i].score[2]);
stu[i].aver = (stu[i].score[0] + stu[i].score[1] + stu[i].score[2]) / 3.0;
}
}
struct Student max(struct Student stu[]) // 定义 max 函数
{ int i, m = 0;
for (i = 0; i < N; i++)
if (stu[i].aver > stu[m].aver) m = i;
return stu[m]; // 返回包含该生信息的结构体元素
}
void print(struct Student stud) // 定义 print 函数
{ printf("\n成绩最高的学生是:\n");
printf("学号:%d\n姓名:%s\n三门课成绩:%5.1f,%5.1f,%5.1f\n平均成绩:%6.2f\n",
stud.num, stud.name, stud.score[0], stud.score[1], stud.score[2], stud.aver);
}逐行看(三个函数传参方式各不同,这是本例题眼):
| 函数 | 实参 | 形参 | 传的是什么 |
|---|---|---|---|
input(p) | 指针 p | 结构体数组 stu[] | 首元素地址(地址传递,函数内改的就是原数组) |
max(p) | 指针 p | 结构体数组 stu[] | 首元素地址,返回值是结构体类型数据 |
print(max(p)) | 结构体变量 stu[m] | 结构体变量 stud | 整张表的内容复制(值传递) |
struct Student max(...):函数的返回类型可以是结构体类型,这是新知识。返回的是stu[m]这一整张表。print(max(p))的调用顺序:先执行max(p)得到一个struct Student值,再把它当实参传给print。- 形参数组
stu和主函数里的stu同名,但互不相干(都是局部名字),虚实结合靠的是地址相同。
运行结果:
请输入各学生的信息:学号、姓名、3门课成绩:
10101 Li 78 89 98
10103 Wang 98.5 87 69
10106 Sun 88 76.5 89
成绩最高的学生是:
学号:10101
姓名:Li
三门课成绩: 78.0, 89.0, 98.0
平均成绩: 88.33例 9.8 建立一个简单的静态链表选学
题目:建立由 3 个学生数据结点组成的简单链表,输出各结点数据。
#include <stdio.h>
struct Student
{ int num;
float score;
struct Student *next;
};
int main()
{ struct Student a, b, c, *head, *p;
a.num = 10101; a.score = 89.5;
b.num = 10103; b.score = 90;
c.num = 10107; c.score = 85;
head = &a; // 头指针指向 a 结点
a.next = &b; // a 的 next 指向 b
b.next = &c; // b 的 next 指向 c
c.next = NULL; // c 的 next 不指向任何结点,链表结束
p = head;
do
{ printf("%ld %5.1f\n", p->num, p->score);
p = p->next; // p 移到下一结点
} while (p != NULL);
return 0;
}看点:三个结点的地址是编译时定死的(&a、&b、&c),所以叫"静态链表"。p = p->next 是遍历链表的唯一方法——没有头指针 head,整条链就找不到了。
例 9.11 用共用体处理"同一格两种含义"选学
题目:人员数据中有学生和教师。学生含姓名、号码、性别、职业、班级;教师含姓名、号码、性别、职业、职务。要求用同一个表格处理。
思路:前 4 项完全一样,第 5 项不同——学生的第 5 项是班级(int),教师的第 5 项是职务(字符串)。第 5 项用共用体,同一段内存按 job 的值决定怎么解释。
#include <stdio.h>
struct
{ int num;
char name[10];
char sex;
char job;
union
{ int clas; // class 是 C++ 关键字,VC++ 下改用 clas
char position[10];
} category; // category 是共用体变量
} person[2]; // 定义结构体数组 person,有两个元素
int main()
{ int i;
for (i = 0; i < 2; i++)
{ printf("please enter the data of person:\n");
scanf("%d %s %c %c", &person[i].num, person[i].name,
&person[i].sex, &person[i].job);
if (person[i].job == 's')
scanf("%d", &person[i].category.clas); // 学生,输入班级
else if (person[i].job == 't')
scanf("%s", person[i].category.position); // 教师,输入职务
else
printf("Input error!\n");
}
printf("\nNo. name sex job class/position\n");
for (i = 0; i < 2; i++)
{ if (person[i].job == 's')
printf("%-6d%-10s%-4c%-4c%-10d\n", person[i].num, person[i].name,
person[i].sex, person[i].job, person[i].category.clas);
else
printf("%-6d%-10s%-4c%-4c%-10s\n", person[i].num, person[i].name,
person[i].sex, person[i].job, person[i].category.position);
}
return 0;
}逐行看:
union { int clas; char position[10]; } category;:结构体里嵌一个共用体,这个共用体没有名字,只有变量category。- 输入时先读 4 项,再根据
job决定第 5 项读成%d(班级)还是%s(职务)——同一段内存,两种读法。 - 输出时同理,按
job决定按整数还是字符串输出。 %-6d、%-10s、%-4c:负号表示左对齐,数字表示占几列。
运行结果:
please enter the data of person:
101 Li f s 501
please enter the data of person:
102 Wang m t prof
No. name sex job class/position
101 Li f s 501
102 Wang m t prof例 9.12 用枚举类型解决"取 3 色球"问题
题目:口袋中有红、黄、蓝、白、黑 5 种颜色的球若干个。每次取出 3 个球,问得到 3 种不同颜色球的可能取法,输出每种排列。
思路:三重循环穷举 i、j、k,要求 i != j、i != k、j != k。因为枚举常量不能当字符串输出,再用 switch 把颜色值翻译成单词。
#include <stdio.h>
int main()
{ enum Color {red, yellow, blue, white, black}; // 声明枚举类型 enum Color
enum Color i, j, k, pri; // 定义枚举变量
int n, loop;
n = 0;
for (i = red; i <= black; i++) // 外循环:第 1 个球
for (j = red; j <= black; j++) // 中循环:第 2 个球
if (i != j) // 如果二球不同色
{ for (k = red; k <= black; k++) // 内循环:第 3 个球
if ((k != i) && (k != j)) // 如果 3 球不同色
{ n = n + 1; // 符合条件的次数加 1
printf("%-4d", n); // 输出第几个组合
for (loop = 1; loop <= 3; loop++) // 先后对 3 个球分别处理
{ switch (loop)
{ case 1: pri = i; break; // loop=1,取第 1 球的颜色
case 2: pri = j; break; // loop=2,取第 2 球的颜色
case 3: pri = k; break; // loop=3,取第 3 球的颜色
default: break;
}
switch (pri) // 根据球的颜色输出相应文字
{ case red: printf("%-10s", "red"); break;
case yellow: printf("%-10s", "yellow"); break;
case blue: printf("%-10s", "blue"); break;
case white: printf("%-10s", "white"); break;
case black: printf("%-10s", "black"); break;
default: break;
}
}
printf("\n");
}
}
printf("\ntotal:%5d\n", n);
return 0;
}逐行看:
enum Color {red, yellow, blue, white, black};:red=0, yellow=1, blue=2, white=3, black=4。for (i = red; i <= black; i++):枚举变量可以自加、可以比较,因为它在 C 里就是整数。- 两个
switch:第一个按loop把第 1/2/3 个球挑出来放进pri;第二个按pri的值打印字符串。 - 关键认知:
red和"red"毫无关系!red是整数 0,"red"是三个字符。输出单词完全是人指定的。
运行结果:共 60 种(5×4×3 = 60),最后一行 total: 60。
四、书上习题精讲
本章书末共 12 题,逐题覆盖。第 1~5 题是结构体的基本应用,第 6~12 题集中在链表。
习题 1 计算某日是本年第几天
题目:定义一个结构体变量(包括年、月、日)。计算该日在本年中是第几天,注意闰年问题。
思路:用一个"每月天数"数组,闰年把 2 月改成 29 天,然后把本月之前的整月天数累加,再加上日。
#include <stdio.h>
struct Date
{ int year;
int month;
int day;
};
int main()
{ struct Date d;
int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int i, sum = 0;
printf("请输入年 月 日:");
scanf("%d %d %d", &d.year, &d.month, &d.day);
if ((d.year % 4 == 0 && d.year % 100 != 0) || d.year % 400 == 0)
days[2] = 29; // 闰年 2 月 29 天
for (i = 1; i < d.month; i++)
sum += days[i]; // 累加本月之前的整月
sum += d.day; // 加上当月天数
printf("%d年%d月%d日是该年的第%d天\n", d.year, d.month, d.day, sum);
return 0;
}解析:days[0] 故意留空,这样 days[1] 就是 1 月,下标和月份直接对应,不用减 1,少犯错。闰年判断式 (year%4==0 && year%100!=0) || year%400==0 要背下来。
习题 2 把第 1 题写成 days 函数
题目:写一个函数 days,实现第 1 题的计算。由主函数将年、月、日传递给 days 函数,计算后将日子数传回主函数输出。
思路:把日期打包成 struct Date 一起传给函数(结构体变量作参数,值传递),函数返回 int。
#include <stdio.h>
struct Date
{ int year;
int month;
int day;
};
int days(struct Date d)
{ int daysOfMonth[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int i, sum = 0;
if ((d.year % 4 == 0 && d.year % 100 != 0) || d.year % 400 == 0)
daysOfMonth[2] = 29;
for (i = 1; i < d.month; i++)
sum += daysOfMonth[i];
return sum + d.day;
}
int main()
{ struct Date d;
printf("请输入年 月 日:");
scanf("%d %d %d", &d.year, &d.month, &d.day);
printf("该日是本年的第 %d 天\n", days(d));
return 0;
}解析:形参 struct Date d 是值传递,函数里改 d 不影响主函数的 d(本题不需要改,所以没问题)。这就是 9.3.3 的第②种传参方式:一次把 3 个数据打包传过去,比传 3 个独立参数清爽。
习题 3 写 print 函数输出 5 个学生记录
题目:编写一个函数 print,打印一个学生的成绩数组,该数组中有 5 个学生的数据记录,每个记录包括 num、name、score[3],用主函数输入这些记录,用 print 函数输出这些记录。
#include <stdio.h>
#define N 5
struct Student
{ int num;
char name[20];
float score[3];
};
void print(struct Student stu[])
{ int i;
printf("%-8s%-12s%-8s%-8s%-8s\n", "num", "name", "score1", "score2", "score3");
for (i = 0; i < N; i++)
printf("%-8d%-12s%-8.1f%-8.1f%-8.1f\n", stu[i].num, stu[i].name,
stu[i].score[0], stu[i].score[1], stu[i].score[2]);
}
int main()
{ struct Student stu[N];
int i;
printf("请输入%d个学生的学号、姓名、3门课成绩:\n", N);
for (i = 0; i < N; i++)
scanf("%d %s %f %f %f", &stu[i].num, stu[i].name,
&stu[i].score[0], &stu[i].score[1], &stu[i].score[2]);
print(stu);
return 0;
}解析:形参写成 struct Student stu[](数组形式),实参写数组名 stu,传的是首地址,函数里操作的就是主函数那个数组。score[3] 是数组作成员,所以要用 &stu[i].score[0] 取地址,三个成绩各取一次。
习题 4 在第 3 题基础上加 input 函数
题目:在第 3 题的基础上,编写一个函数 input,用来输入 5 个学生的数据记录。
#include <stdio.h>
#define N 5
struct Student
{ int num;
char name[20];
float score[3];
};
void input(struct Student stu[])
{ int i;
printf("请输入%d个学生的学号、姓名、3门课成绩:\n", N);
for (i = 0; i < N; i++)
scanf("%d %s %f %f %f", &stu[i].num, stu[i].name,
&stu[i].score[0], &stu[i].score[1], &stu[i].score[2]);
}
void print(struct Student stu[])
{ int i;
for (i = 0; i < N; i++)
printf("%-8d%-12s%-8.1f%-8.1f%-8.1f\n", stu[i].num, stu[i].name,
stu[i].score[0], stu[i].score[1], stu[i].score[2]);
}
int main()
{ struct Student stu[N];
input(stu);
print(stu);
return 0;
}解析:第 3、4 两题合起来就是"输入 → 输出"的标准分工:input 负责写表,print 负责读表,main 只负责调度。这就是例 9.7 那种"功能函数化"的思路,考试写大题时按这个结构写,条理分最稳。
习题 5 10 个学生 3 门课:总平均分 + 最高分学生
题目:有 10 个学生,每个学生的数据包括学号、姓名、3 门课程的成绩,从键盘输入 10 个学生数据,要求输出 3 门课程总平均成绩,以及最高分的学生的数据(包括学号、姓名、3 门课程成绩、平均分数)。
#include <stdio.h>
#define N 10
struct Student
{ int num;
char name[20];
float score[3];
float aver;
};
int main()
{ struct Student stu[N], max;
int i;
float sum = 0;
printf("请输入%d个学生的学号、姓名、3门课成绩:\n", N);
for (i = 0; i < N; i++)
{ scanf("%d %s %f %f %f", &stu[i].num, stu[i].name,
&stu[i].score[0], &stu[i].score[1], &stu[i].score[2]);
stu[i].aver = (stu[i].score[0] + stu[i].score[1] + stu[i].score[2]) / 3.0f;
sum += stu[i].aver;
}
max = stu[0]; // 先假设第 0 个最高
for (i = 1; i < N; i++)
if (stu[i].aver > max.aver) max = stu[i]; // 整张表复制过去
printf("3门课程总平均成绩:%.2f\n", sum / N);
printf("最高分学生:%d %s %.1f %.1f %.1f 平均%.2f\n",
max.num, max.name, max.score[0], max.score[1], max.score[2], max.aver);
return 0;
}解析:本题的关键是 max = stu[i];——结构体变量整体赋值,一次把 5 个成员全拷过去,不用写 5 条语句。aver 是算出来的成员,输入时不读它。
习题 6 13 人围圈报数,出圈问题(链表实现)
题目:13 个人围成一圈,从第 1 个人开始顺序报号 1,2,3。凡报到 3 者退出圈子。找出最后留在圈子中的人原来的序号。要求用链表实现。
思路:建一个循环链表(尾结点的 next 指回头结点),用一个 count 计报数,报到 3 就摘掉当前结点。
#include <stdio.h>
#include <stdlib.h>
#define N 13
struct Person
{ int num;
struct Person *next;
};
int main()
{ struct Person *head = NULL, *tail = NULL, *p, *prev;
int i, count = 0;
for (i = 1; i <= N; i++) // 建 13 个结点
{ p = (struct Person *)malloc(sizeof(struct Person));
p->num = i;
p->next = NULL;
if (head == NULL) { head = p; tail = p; }
else { tail->next = p; tail = p; }
}
tail->next = head; // 首尾相接,围成一圈
p = head;
prev = tail;
while (p->next != p) // 只剩一个人时结束
{ count++;
if (count == 3)
{ prev->next = p->next; // 报到 3,摘掉 p 结点
p = p->next;
count = 0;
}
else
{ prev = p;
p = p->next;
}
}
printf("最后留在圈子里的是原来的第 %d 号\n", p->num);
free(p);
return 0;
}解析:输出 13 号。链表版的约瑟夫环,核心就三句:tail->next = head(围圈)、prev->next = p->next(摘结点)、p->next != p(只剩一个的判断)。数组也能做,但题目要求链表。
习题 7 写 del 函数删除指定结点
题目:在例 9.9 和例 9.10 的基础上,写一个函数 del,用来删除动态链表中指定的结点。
思路:两个指针 p1 找目标、p2 记前驱。找到后分两种情况:删的是头结点(改 head)还是中间/尾结点(改 p2->next)。
下面是函数片段,不能单独运行。用的时候把它和例 9.9 的结构体定义、
int n;、#include <stdio.h>、#include <stdlib.h>放在同一个文件里。
struct Student
{ long num;
float score;
struct Student *next;
};
int n;
struct Student *del(struct Student *head, long num)
{ struct Student *p1, *p2;
if (head == NULL)
{ printf("链表为空!\n"); return head; }
p1 = head;
while (num != p1->num && p1->next != NULL) // 找目标结点
{ p2 = p1;
p1 = p1->next;
}
if (num == p1->num) // 找到了
{ if (p1 == head) head = p1->next; // 删的是头结点
else p2->next = p1->next; // 删的是中间/尾结点
printf("删除:%ld\n", num);
n--;
free(p1);
}
else printf("没找到:%ld\n", num);
return head;
}解析:两个易错点。① 删头结点必须返回新的 head,主函数要写 head = del(head, num);;② 循环条件里 p1->next != NULL 不能少,否则走到表尾会越界。
习题 8 写 insert 函数插入结点
题目:写一个函数 insert,用来向一个动态链表插入结点。
思路:链表按学号升序排列,新结点 p0 从表头开始找位置,插在第一个比它大的结点前面。
struct Student *insert(struct Student *head, struct Student *stud)
{ struct Student *p0, *p1, *p2;
p0 = stud;
if (head == NULL) // 空表:直接当表头
{ head = p0; p0->next = NULL; }
else
{ p1 = head;
while (p0->num > p1->num && p1->next != NULL) // 找插入位置
{ p2 = p1;
p1 = p1->next;
}
if (p0->num <= p1->num)
{ if (p1 == head) { head = p0; p0->next = p1; } // 插在表头前
else { p2->next = p0; p0->next = p1; } // 插在 p2 和 p1 之间
}
else { p1->next = p0; p0->next = NULL; } // 插在表尾
}
n++;
return head;
}解析:插入比删除多一个"表尾"分支(p0->num 比所有人都大)。三种位置——头前、中间、尾后——要全部覆盖,漏一个就出错。
习题 9 综合 creat + print + del + insert
题目:综合例 9.9(creat)、例 9.10(print)、第 7 题(del)、第 8 题(insert),再写一个主函数,用 5 个函数组成一个程序,实现链表的建立、输出、删除和插入。
#include <stdio.h>
#include <stdlib.h>
#define LEN sizeof(struct Student)
struct Student
{ long num;
float score;
struct Student *next;
};
int n;
struct Student *creat(void)
{ struct Student *head = NULL, *p1, *p2;
n = 0;
p1 = p2 = (struct Student *)malloc(LEN);
scanf("%ld,%f", &p1->num, &p1->score);
while (p1->num != 0)
{ n++;
if (n == 1) head = p1;
else p2->next = p1;
p2 = p1;
p1 = (struct Student *)malloc(LEN);
scanf("%ld,%f", &p1->num, &p1->score);
}
p2->next = NULL;
free(p1); // 最后多开的一个结点没用,释放掉
return head;
}
void print(struct Student *head)
{ struct Student *p = head;
printf("\nNow, These %d records are:\n", n);
while (p != NULL)
{ printf("%ld %5.1f\n", p->num, p->score);
p = p->next;
}
}
struct Student *del(struct Student *head, long num)
{ struct Student *p1, *p2;
if (head == NULL) { printf("链表为空!\n"); return head; }
p1 = head;
while (num != p1->num && p1->next != NULL) { p2 = p1; p1 = p1->next; }
if (num == p1->num)
{ if (p1 == head) head = p1->next; else p2->next = p1->next;
n--; free(p1);
}
else printf("没找到:%ld\n", num);
return head;
}
struct Student *insert(struct Student *head, struct Student *stud)
{ struct Student *p0 = stud, *p1, *p2;
if (head == NULL) { head = p0; p0->next = NULL; }
else
{ p1 = head;
while (p0->num > p1->num && p1->next != NULL) { p2 = p1; p1 = p1->next; }
if (p0->num <= p1->num)
{ if (p1 == head) { head = p0; p0->next = p1; }
else { p2->next = p0; p0->next = p1; } }
else { p1->next = p0; p0->next = NULL; }
}
n++;
return head;
}
int main()
{ struct Student *head, stud;
head = creat();
print(head);
printf("请输入要删除的学号:");
scanf("%ld", &stud.num);
head = del(head, stud.num);
print(head);
printf("请输入要插入的学号、成绩:");
scanf("%ld,%f", &stud.num, &stud.score);
stud.next = NULL;
head = insert(head, &stud);
print(head);
return 0;
}解析:creat 里 n 是全局变量,所以 print 能直接用它打印"共几条记录"。insert 的实参是 &stud(指针),形参是 struct Student *。输入格式是 学号,成绩(逗号分隔),别输成空格,否则 scanf 匹配不上。
习题 10 合并两个链表,按学号升序
题目:已有 a、b 两个链表,每个链表中的结点包括学号、成绩。要求把两个链表合并,按学号升序排列。
思路:归并。两个指针各扫一条链,每次挑学号小的那个接到新链尾。
struct Student *merge(struct Student *a, struct Student *b)
{ struct Student *head = NULL, *tail = NULL, *p;
while (a != NULL && b != NULL)
{ if (a->num <= b->num) { p = a; a = a->next; }
else { p = b; b = b->next; }
p->next = NULL;
if (head == NULL) { head = p; tail = p; }
else { tail->next = p; tail = p; }
}
p = (a != NULL) ? a : b; // 剩下的那截直接接到表尾
if (head == NULL) head = p;
else tail->next = p;
return head;
}解析:归并结束一定有一条链先空,把另一条剩下的整段接上就行,不用再循环。p->next = NULL; 是把摘下来的结点"剪断",防止还挂着原来的后继。这就是数据结构里的二路归并。
习题 11 从 a 链表中删去与 b 链表同学号的结点
题目:有两个链表 a 和 b,设结点中包含学号、姓名。从 a 链表中删去与 b 链表中有相同学号的那些结点。
思路:对 a 的每个结点,到 b 里线性查找有没有相同学号;有就摘掉。
struct Student *deleteSame(struct Student *a, struct Student *b)
{ struct Student *pa = a, *prev = NULL, *pb, *q;
int found;
while (pa != NULL)
{ found = 0;
for (pb = b; pb != NULL; pb = pb->next) // 在 b 中找同学号
if (pb->num == pa->num) { found = 1; break; }
if (found)
{ q = pa;
if (prev == NULL) a = pa->next; // 删的是头结点
else prev->next = pa->next; // 删中间/尾结点
pa = pa->next;
free(q);
}
else { prev = pa; pa = pa->next; }
}
return a;
}解析:prev 初值为 NULL 是个好技巧——第一个结点被删时 prev == NULL,直接改 head,不用单独判。q 必须先记住被删结点,否则 free 时找不到它了。
习题 12 按年龄删结点
题目:建立一个链表,每个结点包括:学号、姓名、性别、年龄。输入一个年龄,如果链表中的结点所包含的年龄等于此年龄,则将此结点删去。
思路:和第 11 题骨架完全一样,只是判据从"学号相同"换成"年龄相等",而且可能连删多个(循环天然支持)。
#include <stdio.h>
#include <stdlib.h>
struct Student
{ long num;
char name[20];
char sex;
int age;
struct Student *next;
};
struct Student *delAge(struct Student *head, int age)
{ struct Student *p = head, *prev = NULL, *q;
while (p != NULL)
{ if (p->age == age)
{ q = p;
if (prev == NULL) head = p->next;
else prev->next = p->next;
p = p->next;
free(q);
}
else { prev = p; p = p->next; }
}
return head;
}
int main()
{ struct Student *head = NULL, *tail = NULL, *p;
long num; char name[20]; char sex; int age; int i, del_age;
printf("输入 5 个结点(学号 姓名 性别 年龄):\n");
for (i = 0; i < 5; i++)
{ p = (struct Student *)malloc(sizeof(struct Student));
scanf("%ld %s %c %d", &p->num, p->name, &p->sex, &p->age);
p->next = NULL;
if (head == NULL) { head = p; tail = p; }
else { tail->next = p; tail = p; }
}
printf("输入要删除的年龄:");
scanf("%d", &del_age);
head = delAge(head, del_age);
printf("删除后的链表:\n");
for (p = head; p != NULL; p = p->next)
printf("%ld %s %c %d\n", p->num, p->name, p->sex, p->age);
return 0;
}解析:delAge 里删除后不移动 prev(因为 prev 本来就该指向当前结点的前一个),只移动 p。这个细节和第 11 题一致,是"连续删除"能正常工作的关键。
🛠 随课练习(只给题不给答案)
🔮 猜输出
题 1
#include <stdio.h>
struct Student
{ int num; char name[10]; };
int main(void)
{ struct Student a = {101, "Li"}, b;
b = a;
b.num = 202;
printf("%d %s\n", a.num, a.name);
return 0;
}题 2
#include <stdio.h>
struct Point { int x, y; };
int main(void)
{ struct Point a = {1, 2}, *p = &a;
p->x = 10;
(*p).y = 20;
printf("%d %d\n", a.x, a.y);
return 0;
}题 3
#include <stdio.h>
struct S { int num; };
int main(void)
{ struct S s[3] = {{1}, {2}, {3}};
struct S *p = s;
printf("%d ", (p++)->num);
printf("%d ", p->num);
printf("%d\n", (++p)->num);
return 0;
}题 4
#include <stdio.h>
union Data { int i; char ch; };
int main(void)
{ union Data a;
a.i = 65;
printf("%d %c\n", a.i, a.ch);
return 0;
}🛠 变式题
变式 1(照猫画虎) 定义 struct Book { int id; char name[20]; float price; };,输入 3 本书的信息,输出价格最高的那本书的全部信息。
变式 2(改需求) 把例 9.4 的排序改成从低到高,并且在输出时多打印一列"名次"(第 1 名、第 2 名……)。
变式 3(翻译) 把例 9.7 的"学生成绩表"改写成"职工工资表":成员为工号、姓名、月工资,求平均工资和工资最高的人。
变式 4(补全) 下面这个程序的骨架缺一个 max 函数,请补全,使它输出平均分最高的学生。要求用结构体指针作参数、返回结构体类型。
#define N 3
struct Student
{ int num; char name[20]; float score[3]; float aver; };
/* 请在这里补全:struct Student max(struct Student *p) */
/* 补完之后,在 VS 里自己敲主函数验证,调用方式如下(不是让你抄,是让你敲):
struct Student stu[N] = {{1, "Li", {80, 90, 100}, 90},
{2, "Wang", {70, 75, 80}, 75},
{3, "Sun", {95, 92, 98}, 95}};
struct Student m = max(stu);
printf("%d %s %.2f\n", m.num, m.name, m.aver);
预期输出:3 Sun 95.00
*/🐛 改错题
改错 1
// ❌ 错误示例(故意):请找出两处错并改正
#include <stdio.h>
struct Student
{ int num;
char name[20];
} // ①
int main(void)
{ Student s = {1, "Li"}; // ②
printf("%d %s\n", s.num, s.name);
return 0;
}改错 2
// ❌ 错误示例(故意):请找出错在哪并改正
#include <stdio.h>
struct Student
{ int num;
char name[20];
};
int main(void)
{ struct Student s = {1, "Li"};
printf("%s\n", s); // 这里
return 0;
}改错 3
// ❌ 错误示例(故意):请找出错在哪并改正
#include <stdio.h>
struct Student
{ int num;
char name[20];
};
int main(void)
{ struct Student s = {1, "Li"}, *p = &s;
printf("%d\n", *p.num); // 这里
return 0;
}📈 加练题(你随时喊,我随时追加到这里)
(暂无。想加练就说"再出几道 XX 题",我出的题会标上日期追加在这块。)
💬 问答区(你问过的问题 + 我的解答)
(暂无。你问的问题和我的解答会按 Q/A 整理进这里。)
该带走的 6 句话
- 结构体类型是"空白表格的版式",结构体变量才是"填好字的那张表"——类型不占内存,变量才占。
struct后面的分号不能丢;类型名永远写作struct Student,只有typedef之后才能简写成Student。.是所有运算符里优先级最高的;*p.num一定是错的,要么写(*p).num,要么写p->num。- 结构体只能一个成员一个成员地输入输出,但可以整体赋值、整体作函数参数、整体返回。
- 共用体是"一个格子反复擦写",长度等于最长成员;结构体是"一排格子各写各的",长度是各成员之和。
- 链表 = 每张表的最后一格写着"下一张表在哪",没有头指针就整条链都找不到;
NULL表示到头。
📚 教材覆盖清单(第9章,用户自己建立数据类型)
这张表把本课和教材逐节对上:从网页可以回查书页,从书页也能找到网页位置。标「未展开」的,就是教材里有、本课没讲的部分,已明确列出。
| 教材节号 | 标题 | 书页 | 网站对应 | 状态 |
|---|---|---|---|---|
| 9.1 | 定义和使用结构体变量 | p.293–299 | 9.1 定义和使用结构体变量 | 精讲 |
| 9.1.1 | 自己建立结构体类型 | p.293–294 | 9.1.1 自己建立结构体类型 | 精讲 |
| 9.1.2 | 定义结构体类型变量 | p.295 | 9.1.2 定义结构体类型变量 | 精讲 |
| 9.1.3 | 结构体变量的初始化和引用 | p.296–299 | 9.1.3 结构体变量的初始化和引用 | 精讲 |
| 9.2 | 使用结构体数组 | p.300–302 | 9.2 使用结构体数组 | 精讲 |
| 9.2.1 | 定义结构体数组 | p.300 | 9.2.1 定义结构体数组 | 精讲 |
| 9.2.2 | 结构体数组的应用举例 | p.301–302 | 9.2.2 结构体数组的应用举例 | 精讲 |
| 9.3 | 结构体指针 | p.303–308 | 9.3 结构体指针 | 精讲 |
| 9.3.1 | 指向结构体变量的指针 | p.303 | 9.3.1 指向结构体变量的指针 | 精讲 |
| 9.3.2 | 指向结构体数组的指针 | p.304–305 | 9.3.2 指向结构体数组的指针 | 精讲 |
| 9.3.3 | 用结构体变量和结构体变量的指针作函数参数 | p.306–308 | 9.3.3 用结构体变量和结构体变量的指针作函数参数 | 精讲 |
| 9.4 | 用指针处理链表 | p.309–316 | *9.4 用指针处理链表【选学,了解即可】 | 略讲 |
| 9.4.1 | 什么是链表 | p.309 | *9.4 用指针处理链表【选学,了解即可】 | 略讲(并入 9.4) |
| 9.4.2 | 建立简单的静态链表 | p.310 | *9.4 用指针处理链表【选学,了解即可】 | 略讲(并入 9.4) |
| 9.4.3 | 建立动态链表 | p.311–314 | *9.4 用指针处理链表【选学,了解即可】 | 略讲(并入 9.4) |
| 9.4.4 | 输出链表 | p.315–316 | *9.4 用指针处理链表【选学,了解即可】 | 略讲(并入 9.4) |
| 9.5 | 共用体类型 | p.317–321 | *9.5 共用体类型选学 | 略讲 |
| 9.5.1 | 什么是共用体类型 | p.317 | *9.5 共用体类型选学 | 略讲(并入 9.5) |
| 9.5.2 | 引用共用体变量的方式 | p.318 | *9.5 共用体类型选学 | 略讲(并入 9.5) |
| 9.5.3 | 共用体类型数据的特点 | p.319–321 | *9.5 共用体类型选学 | 略讲(并入 9.5) |
| 9.6 | 使用枚举类型 | p.322–325 | 9.6 使用枚举类型 | 精讲 |
| 9.7 | 用typedef声明新类型名 | p.326–329 | *9.7 用 typedef 声明新类型名选学 | 略讲 |
| 习题 | 第9章书末习题 | p.330 | 本课「书上习题精讲」 | 逐题精讲(第1–12题) |
| 插图 | 本课范围内教材插图 | p.293–329 | 用文字/ASCII 重画 | 未逐张复刻 |
| 附录 | 附录 A–E | p.357–370 | 全书对照页 | 未单独成课 |
- 教材插图(流程图、内存示意图等)本课用文字/ASCII 重画,未逐张复刻,页码见上表。
- 附录 A–E(p.357–370)是速查表,未单独成课;全站对照页列出。
- 标「本课补充」的小节(先补地基、比喻、随课练习等)不是教材原文,是为理解而加的。