CC语言通关
第10课 · 结构体
教材对照
第9章 · 书页 293–330

第10课 · 结构体

结构体变量/数组/指针、链表、共用体、枚举、typedef

9 个小节 66 段代码 建议 2h
教材依据第9章《用户自己建立数据类型》书页 p.293–330
本课覆盖:教材第9章正文 + 书末习题(p.330)。标「教材 p.X」= 教材段落总结;「例题」= 教材例题;「习题」= 书末题目;「本课补充」= 非教材原文。
在全书对照表中查看 →

教材《C程序设计》第五版(谭浩强)第9章,书页 293–330,共 38 页|建议 2h


课前一句话

📖 本课补充 · 非教材原文

前 8 课我们用的都是"系统发给我们的类型":int 装整数、float 装小数、char 数组装字符串——一个变量只能装一样东西。可现实中的一份"学生档案"是混装的:学号是整数、姓名是字符串、成绩是小数,它们必须待在一起,而且一删全删、一存全存。这一课就学一件事:自己设计一种"表格"类型,把不同类型的数据打包成一个整体。学完你能写出「结构体 + 数组 + 指针 + 函数」四件套组合的完整程序,也能看懂链表是怎么把一张张表串起来的。


一、先补地基:本章要解决的问题

📖 本课补充 · 非教材原文

1. 先说一个具体的麻烦

要存一个学生的信息:学号 10010、姓名 Li Fang、性别 M、年龄 18、成绩 87.5、地址 Beijing。

用前 8 课的工具,你只能这么干:

c
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 定义和使用结构体变量

📖 教材 p.293–299 · 正文总结

9.1.1 自己建立结构体类型

📖 教材 p.293–294 · 正文总结

一句话定义:结构体类型就是你亲手设计的一张"表格格式",它规定了这张表有哪几个格子、每个格子装什么类型的数据。

拆名字:

  • struct = structure(结构)的缩写。structure 拆开是 struct(建造)+ ure(名词后缀)→ "建造出来的东西" → 结构。
  • Student 是结构体名,也叫结构体标记(structure tag,tag = 标签)。名字由你起。
  • struct Student 合起来才是类型名,struct 不能省。

生活比喻:你去打印店,先在电脑上设计一张"学生登记表"的版式:第一格写学号、第二格写姓名……。这份版式设计就是结构体类型。版式设计本身不是一张表,你不能在版式上写字。

怎么用(一般形式):

c
struct 结构体名
{ 成员表列 };

本例:

c
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 行:}; 这个分号必须有——初学者第一大坑。

两个重要说明(教材原文):

  1. 结构体类型不止一种。你还可以造 struct Teacher、struct Worker、struct Date,各含不同成员。
  2. 成员本身可以是另一个结构体类型。比如先造一个"日期表",再把它当成学生表里的一格:
c
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 定义结构体类型变量

📖 教材 p.295 · 正文总结

一句话定义:类型只是版式设计,要真正能装数据,得"按版式印表"——也就是定义结构体变量。

生活比喻:版式设计好了,但你手里还没有一张能填字的表。印表并放进抽屉,才叫定义变量。系统只为"表"分配抽屉(内存),不为"版式"分配。

教材给了 3 种定义方法。

方法 1:先声明类型,再定义变量(最常用、最推荐)

c
struct Student student1, student2;

形式和 int a, b; 一模一样:前面是类型名,后面是变量名。

方法 2:声明类型的同时定义变量

c
struct Student
{ int num;
  char name[20];
  char sex;
  int age;
  float score;
  char addr[30];
} student1, student2;      // 花括号后直接跟变量名

方法 3:不指定类型名,直接定义变量

c
struct
{ int num;
  char name[20];
  ...
} student1, student2;      // 连 Student 这个名字都不起

这叫无名结构体类型,因为没名字,以后再也无法用它定义别的变量,用得不多。

三种方法对比考点:

写法优点缺点使用建议
方法1先 struct Student{...}; 再 struct Student a,b;类型与变量分离,随时可加变量,程序结构清晰多写一行大程序首选
方法2struct Student{...} a,b;一眼看到结构,写小程序方便类型和变量搅在一起,不便维护小练习可用
方法3struct {...} a,b;省一个名字类型没名字,无法复用基本不用

