C 语言第 6 章:循环结构——三种循环的差别与韩信点兵

写于 2026-11-01

循环是三种基本结构里唯一能让程序"做重复劳动"的结构,也是初学者第一次感到"程序真的有力量"的地方。三种循环的语法十分钟就能讲完,真正难的是另外三件事:循环的边界写不写等号、累加器的初值取什么、什么时候该用 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,反之亦然,选哪个只看可读性。

三、累加求和类问题的通用模式

所有累加型循环都由四个部分组成,缺一不可:

  1. 初始化累加器(sum = 0 或 fac = 1);
  2. 循环条件(决定做多少次);
  3. 累加语句(sum += 项);
  4. 更新循环变量(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),代价是要先懂数论。

三种解法放在一起看,结论不是"中国剩余定理最厉害",而是:穷举保证正确、优化保证够快、数学保证最优。工程上大多数情况用第二种就够,因为"把最强约束变成步长"这个思路几乎零成本,而收益是数量级的。

易错点

  1. 在 for(...) 或 while(...) 后面多写一个分号,循环体变成空语句,后面的 { } 只执行一次。
  2. do-while 的 while(条件) 后面漏分号。
  3. 累加器未初始化。局部变量的初值是栈上的残留值,不是 0。
  4. 1 / i 整数除法,分数求和恒为 1。
  5. while 配合 continue 时,更新语句被跳过,造成死循环。
  6. 以为 break 能跳出所有层循环。
  7. srand 写在循环里,或用 rand() % 1 这类退化写法,导致"随机数不随机"。
  8. 循环边界差一:求前 n 项和写成 i < n,少算一项。

这一章在整门课里的位置

本章是三种基本结构的最后一块拼图,前面第 4 章提供输入输出、第 5 章提供条件判断,到这里才凑齐"能读数据、能分支、能重复"的完整能力。循环也是后续所有内容的载体:数组遍历、字符串处理、排序查找、递归的迭代改写,全都建立在 for 与 while 之上。学完这一章,已经可以用 C 写出完整的算法程序,下一步该进入数据结构,看这些循环如何被组织成更高效的结构。

本文是《C 语言程序设计》第 6 章的知识点摘要。完整板书与代码见 课程页;上一章 选择结构。下一步建议看 数据结构与算法(93讲)。

相关课程

这篇笔记涉及的主题,我录过对应的视频课程: