上一篇我们让程序学会了"做决策"——if、else、switch。程序终于不再是一条路走到黑了,它可以根据条件选择不同的执行路径。但这还不够。
想象你要在屏幕上打印 1 到 100 的所有数字。用目前学到的知识,你只能写 100 行 printf——这显然不是程序员该干的事。或者你想做一个游戏,让玩家反复猜数字直到猜对为止——你不可能预知玩家要猜几次,所以你没法用 if-else 来写。
这时候就需要循环——让一段代码反复执行,直到满足某个条件才停下来。C 语言提供了三种循环语句:while、for、do-while。这篇文章会逐一吃透它们,还要讲 break 和 continue 这两个控制循环流向的关键字,最后用一篇完整项目——猜数字游戏——把你前面所有知识串起来。
这篇文章还会用到几个 C 标准库中的新函数:rand()、srand()、time(),分别声明在 <stdlib.h> 和 <time.h> 中。你不用提前了解它们——我们会在用到的地方逐一展开。
while 循环——最简单的循环
while 的语法和 if 几乎一模一样:
while (表达式)
语句;唯一的区别:if 的条件满足时,语句只执行一次;while 的条件满足时,语句会反复执行,直到条件不满足为止。
对比一下就更清楚了:
#include <stdio.h>
int main()
{
/* if 只执行一次 */
if (1)
printf("hehe\n"); /* 打印 1 次 */
/* while 反复执行,直到条件为假 */
/* 注意:这里条件永远是 1(真),会死循环! */
/* while (1)
printf("hehe\n"); 打印无数次 */
return 0;
}while 的执行流程:先判断条件 → 为真就执行循环体 → 执行完再判断 → 还为真就再执行 → ... → 直到条件为假才停止。如果你不小心把条件写成了永远为真的表达式(比如 while(1)),程序就会"死循环"——一直跑、停不下来,只能强制关闭。
任何循环都有三个要素:初始化、判断、调整。来看打印 1 到 10 的例子:
#include <stdio.h>
int main()
{
int i = 1; /* 初始化:循环变量从 1 开始 */
while (i <= 10) /* 判断:当 i <= 10 时进入循环 */
{
printf("%d ", i); /* 执行:打印当前的 i */
i = i + 1; /* 调整:i 自增 1,准备下一次循环 */
}
printf("\n");
return 0;
}
/* 输出:1 2 3 4 5 6 7 8 9 10 */while 的三要素分散在不同位置:初始化在循环前面,判断在 while 括号里,调整在循环体内部。这正是 while 的特点——也是它的弱点:代码多了之后,调整部分可能被遗漏或写错位置。
while 的经典坑:忘记写调整语句
看这段"事故代码":
#include <stdio.h>
int main()
{
int i = 1;
while (i <= 10)
{
printf("%d ", i); /* 忘了写 i++! */
}
return 0;
}
/* 输出:1 1 1 1 1 1 ...(死循环!) */i 永远不增长,条件永远为真,程序永远停不下来。这是 while 循环最容易犯的错误——写了判断,忘了调整。for 循环为什么更常用?就是因为它把三个要素集中在一行,不容易漏。
实战:逆序打印一个整数的每一位
再来看一个更巧妙的例子——逆序打印一个整数的每一位。输入 1234,输出 4 3 2 1:
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n); /* 例:输入 1234 */
while (n) /* n 不为 0 就继续 */
{
printf("%d ", n % 10); /* n%10 得到最低位:4, 3, 2, 1 */
n /= 10; /* n=n/10,去掉最低位:123, 12, 1, 0 */
}
printf("\n");
return 0;
}
/* 输入 1234 → 输出 4 3 2 1 */这个例子的精妙之处在于利用了整数的特性:% 10 能得到最低位,/ 10 能去掉最低位。反复这两个操作,就能把每一位都剥出来。而且 while (n) 里的 n 既是循环变量又是判断条件——当 n 变成 0 时,循环自动结束。
注意一个边界问题:如果输入的是 0,while (0) 条件为假,循环一次都不执行,什么都不输出。这在后面用 do-while 时会形成鲜明对比——do-while 正是为了处理这种"至少要执行一次"的场景。
for 循环——最常用的循环
for 是三种循环中使用频率最高的。它的语法把初始化、判断、调整三个要素集中在了同一行:
for (表达式1; 表达式2; 表达式3)
语句;- 表达式1:初始化,只在循环开始前执行一次
- 表达式2:判断条件,每次循环前检查,为假则退出
- 表达式3:调整,每次循环体执行完后执行
执行顺序:表达式1(一次)→ 表达式2(真)→ 循环体 → 表达式3 → 表达式2(真)→ 循环体 → ...
用 for 打印 1 到 10:
#include <stdio.h>
int main()
{
int i = 0;
for (i = 1; i <= 10; i++) /* 初始化 i=1, 判断 i<=10, 调整 i++ */
{
printf("%d ", i); /* 循环体:打印 i */
}
printf("\n");
return 0;
}和 while 版本对比,for 的三要素在同一行,一目了然。当代码量增大时,for 的可维护性明显更好。
for 与 while 的等价转换
任何 for 循环都可以改写成 while,反之亦然:
/* for 版 */
for (i = 1; i <= 10; i++)
{
printf("%d ", i);
}
/* 等价的 while 版 */
i = 1; /* 初始化 */
while (i <= 10) /* 判断 */
{
printf("%d ", i);
i++; /* 调整 */
}它们只是"把三要素写在哪儿"的区别,底层行为完全一样。记住这个等价关系,你就不会在"用 for 还是 while"上纠结——已知循环次数用 for,次数未知用 while。
for 循环的三个表达式都可以省略
for 循环的三个表达式都可以省略(但分号不能省):
- 省略表达式1:在外面初始化
- 省略表达式2:条件永远为真 → 死循环,除非用
break跳出 - 省略表达式3:在循环体内调整
/* 省略所有表达式——死循环 */
for (;;)
{
/* 需要 break 或 return 来跳出 */
}for (;;) 是一种"经典死循环"写法,等价于 while (1)。两者在几乎所有编译器下生成的机器码都一样,用哪个纯属个人喜好。看到 for (;;) 不要慌,它只是"无限循环"的意思。
实战:计算 1 到 100 之间 3 的倍数的和
#include <stdio.h>
int main()
{
int i = 0;
int sum = 0;
/* 方法1:遍历所有数,判断是否能被 3 整除 */
for (i = 1; i <= 100; i++)
{
if (i % 3 == 0)
sum += i;
}
printf("3 的倍数之和(方法1)= %d\n", sum); /* 1683 */
/* 方法2:直接从 3 开始,每次加 3,效率更高 */
sum = 0;
for (i = 3; i <= 100; i += 3)
{
sum += i;
}
printf("3 的倍数之和(方法2)= %d\n", sum); /* 1683 */
return 0;
}方法 2 不需要判断 i % 3 == 0,循环次数也从 100 次减少到 33 次——这是写循环时应该培养的优化意识:让循环变量直接跳过不相关的值。
for 循环的常见错误
错误一:分号位置写错。 for (i = 1; i <= 10; i++); 在 ) 后面多了一个分号——这个分号本身就是空语句,成了循环体。结果是循环"空转"10 次,然后执行下面那条 printf 一次。这是最隐蔽的 bug 之一:
#include <stdio.h>
int main()
{
int i = 0;
for (i = 1; i <= 10; i++); /* 注意这个分号!循环体是空语句 */
{
printf("%d ", i); /* 只执行一次,输出 11 */
}
return 0;
}错误二:循环变量在循环体里被意外修改。 比如有人图省事在循环体内又写 i++,循环次数立刻减半且难以察觉。循环变量的调整只应该出现在表达式 3 或循环体末尾。
错误三:<= 和 < 用错导致多一次或少一次。 for (i = 0; i < n; i++) 和 for (i = 0; i <= n; i++) 差一次。习惯上,C 程序员用 i < n 表示"循环 n 次"(从 0 到 n-1),这样和数组下标 0~n-1 天然对齐,以后学数组你就懂了。
do-while 循环——先斩后奏
while 和 for 都是"先判断,再执行"——条件不满足就一次都不执行。但有些场景下,你希望"至少执行一次,再判断要不要继续"。do-while 就是为这种场景设计的:
do
语句;
while (表达式);注意:while (表达式); 后面有个分号!这个分号很容易忘。
执行流程:先执行一次循环体 → 再判断条件 → 为真则继续循环。
对比 while 和 do-while:
#include <stdio.h>
int main()
{
int i = 100;
/* while:先判断,条件不满足,一次都不执行 */
while (i <= 10)
{
printf("%d ", i);
i++;
}
/* 什么都不输出 */
/* do-while:先执行一次,再判断 */
i = 100;
do
{
printf("%d ", i); /* 输出 100,即使条件不满足也执行了一次 */
i++;
} while (i <= 10);
/* 输出 100 */
return 0;
}实战:统计一个正整数的位数
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n); /* 例:输入 1234 */
int cnt = 0; /* 计数器 */
do
{
cnt++; /* 每去掉一位,计数加 1 */
n = n / 10; /* 去掉最低位 */
} while (n); /* n 不为 0 就继续 */
printf("位数 = %d\n", cnt); /* 输出 4 */
return 0;
}这个场景特别适合 do-while:即使输入的是 0,也应该输出"1 位数"。如果用 while,输入 0 时 n 为 0,条件不满足,一次都不执行,cnt 还是 0——这显然不对。
do-while 的真实使用场景
do-while 用得最少,但有一个场景它几乎是唯一选择:菜单交互。用户至少要看到一次菜单,选择退出后才结束——"先显示,后判断"正好对应 do-while。猜数字游戏的主循环就是经典例子,后面你马上就能看到。
break 和 continue——控制循环的两个开关
break 和 continue 用于在循环体内改变执行流程:
break:永久终止循环。一旦执行到break,直接从循环中跳出来。continue:跳过本次循环的剩余部分,直接进入下一次循环的条件判断(或for的调整部分)。
while 中的 break
#include <stdio.h>
int main()
{
int i = 1;
while (i <= 10)
{
if (i == 5)
break; /* 当 i=5 时,永久跳出循环 */
printf("%d ", i); /* 只打印了 1 2 3 4 */
i++;
}
/* 输出:1 2 3 4 */
return 0;
}while 中的 continue——小心死循环!
#include <stdio.h>
int main()
{
int i = 1;
while (i <= 10)
{
if (i == 5)
continue; /* 跳过 i=5 这次循环的剩余部分 */
printf("%d ", i);
i++; /* 注意:continue 导致 i++ 被跳过! */
}
/* 当 i=5 时,continue 跳过了 i++,i 永远是 5 → 死循环! */
return 0;
}这是一个非常重要的陷阱! 在 while 循环中,如果 continue 跳过了循环变量的调整语句,就会导致死循环。而 for 循环不会有这个问题——因为 for 的调整部分(表达式3)在 continue 之后依然会执行。
为什么会这样?回到执行流程:while 里 continue 的作用是"跳到循环体的结束处",然后去执行条件判断——调整语句如果在 continue 之后,就被跳过了。而 for 的表达式 3 在语法上属于"循环控制部分",continue 跳到的是表达式 3 之前,所以调整照常执行。记住这个本质区别,比死记结论更有用。
for 中的 continue
#include <stdio.h>
int main()
{
int i = 0;
for (i = 1; i <= 10; i++)
{
if (i == 5)
continue; /* 跳过本次打印,但 i++ 仍会执行! */
printf("%d ", i); /* 打印 1 2 3 4 6 7 8 9 10 */
}
return 0;
}
/* 输出:1 2 3 4 6 7 8 9 10 (跳过了 5) */总结:while 中 continue 容易造成死循环(因为跳过了调整语句),for 中 continue 安全得多(调整在头部,不受 continue 影响)。
do-while 里的 break 和 continue 行为与 while 中完全一致:break 直接终止循环,continue 跳过本次循环体剩余代码,去执行 while (表达式) 的条件判断——如果循环变量的调整写在 continue 后面,同样可能陷入死循环,需要注意。
break 和 continue 的适用范围
一个容易混淆的点:break 只能跳出"最近的一层"循环或 switch,continue 只能作用于它所在的循环。如果你有三层循环嵌套,在第三层写 break,它只跳出第三层,外层循环继续跑。想要一次跳出多层?后面讲的 goto 是一个方案,也可以用"标志变量"配合多层判断。
另外注意:continue 在 switch 里是非法的(除非 switch 在循环里)。continue 只用于循环,break 既用于循环也用于 switch。
循环嵌套——循环里面套循环
有时候一层循环不够用。比如打印 9×9 乘法表,你需要外层循环控制行,内层循环控制列。
实战 1:打印 9×9 乘法表
#include <stdio.h>
int main()
{
int i = 0, j = 0;
for (i = 1; i <= 9; i++) /* 外层:控制行 */
{
for (j = 1; j <= i; j++) /* 内层:控制列,每行打印 i 个 */
{
printf("%d*%d=%-2d ", j, i, i * j);
/* %-2d:左对齐,宽度 2,让表格整齐 */
}
printf("\n"); /* 一行结束换行 */
}
return 0;
}
/* 输出:
* 1*1=1
* 1*2=2 2*2=4
* 1*3=3 2*3=6 3*3=9
* ...(完整的 9 行)*/分析这个例子的执行过程:外层 i=1 时,内层 j 从 1 到 1 循环一次;外层 i=2 时,内层 j 从 1 到 2 循环两次……外层每执行一次,内层完整地执行一整轮。这就是嵌套循环的节奏——像时钟的时针和分针,分针(内层)转一圈,时针(外层)才走一格。
实战 2:找出 100 到 200 之间的所有素数
素数(质数)的定义是:只能被 1 和它本身整除的数。判断一个数 i 是不是素数,需要用 2 到 i-1 之间的每个数去试除:
#include <stdio.h>
int main()
{
int i = 0, j = 0;
for (i = 100; i <= 200; i++) /* 外层:遍历 100~200 */
{
int flag = 1; /* 假设 i 是素数 */
for (j = 2; j < i; j++) /* 内层:用 2~i-1 试除 */
{
if (i % j == 0) /* 如果 j 能整除 i */
{
flag = 0; /* i 不是素数 */
break; /* 不用继续试了 */
}
}
if (flag == 1) /* 如果 flag 还是 1 */
printf("%d ", i); /* i 是素数,打印出来 */
}
printf("\n");
return 0;
}两层嵌套:外层 for 产生 100 到 200 的每个数,内层 for 对每个数进行素数判断。flag 是一个"标记变量"——初始设为 1(假设是素数),一旦找到因子就改为 0。这种"先假设为真,再寻找反例"的思路在编程中非常常见。
两个可以优化的点(现在知道即可,以后会深入):
- 试除范围从
j < i缩小到j <= sqrt(i)——因为如果 i 有因子,必有一个不超过它的平方根; - 偶数(除了 2)不可能是素数,外层可以直接跳过偶数。
优化后的内层循环次数从 i 次降到 √i 次,100~200 区间整体耗时下降一个数量级。写循环时先想"能不能少转几圈",这是性能意识的起点。
实战 3:打印直角三角形星号
#include <stdio.h>
int main()
{
int i = 0, j = 0;
int n = 5;
for (i = 1; i <= n; i++) /* 控制行数 */
{
for (j = 1; j <= i; j++) /* 控制每行星号个数 */
{
printf("*");
}
printf("\n");
}
return 0;
}
/* 输出:
* *
* **
* ***
* ****
* *****
*/把内层条件改成 j <= n - i + 1,就能得到倒三角。改成"先打空格再打星号"就能得到金字塔——这些都是锻炼"循环变量关系"的好练习,强烈建议自己动手试一遍。
嵌套循环的总执行次数估算
一个"外层 N 次 × 内层 M 次"的嵌套循环,总执行次数是 N×M 次。三层嵌套就是 N×M×K。嵌套深度每加一层,执行次数就多乘一个维度——这也是为什么嵌套太深(超过 3 层)的代码既难读又可能慢。以后学时间复杂度时你会用 O(N²)、O(N³) 来精确描述它。
goto 语句——谨慎使用的"任意门"
goto 可以让你在同一个函数内跳转到任意一个标号处:
#include <stdio.h>
int main()
{
printf("hehe\n");
goto next; /* 直接跳到 next: 标号处 */
printf("haha\n"); /* 这行永远不会执行 */
next:
printf("跳过了 haha 的打印\n");
return 0;
}
/* 输出:
* hehe
* 跳过了 haha 的打印
*/大多数情况下,goto 会让代码变得难以理解和维护——你永远不知道程序"从哪里跳过来的"。所以建议是:能不用的地方尽量不用。C 语言"结构化编程"的思想核心就是:只用顺序、选择、循环三种结构,就可以表达所有算法,完全不需要 goto。Dijkstra 早在 1968 年就写过著名的《Go To Statement Considered Harmful》来批判它。
但 goto 有一个很实用的场景:跳出多层嵌套循环。break 只能跳出当前这一层,如果你嵌套了三层,想一次性全部跳出,goto 就很方便:
for (...)
{
for (...)
{
for (...)
{
if (disaster)
goto error; /* 一次性跳出所有三层循环 */
}
}
}
error:
// 处理错误...不用 goto 的话,你需要设置多个标记变量并在每一层都判断,代码会臃肿很多。
goto 的使用纪律
如果决定用 goto,遵守三条纪律:
- 只能向后跳(跳到后面的标号),不要向前跳——向前跳可能跳过变量的初始化,是未定义行为;
- **只用于"从深层结构统一跳出/退出"**这一种场景,不用于实现循环(那会写出"意大利面条"代码);
- 标号附近加注释,说明为什么用 goto 而不是其他方式。
猜数字游戏——知识大综合
前面学到的所有知识,现在来一个大综合。我们要写一个完整的猜数字游戏:
- 电脑随机生成 1~100 之间的一个数字
- 玩家输入猜测的数字
- 每次猜完告诉玩家"猜大了"还是"猜小了"
- 猜对就结束,显示恭喜信息
- 玩家可以反复玩,直到选择退出
第一步:生成随机数
C 语言用 rand() 函数生成随机数,它返回 0 到 RAND_MAX(通常是 32767)之间的整数。rand() 声明在 <stdlib.h> 中。
#include <stdio.h>
#include <stdlib.h> /* rand() 和 srand() 在这里 */
int main()
{
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
return 0;
}运行一次看看结果,再运行一次——你会发现两次生成的"随机数"完全一样!
这是因为 rand() 生成的是伪随机数:它不是真正的随机,而是通过一个"种子"值经过数学公式算出来的。默认种子是 1,所以每次运行都得到一样的序列。
为什么是"伪随机"?因为计算机本质上是确定性的机器——同样的输入必然产生同样的输出。真正的随机性需要借助外部物理现象(放射性衰变、热噪声等),普通程序无法直接获得。所以 rand() 采用"线性同余法"之类的数学公式:下一个数 = (a × 当前数 + c) mod m,只要种子相同,后面的序列就完全一样。这在"可复现性"上有好处(调试方便),但在"不可预测性"上是失败的——所以它绝不能用于密码学场景(那要用 rand_s 或操作系统提供的加密随机源)。
要让种子变化,用 srand() 设置种子,再用 time() 获取当前时间(时间戳)作为种子:
#include <stdio.h>
#include <stdlib.h>
#include <time.h> /* time() 在这里 */
int main()
{
/* 用当前时间(不断变化)作为随机种子 */
srand((unsigned int)time(NULL));
/* time(NULL) 返回从 1970-01-01 到现在的秒数,每次都不一样 */
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
return 0;
}
/* 每次运行,结果都不同! */注意:srand() 在程序中只需要调用一次,放在 main 函数开头就行,不要放在循环里——否则每次循环都重新以当前时间设种子,而秒级的时间在瞬间的循环中是不变的,随机性反而更差。一次性播种,反复取数才是正确姿势。
第二步:控制随机数范围
rand() 返回的是 0RAND_MAX 的大范围值,我们需要把它限制在 1100:
rand() % 100; /* 余数范围 0~99 */
rand() % 100 + 1; /* 余数范围 1~100 */通用的产生 a~b 之间随机数的公式:
a + rand() % (b - a + 1)解释一下原理:rand() % (b - a + 1) 把结果压到 0 ~ (b-a) 之间,再加上 a,就得到 a ~ b。比如要 1~100:1 + rand() % 100。
一个专业提醒:% 取余法有一个理论缺陷——如果 b-a+1 不能整除 RAND_MAX+1,各余数的出现概率略有不均(低余数出现的概率稍高)。对游戏来说完全够用,但对需要严格均匀分布的场合(比如抽样),应该用更专业的做法(如拒绝采样)。现在知道有这个说法就行。
第三步:完整游戏
#include <stdio.h>
#include <stdlib.h> /* rand(), srand() */
#include <time.h> /* time() */
void menu() /* 显示菜单的函数 */
{
printf("***********************\n");
printf("****** 1. play ******\n");
printf("****** 0. exit ******\n");
printf("***********************\n");
}
void game() /* 玩一局游戏的函数 */
{
int r = rand() % 100 + 1; /* 生成 1~100 的随机数 */
int guess = 0; /* 玩家猜的数字 */
while (1) /* 死循环,猜对才退出 */
{
printf("请猜数字(1~100): ");
scanf("%d", &guess);
if (guess < r)
printf("猜小了\n");
else if (guess > r)
printf("猜大了\n");
else
{
printf("恭喜你,猜对了!\n");
break; /* 猜对,跳出循环 */
}
}
}
int main()
{
int input = 0; /* 用户菜单选择 */
srand((unsigned int)time(NULL)); /* 设置随机种子,只执行一次 */
do
{
menu(); /* 显示菜单 */
printf("请选择: ");
scanf("%d", &input);
switch (input)
{
case 1:
game(); /* 开始游戏 */
break;
case 0:
printf("游戏结束\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input); /* input 为 0 时退出循环 */
return 0;
}来拆解这个程序的结构,你会发现它用上了前面所有的知识:
do-while做菜单循环:至少显示一次菜单,输入 0 退出——这正是上一篇讲的do-while的典型场景switch处理菜单分支:1 玩游戏、0 退出、其他提示重选while (1)+break做游戏循环:猜对才跳出- 函数封装:
menu()和game()各管一件事,main只做总调度
while (1) 会不会死循环?不会——因为循环体里一定有 break 保证出口。while (1) 本身不可怕,可怕的是循环体里没有任何退出路径。
第四步:加上次数限制
再给游戏加一个功能:玩家只有 5 次猜测机会,5 次猜不中就失败:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void menu()
{
printf("***********************\n");
printf("****** 1. play ******\n");
printf("****** 0. exit ******\n");
printf("***********************\n");
}
void game()
{
int r = rand() % 100 + 1; /* 正确答案 */
int guess = 0; /* 玩家猜测 */
int count = 5; /* 剩余次数 */
while (count) /* count 不为 0 就继续 */
{
printf("\n你还有 %d 次机会\n", count);
printf("请猜数字: ");
scanf("%d", &guess);
if (guess < r)
printf("猜小了\n");
else if (guess > r)
printf("猜大了\n");
else
{
printf("恭喜你,猜对了!\n");
break; /* 猜对了,不用继续 */
}
count--; /* 每次猜完,次数减 1 */
}
if (count == 0) /* 5 次用完,失败了 */
printf("你失败了!正确值是: %d\n", r);
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择: ");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("游戏结束\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}这里有个细节:while (count) 利用"非 0 即真"——count 从 5 递减到 0,0 为假循环结束。循环结束后用 if (count == 0) 判断是"猜对退出"还是"次数耗尽退出"(因为猜对时 count 还没减到 0)。用循环退出后的状态反推退出原因,是一个常用技巧。
改进方向(自己动手试一试)
- 校验输入:玩家输入 150 或 -3 时给出提示,不消耗次数——用
if (guess < 1 || guess > 100)拦截; - 记录次数:猜对时打印"你一共猜了 X 次";
- 难度选择:菜单里加"1.简单(1
50) 2.中等(1100) 3.困难(1~200)"; - 清屏:Windows 下用
system("cls")(需要<stdlib.h>)在每局开始前清屏; - 处理非法输入:
scanf读到非数字时会失败,此时要清空输入缓冲区(while (getchar() != '\n');)并重新提示。
猜数字游戏里的循环总结
这个游戏几乎用到了我们前面所有文章的知识点:数据类型、变量初始化、scanf/printf、if-else 条件判断、switch 分支、while 循环、do-while 循环、break 跳出、函数定义与调用(menu() 和 game())、随机数生成(rand/srand/time)。你现在已经有能力独立写出这样的程序了。
三种循环怎么选?
一个简单的原则:循环次数已知用 for(如遍历数组、打印 1~N);循环次数未知、先判断再进入用 while(如读取文件直到 EOF);至少需要执行一次用 do-while(如输入验证、菜单交互)。实际开发中 for 和 while 用得最多,do-while 只在特定场景下使用。
| 循环 | 判断时机 | 至少执行次数 | 典型场景 |
|---|---|---|---|
while | 先判断 | 0 次 | 读文件直到结束、条件驱动的循环 |
for | 先判断 | 0 次 | 已知次数的遍历、计数循环 |
do-while | 后判断 | 1 次 | 菜单交互、输入校验 |
还有一个经验之谈:循环写完之后,先"走一遍边界"。分别用"第一次进入""最后一次进入""刚好不满足条件"三个值在心里模拟一遍,很多 off-by-one(差一)错误当场就能发现。这是写循环最重要的自检习惯。
本篇思考题
- 下面的循环各执行几次?
for (i = 0; i < 10; i++)、for (i = 0; i <= 10; i++)、for (i = 10; i > 0; i--)? - 为什么
while循环里continue可能造成死循环,而for不会?从两者的执行流程解释。 - 输入一个整数 n,用
for计算 1~n 的累加和;再用while写一遍,对比两种写法。 - 用嵌套循环打印一个 5 层的金字塔(第 i 行有 2i-1 个星号,居中排列)。
for (;;)和while (1)是什么关系?它们的退出条件靠什么提供?rand() % 100 + 1的取值范围是什么?如果要生成 50~100 的随机数怎么写?- 猜数字游戏里,如果玩家输入的不是数字(比如
abc),scanf会怎样?怎么处理?
思考题参考答案与详解
第 1 题:
for (i = 0; i < 10; i++):执行 10 次(i 取 0~9);for (i = 0; i <= 10; i++):执行 11 次(i 取 0~10);for (i = 10; i > 0; i--):执行 10 次(i 取 10~1)。
牢记 i < n 表示"循环 n 次",i <= n 表示"循环 n+1 次"——这是最常见的差一(off-by-one)错误来源。
第 2 题:因为两者的执行流程不同。while 里 continue 的作用是跳到循环体的末尾,随后去执行条件判断;如果循环变量的"调整"语句(如 i++)写在循环体末尾、位于 continue 之后,就被跳过了,循环变量永远不变、条件永远为真 → 死循环。而 for 的 continue 跳转到的是表达式 3(调整部分)——它在语法上属于循环控制部分,与循环体语句独立,不受 continue 影响,所以调整照常执行、循环变量正常推进,一般不产生死循环。
第 3 题:
#include <stdio.h>
int main()
{
int n = 0, i = 0, sum = 0;
scanf("%d", &n);
/* for 版:初始化/判断/调整三要素集中在一行 */
for (i = 1; i <= n; i++)
sum += i;
printf("sum = %d\n", sum);
/* while 版:三要素分散在不同位置 */
sum = 0;
i = 1;
while (i <= n)
{
sum += i;
i++;
}
printf("sum = %d\n", sum);
return 0;
}两种写法结果相同。区别在于:for 的三要素集中,结构紧凑、不容易漏调整语句;while 的调整语句在循环体末尾,代码一多容易遗漏或写错位置。循环次数已知时优先用 for。
第 4 题:5 层金字塔,第 i 行(i=1~5)有 2*i-1 个星号,每行前面补 5-i 个空格即可居中对齐:
#include <stdio.h>
int main()
{
int i = 0, j = 0;
for (i = 1; i <= 5; i++)
{
for (j = 1; j <= 5 - i; j++) /* 先打空格 */
printf(" ");
for (j = 1; j <= 2 * i - 1; j++) /* 再打星号 */
printf("*");
printf("\n");
}
return 0;
}第 5 题:for (;;) 和 while (1) 都表示无限循环(for 的三个表达式都可省略,省略表达式 2 时默认恒真),二者在几乎所有编译器下生成的机器码完全一样,可互相替换。它们的退出条件不由循环语法提供,而是靠循环体内部的 break、return 等语句在满足条件时主动跳出——所以写无限循环时必须保证循环体里有出口,否则程序会真的"永远跑下去"。
第 6 题:rand() % 100 + 1 的取值范围是 1~100(rand()%100 得到 099,加 1 后为 1100)。要生成 50100 的随机数,代入通用公式 50,加 50 后为 50~100)。a + rand() % (b - a + 1):b-a+1 = 100-50+1 = 51,所以写 50 + rand() % 51(余数范围 0
第 7 题:当玩家输入 abc 时,scanf("%d", &guess) 的 %d 遇到非数字字符 a 就匹配失败,scanf 返回 0(没读到任何变量),而 guess 保持之前的值(可能是旧值或未初始化的垃圾值)。若不加处理,程序会拿着这个错误值去和正确答案比较,可能误判;而且 abc 残留在输入缓冲区,下一次循环读入仍会失败。处理方法:先检查 scanf 的返回值,不是 1 就说明输入非法,此时不消耗猜测次数,并清空缓冲区、重新提示:
int ret = scanf("%d", &guess);
if (ret != 1) /* 输入的不是一个整数 */
{
while (getchar() != '\n'); /* 把缓冲区里的非法字符全部清掉 */
printf("请输入一个数字!\n");
continue; /* 跳过后面的比较与 count-- */
}小结
这个猜数字游戏几乎用到了我们前面所有文章的知识点:数据类型、变量初始化、scanf/printf、if-else 条件判断、switch 分支、while 循环、do-while 循环、break 跳出、函数定义与调用(menu() 和 game())、随机数生成(rand/srand/time)。你现在已经有能力独立写出这样的程序了。
三种循环怎么选?一个简单的原则:循环次数已知用 for(如遍历数组、打印 1~N);循环次数未知、先判断再进入用 while(如读取文件直到 EOF);至少需要执行一次用 do-while(如输入验证、菜单交互)。实际开发中 for 和 while 用得最多,do-while 只在特定场景下使用。
随着这篇文章结束,C 语言第一阶段的核心语法也告一段落。下一篇我们将进入一个全新的维度——数组:怎么在连续的内存空间中批量管理数据,以及数组在底层到底是怎么存储的。
还没有评论 — 第一条由你来留。