三条说明(教材原文,全是考点):

  1. 结构体类型和结构体变量是两回事。只能对变量赋值、存取、运算,不能对类型赋值、存取、运算。编译时对类型不分配空间,只对变量分配。
    • 比喻:你可以在表上写字,不能在"版式设计图"上写字。
  2. 成员名可以和程序里的变量名相同,二者互不干扰。你完全可以另外定义一个 int num;,它和 struct Student 里的 num 是两回事。
  3. 成员可以单独使用,它的作用和地位相当于一个普通变量。student1.num 就是一个 int,能加减、能输入输出。

易错 最典型的三连错:

c
struct Student;              // ❌ 光写这一句不算定义完,没有成员表
student1.num = 101;          // ❌ 没定义 student1
Student s;                   // ❌ 类型名是 struct Student,不能只写 Student(除非用了 typedef)

9.1.3 结构体变量的初始化和引用

📖 教材 p.296–299 · 正文总结

一句话定义:定义变量时可以顺便填表(初始化);填好之后,用「变量名.成员名」打开某个格子读写。

1) 初始化

c
struct Student a = {10101, "Li Lin", 'M', "123 Beijing Road"};

花括号里的常量按成员顺序一一对应赋值:10101→num,"Li Lin"→name,'M'→sex,"123 Beijing Road"→addr。

注意:是给结构体变量初始化,不是给结构体类型初始化。类型是版式,版式上不能填字。

C99 还允许只给某一个成员初始化:

c
struct Student b = {.name = "Zhang Fang"};    // 成员名前的 . 是成员运算符

.name 隐含代表"变量 b 中的成员 b.name"。其余没指定的成员:数值型自动为 0,字符型为 '\0',指针型为 NULL。

考点 这个"点+成员名"的写法是 C99 新增的指定初始化,考试里常拿来考"其余成员被初始化成什么"。

2) 引用成员

引用方式就一条公式:

c
结构体变量名.成员名

student1.num 表示 student1 这张表的"学号格"。

关于 . 运算符考点:"." 叫成员运算符,它在所有运算符中优先级最高。所以 student1.num 可以整体看成一个普通变量:

c
student1.num = 10010;         // 把 10010 写进 student1 的 num 格
student2.score = student1.score;
sum = student1.score + student2.score;
student1.age++;               // 等价于 (student1.age)++,不是先给 age 自加

3) 嵌套结构体的引用考点

如果成员本身又是结构体,就要一级一级往下找,直到最低一级:

c
student1.num                  // 变量 student1 中的成员 num
student1.birthday.month       // 变量 student1 中成员 birthday 中的成员 month

不能用 student1.birthday 去访问——因为 birthday 本身是一整个结构体,不是最终数据。

4) 同类结构体变量可以整体赋值考点

c
student1 = student2;          // 前提:两者类型相同

这是整张表复印一份,不是把两张表钉在一起。

5) 不能整体输入输出【高频易错】

c
printf("%s\n", student1);                 // ❌ 错!不能输出整个结构体
scanf("%d,%s,%c", &student1);             // ❌ 错!不能整体读入

只能一个成员一个成员地输入输出。原因:系统不知道你想按什么格式输出哪一个格子。

6) 可以引用成员的地址,也可以引用整个变量的地址

c
scanf("%d", &student1.num);      // 输入 student1.num 的值
printf("%p", &student1);         // 输出 student1 的起始地址

结构体变量的地址主要用作函数参数,把整张表的地址传过去。


9.2 使用结构体数组

📖 教材 p.300–302 · 正文总结

一句话定义:结构体数组就是一叠表,每个数组元素是一整张结构体类型的表。

生活比喻:一个班 30 人,就是 30 张同样格式的表,按学号排好放进一个文件柜。柜子的第 0 格、第 1 格……各是一张完整的表。

为什么需要:一个结构体变量装一个人的数据;10 个人就要用数组——这就是结构体数组。它和以前数值型数组的区别只在于:每个元素本身又是一张表。

9.2.1 定义结构体数组

📖 教材 p.300 · 正文总结

一般形式(两种写法):

c
// 写法①:声明类型的同时定义数组
struct 结构体名
{ 成员表列 } 数组名[数组长度];

// 写法②:先声明类型,再定义数组(推荐)
struct Person leader[3];        // leader 是结构体数组名

初始化:在数组后面加 = {初值表列};,每个元素用一对花括号括起来最清楚:

