写C语言要是没搞懂循环,后面的链表、排序、文件读写这些内容你基本都下不了手。while和for这两个循环结构,差不多覆盖了日常开发里90%以上的重复逻辑场景,甚至可以说,循环玩得溜不溜,直接决定了你写出来的代码是“能跑”还是“像样”。这篇内容我打算从实际使用的角度把循环彻底讲透——不只是背语法,而是告诉你每个循环结构到底在什么场景下用、判断条件怎么写才不出错、死循环怎么排查,全程用大白话加可以直接复制的代码示例,适合刚学C语言被绕晕的初学者,也适合准备计算机二级考试、想快速过一遍循环知识点的同学。
先说清楚一个误区:很多人觉得while和for只是写法不同,随便换个用就行。这话对了一半。它们确实能互换,但在可读性和代码意图表达上,选错会让看代码的人(包括两周后的你自己)很痛苦。这篇文章会把这个边界也一并讲清楚。
1. 循环到底帮我们解决了什么问题
1.1 没有循环的时候,代码能啰嗦到什么程度
假设你要让程序输出1到10这十个数字。没有循环的时候,你得写十行printf。如果要输出1到1000呢?写一千行?这显然不现实。再换一个场景:程序需要循环读取用户输入,直到用户输入了合法的数据,这个“直到”就说明你根本不知道要重复多少次,完全由运行时的条件决定。
循环的本质,就是让一段代码在满足条件的情况下反复执行,它是顺序结构之外的第二个结构化程序基石。理解了这一点,你就知道为什么调试循环问题时,核心是去追踪“条件”和“变化”这两个东西。
1.2 循环三要素:初始化、条件、更新
任何一个循环,哪怕写法五花八门,内部都必须包含三个关键部分:
- 初始化:循环变量(也叫计数器)的起始值是多少。
- 条件:每次循环开始前,判断要不要继续执行。
- 更新:每次执行完循环体后,循环变量怎么变。
我见过很多初学者死循环,根本原因就是只写了初始化和条件,忘记了更新,循环变量永远不变,条件永远成立。这就好比你要从1数到100,结果每次只喊“1、1、1”,永远数不完。
接下来三个循环结构,你都可以对照这三要素去理解,会发现它们骨子里是一样的,只是语法糖长得不同。
2. while循环:先看门槛再进门
2.1 while的基本语法和执行流程
while的语法很直白:
while (条件表达式) { // 循环体语句 }执行流程就是:先判断条件表达式的值,如果为真(非0),就执行一遍循环体;然后再回到条件判断,为真再执行;直到条件为假(0),跳出循环。
把循环三要素套进while,你会发现它的写法长这样:
int i = 1; // 1. 初始化 while (i <= 100) { // 2. 条件 printf("%d\n", i); i++; // 3. 更新 }这里有一个很关键的点:while是先判断后执行。也就是说,如果一开始条件就不成立,循环体一次都不会执行。这个特性在某些场景下恰恰是你想要的,后面讲do-while的区别时会再拿出来对比。
2.2 实战案例:用while计算1到100的和
直接上代码,这是所有C语言教程都绕不过去的经典案例:
#include <stdio.h> int main() { int i = 1; int sum = 0; while (i <= 100) { sum = sum + i; i++; } printf("1+2+3+...+100 = %d\n", sum); return 0; }跑一下,输出结果是5050。我建议你拿到这段代码之后,先别急着运行,自己拿张纸模拟一遍前几次循环:
- 第一轮:i=1,1<=100成立,sum=0+1=1,i变成2。
- 第二轮:i=2,2<=100成立,sum=1+2=3,i变成3。
- 第三轮:i=3,sum=3+3=6,i变成4。
你会发现,每执行一轮,sum里就累加了一次当前的i,而i从1稳步增加到101。当i变成101的时候,101<=100是假的,循环退出。这个时候输出的sum就是5050。如果我把条件改成 i < 100,结果就会变成1加到99的和4950,差了一个100,这就是边界条件的力量。
实际操作中,这类求和、求积、统计个数的题目是初学者练习while的最好素材,因为循环的每一轮变化都能用数学公式验证,方便你排查问题。
2.3 死循环从哪里来的
while写不好最容易出现死循环,踩过坑的同学应该深有体会。常见的死循环有这几种:
第一种:忘了写更新语句。
int i = 1; while (i <= 100) { printf("hello\n"); // 忘写 i++; }这个程序会永远打印hello,直到你强制关掉它。因为i永远是1,条件永远成立。
第二种:更新语句写在了不可达位置。
int i = 1; while (i <= 100) { if (i == 50) { continue; // 跳到条件判断 } i++; }这个程序在i等于50的时候,continue直接跳到了下一次循环的条件判断,跳过了i++这一行。于是i永远停在50,死循环了。这个坑在后面的continue小节还会再强调。
第三种:条件写的边界错位。
比如你要循环10次,i从1开始,条件写成 i > 0,那i一直大于0,循环永远不结束。这种属于逻辑上的错误,运行起来才发现程序卡死。
排查死循环时,我习惯在循环体开头加一行 printf 打印i的值,观察它是否在按预期变化。如果i的值从头到尾没变,基本就是更新语句的问题;如果i在某种情况下没变,那就要检查continue或者带break的分支是不是把更新跳过了。
3. do-while循环:至少得先进来一次
3.1 do-while与while的本质区别
do-while的语法是这样的:
do { // 循环体 } while (条件表达式);注意,这个写法最后有一个分号,不能丢。这也是初学阶段很多人编译报错的原因。
do-while和while最大的区别,用一句话就能说清:while是先判断后执行,do-while是先执行后判断。这就导致do-while的循环体至少会被执行一次,不管条件成不成立。
举个例子感受一下:
int i = 100; while (i < 100) { printf("while执行了\n"); } do { printf("do-while执行了\n"); } while (i < 100);这段代码里,i初始值是100,i<100是假的。while循环直接跳过,什么都不输出;而do-while先执行了一次循环体,输出了“do-while执行了”,然后才去判断条件。
所以核心选择标准就出来了:如果你要执行的代码无论如何都得先做一次,再判断要不要继续,那用do-while最合适。
3.2 do-while的典型应用场景:用户输入校验
实际开发里do-while最常见的场景,就是做输入合法性的校验,比如写一个猜密码的程序:
#include <stdio.h> int main() { int password = 123456; int guess = 0; do { printf("请输入密码: "); scanf("%d", &guess); if (guess != password) { printf("密码错误,请重新输入\n"); } } while (guess != password); printf("密码正确,登录成功\n"); return 0; }你看这个场景,用户第一次输入之前,你根本不需要判断条件,反正得先让用户输一遍,然后才能判断它对不对。如果用while写,你就要在循环之前先让用户输入一次,循环里再输入,代码会多出重复的部分,比较别扭。do-while把这个“先来一次”的逻辑表达得非常自然。
类似的场景还有菜单选项、游戏里的“是否重玩”提示、读取文件直到成功等,凡是“先执行、后判断”的都适合do-while。
4. for循环:最常用的循环武器
4.1 for循环的三个表达式拆解
for循环是C语言里用得最多、也最优雅的循环结构。语法长这样:
for (表达式1; 表达式2; 表达式3) { // 循环体 }- 表达式1:初始化,循环开始前执行一次,通常用来给循环变量赋初值。
- 表达式2:循环条件,每次循环开始前判断,为真执行循环体,为假退出。
- 表达式3:更新,每次执行完循环体后执行,通常用来改变循环变量。
把1到100求和用for写出来,对比着看:
#include <stdio.h> int main() { int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; } printf("1+2+3+...+100 = %d\n", sum); return 0; }for的好处在于,初始化、条件、更新这三件事整整齐齐地写在了一行里,你一眼就能看出循环的完整逻辑,不需要像while那样去循环体里找更新语句。这也是很多公司编码规范里要求“确定次数的循环用for”的原因,代码review的时候省事。
4.2 for循环的常见变形与细节
for的三个表达式并不是都要求必须写的。比如这个写法,用循环变量控制倒计时:
for (int i = 10; i > 0; i--) { printf("%d\n", i); } printf("发射!\n");这就是常见的倒计时场景,表达式3用了自减。再比如你完全可以把i的初始化放在外面,写成这样:
int i = 1; for (; i <= 100; i++) { sum += i; }第一个表达式为空,编译也能通过,前提是i在之前已经初始化过了。如果条件表达式也省略,for(;;)就变成了一个死循环,等价于while(1),这在某些需要“一直运行直到break”的服务器程序里有时会见到,不过初学者别乱用,容易出不来。
还有一个细节是逗号运算符。你可以在表达式1和表达式3里放多个语句,用逗号隔开:
for (int i = 1, j = 100; i < j; i++, j--) { printf("i=%d, j=%d\n", i, j); }这种写法在处理需要两个方向同时移动的场景时非常方便,比如数组反转,一头一尾两个下标同时往中间走。
4.3 for循环和while循环怎么选
很多初学者纠结这个问题,我的建议很简单:
- 知道要循环多少次,或者能用计数器明确描述循环次数的,用for。比如遍历数组、打印九九乘法表、计算前n项和。
- 不知道多少次,只知道什么时候停的,用while。比如读取用户输入直到合法、处理链表直到节点为空。
- 不管条件是否成立,至少要先执行一次的,用do-while。
用for处理“次数明确”的场景,代码清晰;用while处理“条件驱动”的场景,逻辑自然。你不用死记硬背,多写几个程序自然就有手感了。
5. break和continue:循环的刹车与越障
5.1 break:直接跳出整个循环
break在循环里的作用是彻底结束当前这一层循环,不管循环条件还成立不成立,直接跳到循环结构后面的代码。
一个典型场景是查找数组里第一个满足条件的元素:
#include <stdio.h> int main() { int nums[] = {3, 7, 2, 9, 5}; int target = 9; int index = -1; for (int i = 0; i < 5; i++) { if (nums[i] == target) { index = i; break; // 找到了,不用再继续找了 } } printf("target在数组中的下标是: %d\n", index); return 0; }这里break的意图很明显:都已经找到目标值了,后面的循环没必要再跑下去了。如果不加break,虽然结果一样(因为index会被更新到最后一个匹配位置),但白白多做了无用的比较,对于大规模数据这是性能浪费。
注意break有一个大坑:它只能跳出离它最近的那一层循环。如果代码是两层嵌套循环,break跳不出外层,这个后面讲嵌套循环时会再提醒。
5.2 continue:跳过本次循环的剩余部分
continue的作用跟break完全不同,它不结束整个循环,只是跳过本次循环体里continue后面的语句,直接进入下一轮循环。
来看这个对比示例,把1到10里除了4以外的数打印出来,8之后停止:
#include <stdio.h> int main() { for (int i = 1; i <= 10; i++) { if (i == 4) { continue; // 跳过4,不打印 } if (i == 8) { break; // 遇到8,整个循环结束 } printf("%d ", i); } return 0; }输出结果是:
1 2 3 5 6 7这里你能清晰地看到两者的区别:4被continue跳过,但循环照常往下走;8触发了break,循环直接终止,9和10也没机会输出了。
用continue的时候有个必须警惕的场景,就是前面说的那类死循环:
int i = 0; while (i < 10) { if (i == 5) { continue; // 危险! } i++; }当i等于5时,continue让程序跳到了条件判断,i++又没执行,于是i永远是5,条件i<10永远成立,死循环。在for循环里,continue会先执行表达式3再判断条件,所以这个问题不明显;但在while和do-while里,continue碰到更新语句写在循环体尾部的情况就会踩雷。如果非要在while里用continue,建议把更新语句放到continue之前,或者直接用for循环。
6. 嵌套循环:循环里再套循环
6.1 嵌套循环的执行逻辑
循环里套循环,就叫嵌套循环。理解嵌套循环,核心是搞清楚执行顺序:外层循环每执行一次,内层循环要完整地执行一轮。
看这个例子:
#include <stdio.h> int main() { for (int i = 1; i <= 3; i++) { printf("外层第%d次\n", i); for (int j = 1; j <= 2; j++) { printf(" 内层j=%d\n", j); } } return 0; }输出结果:
外层第1次 内层j=1 内层j=2 外层第2次 内层j=1 内层j=2 外层第3次 内层j=1 内层j=2可以看到,外层循环i=1时,内层j从1跑到2,完整输出一遍;然后外层i=2,内层又完整跑一遍。外层循环3次,内层循环总共执行了3x2=6次。
这就是为什么嵌套循环常用来处理二维结构,比如矩阵、表格、坐标等。
6.2 实战:用嵌套循环打印九九乘法表
打印九九乘法表是嵌套循环的经典习题,学校考试和面试笔试都爱考。代码是这样:
#include <stdio.h> int main() { for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { printf("%d*%d=%-2d ", j, i, i * j); } printf("\n"); } return 0; }代码里最关键的是内层循环的条件j <= i,这决定了乘法表是左下三角的形状,而不是全部9行9列。每一行输出的内容数量都不一样,第一行1个式子,第二行2个式子,第i行i个式子,所以输出结果就能形成一个阶梯状的乘法表。
%-2d里的-表示左对齐,2表示占两位长度,这样输出的式子能对齐,看起来整齐美观。这是输出控制的小技巧,演示效果很好。
6.3 进阶案例:用循环嵌套打印菱形
如果你能把乘法表吃透,可以再挑战一下打印菱形的题目。思路是把菱形拆成上半部分和下半部分,分别用循环控制空格和星号的数量:
#include <stdio.h> int main() { int n = 5; // 上半部分的行数 // 上半部分 for (int i = 1; i <= n; i++) { for (int j = 1; j <= n - i; j++) { printf(" "); } for (int j = 1; j <= 2 * i - 1; j++) { printf("*"); } printf("\n"); } // 下半部分 for (int i = n - 1; i >= 1; i--) { for (int j = 1; j <= n - i; j++) { printf(" "); } for (int j = 1; j <= 2 * i - 1; j++) { printf("*"); } printf("\n"); } return 0; }上半部分第i行需要先打印n-i个空格,再打印2*i-1个星号,这样星号就能居中,形成一个等腰三角形。下半部分反过来,从第n-1行往下递减。这类图形题是理解嵌套循环和数学规律的好素材,建议自己动手改改参数n,观察输出变化。
7. 新手最常见的坑和排查技巧
7.1 高频bug速查表
整理了一份初学者最容易踩的坑,对照着自查非常管用:
| 问题现象 | 常见原因 | 解决办法 |
|---|---|---|
| 循环一次都不执行 | while/for的条件一开始就是假 | 检查初始化值和条件方向,比如i < 0写成i > 0 |
| 死循环,程序卡住 | 忘记更新循环变量,或更新被continue跳过 | 用printf打印循环变量观察,确认更新语句是否可达 |
| 循环次数多一次少一次 | 边界条件用了<还是<=没想清楚 | 用具体数值代入心算一遍,比如 n=5 时手动跑几轮 |
| 循环结束后变量值不对 | 循环结束后还依赖循环变量的值,但条件判断有一次额外判断 | 记住循环退出时条件表达式已经为假,所以i的值是“最后合法值+步长” |
| 编译报错 | do-while末尾漏分号、括号不匹配 | 检查do-while的} while(...);末尾分号 |
| 嵌套循环跳出不符合预期 | break只跳出一层,想跳出多层没用 | 改用标志变量,或者把逻辑提取成函数用return |
7.2 调试技巧:printf大法
排查循环问题时,我最常用的工具就是将循环变量打印出来。比如怀疑循环次数不对,直接在循环体里加一行:
for (int i = 0; i < 10; i++) { printf("调试: i=%d\n", i); // 其他代码 }看着输出观察i的变化是否符合预期,很多时候问题一眼就能发现。相比之下,用IDE断点调试虽然更专业,但对于初学者来说,printf更快、更直观,也更符合“自己动手排查”的修行路径。我见过不少同学在IDE里打断点,结果被复杂的调试器界面弄得头昏脑涨,最后连断点设哪都忘了。先学会printf排查,等你熟练了再进阶学习断点调试,反而更有效率。
7.3 关于循环选择的几条实操建议
最后给几条实用建议,都是我实际写代码和带新人过程中的体会。
第一,循环变量命名要有意义。循环次数不多的时候用i、j问题不大,但一旦循环嵌套变深,最好给变量起个有意义的名字,比如row、col、index,不然代码稍微一长,根本分不清哪个i是哪个。这是可读性的大坑。
第二,尽量减少循环内部的重复计算。如果某个表达式的值在循环过程中不变,那就提到循环外面算。举个例子,你写for (int i = 0; i < strlen(s); i++),如果字符串很长,strlen每次循环都重新算一遍,性能白白损耗。改成先算长度再比较,高效很多。虽然这类优化在初学阶段不明显,但养成好习惯以后写算法题会受益。
第三,学会用纸笔模拟循环。特别是做练习题的时候,拿一张草稿纸,一行行写出变量每次循环后的变化,你会发现很多“玄学bug”根本不是玄学,只是你对变量的变化路径不够清楚。程序员常说的“不要相信玄学bug,只相信你看到的数据”,就是这个意思。
第四,循环里的逻辑判断能提前就提前。比如要计算数组里所有正数的平均值,你在循环里每次累加之前判断一下正负就行,不需要把所有数先复制到一个临时数组再遍历一遍。初学者容易写出绕弯的代码,锻炼“把逻辑做在循环内”的思维,会让代码干净非常多。
我从给新手看代码和答疑的经历来看,循环这个知识点,真正难的不是语法,而是边界判断和变量跟踪能力的欠缺。语法记不住翻书就行,变量会不会跟踪、边界能不能判断准,必须靠大量手写练习才能提上来。建议你把文章里的几个例子都手动敲一遍,再改改参数看看结果变没变,练上两三天,while和for基本就长在你身上了。