CC语言通关
第05课 · 循环结构程序设计
教材对照
第5章 · 书页 110–138

第05课 · 循环结构程序设计

while、do…while、for、循环嵌套、break/continue、经典算法

9 个小节 55 段代码 建议 2h
教材依据第5章《循环结构程序设计》书页 p.110–138
本课覆盖:教材第5章正文 + 书末习题(p.137–138)。标「教材 p.X」= 教材段落总结;「例题」= 教材例题;「习题」= 书末题目;「本课补充」= 非教材原文。
在全书对照表中查看 →

教材《C程序设计》第五版(谭浩强)第 5 章,书页 110–138,共 29 页|建议 2h

前置:第 03–04 课(变量、数据类型、运算符、printf/scanf、if/switch)。这一章和选择结构一起,构成期末考试"读程序写结果"和"编程题"的主战场。


课前一句话

📖 本课补充 · 非教材原文

顺序结构让程序"走一遍",选择结构让程序"拐个弯",循环结构让程序"绕圈跑"。这一课就干三件事:学会三种让程序绕圈的写法(while / do…while / for)、学会控制绕圈的节奏(break / continue / 嵌套)、用循环解决教材 11 道例题和书后 17 道习题。学完你应该能:拿到"求和、阶乘、素数、水仙花、斐波那契、最大公约数、九九乘法表、打印图形"这类题,不用想就能搭出循环骨架。


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

📖 本课补充 · 非教材原文

1.1 一个"笨办法"引出的问题

教材开篇的例子:统计全班 50 个学生的平均成绩。最原始的做法,是先写好一个学生的:

c
scanf("%f%f%f%f%f", &score1, &score2, &score3, &score4, &score5);
aver = (score1 + score2 + score3 + score4 + score5) / 5;
printf("aver=%7.2f\n", aver);

然后把这三行原样再抄 49 遍。缺点一目了然:

  • 程序长、重复、难读;
  • 改一处(比如 5 门课变 6 门)要改 50 处;
  • 50 个还能忍,5 万条数据怎么办?

于是几乎所有高级语言都提供了循环控制:同一段代码只写一次,让计算机自己重复执行。

1.2 本章的比喻:操场跑圈

这一章从头到尾只用一个比喻:你被体育老师罚跑圈。

跑圈里的角色C 语言里的名字例子
要跑的那个"圈"循环(loop,环、圈)while / for
每圈里做的事循环体sum = sum + i;
站在起跑线上循环变量初值i = 1
裁判问"还跑吗"循环条件i <= 100
每跑完一圈记一笔循环变量更新i++
没有停下来的条件死循环(infinite loop)忘了 i++
跑到一半直接退赛break立刻离开操场
这圈不算,直接进下一圈continue跳过剩下的动作

1.3 循环三要素(本章最重要的一张表)

写任何循环之前,先在草稿纸上回答三个问题:

要素问自己写在哪
初值从几开始?while / do…while:循环之前;for:表达式 1
条件跑到什么时候停?while / do…while:括号里;for:表达式 2
更新每圈怎么变?while / do…while:循环体内;for:表达式 3

考点考试给一段循环让你写输出,正确姿势是:先列三要素,再在草稿纸上手动跑前 3 圈,比盯着代码看快得多。

易错三要素少一个都不行:

  • 少初值 → 变量是垃圾值,结果不可预测;
  • 少条件 → 不知道什么时候停;
  • 少更新 → 死循环(本章最高频的错误)。

二、知识点讲解

2.1(教材 5.1)为什么需要循环控制

📖 教材 p.110 · 正文总结

一句话:生活里大量"重复做同一件事"的问题,用顺序和选择都写不出来,只能用循环。

教材举的四个例子:

  • 输入全班 50 个学生的成绩 —— 重复 50 次相同的输入操作;
  • 分别统计全班 50 个学生的平均成绩 —— 重复 50 次相同的计算操作;
  • 求 30 个整数之和 —— 重复 30 次相同的加法操作;
  • 检查 30 个学生的成绩是否及格 —— 重复 30 次相同的判别操作。

为什么:不用循环,就只能把相同或相似的语句写若干遍,程序冗长、重复、难以阅读和维护。

结论(教材原话):顺序结构、选择结构、循环结构是结构化程序设计的 3 种基本结构,它们是各种复杂程序的基本构成单元。

考点三种基本结构 = 顺序 + 选择 + 循环;循环结构又称"重复结构"。

2.2(教材 5.2)用 while 语句实现循环

📖 教材 p.111–112 · 正文总结

一句话定义:只要条件成立,就一遍一遍地执行循环体。

拆名字:while 不是缩写,是英语连词"当……的时候"。while (i <= 100) 读作"当 i 小于等于 100 的时候"。

生活比喻:你跑到裁判面前,裁判先问"还跑吗?"——你说"跑",才放你跑一圈;跑完回来再问一次。先问后跑,这就是 while。

怎么用(一般形式):

c
while (表达式)
    语句
  • 表达式:循环条件表达式,值为非 0(真)就执行循环体,为 0(假)就结束循环;
  • 语句:循环体。只能是一条语句;要写多条,必须用 { } 括成复合语句。

执行过程(4 步,背下来):

  1. 计算表达式的值;
  2. 非 0(真)→ 执行循环体;为 0(假)→ 跳过循环体,循环结束;
  3. 循环体执行完,回到第 1 步;
  4. 重复,直到表达式为假。

易错三个高频坑:

  1. while 后面多写一个分号:while (i <= 100); —— 循环体变成空语句,i 永远不变 → 死循环。
  2. 循环体多语句不加花括号:while (i <= 100) sum = sum + i; i++; —— while 只管到第一个分号,i++ 不属于循环 → 死循环。
  3. 忘了给循环变量赋初值:int i; while (i <= 100) —— i 是随机值,可能一次都不执行,也可能跑疯。

教材对例 5.1 的三条分析(原文要点,考试爱考):

  1. 循环体如果包含一个以上的语句,应该用花括号括起来;不加花括号,while 的范围只到后面第 1 个分号处。
  2. 不要忽略给 i 和 sum 赋初值(这是未累加前的初始情况),否则值不可预测。
  3. 在循环体中应有使循环趋于结束的语句(本例的 i++),否则循环永不结束。

2.3(教材 5.3)用 do…while 语句实现循环

📖 教材 p.113–115 · 正文总结

一句话定义:先无条件做一次,再问要不要继续。

拆名字:do = 英语"做";while = "当……的时候"。合起来:"先做,再当……的时候继续做"。

生活比喻:体育老师这次改规则了——"先跑一圈,跑完我再问你还要不要跑"。所以哪怕你一开始就说"我不跑",这一圈也躲不掉。先跑后问,至少跑一圈。

怎么用(一般形式):

c
do
    语句
while (表达式);

执行过程:

  1. 先执行一次循环体;
  2. 计算 while 后面的表达式;
  3. 非 0(真)→ 回到第 1 步;为 0(假)→ 循环结束。

易错while (表达式) 后面的分号不能丢!这是 do…while 独有的、也是考试最爱挖的坑。丢了分号不会"把下一行当成循环体",而是直接语法错误、编译不通过(MSVC 报 C2146,GCC 报 expected ';')——因为 do…while 是一个完整语句,末尾必须用分号收尾。

