6. 流程控制(二):while循环、do-while循环、for循环、循环嵌套、break与continue语句、goto语句
好,咱们接着聊流程控制。上一章讲了 if-else 和 switch,那是做「判断」的。这一章要聊的,是让代码「反复干同一件事」——循环。
循环这东西,说白了就是让计算机替你重复劳动。你想想看,要是让你手动打印 1 到 100,你肯定烦死了。但计算机不会烦,它只会老老实实执行你的指令。我刚开始学 C 语言时,觉得循环不就是个「重复」嘛,有啥好学的?后来在项目里栽了跟头,才发现循环里的门道比我想象的多得多。
本章核心:掌握 while、do-while、for 三种循环的适用场景,理解循环嵌套的执行流程,学会用 break 和 continue 控制循环节奏。至于 goto,嗯,我会告诉你什么时候该用它,什么时候千万别碰。
6.1 while 循环:先看门,再进门
while 循环的语法很简单:
while (条件表达式) {
// 循环体
}
它的执行逻辑是:先判断条件,条件为真就执行循环体,执行完再回来判断。条件为假,直接跳过。说白了就是「先看门开了没,开了再进去」。
我在项目中遇到过一个问题:用 while 循环读取传感器数据,结果条件写反了,循环一次都没执行。排查了半天才发现是逻辑取反的问题。嗯,这里要注意——while 的条件一定要确保在某个时刻能变成假,否则就是死循环。
个人习惯:我写 while 循环时,喜欢在循环体末尾显式更新循环变量。这样代码读起来一目了然,不容易漏掉。
int i = 0;
while (i < 5) {
printf("i = %d\n", i);
i++; // 显式更新,别漏了
}
6.2 do-while 循环:先进门,再看门
do-while 和 while 的区别就一句话:do-while 至少执行一次循环体。
do {
// 循环体
} while (条件表达式); // 注意这里有个分号!
为什么需要这种结构?你想想看,有些场景下你无论如何都要先干一次活,然后再判断要不要继续。比如:
- 向用户显示菜单,至少显示一次
- 读取一个数据包,先读一次,如果还有数据就继续读
- 发送一个命令,先发一次,根据响应决定是否重试
我曾经在写串口通信驱动时,就用 do-while 实现了「先发送一次命令,如果没收到应答就重试最多 3 次」的逻辑。do-while 在这里比 while 更自然,因为无论如何你都得先发一次。
注意:do-while 后面的 while 括号后面必须加分号!这是初学者最容易犯的错误。我见过有人因为这个分号漏了,编译报错找了半小时。
6.3 for 循环:计数循环的王者
for 循环是 C 语言里最常用的循环结构。它的语法把「初始化」「条件判断」「变量更新」三件事放在一行,非常紧凑:
for (初始化; 条件; 更新) {
// 循环体
}
执行顺序是:先执行初始化(只执行一次),然后判断条件,条件为真就执行循环体,执行完再执行更新,然后再次判断条件。直到条件为假退出。
我个人习惯用 for 循环处理「知道循环次数」的场景。比如遍历数组:
int arr[5] = {10, 20, 30, 40, 50};
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
这里有个小细节:C99 标准允许在 for 的初始化部分声明变量(比如上面的 int i = 0)。如果你用的是老编译器,可能不支持,那就得把 i 提前声明。
避坑指南:我曾经在 for 循环的条件里写了个函数调用,比如 for (i = 0; i < strlen(s); i++)。每次循环都调用 strlen,字符串一长,性能直接崩了。正确的做法是提前把 strlen 的结果存到变量里。
6.4 循环嵌套:循环里面套循环
循环嵌套,说白了就是一个循环的循环体里又写了另一个循环。外层循环每执行一次,内层循环就要完整地跑一轮。
看个经典例子——打印九九乘法表:
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
printf("%d*%d=%2d ", j, i, i * j);
}
printf("\n");
}
执行过程是这样的:i=1 时,内层循环 j 从 1 到 1,打印 1*1=1;然后换行。i=2 时,内层循环 j 从 1 到 2,打印 1*2=2 和 2*2=4;然后换行。以此类推。
我在项目中用循环嵌套处理过二维数组的矩阵运算。当时有个坑:内层循环的边界条件写错了,导致数组越界。排查时我用了打印调试,把 i 和 j 的值打出来,才找到问题。
调试技巧:遇到循环嵌套的 bug,先确认外层循环的边界,再确认内层循环的边界。用 printf 把循环变量打出来,比瞪眼找 bug 快得多。
6.5 break 与 continue:循环的刹车和跳过
break 和 continue 都是用来控制循环流程的关键字,但作用完全不同。
| 关键字 | 作用 | 类比 |
|---|---|---|
| break | 立即终止当前循环,跳出循环体 | 刹车——直接停车 |
| continue | 跳过本次循环剩余代码,进入下一轮 | 跳过——这圈不跑了,直接下一圈 |
看个例子:
for (int i = 0; i < 10; i++) {
if (i == 3) {
continue; // 跳过 i=3,不打印
}
if (i == 7) {
break; // i=7 时直接跳出循环
}
printf("i = %d\n", i);
}
// 输出:0, 1, 2, 4, 5, 6
注意:break 只跳出它所在的那一层循环。如果是嵌套循环,break 只跳出内层循环,外层继续跑。
我曾经踩过的坑:在嵌套循环里用了 break,以为能跳出所有循环,结果外层还在跑。后来我用了 goto 才跳出多层循环。嗯,goto 在特定场景下确实有用。
6.6 goto 语句:双刃剑
goto 语句可以让程序跳转到任意一个标签位置。语法很简单:
goto label;
// ... 跳过的代码
label:
// 跳转到这里
很多教科书说「不要用 goto」,但我觉得这话说得太绝对了。goto 用好了是神器,用烂了是灾难。
什么时候该用 goto?
- 跳出多层嵌套循环:break 只能跳出一层,goto 可以直接跳到最外层
- 统一错误处理:多个地方出错都要执行同样的清理代码,用 goto 跳到一个统一的错误处理标签
看个例子——从多层循环中直接跳出:
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
if (some_condition) {
goto exit_loops; // 直接跳出两层循环
}
}
}
exit_loops:
printf("跳出循环了\n");
什么时候别用 goto?
- 向前跳转(跳回前面)——容易造成 spaghetti code(意大利面条式代码)
- 在函数内部到处乱跳——代码可读性直接归零
- 能用 break/continue 解决的问题——别用 goto 装酷
我的原则:goto 只用于「向下跳转」——从深层嵌套跳到后面的错误处理或退出点。绝不向上跳,绝不跳来跳去。这样代码既清晰又安全。
6.7 三种循环怎么选?
我总结了一个简单的选择原则:
| 场景 | 推荐循环 | 原因 |
|---|---|---|
| 知道循环次数 | for | 初始化、条件、更新写在一起,结构清晰 |
| 不知道次数,但可能一次都不执行 | while | 先判断后执行,符合逻辑 |
| 至少执行一次 | do-while | 先执行后判断,保证至少跑一次 |
当然,这三种循环在功能上是等价的——任何一个 while 都可以改成 for,任何一个 for 都可以改成 while。但写代码不只是给机器看的,更是给人看的。选对循环结构,代码读起来就舒服得多。
好了,循环控制的内容就聊到这儿。记住:循环是让计算机替你干苦力活,但你要确保它知道什么时候停下来。break 和 continue 是你的控制手柄,goto 是紧急逃生通道——别滥用,但关键时刻别怕用。