13、循环结构(上):while循环、do-while循环、循环的适用场景、循环的嵌套

说到循环,我脑子里第一个蹦出来的画面,是当年调试一个串口接收程序时的场景。那个程序需要不停地轮询接收标志位,直到数据到达。那时候我刚入行,写了个死循环把自己卡死了——嗯,从那以后我就对循环结构有了刻骨铭心的认识。

循环说白了,就是让一段代码反复执行。你想想看,如果让你写一个程序,打印1到100,没有循环的话你得写100行printf。有了循环,三行代码搞定。这就是循环的价值。

13.1 while循环:先判断,后执行

while循环的结构很简单,我直接上代码:

while (条件表达式) {
    // 循环体
    // 当条件为真时,反复执行这里的代码
}

它的执行流程是这样的:先判断条件,条件成立就执行循环体,执行完再回来判断。条件不成立,直接跳过。所以while循环有可能一次都不执行——如果一开始条件就是假的。

我举个例子,咱们写一个倒计时程序:

#include <stdio.h>

int main() {
    int count = 5;
    
    while (count > 0) {
        printf("倒计时:%d\n", count);
        count--;  // 别忘了改变条件,否则死循环
    }
    
    printf("发射!\n");
    return 0;
}

运行结果:

倒计时:5
倒计时:4
倒计时:3
倒计时:2
倒计时:1
发射!
⚠️ 我曾经踩过的坑:忘记在循环体内改变条件变量的值。比如上面那个count--,如果漏写了,count永远大于0,循环就永远跑下去。这就是传说中的死循环。我当年调试串口程序时,就是忘了更新标志位,结果程序卡死,板子还得重新上电。

13.2 do-while循环:先执行,后判断

do-while和while的区别,说白了就是一个「先斩后奏」。先执行一次循环体,然后再判断条件。所以do-while至少会执行一次。

do {
    // 循环体
    // 先执行一次,再判断条件
} while (条件表达式);  // 注意这里有个分号!

我拿刚才的倒计时改一下:

#include <stdio.h>

int main() {
    int count = 0;  // 注意,这次count初始就是0
    
    do {
        printf("count = %d\n", count);
        count++;
    } while (count < 5);
    
    return 0;
}

运行结果:

count = 0
count = 1
count = 2
count = 3
count = 4

你想想看,如果换成while,count初始为0,条件count < 5一开始就不成立,循环体一次都不会执行。但do-while至少会打印一次。这个特性在某些场景下特别有用。

💡 我个人习惯:当需要「至少执行一次」的场景,比如菜单选择、用户输入验证,我倾向于用do-while。因为用户至少得输入一次,然后我们再判断要不要继续。

13.3 循环的适用场景

我做了这么多年嵌入式,总结下来,循环主要用在以下几个场景:

场景 说明 推荐循环
已知循环次数 比如遍历数组、打印固定次数 for循环(下节讲)
未知循环次数,但知道结束条件 比如等待某个标志位、读取传感器直到数据稳定 while循环
至少需要执行一次 比如菜单选择、用户输入验证 do-while循环
无限循环 比如嵌入式主循环、操作系统调度 while(1) 或 for(;;)

我在项目中遇到过这样一个场景:读取一个温度传感器,传感器需要100ms才能稳定输出。这时候我就用while循环,不断读取状态寄存器,直到状态位变成「数据就绪」。循环次数不确定,但结束条件很明确——这就是while的典型应用。

13.4 循环的嵌套

循环嵌套,就是一个循环里面再套一个循环。说白了就是「外层循环每执行一次,内层循环完整跑一遍」。

我拿打印九九乘法表来举例,这个例子很经典:

#include <stdio.h>

int main() {
    int i = 1, j;
    
    while (i <= 9) {
        j = 1;
        while (j <= i) {
            printf("%d*%d=%-2d ", j, i, i * j);
            j++;
        }
        printf("\n");
        i++;
    }
    
    return 0;
}

运行结果:

1*1=1  
1*2=2  2*2=4  
1*3=3  2*3=6  3*3=9  
1*4=4  2*4=8  3*4=12 4*4=16 
...(后面省略)

这里外层循环控制行数(i从1到9),内层循环控制每行的列数(j从1到i)。每次外层循环i加1,内层循环就重新从1开始跑。

🔑 嵌套循环的关键点:
  • 内层循环的初始化要放在外层循环里面。你看上面代码,j = 1 是放在外层while里面的,每次外层循环开始,j都重新赋值为1。
  • 嵌套层数不要太深。我见过有人嵌套了五六层,代码读起来跟迷宫一样。一般两层就够用了,三层已经是极限。
  • 注意性能。内层循环的执行次数 = 外层次数 × 内层次数。如果外层1000次,内层1000次,那就是100万次。在嵌入式系统里,这可能会影响实时性。

下面这张图,是我画的一个循环嵌套的执行流程,你看一眼就明白了:

循环嵌套执行流程图 外层循环开始 条件成立? 不成立 外层循环体(包含内层循环) 内层循环(完整执行) 执行完内层,回到外层条件判断 退出循环
⚠️ 嵌套循环的常见错误:
  • 内层循环的变量和外层循环的变量搞混了。我见过有人内外层都用i,结果内层把i改了,外层循环乱套了。
  • 忘记重置内层循环的计数器。比如上面乘法表的例子,如果j = 1写在while外面,那内层循环只跑一次就完了。
  • 嵌套太深导致代码可读性差。我个人建议,如果超过三层,就考虑拆成函数吧。

13.5 实际项目中的循环选择

说了这么多,到底什么时候用while,什么时候用do-while?我分享几个实际经验:

  • 等待硬件就绪:用while。比如等待ADC转换完成、等待SPI发送完毕。因为硬件可能已经就绪了,不需要先执行一次。
  • 用户输入验证:用do-while。用户至少得输入一次,然后我们判断输入是否合法,不合法就继续问。
  • 主循环:用while(1)。嵌入式程序的主循环就是无限循环,不需要结束条件。
  • 遍历链表:用while。你不知道链表有多长,但知道结束条件是p != NULL。

我记得有一次,一个同事用do-while去等待一个串口标志位。结果标志位在进入循环前就已经置位了,do-while先执行了一次发送操作,导致多发送了一帧数据。从那以后,他再也不敢乱用do-while了。

💡 我的建议:拿不准的时候,就用while。while是最通用的,do-while只在你确定「至少需要执行一次」的时候用。这样不容易出错。

循环结构就讲到这里。你想想看,while和do-while的区别其实就一句话:while是先看再干,do-while是先干再看。记住这个,就不会搞混了。下一节我们讲for循环,那又是另一种风格了。


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