c
struct Person leader[3] = {"Li", 0, "Zhang", 0, "Sun", 0};
// 更清楚的写法:
struct Person leader[3] = {{"Li", 0}, {"Zhang", 0}, {"Sun", 0}};

怎么引用:数组名[下标].成员名

c
leader[0].name        // 第 0 张表的姓名格
leader[j].count++     // 第 j 张表的票数加 1

注意 leader[j].count++ 里,. 的优先级高于 ++,所以它等价于 (leader[j].count)++,是给那个格子加 1。

9.2.2 结构体数组的应用举例

📖 教材 p.301–302 · 正文总结

结构体数组最常见的用法就是:按某个成员排序。排序时交换的不是单个数字,而是整张表——所以要一个同类型的临时变量来周转。这一点在例 9.4 里会看到。


9.3 结构体指针

📖 教材 p.303–308 · 正文总结

一句话定义:结构体指针就是存放结构体变量起始地址的指针变量。它不装表,它装"表在哪个抽屉"的便条。

生活比喻:你不必把整张表复印给别人,只需递一张便条:"表在 3 号柜第 2 层"。这张便条就是结构体指针。

关键前提考点:指针变量的基类型必须与结构体变量的类型相同。

c
struct Student *pt;      // pt 可以指向 struct Student 类型的变量或数组元素

9.3.1 指向结构体变量的指针

📖 教材 p.303 · 正文总结

三种等价的访问写法【必考,必背】:

设 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.304–305 · 正文总结

指针可以指向结构体数组的元素。这里有个极其重要的性质考点:

p++ 不是加 1 个字节,而是加"一个结构体元素的长度"。

例:struct Student 一个元素理论上 29 字节(4+20+1+4),VC++ 实际分配 32 字节。所以 p++ 让 p 的地址值增加 32。指针的加法永远是"按它所指类型的大小"跳。

(++p)->num 与 (p++)->num 的区别【高频考点】:

c
(++p)->num      // 先 p 自加 1(指向下一个元素),再取 num
(p++)->num      // 先取当前 p 的 num,然后 p 自加 1

记忆法:++ 在前,先动指针再取值;++ 在后,先取值再动指针。

易错 别把成员的地址赋给结构体指针:

c
p = stu[0].name;                  // ❌ 类型不匹配,name 是 char*,p 是 struct Student*
p = (struct Student *)stu[0].name; // 语法上能过,但语义很怪,不要这么写

9.3.3 用结构体变量和结构体变量的指针作函数参数

📖 教材 p.306–308 · 正文总结

把结构体的值传给函数,有 3 种方法【考点,常出对比题】:

方式实参形参传递内容特点
① 用成员作参数stu[i].num普通变量一个成员的值就是普通值传递,类型要一致
② 用结构体变量作参数stu[i]同类型结构体变量整张表全部内容按顺序复制值传递;空间时间开销大;函数内改了形参传不回主调函数
③ 用指向结构体的指针作参数p 或 &stu[i]结构体指针地址开销小;函数内改的就是原来的表;最常用

比喻:

  • ① 是"我只告诉你学号是多少";
  • ② 是"我把整张表复印一份给你"——你在我复印的那份上改,我原件没变;
  • ③ 是"我把抽屉钥匙给你"——你改的就是原件。

*9.4 用指针处理链表【选学,了解即可】

📖 教材 p.309–316 · 正文总结

这一节教材打了星号。非计算机专业了解核心概念即可,能看懂"链表长什么样"就够了,不必背建表代码。

一句话定义:链表是把一批动态分配的结点,用指针一个扣一个串起来的结构。

为什么需要:数组长度必须事先定死。一个班 100 人、另一个班 30 人,用同一个数组就得开 100 个,浪费 70 个。链表要一个开一个,不多不少。

生活比喻(接本课的"表格"):一叠表,每张表的最后一格不填数据,改写"下一张表在哪个抽屉"。你要找第 3 张表,必须先找到第 1 张,顺着它最后一格的地址找到第 2 张,再找到第 3 张。中间断一环,后面全找不到。

三个术语考点:

术语含义
头指针 head存放第 1 个结点地址的指针。没有它,整个链表都访问不到
结点(node)链表中的每个元素,包含两部分:数据域(用户要用的数据)+ 指针域(下一个结点的地址)
表尾最后一个结点,它的指针域放 NULL(空地址),表示"到此结束"

结点怎么定义(结构体套自己的指针):

c
struct Student
{ long num;
  float score;
  struct Student *next;      // next 是指针变量,指向 struct Student 类型
};

这叫自引用结构:结构体里的一个成员,指向它自己这种类型。

静态链表 vs 动态链表:

静态链表动态链表
结点怎么来程序中直接定义 struct Student a,b,c;运行时用 malloc 临时开辟
能否释放不能能用 free 释放
用途理解链表原理实际使用

建立动态链表的三件套(了解即可):

  1. malloc(LEN) 开辟一个结点,LEN 定义为 sizeof(struct Student);
  2. p2->next = p1; 把新结点挂到表尾;
  3. 结束时 p2->next = NULL; 封尾。

教材例 9.9(creat 建表)、例 9.10(print 输出)就是干这个的,本课在第四节习题 6~12 里会用到它的简化版。


*9.5 共用体类型选学

📖 教材 p.317–321 · 正文总结

一句话定义:共用体(union)让几个不同类型的变量共用同一段内存,同一时刻只有一个成员有效。

拆名字:union = 联合、合并。国内有些书直译成"联合",谭浩强认为译成"共用体"更贴切——几个变量共用一个内存区。

生活比喻:一个格子,用铅笔写。你今天写"整数 97",明天擦掉写字符 'a'——格子里永远只有一样东西。而结构体是一排格子,各写各的、互不影响。

怎么用:

c
union Data
{ int i;
  char ch;
  float f;
} a, b, c;              // 声明类型的同时定义变量

内存怎么分【必考】:

结构体 struct共用体 union
占用长度各成员长度之和(还要考虑对齐)最长的成员的长度
成员之间各有各的内存,互不干扰共用同一段内存,互相覆盖
同一时刻所有成员同时有效只有一个成员有效(最后一次赋值的那个)
关键字structunion

上例中 a 占 4 字节(因为 float 最长,4 字节),而不是 4+1+4=9 字节。

三条最要命的特点考点:

  1. 同一时刻只能存一个成员。写了 a.i = 97;,此时 a.ch、a.f 都是"97 这个二进制串的另一种解释":
c
   printf("%d", a.i);   // 输出 97
   printf("%c", a.ch);  // 输出字符 'a'(97 正好是 'a' 的 ASCII 码)
   printf("%f", a.f);   // 输出 0.000000(按浮点规则解释这串二进制)
  1. 初始化只能有一个常量:
c
   union Data a = {16};          // ✅ 只给第 1 个成员
   union Data a = {16, 'a', 1.5}; // ❌ 不能初始化 3 个,它们共用一段内存
  1. 起作用的是最后一次赋值的成员。依次执行 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 使用枚举类型

📖 教材 p.322–325 · 正文总结

一句话定义:一个变量只有几种可能的值时,把这几种值一个个列出来,这种类型叫枚举类型。

拆名字:enum = enumeration 的缩写。e-(出) + numer(数) + -ation(名词后缀) → 把可能的数一个个点出来 → 枚举。

生活比喻:表格上原本是空格,你把它改成五个勾选框:红、黄、蓝、白、黑。只能勾一个,想勾"紫色"?没这个框。

怎么用:

c
enum Weekday {sun, mon, tue, wed, thu, fri, sat};
enum Weekday workday, weekend;      // 枚举类型  枚举变量

花括号里的 sun, mon, ... 叫枚举元素或枚举常量,是你自己起的名字。

枚举变量的值只能取列出来的那几个:

c
workday = mon;        // ✅ 正确,mon 是指定的枚举常量之一
workday = sun;        // ✅ 正确
workday = monday;     // ❌ 错,monday 不是指定的枚举常量之一

四条核心规则【全是考点】:

  1. 枚举元素按常量处理,不能对它们赋值。sun = 0; ❌
  2. 每个枚举元素代表一个整数,默认从 0 开始。上例中 sun=0, mon=1, tue=2, ... sat=6。 所以 workday = mon; 等价于 workday = 1;,printf("%d", workday); 输出 1。
  3. 可以人为指定数值,后面的元素在它基础上依次加 1:
c
   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
  1. 枚举元素可以比较大小,规则是按它们代表的整数比。默认规则下 mon > sun,sat > fri。

