6. 流程控制(二):while循环、do-while循环、for循环、循环嵌套、break与continue语句、goto语句

好,咱们接着聊流程控制。上一章讲了 if-else 和 switch,那是做「判断」的。这一章要聊的,是让代码「反复干同一件事」——循环。

循环这东西,说白了就是让计算机替你重复劳动。你想想看,要是让你手动打印 1 到 100,你肯定烦死了。但计算机不会烦,它只会老老实实执行你的指令。我刚开始学 C 语言时,觉得循环不就是个「重复」嘛,有啥好学的?后来在项目里栽了跟头,才发现循环里的门道比我想象的多得多。

本章核心:掌握 while、do-while、for 三种循环的适用场景,理解循环嵌套的执行流程,学会用 break 和 continue 控制循环节奏。至于 goto,嗯,我会告诉你什么时候该用它,什么时候千万别碰。

循环控制结构总览 while 循环 先判断,后执行 do-while 循环 先执行,后判断 for 循环 计数循环首选 循环嵌套:外层循环每执行一次,内层循环完整执行一轮 典型应用:打印九九乘法表、二维数组遍历 break:跳出当前循环 遇到 break,循环立即终止 continue:跳过本次循环 遇到 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 是紧急逃生通道——别滥用,但关键时刻别怕用。


公众号:蓝海资料掘金营,微信deep3321