☰
C语言while循环详解:从基础语法到死循环排查与break/continue实战
2026/10/3 4:17:29 网站建设 项目流程

刚接触C语言的人,最容易在while循环上卡住。语法本身不难,while(条件)后面跟一段要重复执行的代码,可一旦循环体复杂起来,漏掉变量更新、忘记大括号、scanf读进非法字符、EOF判断写错,都会让程序要么卡死、要么疯狂刷屏。这篇就把C语言while循环的核心要点一次性捋清楚,从执行顺序、do-while的差异,到break、continue、return的用法,再到最常见的翻车现场和经典练习题。不管你是正在啃C语言基础的新手,还是准备PTA、课堂作业,这篇内容都够用。

1. while循环的走向:先判断,再执行,别把顺序背反了

1.1 大多数人理解上的偏差:while不是“重复”,而是“有条件的重复”

很多初学者会把while理解成“让某段代码重复执行”,这个说法不算错,但它忽略了一个关键点:while是“先判断,再执行”。

while (表达式) { 循环体; }

在C语言里,表达式为“真”的时候,循环体才会执行;执行完之后,程序会重新回到表达式那里,再做一次判断。只有表达式的值为0(也就是“假”)时,循环才会彻底结束,程序继续往下走。

展开说就是这样一个顺序:

  1. 判断表达式是否成立。
  2. 成立,则执行一次循环体。
  3. 再次回到第1步,重新判断。
  4. 不成立,跳出循环。

举例:

int i = 1; while (i <= 3) { printf("第%d次\n", i); i++; }

这段代码的输出是:

第1次 第2次 第3次

执行流程是这样的:一开始i=1,1<=3成立,进入循环打印“第1次”,然后执行i++,i变成2;回到判断,2<=3又成立,继续打印“第2次”;直到i变成4,4<=3不成立,循环结束。

这里有一个真正重要的地方:循环体里必须要有改变条件的语句,否则循环就会一直进行下去。上面的i++就是这个“改变条件的动作”。如果把i++删掉,i永远是1,1<=3永远成立,程序就永远出不来,屏幕上无限刷“第1次”。

另外,C语言里“真假”不是只有true和false。非0就是真,0就是假。这也是为什么你能看到很多代码直接写while(1),它的意思是“条件恒为真”,配合break来跳出循环。记住这个特点,后面看别人的代码就不会觉得奇怪了。

1.2 循环体加不加花括号,差别非常大

C语言的while语法里,循环体本质上是“一条语句”,如果你有两条以上的语句要重复执行,必须用花括号{}包起来。

新手最容易犯的一个错误是这样:

int n = 1; while (n <= 5) printf("n = %d\n", n); n++;

这段代码编译不会报错,但运行起来就是一个死循环。因为while(n <= 5)后面只跟了printf这一条语句,n++并不属于循环体。结果就是:n永远不增加,printf无限执行。

正确的写法当然是:

int n = 1; while (n <= 5) { printf("n = %d\n", n); n++; }

这里我给一个非常朴素的建议:只要写while,不管循环体有几条语句,一律把花括号写上。哪怕循环体只有一行,也先写{}再往里面填代码。这样做看似多打两个字符,实际上能避免掉一大批因为作用域理解不到位而产生的死循环bug。

还有一个更隐蔽的写法,新手很容易被它误导:

while (n <= 5); { printf("n = %d\n", n); n++; }

注意第一行结尾的那个分号,它表示循环体是空语句,while在这里反复判断,什么都不做,是个空转死循环。后面那对花括号只是普通代码块,压根不在循环里。这种写法有时会被故意用来做“等待”或者“清空输入缓冲区”,但如果你不是有意为之,看到while(...);这种结构一定要警惕。

提示:在调试现场看到“程序没输出就卡住不动”,第一反应先看看是不是while后面多了个分号,或者循环体少了大括号。

2. 需要“至少执行一次”时,用do-while更稳妥

2.1 do-while的语法与执行顺序

while是“先判断后执行”,那就必然存在一种情况:如果第一次判断条件就不成立,循环体一次都不会执行。这在很多场景下没问题,但有些业务逻辑需要“不管三七二十一,先做一次再说”,这时候就该用do-while。