易错do 后面的花括号不是必须的(循环体只有一条语句时可以省),但教材建议一律加花括号,否则看到 while (...) 那一行末尾的分号,容易误以为整个语句结束了。

while 与 do…while 的本质区别:

whiledo…while
判断时机先判断,后执行先执行,后判断
最少执行次数0 次(条件一开始就假)1 次(雷打不动)
分号while (表达式) 后没有分号while (表达式); 后必须有分号

教材结论(原话):当 while 后面的表达式的第 1 次值为"真"时,两种循环结果相同;否则结果不同(指二者循环体相同的情况)。

2.4(教材 5.4)用 for 语句实现循环

📖 教材 p.116–119 · 正文总结

一句话定义:把"初值、条件、更新"三件事写在同一行括号里的循环。

拆名字:for 是英语"对于 / 为了"。C 里读作"对于(初值;条件;增量)"。

生活比喻:标准跑道边上立了一块牌子,上面写三行字——"从第 1 圈开始;跑到第 100 圈;每圈加 1"。三件事一次说清,不用你另外记。这就是 for。

怎么用(一般形式):

c
for (表达式1; 表达式2; 表达式3)
    语句
表达式作用执行时机
表达式 1设置初始条件(给循环变量赋初值)整个循环只执行 1 次
表达式 2循环条件表达式,判断是否继续循环每次执行循环体前都要算
表达式 3循环的调整(如循环变量增值)每次执行完循环体后执行

最常用的形式:

c
for (循环变量赋初值; 循环条件; 循环变量增值)
    语句

例如 for (i = 1; i <= 100; i++):i 初值 1,终值 100,每圈加 1,共 100 圈。

执行过程(5 步):

  1. 求表达式 1(只做一次);
  2. 求表达式 2:为真 → 执行循环体,转第 3 步;为假 → 结束循环,转第 5 步;
  3. 求表达式 3;
  4. 转回第 2 步;
  5. 循环结束,执行 for 下面的语句。

考点循环结束后循环变量的值 = "超过"终值的那个数。例如 for (i = 1; i <= 100; i++) 结束后 i = 101;for (i = 100; i >= 1; i--) 结束后 i = 0。规律:循环变量沿着变化方向"超过"循环终值,循环就结束了。这一条几乎每年都考。

for 与 while 等价改写(二者无条件等价):for (表达式1; 表达式2; 表达式3) 语句; 等价于 表达式1; while (表达式2) { 语句; 表达式3; }。

for 的花样(教材 10 条说明里的重点):

写法含义
for (; i <= 100; i++)省略表达式 1(初值在 for 之前给),分号不能省
for (i = 1; ; i++)省略表达式 2 → 条件恒真 → 死循环
for (i = 1; i <= 100;)省略表达式 3,更新写在循环体里
for (; i <= 100;)只留条件,初值和更新都在外面/体内
for (;;)三个都省 → 等价 while (1) → 死循环
for (sum = 0, i = 1; i <= 100; i++)表达式 1 可以是逗号表达式,同时给多个变量赋初值
for (i = 0, j = 100; i <= j; i++, j--)表达式 1、3 都是逗号表达式,两个变量同时变
for (i = 1; i <= 100; i++, i++)每圈 i 加 2,等价于 i = i + 2
for (; (c = getchar()) != '\n'; )表达式 2 可以是任意表达式,只要值非 0 就继续
for (int i = 1; i <= 100; i++)C99 允许在表达式 1 中定义变量,作用域仅限本循环

易错for 括号里三个部分之间用的是分号 ;,不是逗号!写成 for (i = 1, i <= 100, i++) 是编译错误(分号数量不对)。逗号只能出现在某一个表达式内部(逗号表达式)。

易错for (int i = 1; ...) 定义的 i 出了循环就不能用。想在循环外再用 i,要在 for 之前定义。

易错表达式 2 是"非 0 即真"。for (i = 0; i = 5; i++) 里 i = 5 是赋值,值恒为 5 → 死循环(应该是 i == 5)。

2.5(教材 5.5)循环的嵌套

📖 教材 p.120 · 正文总结

一句话定义:一个循环体里又包含另一个完整的循环结构,叫循环的嵌套。

拆名字:嵌套 = "套娃",一层套一层。内嵌的循环里还可以再嵌套,叫多层循环。

生活比喻:大圈套小圈——"跑 4 个大圈,每个大圈里做 5 个俯卧撑"。大圈(外循环)跑 4 次,小圈(内循环)每次都要从头做 5 个。

怎么用:3 种循环(while、do…while、for)可以互相嵌套,教材列了 6 种合法组合(while 套 while、do 套 while、for 套 for、while 套 for、for 套 while、do 套 for),全部合法。

关键规则:

  • 外层循环执行 1 次,内层循环要完整地执行一遍;
  • 无 break 时,总执行次数 = 外层次数 × 内层次数;
  • 内层循环变量每次都要重新赋初值(用 for 最省心,因为表达式 1 每次进内层都会执行)。

易错用 while 写嵌套时,内层循环变量忘了重新赋初值,内层只会执行第一次,后面全部跳过。这是嵌套最经典的 bug。

考点双层 for 循环的执行次数、输出图形、输出矩阵,是期末必考。

2.6(教材 5.6)几种循环的比较

📖 教材 p.121 · 正文总结

一句话:三种循环大多数情况下可以互相替代,但各有最顺手的场合。

比较项whiledo…whilefor
判断与执行的先后先判断后执行先执行后判断先判断后执行
最少执行次数010
初值写在哪循环之前循环之前表达式 1
更新写在哪循环体内循环体内表达式 3
适合场景次数不确定至少要执行一次次数确定
末尾分号无有无

教材的 4 条结论:

  1. 3 种循环都可以用来处理同一问题,一般情况下它们可以互相代替。
  2. while 和 do…while 只在 while 后面括号内指定循环条件,因此循环体内应包含使循环趋于结束的语句;for 可以在表达式 3 中包含该操作,功能更强,凡 while 能完成的,for 都能实现。
  3. while 和 do…while 的循环变量初始化应在语句之前完成;for 可以在表达式 1 中完成。
  4. 三种循环都可以用 break 跳出循环,用 continue 结束本次循环。

考点"for 比 while 功能强"这句话常出现在选择题里;"do…while 至少执行一次"是判断题常客。

2.7(教材 5.7)改变循环执行的状态

📖 教材 p.121–126 · 正文总结

正常循环靠"条件变假"结束。但有时需要提前结束:例如慈善募捐,收到 10 万元就结束,事先并不知道要收多少次。C 提供两个语句:break 和 continue。

2.7.1 用 break 语句提前终止循环

📖 教材 p.122 · 正文总结

一句话定义:立刻终止整个循环,跳到循环体外面,接着执行循环下面的语句。

拆名字:break = 英语"打断、折断"。你在 switch 里见过它(第 04 课),它的作用就是"打断当前结构"。

生活比喻:跑到第 3 圈,裁判突然吹哨"停!不用跑了"。你直接离开操场,后面的圈数全部作废。

怎么用:

c
break;
  • 一般形式就是 break;;
  • 通常写成 if (条件) break;,即"满足某个条件就提前退出"。