易错 枚举常量不是字符串。printf("%s", red); 是错的——red 是个整数 0,不是字符串 "red"。想输出单词,得用 switch 一个个判断再打印(例 9.12 就是这么干的)。

考点 C99 把枚举类型也归入整型数据,是"用户自己定义的整数类型"。


*9.7 用 typedef 声明新类型名选学

📖 教材 p.326–329 · 正文总结

一句话定义:typedef 给已经存在的类型起一个新名字(别名)。

拆名字:typedef = type(类型)+ def(define,定义)→ 类型定义。

生活比喻:这套"日期表格"格式,每次都要写 struct Date 太啰嗦,你跟同事约定:"以后说 Date 就是指这套格式。"——只是起了个外号,表格格式一点没变。

最核心的一句:typedef 只是起别名,没有创造新类型。

四步法(教材给的通用套路,背这个就够):

  1. 先按定义变量的方法写出定义体,例如 int i;
  2. 把变量名换成你想用的新类型名,例如把 i 换成 Count → int Count
  3. 在最前面加 typedef → typedef int Count;
  4. 以后就能用新名字定义变量:Count i, j;(等价于 int i, j;)

四个常见用法:

c
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
处理阶段编译阶段预编译阶段(预处理)
本质给类型起别名,编译器真正认识这个类型名简单的字符串替换
复杂类型能处理数组、指针、结构体等复杂类型只能做机械替换,容易出错
作用域遵守作用域规则从定义处到文件末尾一律替换

三、现场写代码

📖 教材例题 · 本课范围 p.293–329

本章例题多,这里挑 10 个最典型的。例 9.9、9.10(动态链表 creat/print)放在第四节习题 6~9 里讲,效果一样。

例 9.1 把一个学生的信息放进结构体变量并输出

📖 教材 p.297 · 例题 9.1

题目:把一个学生的信息(学号、姓名、性别、住址)放在一个结构体变量中,然后输出。

思路:自己建一个结构体类型 → 用它定义变量并顺便初始化 → 用 printf 逐个成员输出。

c
#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。

运行结果:

c
NO.:10101
name:Li Lin
sex:M
address:123 Beijing Road

例 9.2 输入两个学生,输出成绩高的那个

📖 教材 p.298 · 例题 9.2

题目:输入两个学生的学号、姓名和成绩,输出成绩较高的学生的学号、姓名和成绩。

思路:定义两个同类型结构体变量 → 分别读入 3 个成员 → 比较 score 成员 → 输出对应的那张表。

c
#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 个候选人的得票

📖 教材 p.300 · 例题 9.3

题目:有 3 个候选人,每个选民只能投一人,编程序统计选票,先输入被选人名字,最后输出各人得票结果(共 10 票)。

思路:用结构体数组存 3 个候选人(姓名 + 票数,票数先置 0)→ 循环 10 次读入姓名 → 和 3 个候选人的 name 逐一 strcmp → 相同就给那个元素的 count 加 1。

c
#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 个名字后):

c
Result:
   Li:4
Zhang:2
  Sun:3

例 9.4 按成绩高低输出学生信息(结构体数组排序)

📖 教材 p.301 · 例题 9.4

题目:有 n 个学生的信息(学号、姓名、成绩),按成绩高低顺序输出。

思路:结构体数组 + 选择排序。不同的是:交换的是整张表,所以要一个 struct Student 类型的临时变量 temp。

c
#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 类型——只有同类型结构体变量才能互相赋值。

运行结果:

c
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 通过指向结构体变量的指针输出成员

📖 教材 p.303 · 例题 9.5

题目:通过指向结构体变量的指针变量输出结构体变量中成员的信息。

c
#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 用指针变量遍历结构体数组

📖 教材 p.304 · 例题 9.6

题目:有 3 个学生的信息放在结构体数组中,要求输出全部学生信息。

c
#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。

运行结果:

c
 No.   Name                sex age
10101 Li Lin                M   18
10102 Zhang Fang            M   19
10104 Wang Min              F   20

例 9.7 用结构体指针作函数参数(三个函数配合)

📖 教材 p.306 · 例题 9.7

题目:有 n 个学生,每人有学号、姓名和 3 门课成绩。输出平均成绩最高的学生的信息。

思路:功能函数化,分 3 个函数:input 输入并算平均分,max 找平均分最高的元素,print 输出。主函数用指针把数组传给它们。