do { 循环体; } while (条件);

注意do-while的结尾有一个分号,而且这个分号不能漏。漏掉的后果是编译报错,虽然不是什么难题,但确实很烦。

执行顺序与while正好相反:

  1. 先执行一次循环体。
  2. 再判断条件。
  3. 条件成立,回到第1步继续执行。
  4. 条件不成立,退出循环。

也就是说,do-while至少会执行一次循环体。

2.2 典型场景:用户输入校验和菜单选择

最典型的场景是“用户输入一个成绩,范围必须在0到100之间,不合法就重新输入”。

int score; do { printf("请输入成绩(0~100):"); scanf("%d", &score); } while (score < 0 || score > 100);

这里为什么用do-while而不是while?因为“弹出提示、读取输入”这个动作,无论如何你都想让它先执行一次。如果改用while,你得这样写:

int score; printf("请输入成绩(0~100):"); scanf("%d", &score); while (score < 0 || score > 100) { printf("请输入成绩(0~100):"); scanf("%d", &score); }

这代码虽然也能工作,但“提示+输入”被重复写了两遍,一旦提示文本很长,维护起来就很痛苦。用do-while,逻辑清晰,代码也更简短。

菜单驱动的程序设计里,do-while也很常见:

int choice; do { printf("1. 查询\n"); printf("2. 修改\n"); printf("0. 退出\n"); scanf("%d", &choice); switch (choice) { case 1: /* 查询逻辑 */ break; case 2: /* 修改逻辑 */ break; case 0: break; default: printf("无效选项\n"); break; } } while (choice != 0);

先显示菜单,再让用户选择,最后根据选择判断是否继续循环。这正是do-while的用武之地。

2.3 位数的统计:while和do-while的边界差异

有一个很经典的入门题能看出两者的区别:统计一个整数n的位数。

用while写:

int n, count = 0; scanf("%d", &n); while (n != 0) { count++; n /= 10; } printf("%d\n", count);

如果输入的是123,循环过程是:123/10=12,12/10=1,1/10=0,到0退出,count等于3,正确。但如果输入的是0,循环一次都不进,count是0。从数学角度看,0当然只有1位,这就错了。

改成do-while:

int n, count = 0; scanf("%d", &n); do { count++; n /= 10; } while (n != 0);

输入0的时候,先执行一次count++,然后0/10=0,条件不成立,退出,count是1,符合直觉。所以当你明确知道“这个循环体至少应该执行一次”的时候,优先考虑do-while。

提示:不要只是在“固定用while”和“固定用do-while”之间凭感觉选。判断标准就一句话:这段代码是否必须无条件执行一次?是,就do-while;不是,就while。

3. 退出循环的三条路:break、条件自然变假、return

3.1 break:提前拉闸,直接跳出当前循环

正常情况下,while靠条件变假结束。但有时候循环刚开始时没办法预测什么时候该停,需要在循环体内部根据某个结果决定立即停止,这就是break的用途。