易错break 只能用于循环语句和 switch 语句之中,不能单独使用(不能写在 if 里单独用——if 里用 break 的前提是 if 外面有循环或 switch)。

考点双层循环里,break 只终止内层循环,外层循环继续。教材原文的结论:内循环体内的 break"提前终止内循环",流程跳到内循环体之外(执行内循环体下面的语句),不是跳到外循环体之外。

2.7.2 用 continue 语句提前结束本次循环

📖 教材 p.123 · 正文总结

一句话定义:只结束这一次循环,跳过循环体中 continue 后面尚未执行的语句,直接进入下一次循环。

拆名字:continue = con-(一起、继续)+ tinue(保持),英语"继续"。注意:是"继续循环",不是"结束循环"。

生活比喻:跑到第 3 圈,裁判说"这一圈不算,剩下的不用做了,直接进第 4 圈"。圈数照记,只是这一圈的活儿没干完。

怎么用:

c
continue;
  • 一般形式 continue;;
  • 作用:跳到循环体结束点之前,然后:
    • 对 for:执行表达式 3(如 i++),再判断表达式 2;
    • 对 while / do…while:直接回到条件判断处(不会自动执行循环体里的 i++!)。

易错在 while 里把 i++ 写在 continue 后面,会导致 i 永远不增加 → 死循环。要么把 i++ 挪到 continue 之前,要么改用 for。

2.7.3 break 语句和 continue 语句的区别

📖 教材 p.124–126 · 正文总结

一句话:break 是"整个循环不干了",continue 是"这一圈不干了,下一圈照干"。

比较项breakcontinue
作用范围整个循环本次循环
之后流程跳出循环体,执行循环下面的语句回到循环条件处(for 先执行表达式 3)
还判断循环条件吗不再判断继续判断
能否用于 switch能不能
双层循环中只跳出内层只跳过内层的本次

教材用两个 while 结构对比(图 5.13、图 5.14):表达式 2 为真时,break 的箭头跳出整个循环,continue 的箭头回到循环开头。

2.8(教材 5.8)循环程序举例

📖 教材 p.127–136 · 正文总结

一句话:这一节是前面所有语法知识的"综合演练场",5 道例题分别代表 5 类经典算法:

例题算法类型核心手法
例 5.7求 π 的近似值迭代:由上一项推出下一项,累加到精度够为止
例 5.8Fibonacci 数列递推:f3 = f1 + f2,再滚动 f1 = f2; f2 = f3;
例 5.9判断素数试除 + break:能被 2~(n-1) 中任一数整除就不是素数
例 5.10求 100~200 全部素数嵌套循环:外层遍历,内层判定
例 5.11译密码字符循环 + ASCII 运算

这 5 道题的完整代码、逐行解释和运行结果都放在第三部分。


三、现场写代码

📖 教材例题 · 本课范围 p.110–136

例 5.1 / 例 5.2:1+2+…+100 的三种写法

📖 教材 p.111 · 例题 5.1

题目:求 1+2+3+…+100。 思路:累加。sum 存和(初值 0),i 存当前要加的数(初值 1);重复 100 次"sum = sum + i; i++;"。

(1)while 版

c
#include <stdio.h>
int main()
{
    int i = 1, sum = 0;
    while (i <= 100)
    {
        sum = sum + i;
        i++;
    }
    printf("sum=%d\n", sum);
    return 0;
}

运行结果:sum=5050

逐行:

  • #include <stdio.h>:引入标准输入输出库,后面才能用 printf。
  • int main():主函数,程序从这里开始。
  • int i = 1, sum = 0;:i 是循环变量(当前加数),初值 1;sum 是累加器,初值 0(求和一定要先清零)。
  • while (i <= 100):当 i 不超过 100 时继续;i 变成 101 时条件为假,循环结束。
  • sum = sum + i;:把当前 i 累加到 sum。
  • i++;:i 加 1,为下一圈准备;没有这一行就是死循环。
  • printf(...):循环结束后输出,注意它在花括号外面。
  • return 0;:正常结束。

(2)do…while 版

c
#include <stdio.h>
int main()
{
    int i = 1, sum = 0;
    do
    {
        sum = sum + i;
        i++;
    } while (i <= 100);
    printf("sum=%d\n", sum);
    return 0;
}

运行结果:sum=5050

逐行:与 while 版唯一的区别是先执行一次循环体再判断;本题 i=1 满足条件,所以结果相同。注意 } while (i <= 100); 末尾的分号。

(3)for 版

c
#include <stdio.h>
int main()
{
    int i, sum = 0;
    for (i = 1; i <= 100; i++)
        sum = sum + i;
    printf("sum=%d\n", sum);
    return 0;
}

运行结果:sum=5050

逐行:

  • int i, sum = 0;:i 只声明,不赋初值(初值放到 for 里)。
  • for (i = 1; i <= 100; i++):i=1 只做一次;每圈先判断 i<=100;每圈结束 i++。
  • sum = sum + i;:循环体只有一条,可以不写花括号(但建议写)。
  • 循环结束时 i = 101。

考点三种写法等价;考试让你"用 while 改写 for"或反过来,照上面模板搬。

例 5.3:while 和 do…while 的比较

📖 教材 p.115 · 例题 5.3

题目:输入 i,分别用 while 和 do…while 求 1+2+…+i。 思路:循环体相同,只有判断时机不同。

(1)while 版

c
#include <stdio.h>
int main()
{
    int i, sum = 0;
    printf("please enter i,i=?");
    scanf("%d", &i);
    while (i <= 10)
    {
        sum = sum + i;
        i++;
    }
    printf("sum=%d\n", sum);
    return 0;
}

(2)do…while 版

c
#include <stdio.h>
int main()
{
    int i, sum = 0;
    printf("please enter i,i=?");
    scanf("%d", &i);
    do
    {
        sum = sum + i;
        i++;
    } while (i <= 10);
    printf("sum=%d\n", sum);
    return 0;
}

运行结果(教材两次):

  • 输入 1:两者都输出 sum=55;
  • 输入 11:while 版输出 sum=0;do…while 版输出 sum=11。

逐行解释:

  • 输入 11 时,while 的判断 11 <= 10 一开始就是假,循环体一次都不执行,sum 保持 0。
  • do…while 先执行一次:sum = 0+11 = 11,i 变 12,再判断 12 <= 10 为假,退出。
  • 这一组对比就是"do…while 至少执行一次"的最好证据,考试常拿它出读程序题。

例 5.4:break 提前终止循环(慈善募捐)

📖 教材 p.122 · 例题 5.4

题目:全系 1000 名学生募捐,总数达到 10 万元就结束,统计此时捐款人数和人均捐款数。 思路:for 最多循环 1000 次;每输入一笔就累加,一旦 total ≥ 100000 就 break。

c
#include <stdio.h>
#define SUM 100000
int main()
{
    float amount, aver, total;
    int i;
    for (i = 1, total = 0; i <= 1000; i++)
    {
        printf("please enter amount:");
        scanf("%f", &amount);
        total = total + amount;
        if (total >= SUM) break;
    }
    aver = total / i;
    printf("num=%d\naver=%10.2f\n", i, aver);
    return 0;
}