c
#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 同名,但互不相干(都是局部名字),虚实结合靠的是地址相同。

运行结果:

c
请输入各学生的信息:学号、姓名、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 建立一个简单的静态链表选学

📖 教材 p.310 · 例题 9.8

题目:建立由 3 个学生数据结点组成的简单链表,输出各结点数据。

c
#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 用共用体处理"同一格两种含义"选学

📖 教材 p.320 · 例题 9.11

题目:人员数据中有学生和教师。学生含姓名、号码、性别、职业、班级;教师含姓名、号码、性别、职业、职务。要求用同一个表格处理。

思路:前 4 项完全一样,第 5 项不同——学生的第 5 项是班级(int),教师的第 5 项是职务(字符串)。第 5 项用共用体,同一段内存按 job 的值决定怎么解释。

c
#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:负号表示左对齐,数字表示占几列。

运行结果:

c
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 色球"问题

📖 教材 p.324 · 例题 9.12

题目:口袋中有红、黄、蓝、白、黑 5 种颜色的球若干个。每次取出 3 个球,问得到 3 种不同颜色球的可能取法,输出每种排列。

思路:三重循环穷举 i、j、k,要求 i != j、i != k、j != k。因为枚举常量不能当字符串输出,再用 switch 把颜色值翻译成单词。

c
#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。


四、书上习题精讲

📖 教材 p.330 · 习题

本章书末共 12 题,逐题覆盖。第 1~5 题是结构体的基本应用,第 6~12 题集中在链表。

习题 1 计算某日是本年第几天

📖 教材 p.330 · 习题 1

题目:定义一个结构体变量(包括年、月、日)。计算该日在本年中是第几天,注意闰年问题。

思路:用一个"每月天数"数组,闰年把 2 月改成 29 天,然后把本月之前的整月天数累加,再加上日。

c
#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 函数

📖 教材 p.330 · 习题 2

题目:写一个函数 days,实现第 1 题的计算。由主函数将年、月、日传递给 days 函数,计算后将日子数传回主函数输出。

思路:把日期打包成 struct Date 一起传给函数(结构体变量作参数,值传递),函数返回 int。

c
#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 个学生记录

📖 教材 p.330 · 习题 3

题目:编写一个函数 print,打印一个学生的成绩数组,该数组中有 5 个学生的数据记录,每个记录包括 num、name、score[3],用主函数输入这些记录,用 print 函数输出这些记录。

c
#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 函数

📖 教材 p.330 · 习题 4

题目:在第 3 题的基础上,编写一个函数 input,用来输入 5 个学生的数据记录。

c
#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 门课:总平均分 + 最高分学生

📖 教材 p.330 · 习题 5

题目:有 10 个学生,每个学生的数据包括学号、姓名、3 门课程的成绩,从键盘输入 10 个学生数据,要求输出 3 门课程总平均成绩,以及最高分的学生的数据(包括学号、姓名、3 门课程成绩、平均分数)。

c
#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 人围圈报数,出圈问题(链表实现)

📖 教材 p.330 · 习题 6

题目:13 个人围成一圈,从第 1 个人开始顺序报号 1,2,3。凡报到 3 者退出圈子。找出最后留在圈子中的人原来的序号。要求用链表实现。

思路:建一个循环链表(尾结点的 next 指回头结点),用一个 count 计报数,报到 3 就摘掉当前结点。

c
#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 函数删除指定结点

📖 教材 p.330 · 习题 7

题目:在例 9.9 和例 9.10 的基础上,写一个函数 del,用来删除动态链表中指定的结点。

思路:两个指针 p1 找目标、p2 记前驱。找到后分两种情况:删的是头结点(改 head)还是中间/尾结点(改 p2->next)。

下面是函数片段,不能单独运行。用的时候把它和例 9.9 的结构体定义、int n;、#include <stdio.h>、#include <stdlib.h> 放在同一个文件里。

c
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 函数插入结点

📖 教材 p.330 · 习题 8

题目:写一个函数 insert,用来向一个动态链表插入结点。

思路:链表按学号升序排列,新结点 p0 从表头开始找位置,插在第一个比它大的结点前面。

c
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

📖 教材 p.330 · 习题 9

