
讲真学会while的那天你才开始真正编程我带C语言课这些年每次讲到循环这一章都能看到一部分学生眼睛突然亮起来。前面学变量、学运算符、学if分支都是在准备工具到了while和do-while才第一次摸到程序真正的引擎——重复执行。很多人学完循环回去刷题才意识到原来自己之前用if写了一大坨重复代码其实三行while就能解决。这篇文章就围绕C语言的while和do-while展开把执行机制、典型写法、实战案例和常见坑一次讲透。不管你是刚学C语言的新手还是学到一半发现循环总出毛病的自学者这篇文章都能帮你把循环这块地基打牢。1. 第15章的起点顺序、分支之后的第三个能力1.1 五种基本流程控制里循环为什么单独成章C语言的程序执行流程本质上是五种结构的组合顺序、选择、循环、跳转和函数调用。前面十四章基本都在处理前两种——程序按行往下走遇到if就看条件决定走哪条路。但现实中的问题哪有那么简单让你计算1到100的和你要写100个变量然后挨个加起来吗让你处理班里50个学生的成绩你要写50次printf吗显然不是。循环解决的就是同一件事反复执行但每次执行时的数据状态不同这种场景。它有两个核心要素循环体要重复的动作和循环条件什么时候停。这个什么时候停是整个循环设计的灵魂也是新手最容易翻车的地方。while和do-while恰恰是C语言里最基础的两个循环结构它们共用一个条件表达式但在先判断还是先执行这个问题上走了两条完全不同的路。1.2 没有循环的代码有多恐怖我经常让学员做一个思想实验不用循环写一个程序读取用户输入的5个整数然后求平均值。标准答案是int a1, a2, a3, a4, a5; scanf(%d%d%d%d%d, a1, a2, a3, a4, a5); printf(%.2f\n, (a1 a2 a3 a4 a5) / 5.0);代码倒是能跑但这暴露了一个大问题程序能处理的输入数量被写死了。用户想输入6个数怎么办10个数呢1000个数呢难道要把变量声明也从a1写到a1000然后从头到尾拼一长串scanf和加法表达式这不仅工作量爆炸而且代码根本没法维护。而用while循环代码结构不随数据规模变化int n, sum 0, count 0; while (scanf(%d, n) 1) { sum n; count; } printf(平均值: %.2f\n, (double)sum / count);同样是读一堆数求平均这段代码能接收任意多个输入灵活性和之前完全不是一个量级。这就是「接受循环」的意义——程序不再害怕数据变多只需要关心单个数据的处理逻辑。这也是为什么几乎所有C语言教材都要为循环单独开一章的原因。1.3 循环设计中的核心矛盾重复与终止理解循环你只需要抓住一对矛盾循环体负责重复条件负责终止。这两者必须同时存在。如果你只写了重复动作却忘了让条件最终变成假程序就会陷入死循环——CPU在那里空转窗口卡死你按CtrlC都来不及。我常跟学生打一个比方while循环就像爬山。循环体是你迈出的一步循环条件是山顶还没到而变量更新比如i就是你在海拔表上做的标记。如果只抬脚不记录海拔你永远不知道自己走到哪了程序也就永远跑不完。这个类比在后面排查死循环时特别有用。2. while循环的执行机制先问后做条件说了算2.1 语法结构与执行流程逐行拆解while循环的语法非常简单while (条件表达式) { 循环体语句; }它的执行流程只有一条规则先判断条件条件为真非0就执行一次循环体然后回头重新判断条件条件为假0就跳出循环继续执行后面的代码。注意这个回头是自动的不需要写goto也不需要写跳转结构本身保证了回跳。有一个细节新手经常忽略while的条件表达式在每次进入循环体之前都会重新求值。所以如果你在循环体里修改了参与条件判断的变量很可能导致循环次数和你的预期不一样。反过来讲控制循环次数的变量的更新语句必须写进循环体里或者写在某个能被执行到的地方。我建议第一次学while先照着下面这个最小示例敲一遍跑出结果后再去分析流程#include stdio.h int main(void) { int i 1; while (i 5) { printf(第%d次循环\n, i); i; } return 0; }输出就是五行第1次循环到第5次循环。那个i就是海拔表上的标记它保证了循环能正常终止。2.2 循环变量更新的三种写法与习惯在C语言里循环变量自增的写法有三种效果等同但风格不同i i 1;最直白任何语言都懂适合刚接触编程的读者i 1;复合赋值运算符少写几个字符更体现C语言风格i;和i;都是自增运算符用在循环体里效果一样我见过不少书强调i比i效率高那是古老编译器的历史遗留。现在的编译器早就优化得没差别了。你自己习惯用哪种都行但考试和项目中建议统一风格。我自己常年用i因为读起来更像用当前的i用完再加1思维负担小。真正需要注意的是自减、步长大于1等情况。比如要打印10以内的偶数你可以这样写int i 0; while (i 10) { printf(%d , i); i 2; }这里i 2就是步长为2。如果循环体内忘记写这句i永远停在0条件永远成立死循环没跑。这是一个经典错误后面专门讲。2.3 边界条件和的区别是差一错误的老家边界条件off-by-one error是循环里最常见的逻辑错误没有之一。判断条件到底写i 10还是i 10差一次循环。我这个在讲解时有个笨但可靠的方法直接代入边界值试算。你要执行10次循环i从0开始判断条件写死while (i 10)。当i 9时条件为真执行第10次循环i变成10再回头判断10 10为假结束。完美正好10次。如果你写while (i 10)当i 10时条件仍然为真多执行一次变成11次。想通这个逻辑很简单判断的是当前状态不是下一次的状态。新手之所以容易错是因为心里默认到10就该停但循环判断发生在这次循环开始之前。如果你每次写循环都把起点、终点、步长列成一个小表逐个推演一次基本能杜绝这类错误。2.4 while循环的三个经典模式看多了代码你会发现while循环的用法虽然千变万化但归结起来逃不出三个模式计数器模式用循环变量控制次数比如打印N次、遍历数组。int i 0; while (i N) { // 处理第i个元素 i; }事件驱动模式循环条件来自程序运行时的状态比如读文件直到结束、等待用户输入特定字符。int ch; while ((ch getchar()) ! EOF) { putchar(ch); }哨兵模式用一个特殊值标记该结束了比如输入999退出循环。int score; scanf(%d, score); while (score 0) { // 处理非负分数 scanf(%d, score); }这三个模式覆盖了绝大多数C语言程序。做题时候先判断题目属于哪种模式再动手写循环思路会清晰很多。3. do-while的独特价值先做一次再问要不要继续3.1 两种循环一字之差语义差了一个世代do-while的语法长这样do { 循环体语句; } while (条件表达式);注意结束的地方有个分号别漏了。它的执行流程和while正好相反先无条件执行一次循环体再判断条件条件为真就回到循环体开头再来一次为假就结束。直观对比就是这样对比项whiledo-while判断时机先判断后执行先执行后判断最少执行次数0次1次结束分号不需要需要典型场景循环次数可能为0至少要执行一次这个最少执行次数的差异就是while和do-while的分水岭。3.2 为什么要先做一次真实场景推导用一个最经典的例子说明白编写一个程序不断地询问用户是否继续直到用户输入n才结束。用while写会很别扭char answer y; while (answer y) { // 干点正事 printf(继续吗(y/n) ); scanf( %c, answer); }这段代码能跑但循环条件第一次判断用了初始化的y逻辑上绕了一道弯。再用do-while写char answer; do { // 干点正事 printf(继续吗(y/n) ); scanf( %c, answer); } while (answer y);第二种写法更贴近真实需求用户只要进入程序游戏界面至少显示一次然后才问要不要继续。这就是至少执行一次的价值。再比如验证输入合法性让用户重新输入直到数字落在合法区间登录系统提示密码错误后重新输入菜单程序至少展示一次菜单供用户选择。这些场景的共同特点是第一次执行不需要任何前置条件后续是否执行取决于第一次的结果。3.3 为什么实战中do-while用得少但你依然要学好诚实地说现实中while循环的使用频率远高于do-while。很多程序员写代码时倾向于把所有条件都提前想清楚用while让逻辑先判断再执行避免任何不可控的第一次执行。但do-while在C语言里占据着一个不可替代的位置。最典型的例子是宏定义里的循环封装。如果你写过带副作用的宏会发现do-while是唯一能安全封装多条语句的语法结构。比如#define SAFE_FREE(p) do { if (p) { free(p); p NULL; } } while (0)在C语言里宏的展开是文本替换如果写成{...}在if/else语句里会出分号匹配问题只有do-while加上末尾的while(0)既保证宏体只执行一次又能让调用处像普通函数一样加分号。这个问题只可意会等你学到宏定义那章就明白了。但现在先记住do-while不是没用只是它的用武之地和直觉想象的不太一样。3.4 如何快速决定用while还是do-while我给学生一个非常简单的决策规则如果循环体一次都不执行也无所谓用while如果循环体至少要执行一次比如先显示菜单再问选择先执行操作再问是否继续用do-while。另一种判断方式是看条件里是否用到循环体产生的新数据。如果循环条件依赖循环体里刚计算出来的结果比如接收用户输入后决定是否结束do-while通常更自然如果循环条件在第一次执行前就能确定比如i10这种用while。这两种判断方法配合使用基本不会选错。4. 从语法到实战三个必练的经典案例推导4.1 数字逆序%和/是循环的最佳搭档很多教材喜欢把输入一个整数逆序输出它的每一位放在循环章节的练习题里这个题目值得你亲手写一遍。先想清楚算法要取出一个数的个位用n % 10要丢掉个位用n / 10。反复执行这两个操作直到n变成0。#include stdio.h int main(void) { int n, last; scanf(%d, n); while (n 0) { last n % 10; printf(%d, last); n n / 10; } printf(\n); return 0; }这个循环的执行过程我建议你拿个数字实际推演一遍。比如n 12345第一次循环last 5打印5n 1234第二次循环last 4打印4n 123第三次循环last 3打印3n 12第四次循环last 2打印2n 1第五次循环last 1打印1n 0循环结束输出正好是54321。这里有一个细节要提示如果输入0while循环条件n 0一开始就是假导致什么都不输出这显然不对。这就是前面提到的先判断后执行带来的问题。修正方案有两种把条件改成n 0 || n 0多余或者干脆用do-while让它先做一次。这个例子完美展示了do-while的优势do { last n % 10; printf(%d, last); n n / 10; } while (n 0);输入0时输出一个0输入负数呢负数的情况需要特殊处理通常先取绝对值再输出负号这属于分支结构的内容不在这章展开了。但你能看到同一个需求技术选型不同代码的健壮性完全不同。4.2 累加求和循环变量的生命周期陷阱第二个必练案例是求1到100的和。代码很常见int i 1, sum 0; while (i 100) { sum i; i; } printf(sum %d\n, sum);但这里藏着一个新手容易犯的错。如果把sum的初始化放到循环体内部int i 1; while (i 100) { int sum 0; sum i; i; } printf(sum %d\n, sum); // 编译报错这段代码首先会编译报错因为sum在循环内部声明作用域只在循环体内循环外访问不到。即使把它改到外面声明写成sum 0放在循环体里也会导致每次循环都把sum清零最后输出还是100。这种初始化位置错误的bug往往不会引发程序崩溃只在结果上体现最坑人。我建议这个案例你要动手验证两件事一是把i 100改成i 100看结果差多少体会边界条件二是把i放到sum i前面看结果完全变掉体会更新语句的位置决定计算内容。这两个试验比背十遍概念都管用。4.3 连续读取输入直到EOFscanf返回值是被忽视的钥匙很多初学者不知道scanf是有返回值的。它返回成功匹配的输入项个数如果遇到文件结尾EOF或匹配失败返回EOF通常是-1。这个返回值就是处理不知道要读多少数据这类问题的关键钥匙。假设题目要求不断读入整数求它们的平均值直到输入结束。标准解法int n, sum 0, count 0; while (scanf(%d, n) 1) { sum n; count; } if (count 0) { printf(%.2f\n, (double)sum / count); }这段代码里scanf(%d, n) 1就是循环条件。每读到一个整数条件成立进入循环体处理读不到整数到了EOF或者输入了非数字条件变成假循环自然结束。这种写法非常常见在线判题系统OJ里的接受循环输入类题目全靠它。注意最后的(double)sum / count。C语言里两个整数除法结果还是整数直接用sum / count会把0.5这种小数丢掉必须先转成double。这个坑在求平均值的题里出现概率极高建议你形成条件反射。4.4 九九乘法表嵌套循环的逻辑分层学完一个循环就该学嵌套循环了。九九乘法表是入门嵌套循环的经典题目标输出是9行第n行有n个算式。用while嵌套while来写#include stdio.h int main(void) { int i 1; while (i 9) { int j 1; while (j i) { printf(%d*%d%-2d , j, i, j * i); j; } printf(\n); i; } return 0; }第一次看到这段代码的人容易把内层循环变量j的初始化位置写错。j必须在每行开始前重新初始化为1如果放在外层循环之前定义一次内层循环第二轮就会直接跳过。理解嵌套循环的关键是外层循环每执行一次内层循环都要从头完整跑一遍。像这样i 1时内层j从1跑到1输出1个算式i 2时内层j从1跑到2输出2个算式i 3时内层j从1跑到3输出3个算式所以内层循环的终止条件j i依赖外层变量i这是嵌套循环最常见的数据关系。那个%-2d是左对齐占2个宽度让输出整齐你也可以把%-2d改成%2d看效果对比。5. 循环里的四个大坑和我的调试习惯5.1 死循环大多数时候是更新语句丢了或位置错死循环是循环章节的头号杀手。我自己上课时统计过学员报错最多的问题就是程序跑起来没反应窗口卡死。排查顺序我总结成一个清单检查循环变量有没有初始化检查循环体里有没有更新循环变量的语句检查更新语句的位置是不是写在continue之后了如果用了continue更新会被跳过检查条件表达式是否在某种输入下永远为真举一个最典型的错误示范int i 1; while (i 10) { printf(%d\n, i); // 忘了 i; }这个循环会一直打印1直到你手动终止。道理很简单i永远是1条件1 10永远为真。修复方法就是在循环体某处加上i。下面这个例子稍微隐蔽一些int i 0; while (i 10) { if (i % 2 0) { continue; } printf(%d, i); i; }这里continue跳过了i遇到偶数时i永远不变死循环。这种情况在while里特别容易发生因为while不像for循环那样把更新语句固定写在语法结构里。很多人会建议你都用for别用while确实能规避一部分问题但C语言考试和底层代码大量使用while你躲不掉。5.2 差一错误从推演一张小表开始差一错误off-by-one error发生在判断条件多了一次或少了一次。前面用i 10和i 10的例子说明过原理这里再讲一个排查方法。拿一张纸把循环变量的取值变化列成表格一行一行推演循环变量i条件i 3执行动作循环后i0真执行11真执行22真执行33假结束-这张表一画到底执行了几次一目了然。我遇到边界问题时从不靠猜一定列这样的表。列完表你还发现i的终点值3恰好比最后执行的次数多1这就是while循环的直觉规律循环变量最终停在不满足条件的位置上。5.3 break与continue位置决定命运break是跳出当前循环层continue是跳过本次循环剩余部分、直接进入下一次判断。这两个关键字本身不难但和while搭配时容易出问题。int i 0; while (i 10) { i; if (i % 2 0) { continue; } printf(%d , i); }输出是1 3 5 7 9。continue跳过printf但i已经执行过了所以不会死循环。如果我把i放到printf后面再配合continue问题就出来了int i 0; while (i 10) { if (i % 2 0) { continue; } i; printf(%d , i); }这个版本在i0时就会continue永远到不了i又是一个死循环。所以记住一句话在while循环里continue之前要让循环变量具备更新的机会。这也是为什么很多程序员偏好for循环——for的更新语句写在第三段continue不会跳过它。5.4 循环变量类型与判断条件float比较是重灾区C语言的while条件可以是任何表达式但浮点数比较是个隐藏坑。浮点数在计算机里是近似存储的0.1 0.2 0.3的结果通常是假。如果你写了类似while (x ! 0.0)这样的循环理论上x通过除法逐渐变小最终应该变成0但实际可能会因为精度问题永远不等于0循环停不下来。解决办法是使用范围判断while (x 1e-9) { // 小于一个很小的阈值就认为等于0 x x / 2; }另一个容易踩的是无符号整型。如果循环变量是unsigned int你写while (i 0)这个条件永远为真因为无符号数永远不小于0。定义循环变量前想清楚它的取值范围这是C语言老手的本能。5.5 调试循环的三个实用手段遇到循环逻辑错误别急着加代码我的调试顺序是第一printf大法。在循环体关键位置打印循环变量的值。while (i 10) { printf(DEBUG: i %d\n, i); // 其他逻辑 i; }看输出就能发现变量更新是否符合预期。很多大学生调试完忘记删printf交上来的OJ代码带着一堆DEBUG输出被判错这个要注意。第二用调试器单步执行。如果你在用VS Code配C语言环境或者CLion设置断点一步一步观察变量变化。这一步不靠看代码猜而是直接盯着内存里的值变逻辑错误基本逃不掉。第三缩小输入规模。把大输入改成最小的测试用例。比如处理N个元素时先把N改成5或者3用能心算出来的数据人工推演推演结果和程序输出对不上说明逻辑有误。我当年学循环时就靠这一招把每个循环题都当数学题做先手算一遍再跑代码。写到最后的一点体会循环是C语言第一个让代码拥有规模感的语法结构。你学会while和do-while之后突然能解决求和、统计、逆序、读文件等一系列实际问题这种感觉很有成就感。但别急着往下学for和函数建议先把本章的经典例题做到不看书能独立写出来的程度。我自己这些年带学生的经验是循环掌握得越扎实的人后面学数组和指针时越轻松因为数组遍历、链表查找这些内容本质上就是循环的变体。如果你当前正卡在某个循环题上记住我的方法拿着笔把循环变量每一步的变化写下来答案往往会自己浮出来。