运行结果(教材输入 12000、24600、3200、5643、21900、12345、23000):

c
num=7
aver= 14669.71

逐行:

  • #define SUM 100000:符号常量,编译前把 SUM 全部替换成 100000。
  • float amount, aver, total;:amount 单人捐款,total 总额,aver 人均。
  • for (i = 1, total = 0; ...):表达式 1 是逗号表达式,同时给 i 和 total 赋初值(total 必须清零)。
  • scanf("%f", &amount);:读入一笔捐款。
  • total = total + amount;:累加。
  • if (total >= SUM) break;:达到 10 万立刻跳出 for。
  • aver = total / i;:break 时 i 正好是已捐款人数(第 7 个人捐完触发,i=7 还没执行 i++)。
  • printf("num=%d\naver=%10.2f\n", i, aver);:%10.2f 表示总宽 10 列、两位小数。

考点break 之后 i 的值是多少?—— 已经输入数据的人数,不会再 +1。这是本题最值钱的一句话。

例 5.5:continue 提前结束本次循环

📖 教材 p.123 · 例题 5.5

题目:输出 100~200 之间不能被 3 整除的数。 思路:逐个检查;能被 3 整除就 continue 跳过输出,否则输出。

c
#include <stdio.h>
int main()
{
    int n;
    for (n = 100; n <= 200; n++)
    {
        if (n % 3 == 0)
            continue;
        printf("%d ", n);
    }
    printf("\n");
    return 0;
}

运行结果(前几个):

c
100 101 103 104 106 107 109 110 112 113 ...

共 68 个数;循环固定执行 101 次(100~200 含两端)。

逐行:

  • for (n = 100; n <= 200; n++):n 从 100 到 200。
  • if (n % 3 == 0):n 能被 3 整除(余数为 0)。
  • continue;:跳过本轮剩下的 printf,直接去执行 n++。
  • printf("%d ", n);:只有不能被 3 整除的 n 才会走到这里。
  • printf("\n");:循环结束后换行,注意在循环外。

易错continue 跳过的是"本轮剩下的语句",不跳过 n++(因为 n++ 在 for 的表达式 3 里)。如果这是 while 循环,且 n++ 写在 continue 后面,就会死循环。

例 5.6:循环的嵌套(输出 4×5 矩阵)

📖 教材 p.125 · 例题 5.6

题目:输出

c
1    2    3    4    5
2    4    6    8   10
3    6    9   12   15
4    8   12   16   20

思路:外循环 i 控制行(1~4),内循环 j 控制列(1~5),每格输出 i*j;用 n 计数,每输出 5 个数换行。

c
#include <stdio.h>
int main()
{
    int i, j, n = 0;
    for (i = 1; i <= 4; i++)
        for (j = 1; j <= 5; j++, n++)
        {
            if (n % 5 == 0) printf("\n");
            printf("%d\t", i * j);
        }
    printf("\n");
    return 0;
}

运行结果:4 行 5 列,如题(教材截图中第一行前会先空一行,因为 n=0 时先执行了换行)。

逐行:

  • int i, j, n = 0;:i 外层(行),j 内层(列),n 记录已输出个数。
  • for (i = 1; i <= 4; i++):外循环 4 次。
  • for (j = 1; j <= 5; j++, n++):内循环 5 次,同时 n 每格加 1(逗号表达式)。
  • if (n % 5 == 0) printf("\n");:n 是 5 的倍数就换行。
  • printf("%d\t", i * j);:输出乘积,\t 是制表符,对齐用。
  • 内循环结束一轮,外循环 i 加 1,内层 j 重新从 1 开始。

考点嵌套循环的执行次数 = 4×5 = 20 次。

教材的两次改动(必看,专治 break/continue 混淆):

  1. 在内循环体最前面加 if (i == 3 && j == 1) break; → 第 3 行整行不输出,因为 break 结束的是内层循环,流程进入下一次外循环(i=4)。
  2. 改成 if (i == 3 && j == 1) continue; → 第 3 行只少了第一个数 3,从 6 开始输出,后面 4 个数整体左移一格(因为少输出了一次 \t)。

例 5.7:求 π 的近似值

📖 教材 p.127 · 例题 5.7

题目:π/4 = 1 - 1/3 + 1/5 - 1/7 + …,直到某一项绝对值小于 10⁻⁶ 为止(该项不累加)。 思路:找规律——分子恒为 1;分母 n 每项 +2;符号每项取反。用 term 存当前项,sign 存符号。

c
#include <stdio.h>
#include <math.h>
int main()
{
    int sign = 1;
    double pi = 0.0, n = 1.0, term = 1.0;
    while (fabs(term) >= 1e-6)
    {
        pi = pi + term;
        n = n + 2;
        sign = -sign;
        term = sign / n;
    }
    pi = pi * 4;
    printf("pi=%10.8f\n", pi);
    return 0;
}

运行结果:pi=3.14159065

逐行:

  • #include <math.h>:用到 fabs,必须包含数学库头文件。
  • int sign = 1;:符号,第一项为正。
  • double pi = 0.0, n = 1.0, term = 1.0;:pi 先存多项式的和(最后才乘 4);n 是分母;term 是当前项,初值 1。
  • while (fabs(term) >= 1e-6):当前项绝对值 ≥ 10⁻⁶ 就继续;fabs 求双精度绝对值(整数用 abs)。
  • pi = pi + term;:累加当前项。
  • n = n + 2;:下一项的分母。
  • sign = -sign;:符号取反。
  • term = sign / n;:算出下一项(sign 是 int、n 是 double,除法自动按 double 算)。
  • pi = pi * 4;:和乘以 4 才是 π。
  • printf("pi=%10.8f\n", pi);:输出 8 位小数。

教材的三个补充(考点密集):

  1. fabs 和 abs 的区别:abs(x) 求整数绝对值,结果是整型;fabs(x) 求双精度绝对值。term 是 double,必须用 fabs。
  2. 想提高精度只改循环条件即可:while (fabs(term) >= 1e-8) → 输出 3.14159263。
  3. 本程序 1e-6 时循环体执行 50 万次,1e-8 时执行 5000 万次(这就是习题 2 的答案)。

例 5.8:Fibonacci 数列前 40 个数

📖 教材 p.129 · 例题 5.8

题目:前两个数为 1、1,从第 3 个数起每个数等于前两个数之和,求前 40 个。 思路:滚动法。只用 f1、f2 两个变量:先输出 f1、f2,然后 f1 = f1 + f2; 得到下一个,f2 = f2 + f1; 再得到下一个。

c
#include <stdio.h>
int main()
{
    int f1 = 1, f2 = 1;
    int i;
    for (i = 1; i <= 20; i++)
    {
        printf("%12d %12d", f1, f2);
        if (i % 2 == 0) printf("\n");
        f1 = f1 + f2;
        f2 = f2 + f1;
    }
    return 0;
}

运行结果(前两行):

c
           1            1            2            3
           5            8           13           21

最后一行:24157817 39088169 63245986 102334155。