题目:综合例 9.9(creat)、例 9.10(print)、第 7 题(del)、第 8 题(insert),再写一个主函数,用 5 个函数组成一个程序,实现链表的建立、输出、删除和插入。

c
#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 合并两个链表,按学号升序

📖 教材 p.330 · 习题 10

题目:已有 a、b 两个链表,每个链表中的结点包括学号、成绩。要求把两个链表合并,按学号升序排列。

思路:归并。两个指针各扫一条链,每次挑学号小的那个接到新链尾。

c
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 链表同学号的结点

📖 教材 p.330 · 习题 11

题目:有两个链表 a 和 b,设结点中包含学号、姓名。从 a 链表中删去与 b 链表中有相同学号的那些结点。

思路:对 a 的每个结点,到 b 里线性查找有没有相同学号;有就摘掉。

c
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 按年龄删结点

📖 教材 p.330 · 习题 12

题目:建立一个链表,每个结点包括:学号、姓名、性别、年龄。输入一个年龄,如果链表中的结点所包含的年龄等于此年龄,则将此结点删去。

思路:和第 11 题骨架完全一样,只是判据从"学号相同"换成"年龄相等",而且可能连删多个(循环天然支持)。

c
#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

c
#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

c
#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

c
#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

c
#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 函数,请补全,使它输出平均分最高的学生。要求用结构体指针作参数、返回结构体类型。

c
#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

c
// ❌ 错误示例(故意):请找出两处错并改正
#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

c
// ❌ 错误示例(故意):请找出错在哪并改正
#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

c
// ❌ 错误示例(故意):请找出错在哪并改正
#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 句话

📖 本课补充 · 非教材原文
  1. 结构体类型是"空白表格的版式",结构体变量才是"填好字的那张表"——类型不占内存,变量才占。
  2. struct 后面的分号不能丢;类型名永远写作 struct Student,只有 typedef 之后才能简写成 Student。
  3. . 是所有运算符里优先级最高的;*p.num 一定是错的,要么写 (*p).num,要么写 p->num。
  4. 结构体只能一个成员一个成员地输入输出,但可以整体赋值、整体作函数参数、整体返回。
  5. 共用体是"一个格子反复擦写",长度等于最长成员;结构体是"一排格子各写各的",长度是各成员之和。
  6. 链表 = 每张表的最后一格写着"下一张表在哪",没有头指针就整条链都找不到;NULL 表示到头。

📚 教材覆盖清单(第9章,用户自己建立数据类型)

这张表把本课和教材逐节对上:从网页可以回查书页,从书页也能找到网页位置。标「未展开」的,就是教材里有、本课没讲的部分,已明确列出。

教材节号标题书页网站对应状态
9.1定义和使用结构体变量p.293–2999.1 定义和使用结构体变量精讲
9.1.1自己建立结构体类型p.293–2949.1.1 自己建立结构体类型精讲
9.1.2定义结构体类型变量p.2959.1.2 定义结构体类型变量精讲
9.1.3结构体变量的初始化和引用p.296–2999.1.3 结构体变量的初始化和引用精讲
9.2使用结构体数组p.300–3029.2 使用结构体数组精讲
9.2.1定义结构体数组p.3009.2.1 定义结构体数组精讲
9.2.2结构体数组的应用举例p.301–3029.2.2 结构体数组的应用举例精讲
9.3结构体指针p.303–3089.3 结构体指针精讲
9.3.1指向结构体变量的指针p.3039.3.1 指向结构体变量的指针精讲
9.3.2指向结构体数组的指针p.304–3059.3.2 指向结构体数组的指针精讲
9.3.3用结构体变量和结构体变量的指针作函数参数p.306–3089.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–3259.6 使用枚举类型精讲
9.7用typedef声明新类型名p.326–329*9.7 用 typedef 声明新类型名选学略讲
习题第9章书末习题p.330本课「书上习题精讲」逐题精讲(第1–12题)
插图本课范围内教材插图p.293–329用文字/ASCII 重画未逐张复刻
附录附录 A–Ep.357–370全书对照页未单独成课
  • 教材插图(流程图、内存示意图等)本课用文字/ASCII 重画,未逐张复刻,页码见上表。
  • 附录 A–E(p.357–370)是速查表,未单独成课;全站对照页列出。
  • 标「本课补充」的小节(先补地基、比喻、随课练习等)不是教材原文,是为理解而加的。