int arr[] = {3, 7, 9, 2}; int i = 0, found = 0; while (i < 4) { if (arr[i] == 9) { found = 1; break; // 找到目标,不用再找了 } i++; } if (found) { printf("找到了\n"); } else { printf("没找到\n"); }

break执行后,循环立即结束,程序跳到循环语句后面继续执行。注意,break只跳出它所在的那一层循环。如果你是嵌套循环,里层break只跳出内层,外层循环不受影响。

3.2 while(1) 配合break:干净地实现“反复读取直到成功”

有些场景很难直接写一个明确的循环条件,比如用户输入校验,既可能成功退出,也可能持续很久。这时候可以用while(1)加break。

int num; while (1) { printf("请输入一个正整数:"); if (scanf("%d", &num) == 1 && num > 0) { break; } printf("输入无效,请重新输入。\n"); }

这段代码的运行逻辑是:死循环,不断读用户输入;直到读到一个正整数才用break退出。相比之下,硬要构造一个复杂的while条件反而会把自己绕晕。

不过我要提醒一句:while(1)里的循环一定要有出口,否则就是真正的死循环。我见过很多新手写while(1)写得太顺手,结果忘记在某个分支里补break,程序一跑就卡死了。如果循环里的逻辑比较复杂,建议加一个running之类的标志变量:

int running = 1; while (running) { // ... if (满足退出条件) { running = 0; } }

这样比裸break稍微多几行,但退出路径更清晰,改起来也不容易漏。

3.3 continue:跳回条件判断,而不是“原地重来”

continue经常和break被放在一起说,但作用完全不同。break是彻底结束循环;continue是结束本次循环体的执行,直接跳回条件判断那里,重新开始下一轮。

举一个输出1到9之间奇数的小例子:

int i = 1; while (i < 10) { if (i % 2 == 0) { i++; continue; // 跳过printf,直接回到条件判断 } printf("%d ", i); i++; }

这里最关键的是:在continue之前,一定要先处理循环变量的更新。上面的代码里,continue之前执行了i++,所以循环可以继续前进。如果把i++放在continue之后,i就永远停在当前值,循环就会卡在同一个地方死循环。

同理,如果你在循环体里写:

while (i < 10) { if (某个条件) { continue; } printf("%d ", i); i++; }

当某个条件成立时,后面的printf和i++都被跳过,i很可能一直不变,死循环就在所难免。记住这个教训:使用continue之前,先检查所有循环变量是否已经更新。

3.4 return:连函数一起退出

return和break不一样。break只是跳出循环,如果循环后面还有代码,程序依然会继续执行。return是直接结束整个函数,后面的代码一概不跑。

比如写一个查找函数:

int find_target(int arr[], int n, int target) { int i = 0; while (i < n) { if (arr[i] == target) { return i; // 找到了,直接返回下标 } i++; } return -1; // 没找到,返回-1 }

在循环里用return是非常自然的写法,尤其适合“找到目标就结束整个任务”的场景。判断while(scanf("%d", &n) != EOF)这类循环结束时,也经常会配合return来提前终止处理。比如PTA里不少题目要求“读入数据,直到文件结束”,如果中途遇到非法数据,很多人会直接用return退出整个程序,这种做法虽然简单,但要注意它会把循环之后的所有逻辑都跳过。

顺便把三类退出手段整理一下,方便对照:

方式作用范围典型场景
break跳出当前一层循环找到目标元素、用户输入有效后退出
continue跳过本次循环剩余代码,进入下一次判断过滤某些值,比如只处理奇数
return结束当前函数查找函数提前返回结果、出错直接退出

4. 实测下来最高频的while循环翻车现场

4.1 条件变量不更新:程序“假死”的元凶

死循环是很常见的现象,但很多初学者的第一反应是“程序是不是崩溃了”。其实大多数情况不是崩溃,而是while条件一直成立,程序在疯狂空转。

看这段代码:

int n = 10; while (n > 0) { printf("n = %d\n", n); // 忘记写 n--; }

如果没有n--,n永远是10,n > 0永远成立。程序会一直打印,直到终端被刷屏。不同环境里表现不一样:在在线判题系统里可能因为超时被掐断;在本地终端里你会看到一个停不下来的输出。

这种问题排查思路很简单:

  1. 先看循环条件依赖哪些变量。
  2. 再到循环体里找这些变量有没有被改变。
  3. 如果循环体很长,可以把条件变量用printf打出来,判断它有没有按预期变化。

用gdb调试时,直接在循环开始处打断点,逐步执行并观察变量值,很快就能定位。

除了忘记更新变量,还有一种情况是浮点数比较造成死循环。写一个while (x != 1.0),然后循环体里x += 0.2,看起来好像0.2 + 0.2 + ...总有一天能到1.0,但浮点数在计算机里不是精确的十进小数,可能累计出0.9999999999999998,永远不等于1.0,循环就停不下来。我的建议是:浮点数不要用!=或==作为循环结束条件,换成<、>或限制循环次数更安全。

4.2 优先级坑:ch = getchar() != EOF到底在做什么

这是自己写代码时极容易踩的一个坑,也可能是读别人代码时最迷惑的地方。

很多人在练习字符输入时,会写出类似这样的代码:

char ch; while (ch = getchar() != EOF) { putchar(ch); }

这段代码的本意是“不断读入字符,直到文件结束”。但问题出在运算符优先级上:!=的优先级比=高,所以ch = getchar() != EOF实际上等价于ch = (getchar() != EOF)。

也就是说,程序先把getchar()读到的字符和EOF做比较,得到一个整数结果:如果没读到EOF,比较结果是1;读到了EOF,比较结果是0。然后把1或0赋值给ch,接着让这个值去决定循环是否继续。

后果就是:ch里存的根本不是输入的字符,而永远是1或0。getchar()读到的原始字符直接被丢弃了。循环碰到EOF才会停,但putchar(ch)打印出来的东西完全不对。

修复方法很简单,加一对括号,并且把ch声明成int:

int ch; while ((ch = getchar()) != EOF) { putchar(ch); }

顺便说一个细节:为什么用int ch而不是char ch?因为EOF在标准里通常是-1,可能超出char能表示的范围。用int去接收getchar()的返回值,才能可靠地判断EOF。

4.3 scanf失败后的残留数据:循环疯狂刷屏的真相

比死循环更让人头疼的是“输入一个字母,程序开始疯狂刷屏重试”。这种问题一般出在scanf失败后的缓冲区残留。

看一个典型的菜单循环:

int choice; do { printf("请输入选择(1~3):"); scanf("%d", &choice); // 根据choice执行操作 } while (choice != 0);

如果你输入的是整数,一切正常。但如果你不小心输入了abc,scanf("%d", &choice)会读取失败,choice保持原来的值不变,而abc还留在输入缓冲区里。下一次循环,scanf依然试图读取整数,看到的还是abc,又一次失败,又一次残留……于是程序在“提示输入”和“读取失败”之间无限循环,屏幕疯狂刷屏。

解决的思路有两个:一是判断scanf的返回值,二是清空缓冲区残留。

我在实际项目里一般会写一个函数:

void clear_input(void) { int c; while ((c = getchar()) != '\n' && c != EOF) { // 把这一行的剩余字符全部读掉 } }

然后在每次scanf失败之后调用它:

int choice; while (1) { printf("请输入选择(1~3):"); if (scanf("%d", &choice) == 1) { break; } clear_input(); printf("输入无效,请重新输入。\n"); }

scanf返回值是“成功读取并赋值的变量个数”。读到合法整数时返回1;读到非数字时返回0;文件结束或出错时返回EOF。用==1判断比用!= EOF更严谨,因为万一用户输入了字母,scanf可能返回0,这种情况下如果不清理残留,循环也一样会出问题。

提示:不要用fflush(stdin)去清空输入缓冲区。fflush在标准C里只对输出流有明确定义,对输入流使用属于未定义行为,换一个编译器可能就失效了。用上面getchar()配合while循环清理是最稳妥的做法。

4.4 循环里直接操作数组时的边界问题

用while遍历数组,最大的风险是越界。常见写法是:

int arr[5] = {1, 2, 3, 4, 5}; int i = 0; while (i <= 5) { arr[i] = 0; // 当i=5时,arr[5]越界 i++; }

数组下标范围是0到长度减1。i <= 5会多访问一次arr[5],这是未定义行为,程序可能不会立刻崩溃,但会悄悄破坏内存里的其他数据。安全的写法是i < 5。判断边界时,建议心里默念一句口诀:从0开始,到n-1结束,条件是i < n,不是i <= n。

5. 练透while的几个经典场景,以及while与for的互相改写

5.1 经典题:输入一串数求最大最小值

这个场景在许多练习里出现,和标准库头文件limits.h配合起来很典型。

题目要求:不断输入整数,输出最小值。因为不知道输入数量,所以用while读入直到文件结束。

#include <stdio.h> #include <limits.h> int main(void) { int x, min = INT_MAX; while (scanf("%d", &x) == 1) { if (x < min) { min = x; } } printf("最小值:%d\n", min); return 0; }

这里INT_MAX是limits.h提供的当前环境下int类型的最大值。把最小值初始值设为INT_MAX,不管输入什么数,第一次比较肯定能更新为实际输入值。如果你手工设一个“很大的数”比如99999,万一输入的数据比它还大,结果就错了。limits.h的作用就在这种边界处理上体现出来。

5.2 九九乘法表:嵌套while的经典练习

九九乘法表是C语言初学者最经典的嵌套循环题,几乎绕不开。

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

这个题目值得反复练的原因有两点。第一,外层循环控制行数,内层循环控制列数,这个“控制权”关系清楚了,很多打印图形的题都迎刃而解。第二,注意int j = 1;写在外层循环体内部,每次外层循环重新开始时,j都会重新初始化为1。如果把它挪到外层循环外面,j只会从1跑到9一次,之后内层循环条件j <= i永远不成立,后面的行就什么也不打印了。

5.3 字符串逆序:while处理双指针边界

PTA上有不少和“字符串逆序”有关的题目。用while写双指针交换,是非常基础但值得反复消化的写法。

#include <stdio.h> #include <string.h> int main(void) { char s[] = "hello"; int left = 0; int right = strlen(s) - 1; while (left < right) { char temp = s[left]; s[left] = s[right]; s[right] = temp; left++; right--; } printf("%s\n", s); return 0; }

输出结果是olleh。

这里要注意的是边界条件left < right。如果写成left <= right,当left和right指向同一个字符(字符串长度为奇数时),交换操作会把这个中间字符和自己交换两次,结果虽然没有越界,但等于白做一次。更重要的是,它会让“左右指针相遇”的判断变得不精确,不利于理解双指针的退出时机。

5.4 while与for的等价改写:理解continue的差异

很多人学到后面会有疑问:既然for能做循环,为什么还要用while?两者什么时候可以互相替换?

从结构上看,一个标准的for循环:

for (初始化; 条件; 更新) { 循环体; }

完全可以用while改写:

初始化; while (条件) { 循环体; 更新; }

比如从1加到100:

int sum = 0; int i = 1; while (i <= 100) { sum += i; i++; }

等价于:

int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; }

但有一个隐蔽的区别:如果循环体里用了continue,for和while的行为不一样。

在for循环里,执行到continue后,会先执行更新表达式,再进行条件判断。而在while循环里,continue直接跳回条件判断,循环体末尾的更新语句就被跳过了。所以前面反复强调“while里用continue,得先把循环变量更新了”,就是这个原因。

举个例子,打印100以内的偶数,用for写很自然:

for (int i = 0; i < 100; i++) { if (i % 2 != 0) { continue; } printf("%d ", i); }

不管怎么continue,i++都会被for的更新表达式执行,循环不会卡死。如果把这个逻辑直接翻译成while,你就得自己小心更新位置。建议是:循环次数明确、需要递增递减变量的,优先写for;循环条件受外部事件影响、或者要一直运行到某个状态变化的,用while更自然。

5.5 读文件的while循环:fgets与逐行处理

C语言里读文件、逐行处理输入,也常常用while来写,这也是很多实际项目里的通用模式。

#include <stdio.h> int main(void) { char line[1024]; while (fgets(line, sizeof(line), stdin) != NULL) { printf("%s", line); } return 0; }

fgets每次从输入流读一行,读到文件末尾时返回NULL,循环结束。这个写法和while (scanf("%d", &x) == 1)的模式本质相同:用读取函数的返回值作为循环条件,而不是在循环体里硬编一个次数。处理文件内容、统计行数、输出每行长度,都可以在这个框架上扩展。

我在实际写代码的时候,已经习惯性地把while循环当成一个“反复处理直到某个信号发生”的工具。这个信号可以是条件变量变化、可以是break、可以是return,也可以是函数返回的特殊值。把“退出信号”想清楚,再往里填逻辑,基本就不会出现那种连自己都看不懂的死循环。

如果只想记住这一条经验,那就是:写while之前,先问自己一句,这个循环凭什么停下来?能明确回答,再动手写循环体。

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

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

立即咨询