逐行:

  • int f1 = 1, f2 = 1;:前两个月各 1 对兔子。
  • for (i = 1; i <= 20; i++):每圈输出 2 个数,20 圈正好 40 个。
  • printf("%12d %12d", f1, f2);:每个数占 12 列,右对齐。
  • if (i % 2 == 0) printf("\n");:每输出 4 个数(2 圈)换行。
  • f1 = f1 + f2;:f1 变成下一个数(第 3 个月)。
  • f2 = f2 + f1;:此时 f1 已是新数,f2 变成再下一个(第 4 个月)。

易错两个赋值顺序不能反。如果先写 f2 = f2 + f1;,f2 被改掉后,f1 = f1 + f2; 用的就不是原来的 f2,整个数列全错。

考点"滚动变量"是递推题的标准套路,第 6 章数组、第 7 章函数都会再用。

例 5.9:判断素数

📖 教材 p.131 · 例题 5.9

题目:输入一个大于 3 的整数 n,判断它是否为素数。 思路:让 n 被 2~(n-1) 的每个整数除;只要有一个能整除,就 break。循环结束后看 i:i<n 说明是 break 出来的 → 不是素数;i==n 说明正常结束 → 是素数。

c
#include <stdio.h>
int main()
{
    int n, i;
    printf("please enter a integer number,n=?");
    scanf("%d", &n);
    for (i = 2; i < n; i++)
        if (n % i == 0) break;
    if (i < n)
        printf("%d is not a prime number.\n", n);
    else
        printf("%d is a prime number.\n", n);
    return 0;
}

运行结果:17 is a prime number.

逐行:

  • for (i = 2; i < n; i++):除数从 2 试到 n-1。
  • if (n % i == 0) break;:能整除就提前退出。
  • if (i < n):关键判断——break 出来时 i 还没走到 n;正常结束时 i 已经等于 n。
  • 否则(i == n)就是素数。

改进版(教材例 5.9 第二个程序):只需试除到 √n。

c
#include <stdio.h>
#include <math.h>
int main()
{
    int n, i, k;
    printf("please enter a integer number,n=?");
    scanf("%d", &n);
    k = sqrt(n);
    for (i = 2; i <= k; i++)
        if (n % i == 0) break;
    if (i >= k + 1)
        printf("%d is a prime number.\n", n);
    else
        printf("%d is not a prime number.\n", n);
    return 0;
}

运行结果:327 is not a prime number.

逐行:

  • k = sqrt(n);:k 取 √n 的整数部分(sqrt 返回 double,赋给 int 自动截断)。
  • for (i = 2; i <= k; i++):只试到 k。
  • if (i >= k + 1):正常结束(一次都没 break)→ 素数。

易错教材这个改进版写的是 if (i < k),当 n 是平方数(如 49,k=7)时会误判成素数。正确写法是 if (i >= k + 1)(与例 5.10 一致)。考试一律按"i 是否超过 k"判断最稳。

例 5.10:求 100~200 的全部素数

📖 教材 p.134 · 例题 5.10

题目:求 100~200 的全部素数。 思路:外层遍历 101~199 的奇数,内层试除到 √n;用 m 计数,每 10 个换行。

c
#include <stdio.h>
#include <math.h>
int main()
{
    int n, k, i, m = 0;
    for (n = 101; n <= 200; n = n + 2)
    {
        k = sqrt(n);
        for (i = 2; i <= k; i++)
            if (n % i == 0) break;
        if (i >= k + 1)
        {
            printf("%d ", n);
            m = m + 1;
            if (m % 10 == 0) printf("\n");
        }
    }
    printf("\n");
    return 0;
}

运行结果:

c
101 103 107 109 113 127 131 137 139 149
151 157 163 167 173 179 181 191 193 197
199

逐行:

  • for (n = 101; n <= 200; n = n + 2):偶数一定不是素数,只查奇数。
  • k = sqrt(n);:内层上限。
  • for (i = 2; i <= k; i++):内层试除。
  • if (i >= k + 1):内层正常结束 → n 是素数。
  • m = m + 1;:素数个数加 1。
  • if (m % 10 == 0) printf("\n");:每 10 个换一行。

例 5.11:译密码

📖 教材 p.135 · 例题 5.11

题目:把字母变成其后第 4 个字母(A→E,…,W→A,X→B,Y→C,Z→D),非字母不变。输入一行字符,输出密码。 思路:逐个读字符,是字母就 +4;如果在 W~Z(或 w~z)范围内,+4 会超出字母表,要再 -26 绕回来。

c
#include <stdio.h>
int main()
{
    char c;
    c = getchar();
    while (c != '\n')
    {
        if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
        {
            if (c >= 'W' && c <= 'Z' || c >= 'w' && c <= 'z')
                c = c - 22;
            else
                c = c + 4;
        }
        printf("%c", c);
        c = getchar();
    }
    printf("\n");
    return 0;
}

运行结果:输入 China! 输出 Glmre!

逐行:

  • char c;:字符变量。
  • c = getchar();:读入一个字符。
  • while (c != '\n'):没读到换行就继续。
  • if (...字母判断...):只处理字母。
  • if (c >= 'W' && c <= 'Z' || c >= 'w' && c <= 'z'):最后 4 个字母,+4 会越界,所以等价于 +4-26 = -22。
  • else c = c + 4;:其余字母直接 +4。
  • printf("%c", c);:输出变换后的字符。
  • c = getchar();:读下一个字符(这一行不能漏,否则死循环)。

改进版(教材):把读字符合并进 while 条件。

c
#include <stdio.h>
int main()
{
    char c;
    while ((c = getchar()) != '\n')
    {
        if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
        {
            c = c + 4;
            if (c > 'Z' && c <= 'Z' + 4 || c > 'z')
                c = c - 26;
        }
        printf("%c", c);
    }
    printf("\n");
    return 0;
}

逐行:

  • while ((c = getchar()) != '\n'):先读入再判断,省掉两处 getchar。
  • c = c + 4;:字母先统一 +4。
  • if (c > 'Z' && c <= 'Z' + 4 || c > 'z'):+4 后超出 Z(或 z)的,减 26 绕回。
  • 输出。

易错教材特别提醒:内嵌 if 不能写成 if (c > 'Z' || c > 'z'),因为所有小写字母都满足 c > 'Z',会被错误地减 26。必须限定范围。


四、书上习题精讲

📖 教材 p.137–138 · 习题

书页 137–138,共 17 题,逐题覆盖。

第 1 题(画流程图)

📖 教材 p.137 · 习题 1

题目:请画出例 5.6 中给出的 3 个程序段的流程图。

答案要点:三个流程图骨架相同,差别只在内循环体开头。

  1. 原程序:外层 i=1 → 判断 i≤4 → 内层 j=1 → 判断 j≤5 → 判断 n%5==0(是则换行)→ 输出 i*j → j++, n++ → 回内层判断 → 内层结束 → i++ → 回外层判断。
  2. 加 break 的程序:在内层循环体最前面加判断框"i==3 && j==1?",为是则执行 break,流程跳出内层循环(不是跳出外层),直接去做 i++。
  3. 加 continue 的程序:同一位置判断,为是则执行 continue,流程回到内层循环的 j++, n++,继续内层的下一次,只是跳过本次的 printf。

考点画图时注意:break 的箭头指向内层循环框外,continue 的箭头指回内层循环的调整部分。

第 2 题(统计例 5.7 的循环次数)

📖 教材 p.127 · 例题 5.7

