C 语言第 6 章:循环结构——三种循环的差别与韩信点兵
循环是三种基本结构里唯一能让程序"做重复劳动"的结构,也是初学者第一次感到"程序真的有力量"的地方。三种循环的语法十分钟就能讲完,真正难的是另外三件事:循环的边界写不写等号、累加器的初值取什么、什么时候该用 break 什么时候该用 continue。边界差一,结果就全错;初值取错,累加恒为 0;跳转语句用错,直接死循环。本章最后的韩信点兵值得单独看:同一个问题有穷举、步长优化、中国剩余定理三种解法,它们之间的差距不是代码长短,而是有没有利用问题本身的结构。
一、for 语句:计数型循环
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
执行流程固定为五步:执行表达式 1(初始化,全程只做一次)→ 判断表达式 2 → 为真则执行循环体 → 执行表达式 3(更新)→ 回到判断。注意更新发生在循环体之后、下一次判断之前,所以 i++ 写在表达式 3 里,循环体里就不用再管它。
三个表达式都可以省略,但两个分号不能省:
for (;;)是死循环,靠内部的 break 退出。- 表达式 1 省略时,循环变量的初始化要提前到循环外。
- 表达式 3 省略时,更新要写进循环体,否则死循环。
C99 起允许在表达式 1 里定义变量(for (int i = 0; ...)),该变量的作用域仅限于这个循环。老教材常把 i 定义在函数开头,是因为 C89 不允许这种写法。
for 适合循环次数事先已知的场景:遍历数组、求前 n 项和、打印九九表。
二、while 与 do-while
while (条件) { 语句; } /* 先判断,后执行 */
do { 语句; } while (条件); /* 先执行,后判断;结尾必须有分号 */
两者的语法差别只有一个分号,语义差别却是根本的:while 可能一次都不执行,do-while 至少执行一次。当初始条件就不成立时,这个差别会直接改变结果。
| 循环 | 判断时机 | 最少执行次数 | 典型场景 |
|---|---|---|---|
| for | 循环体之前 | 0 | 次数已知、计数遍历 |
| while | 循环体之前 | 0 | 次数未知、条件驱动(读到文件尾、输入非法值重试) |
| do-while | 循环体之后 | 1 | 菜单、至少要做一次的操作 |
选型判据可以压成两句:知道要做多少次,用 for;不知道多少次、只知道"做到什么条件为止",用 while;无论如何都得先做一次,用 do-while。三者在表达能力上等价——任何 for 都能改写成 while,反之亦然,选哪个只看可读性。
三、累加求和类问题的通用模式
所有累加型循环都由四个部分组成,缺一不可:
- 初始化累加器(
sum = 0或fac = 1); - 循环条件(决定做多少次);
- 累加语句(
sum += 项); - 更新循环变量(
i++)。
初值由运算的单位元决定:加法初值 0,乘法初值 1。写成 fac = 0 再累乘,结果永远是 0,而且编译器不会提醒。
三个高频变体:
- 分数求和 1 + 1/2 + … + 1/n:必须写
sum += 1.0 / i。1 / i是整数除法,i ≥ 2 时恒为 0。 - 正负交替 1 − 1/2 + 1/3 − …:用符号变量
sign = -sign每轮翻转,或写成(i % 2 ? -1 : 1) * 1.0 / i。 - 阶乘:注意溢出。32 位 int 的上限是 2147483647,而 12! = 479001600 还没超,13! = 6227020800 已经溢出;改用 64 位后,20! ≈ 2.43×10¹⁸ 仍能放下,21! 就超了。溢出不报错,只会得到一个负数或莫名其妙的数。
求平均值的坑在最后一步:sum / n 若两者都是 int,会截断小数,应写 (double)sum / n。
四、随机数与猜数字游戏
#include <stdlib.h>
#include <time.h>
srand((unsigned)time(NULL)); /* 整个程序只需调用一次 */
int target = rand() % 100 + 1; /* 1 ~ 100 */
四条必须知道的事实:
rand()返回 0 到RAND_MAX之间的伪随机整数,RAND_MAX至少为 32767(MSVC 下正是 32767,glibc 下是 2147483647)。- 不调用 srand 时,种子默认为 1,每次运行的序列完全相同。这就是"我的随机数每次都一样"的全部原因。
srand只需在程序开头调用一次。放进循环里会导致每次拿到同一个数(同一秒内 time 的返回值不变)。rand() % n存在轻微偏差:RAND_MAX + 1 通常不能被 n 整除,前面的余数出现概率略高。要求严格均匀时应写成(int)(rand() / (RAND_MAX + 1.0) * n)。
猜数字的主循环是典型的"次数未知 + 靠 break 退出":
int guess, cnt = 0;
while (1) {
scanf("%d", &guess);
cnt++;
if (guess > target) printf("大了\n");
else if (guess < target) printf("小了\n");
else { printf("猜中,用了 %d 次\n", cnt); break; }
}
五、break、continue 与 return
| 语句 | 作用范围 | 执行后去哪 |
|---|---|---|
| break | 最内层的循环或 switch | 整个循环结束,执行循环后的语句 |
| continue | 本次迭代 | 跳过剩余语句,进入下一次 |
| return | 整个函数 | 立即返回,循环一并结束 |
| goto | 函数内任意标号 | 跳转(破坏结构化,不推荐) |
两条容易出错的细节:
- break 只跳出一层。双层循环里内层 break 之后,外层照常继续。要一次跳出多层,只能用标志变量加外层判断,或把循环封成函数用 return。
- continue 在 for 与 while 中的落点不同。for 里 continue 会执行表达式 3(更新),所以不会死循环;while 里 continue 直接回到条件判断,如果更新语句写在 continue 之后,就永远执行不到,形成死循环。这是 while + continue 最典型的陷阱。
六、韩信点兵的三种解法
问题:士兵 3 人一排余 2,5 人一排余 3,7 人一排余 2,求最少人数。
解法一,穷举。 从 1 开始逐个试,第一个满足三个条件的数就是答案。
for (int n = 1; ; n++)
if (n % 3 == 2 && n % 5 == 3 && n % 7 == 2) {
printf("%d\n", n); break;
}
思路最直白,但要试 23 次。
解法二,步长优化。 既然要求 n % 7 == 2,那候选数必然是 2、9、16、23…,直接以 7 为步长跳跃,只需试 4 次:
for (int n = 2; ; n += 7)
if (n % 3 == 2 && n % 5 == 3) { printf("%d\n", n); break; }
这一步体现的思路是用最严格的约束缩小搜索空间——把条件最强的那个同余式变成"生成规则",而不是"检验规则"。
解法三,中国剩余定理。 模 3、5、7 两两互质,解的周期是 3×5×7 = 105,在周期内唯一。手算:M₁ = 35,M₂ = 21,M₃ = 15;35 ≡ 2 (mod 3),其逆元为 2;21 ≡ 1 (mod 5),逆元为 1;15 ≡ 1 (mod 7),逆元为 1。于是
x = 2×35×2 + 3×21×1 + 2×15×1 = 140 + 63 + 30 = 233 ≡ 23 (mod 105)
验证:23 % 3 = 2,23 % 5 = 3,23 % 7 = 2,全部吻合。复杂度从 O(n) 降到 O(1),代价是要先懂数论。
三种解法放在一起看,结论不是"中国剩余定理最厉害",而是:穷举保证正确、优化保证够快、数学保证最优。工程上大多数情况用第二种就够,因为"把最强约束变成步长"这个思路几乎零成本,而收益是数量级的。
易错点
- 在
for(...)或while(...)后面多写一个分号,循环体变成空语句,后面的{ }只执行一次。 - do-while 的
while(条件)后面漏分号。 - 累加器未初始化。局部变量的初值是栈上的残留值,不是 0。
1 / i整数除法,分数求和恒为 1。- while 配合 continue 时,更新语句被跳过,造成死循环。
- 以为 break 能跳出所有层循环。
srand写在循环里,或用rand() % 1这类退化写法,导致"随机数不随机"。- 循环边界差一:求前 n 项和写成
i < n,少算一项。
这一章在整门课里的位置
本章是三种基本结构的最后一块拼图,前面第 4 章提供输入输出、第 5 章提供条件判断,到这里才凑齐"能读数据、能分支、能重复"的完整能力。循环也是后续所有内容的载体:数组遍历、字符串处理、排序查找、递归的迭代改写,全都建立在 for 与 while 之上。学完这一章,已经可以用 C 写出完整的算法程序,下一步该进入数据结构,看这些循环如何被组织成更高效的结构。
本文是《C 语言程序设计》第 6 章的知识点摘要。完整板书与代码见 课程页;上一章 选择结构。下一步建议看 数据结构与算法(93讲)。
相关课程
这篇笔记涉及的主题,我录过对应的视频课程:
- C 语言程序设计 15 讲