☰
C语言while与do-while循环:从执行机制到实战避坑指南
2026/9/30 13:02:23 网站建设 项目流程

讲真,学会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在那里空转,窗口卡死,你按Ctrl+C都来不及。

我常跟学生打一个比方: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通常更自然;如果循环条件在第一次执行前就能确定(比如"i<10"这种),用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,打印5,n = 1234
  • 第二次循环:last = 4,打印4,n = 123
  • 第三次循环:last = 3,打印3,n = 12
  • 第四次循环:last = 2,打印2,n = 1
  • 第五次循环:last = 1,打印1,n = 0
  • 循环结束

输出正好是54321。这里有一个细节要提示:如果输入0,while循环条件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 连续读取输入直到EOF:scanf返回值是被忽视的钥匙

很多初学者不知道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 死循环:大多数时候是更新语句丢了或位置错

死循环是循环章节的头号杀手。我自己上课时统计过,学员报错最多的问题就是"程序跑起来没反应,窗口卡死"。排查顺序我总结成一个清单:

  1. 检查循环变量有没有初始化
  2. 检查循环体里有没有更新循环变量的语句
  3. 检查更新语句的位置是不是写在continue之后了(如果用了continue,更新会被跳过)
  4. 检查条件表达式是否在某种输入下永远为真

举一个最典型的错误示范:

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执行动作循环后i
0真执行1
1真执行2
2真执行3
3假结束-

这张表一画,到底执行了几次一目了然。我遇到边界问题时从不靠猜,一定列这样的表。列完表你还发现,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); }

这个版本在i=0时就会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和函数,建议先把本章的经典例题做到"不看书能独立写出来"的程度。我自己这些年带学生的经验是:循环掌握得越扎实的人,后面学数组和指针时越轻松,因为数组遍历、链表查找这些内容本质上就是循环的变体。如果你当前正卡在某个循环题上,记住我的方法:拿着笔把循环变量每一步的变化写下来,答案往往会自己浮出来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询