题目:请补充例 5.7 程序,分别统计当 fabs(t) >= 1e-6 和 fabs(t) >= 1e-8 时执行循环体的次数。

思路:加一个计数器 count,每进一次循环体加 1。

c
#include <stdio.h>
#include <math.h>
int main()
{
    int sign = 1, count = 0;
    double pi = 0.0, n = 1.0, term = 1.0;
    while (fabs(term) >= 1e-6)
    {
        pi = pi + term;
        n = n + 2;
        sign = -sign;
        term = sign / n;
        count++;
    }
    pi = pi * 4;
    printf("pi=%10.8f count=%d\n", pi, count);
    return 0;
}

答案:

  • fabs(term) >= 1e-6:500000 次(50 万次),输出 pi=3.14159065;
  • 把条件改成 >= 1e-8:50000000 次(5000 万次),输出 pi=3.14159263。

考点这道题说明"精度提高 100 倍,计算量也大约提高 100 倍",是算法效率的直观例子。

第 3 题(最大公约数和最小公倍数)

📖 教材 p.137 · 习题 3

题目:输入两个正整数 m 和 n,求其最大公约数和最小公倍数。

思路:辗转相除法求最大公约数——a % b 的余数记作 t,然后 a = b; b = t;,直到 b 为 0,此时的 a 就是 gcd。最小公倍数 = m / gcd * n(先除后乘,防止溢出)。

c
#include <stdio.h>
int main()
{
    int m, n, a, b, t, gcd, lcm;
    printf("请输入两个正整数 m n: ");
    scanf("%d %d", &m, &n);
    a = m; b = n;
    while (b != 0)
    {
        t = a % b;
        a = b;
        b = t;
    }
    gcd = a;
    lcm = m / gcd * n;
    printf("最大公约数=%d 最小公倍数=%d\n", gcd, lcm);
    return 0;
}

运行结果(输入 12 18):最大公约数=6 最小公倍数=36

易错lcm = m * n / gcd 在 m、n 较大时会溢出;写成 m / gcd * n 更安全。若用 while 求 gcd,记得用副本 a、b,否则 m、n 被改掉,后面算 lcm 就错了。

第 4 题(统计一行字符)

📖 教材 p.137 · 习题 4

题目:输入一行字符,分别统计出其中英文字母、空格、数字和其他字符的个数。

思路:用 getchar() 一次读一个字符,读到 '\n' 结束;用四个计数器分别累加。

c
#include <stdio.h>
int main()
{
    int letters = 0, spaces = 0, digits = 0, others = 0;
    char c;
    printf("请输入一行字符:\n");
    while ((c = getchar()) != '\n')
    {
        if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
            letters++;
        else if (c == ' ')
            spaces++;
        else if (c >= '0' && c <= '9')
            digits++;
        else
            others++;
    }
    printf("字母=%d 空格=%d 数字=%d 其他=%d\n", letters, spaces, digits, others);
    return 0;
}

运行结果(输入 Abc 12!@#):字母=3 空格=1 数字=2 其他=3

易错判断字母必须大小写两段都写;c >= 'a' && c <= 'z' 不能写成 c >= 'a' || c <= 'z'(后者恒真)。

第 5 题(Sn = a + aa + aaa + …)

📖 教材 p.137 · 习题 5

题目:求 Sn = a + aa + aaa + … + aa…a 之值,其中 a 是一个数字,n 表示 a 的位数,n 由键盘输入。例如 n=5、a=2 时为 2+22+222+2222+22222。

思路:后一项 = 前一项 × 10 + a;边生成边累加。

c
#include <stdio.h>
int main()
{
    int a, n, i;
    long term = 0, sum = 0;
    printf("请输入 a 和 n: ");
    scanf("%d %d", &a, &n);
    for (i = 1; i <= n; i++)
    {
        term = term * 10 + a;
        sum = sum + term;
    }
    printf("Sn=%ld\n", sum);
    return 0;
}

运行结果(输入 2 5):Sn=24690

考点"后一项由前一项推出来"是循环题的通用套路(和 Fibonacci 一个思路)。

第 6 题(1!+2!+…+20!)

📖 教材 p.137 · 习题 6

题目:求 Σn!(即求 1!+2!+3!+4!+…+20!)。

思路:term = term * i 边乘边加。不要每项都从 1 重新乘一遍(那样是 O(n²))。

c
#include <stdio.h>
int main()
{
    int i;
    double term = 1, sum = 0;
    for (i = 1; i <= 20; i++)
    {
        term = term * i;
        sum = sum + term;
    }
    printf("1!+2!+...+20!=%.0f\n", sum);
    return 0;
}

运行结果:1!+2!+...+20!=2561327494111820288

易错真实值是 2561327494111820313,double 只有约 15~16 位有效数字,后几位是舍入误差。要精确结果,把两个变量改成 64 位整数 long long,输出用 %lld,结果就是 2561327494111820313。

第 7 题(三个求和)

📖 教材 p.137 · 习题 7

题目:求 Σ(k=1→100)k + Σ(k=1→50)k² + Σ(k=1→10)1/k。

思路:三个独立的循环,分别累加;第三个要用浮点除法 1.0 / k。

c
#include <stdio.h>
int main()
{
    int k;
    double s1 = 0, s2 = 0, s3 = 0;
    for (k = 1; k <= 100; k++) s1 = s1 + k;
    for (k = 1; k <= 50; k++)  s2 = s2 + k * k;
    for (k = 1; k <= 10; k++)  s3 = s3 + 1.0 / k;
    printf("sum=%.6f\n", s1 + s2 + s3);
    return 0;
}

运行结果:sum=47977.928968

易错1 / k 是整数除法,k>1 时结果全是 0;必须写 1.0 / k(或 (double)1 / k)。

第 8 题(水仙花数)

📖 教材 p.137 · 习题 8

题目:输出所有的"水仙花数"。所谓"水仙花数"是指一个 3 位数,其各位数字立方和等于该数本身。例如 153 是水仙花数,因为 153 = 1³ + 5³ + 3³。

思路:遍历 100~999;用 / 和 % 拆出百位 a、十位 b、个位 c;判断 a³+b³+c³ == n。

c
#include <stdio.h>
int main()
{
    int n, a, b, c;
    for (n = 100; n <= 999; n++)
    {
        a = n / 100;
        b = n / 10 % 10;
        c = n % 10;
        if (a*a*a + b*b*b + c*c*c == n) printf("%d ", n);
    }
    printf("\n");
    return 0;
}

运行结果:153 370 371 407

考点拆位公式:百位 n/100、十位 n/10%10、个位 n%10,第 6 章数组还会再用。

第 9 题(完数)

📖 教材 p.137 · 习题 9

题目:一个数如果恰好等于它的因子之和,这个数就称为"完数"。例如 6 的因子为 1、2、3,而 6 = 1+2+3,因此 6 是"完数"。编程找出 1000 之内的所有完数,并按下面格式输出其因子:6 its factors are 1,2,3。

思路:对每个 i,用内层循环累加它的真因子(1 ~ i-1 中能整除 i 的);和等于 i 就是完数,再输出因子。

c
#include <stdio.h>
int main()
{
    int i, j, s;
    for (i = 2; i < 1000; i++)
    {
        s = 0;
        for (j = 1; j < i; j++)
            if (i % j == 0) s = s + j;
        if (s == i)
        {
            printf("%d its factors are 1", i);
            for (j = 2; j < i; j++)
                if (i % j == 0) printf(",%d", j);
            printf("\n");
        }
    }
    return 0;
}

运行结果:

c
6 its factors are 1,2,3
28 its factors are 1,2,4,7,14
496 its factors are 1,2,4,8,16,31,62,124,248

易错内层每次都要把 s 清零(s = 0; 写在外循环体内、内循环之前),否则会一直累加。

第 10 题(分数序列前 20 项和)

📖 教材 p.137 · 习题 10

题目:有一个分数序列 2/1, 3/2, 5/3, 8/5, 13/8, 21/13, …,求出这个数列的前 20 项之和。

思路:分子、分母都符合 Fibonacci 规律:后一项的分子 = 前一项的分子 + 分母,后一项的分母 = 前一项的分子。用 a、b 两个变量滚动。

c
#include <stdio.h>
int main()
{
    int i;
    double a = 2, b = 1, t, sum = 0;
    for (i = 1; i <= 20; i++)
    {
        sum = sum + a / b;
        t = a;
        a = a + b;
        b = t;
    }
    printf("sum=%.6f\n", sum);
    return 0;
}

运行结果:sum=32.660261

易错交换 a、b 时必须用临时变量 t;写成 a = a + b; b = a; 会让 b 拿到新值,序列全错。

第 11 题(球落地)

📖 教材 p.138 · 习题 11

题目:一个球从 100m 高度自由落下,每次落地后反弹回原高度的一半,再落下,再反弹。求它在第 10 次落地时共经过多少米,第 10 次反弹多高。

思路:第一次落地只算 100m 的下落;之后每次"反弹 + 下落"是 2 倍当前反弹高度。

c
#include <stdio.h>
int main()
{
    double h = 100, s = 100;
    int i;
    for (i = 2; i <= 10; i++)
    {
        h = h / 2;
        s = s + 2 * h;
    }
    printf("第10次落地共经过 %.6f 米\n", s);
    printf("第10次反弹高度 %.8f 米\n", h / 2);
    return 0;
}

运行结果:

c
第10次落地共经过 299.609375 米
第10次反弹高度 0.09765625 米

易错循环结束后 h 是第 9 次反弹高度(0.1953125),第 10 次反弹还要再除以 2。

第 12 题(猴子吃桃)

📖 教材 p.138 · 习题 12

题目:猴子第 1 天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了一个。第 2 天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第 10 天早上想再吃时,就只剩一个桃子了。求第 1 天共摘多少个桃子。

思路:倒推。设第 10 天早上剩 1 个;前一天的数量 = (今天数量 + 1)× 2。从第 9 天倒推到第 1 天,共倒推 9 次。

c
#include <stdio.h>
int main()
{
    int day, x = 1;
    for (day = 9; day >= 1; day--)
        x = (x + 1) * 2;
    printf("第1天共摘了 %d 个桃子\n", x);
    return 0;
}

运行结果:第1天共摘了 1534 个桃子

考点倒推题的公式是"把正向操作反过来做":正向是 (x/2 - 1),反向就是 (x+1)*2。

第 13 题(迭代法求平方根)

📖 教材 p.138 · 习题 13

题目:用迭代法求 x = √a。求平方根的迭代公式为 x(n+1) = (x(n) + a/x(n)) / 2,要求前后两次求出的 x 的差的绝对值小于 10⁻⁵。

c
#include <stdio.h>
#include <math.h>
int main()
{
    double a, x0, x1;
    printf("请输入 a: ");
    scanf("%lf", &a);
    x0 = a / 2;
    x1 = (x0 + a / x0) / 2;
    while (fabs(x1 - x0) >= 1e-5)
    {
        x0 = x1;
        x1 = (x0 + a / x0) / 2;
    }
    printf("sqrt(%.2f)=%.6f\n", a, x1);
    return 0;
}

运行结果(输入 2):sqrt(2.00)=1.414214

考点迭代法的固定骨架:给初值 → 算新值 → 判断差值 → 不满足就滚回去。

第 14 题(牛顿迭代法)

📖 教材 p.138 · 习题 14

题目:用牛顿迭代法求方程 2x³ - 4x² + 3x - 6 = 0 在 1.5 附近的根。

思路:牛顿公式 x = x - f(x)/f'(x)。这里 f(x)=2x³-4x²+3x-6,f'(x)=6x²-8x+3。

c
#include <stdio.h>
#include <math.h>
int main()
{
    double x0 = 1.5, x1, f, d;
    do
    {
        f = 2*x0*x0*x0 - 4*x0*x0 + 3*x0 - 6;
        d = 6*x0*x0 - 8*x0 + 3;
        x1 = x0 - f / d;
        if (fabs(x1 - x0) < 1e-5) break;
        x0 = x1;
    } while (1);
    printf("root=%.6f\n", x1);
    return 0;
}

运行结果:root=2.000000

考点本题的根恰好是 2,可以手算验证 f(2)=16-16+6-6=0。

第 15 题(二分法)

📖 教材 p.138 · 习题 15

题目:用二分法求方程 2x³ - 4x² + 3x - 6 = 0 在 (-10, 10) 的根。

思路:区间两端函数值异号,取中点;中点函数值与左端同号就把左端移到中点,否则右端移到中点;区间足够小就结束。

c
#include <stdio.h>
#include <math.h>
int main()
{
    double a = -10, b = 10, m;
    double fa = 2*a*a*a - 4*a*a + 3*a - 6;
    while (b - a > 1e-6)
    {
        m = (a + b) / 2;
        if ((2*m*m*m - 4*m*m + 3*m - 6) * fa < 0) b = m;
        else a = m;
    }
    printf("root=%.6f\n", (a + b) / 2);
    return 0;
}

运行结果:root=2.000000

易错判据是"两端函数值异号(乘积小于 0)",不是"中点函数值为 0"。本题中 fa 只用来提供"左端函数值的符号":a 被换成 m 时,f(m) 与 fa 必然同号,所以 fa 不必重算;如果觉得不放心,可以在每轮循环里重新计算 fa,结果一样。

第 16 题(输出菱形图案)

📖 教材 p.138 · 习题 16

题目:输出以下图案:

c
   *
  ***
 *****
*******
 *****
  ***
   *

思路:上半部分 4 行(i=1~4),每行先打 4-i 个空格,再打 2*i-1 个星号;下半部分 3 行(i=3~1),规律相同。

c
#include <stdio.h>
int main()
{
    int i, j;
    for (i = 1; i <= 4; i++)
    {
        for (j = 1; j <= 4 - i; j++) printf(" ");
        for (j = 1; j <= 2 * i - 1; j++) printf("*");
        printf("\n");
    }
    for (i = 3; i >= 1; i--)
    {
        for (j = 1; j <= 4 - i; j++) printf(" ");
        for (j = 1; j <= 2 * i - 1; j++) printf("*");
        printf("\n");
    }
    return 0;
}

运行结果:如上图(7 行)。

考点图形打印的通用方法:先找行号 i 与"空格数""星号数"的函数关系,再用两个内层循环分别打印。

第 17 题(乒乓球比赛名单)

📖 教材 p.138 · 习题 17

题目:两个乒乓球队进行比赛,各出 3 人。甲队为 A、B、C 3 人,乙队为 X、Y、Z 3 人。已抽签决定比赛名单。有人向队员打听比赛的名单,A 说他不和 X 比,C 说他不和 X、Z 比,请编程序找出 3 对赛手的名单。

思路:三重循环枚举所有配对;用 a != b && c != a && c != b 保证每人只配一次;再加上题目的两个条件。

c
#include <stdio.h>
int main()
{
    char a, b, c;
    for (a = 'X'; a <= 'Z'; a++)
        for (b = 'X'; b <= 'Z'; b++)
            if (a != b)
                for (c = 'X'; c <= 'Z'; c++)
                    if (c != a && c != b && a != 'X' && c != 'X' && c != 'Z')
                        printf("A-%c B-%c C-%c\n", a, b, c);
    return 0;
}

运行结果:A-Z B-X C-Y

考点"枚举 + 约束"是逻辑推理题的标准解法;注意三个 != 保证 A、B、C 的对手互不相同。


🛠 随课练习(只给题不给答案)

📖 本课补充 · 非教材原文

🔮 猜输出

题 1

c
#include <stdio.h>
int main()
{
    int i;
    for (i = 1; i <= 5; i++)
    {
        if (i == 3) continue;
        printf("%d ", i);
    }
    return 0;
}

题 2

c
#include <stdio.h>
int main()
{
    int i = 5, sum = 0;
    do
    {
        sum = sum + i;
        i++;
    } while (i < 5);
    printf("%d %d\n", sum, i);
    return 0;
}

题 3

c
#include <stdio.h>
int main()
{
    int i, j;
    for (i = 1; i <= 3; i++)
    {
        for (j = 1; j <= 3; j++)
        {
            if (j == 2) break;
            printf("%d%d ", i, j);
        }
    }
    return 0;
}

题 4

c
#include <stdio.h>
int main()
{
    int i = 0, s = 0;
    while (i < 5)
    {
        i++;
        if (i == 3) continue;
        s = s + i;
    }
    printf("%d\n", s);
    return 0;
}

🛠 变式题

题 5(照猫画虎):把下面的 while 循环改写成等价的 for 循环,填在横线上。

c
int i = 1, sum = 0;
while (i <= 100)
{
    sum = sum + i;
    i++;
}
/* 改写为: */
for (______; ______; ______)
    ______;

题 6(改需求):用循环求 1~100 中所有偶数的和(分别用 while 和 for 各写一遍)。

题 7(改需求):写一个程序模拟"输入密码,最多允许输错 3 次":用 do…while 实现,密码正确就提示"欢迎"并结束,3 次都错就提示"锁定"。

题 8(补全):补全下面的程序片段,输出九九乘法表(下三角)。

c
int i, j;
for (i = 1; i <= 9; i++)
{
    for (j = 1; ______; j++)
        printf("%d*%d=%-3d", j, i, ______);
    ______;
}

🐛 改错题

题 9:下面的程序想求 1~100 的和,找出错误并改正。

c
#include <stdio.h>
int main()
{
    int i = 1, sum = 0;
    while (i <= 100)
    {
        sum = sum + i;
    }
    printf("sum=%d\n", sum);
    return 0;
}

题 10:下面的 for 语句有语法错误,指出并改正。

c
for (i = 1, i <= 100, i++)
    sum = sum + i;

题 11:下面的程序想输出 1~5 中除 3 以外的数,找出错误并说明为什么。

c
#include <stdio.h>
int main()
{
    int i = 1;
    while (i <= 5)
    {
        if (i == 3) continue;
        printf("%d ", i);
        i++;
    }
    return 0;
}

📈 加练题(你随时喊,我随时追加到这里)

📖 本课补充 · 非教材原文

(暂无。想加练就说"再出几道 XX 题",我出的题会标上日期追加在这块。)


💬 问答区(你问过的问题 + 我的解答)

📖 本课补充 · 非教材原文

(暂无。你问的问题和我的解答会按 Q/A 整理进这里。)


该带走的 6 句话

📖 本课补充 · 非教材原文
  1. 循环三要素:初值、条件、更新——写任何循环前,先在草稿纸上回答这三个问题。
  2. while 先问后跑(可能 0 次),do…while 先跑后问(至少 1 次),for 把三要素写在一行。
  3. 循环结束后循环变量的值是"超过终值"的那个数:for (i=1; i<=100; i++) 结束后 i=101。
  4. break 是整个循环不干了,continue 是这一圈不干了;双层循环里 break 只跳内层。
  5. 死循环三大来源:忘了更新、while 后多写分号、循环体不加花括号。
  6. 经典算法都有固定骨架:累加 sum += i、累乘 term *= i、素数(试除 + break 后看 i)、递推(滚动变量)。

学完这一课,第 5 章的循环就算拿下了。下一课进入第 6 章《利用数组处理批量数据》——循环的"最佳拍档":一维数组、二维数组、字符数组,还有排序和查找。到那时你会发现,本课的"拆位公式"和"滚动变量"马上就能用上。

🔁 循环追踪器改 n,点「单步」,看每一圈 i 和 sum 怎么变
for (i = 1; i <= n; i++)  sum = sum + i;
i1
sum0
i <= n ?—
点「单步」开始。每一圈先判断条件,再执行 sum = sum + i,最后 i++。

📚 教材覆盖清单(第5章,循环结构程序设计)

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

教材节号标题书页网站对应状态
5.1为什么需要循环控制p.1102.1(教材 5.1)为什么需要循环控制精讲
5.2用while语句实现循环p.111–1122.2(教材 5.2)用 while 语句实现循环精讲
5.3用do…while语句实现循环p.113–1152.3(教材 5.3)用 do…while 语句实现循环精讲
5.4用for语句实现循环p.116–1192.4(教材 5.4)用 for 语句实现循环精讲
5.5循环的嵌套p.1202.5(教材 5.5)循环的嵌套精讲
5.6几种循环的比较p.1212.6(教材 5.6)几种循环的比较精讲
5.7改变循环执行的状态p.121–1262.7(教材 5.7)改变循环执行的状态精讲
5.7.1用break语句提前终止循环p.1222.7.1 用 break 语句提前终止循环精讲
5.7.2用continue语句提前结束本次循环p.1232.7.2 用 continue 语句提前结束本次循环精讲
5.7.3break语句和continue语句的区别p.124–1262.7.3 break 语句和 continue 语句的区别精讲
5.8循环程序举例p.127–1362.8(教材 5.8)循环程序举例精讲
习题第5章书末习题p.137–138本课「书上习题精讲」逐题精讲(第1–17题)
插图本课范围内教材插图p.110–136用文字/ASCII 重画未逐张复刻
附录附录 A–Ep.357–370全书对照页未单独成课
  • 教材插图(流程图、内存示意图等)本课用文字/ASCII 重画,未逐张复刻,页码见上表。
  • 附录 A–E(p.357–370)是速查表,未单独成课;全站对照页列出。
  • 标「本课补充」的小节(先补地基、比喻、随课练习等)不是教材原文,是为理解而加的。