说实话,C语言学到循环这一章,才算真正开始"编程"。前面那些变量、运算符、分支语句,说到底都是在命令计算机做一件件具体的事;而循环控制结构,是第一次让程序自己重复劳动,也是第一次逼着你用"机器的视角"去思考问题。很多初学者在这一章卡住,不是语法背不下来,而是思维方式还没换过来——这恰恰是本篇想要解决的问题。这篇文章把 while、do-while、for 三种循环,以及 break、continue 这些跳转控制逐层拆开,配合可以直接上机跑的代码示例,讲清楚每个结构背后的设计逻辑和适用场景,也把死循环、边界条件、循环变量作用域这些入门阶段最容易踩的坑一并排一遍。适合刚学完顺序结构和分支结构、准备进入循环部分的自学者,也适合正在给初学者讲这一章的"半桶水"师兄师姐们用来参考。
1. 循环不是少写几行代码,是第一道"思维换挡"
很多教材把循环简单概括成"重复执行某段代码",这个说法没错,但太容易让人误解成"循环就是偷懒用的少写几行代码"。实际上,循环的意义远不止省键盘寿命——它是程序能够处理批量数据、实现复杂算法的基础。后面学数组、字符串、排序、链表,全都要靠循环来驱动。如果这一章只是背下了三个循环的语法格式,后面会越学越吃力。
1.1 从"手写重复"到"让机器重复"的思维转变
我经常给初学者打一个比方:假设要你在黑板上写 1 到 100 的数字,你当然可以一个个写,写到 100 个也就几分钟的事。但如果要求你写 1 到 100000 呢?手写是不现实的,你得找到规律,用某种"规则"让这个书写过程自己滚动起来。循环干的就是这件事。
在代码里,这种"规律"由三个要素构成:从哪里开始(初始条件)、到什么程度继续(循环条件)、每次做完之后怎么变(迭代更新)。你可以把循环想象成一条流水线:工件从入口进来,检查是否满足加工条件,满足就加工一遍,加工完推进一格,再检查,再加工……直到某个时刻条件不再满足,流水线停下来。
理解了这个模型,再看任何一个循环语句都不会慌:它本质上就是在描述一条"推进—检查—执行—再推进"的自动流水线。这也是为什么这一章值得认真停下来思考,而不是急着敲代码。脑子里先建立起这个动态模型,比多敲十遍语法都管用。
1.2 三种循环的"性格差异"要先有个整体印象
C语言提供了三种循环控制结构:while、do-while、for。它们在功能上可以互相替代——同一个问题用哪个写都能解,但每个都有自己的脾气和最适合的用法。我先给一张整体对照表,大家有个印象,后面的章节再逐个拆开细讲。
| 循环类型 | 判断时机 | 最少执行次数 | 典型适用场景 |
|---|---|---|---|
while | 先判断后执行 | 0 次 | 循环次数不确定,先看条件再说 |
do-while | 先执行后判断 | 1 次 | 至少要做一次,比如菜单选择、输入校验 |
for | 先判断后执行 | 0 次 | 循环次数明确,有清晰的初始化、条件、更新三步 |
从这张表能看出来,选择哪种循环,核心取决于两个问题:你要不要保证至少执行一次?以及你事前清不清楚要循环多少次?这两个问题想透了,三种循环的选用就不是靠死记硬背,而是靠需求自然推导出来。
1.3 这一章在整个C语言体系里的位置
回到系列的整体进度:前面几篇讲了数据类型、运算符、输入输出和分支结构,那些内容组合起来,能写出"一条路走到黑"或者"分岔路口选一条"的程序。但真实问题往往没有这么简单——你要统计一组成绩的平均分,要找出 100 以内的素数,要读取文件里不知道有多少行的数据,这些任务没有循环根本无从下手。而且循环结构还会和分支结构频繁嵌套出现(比如循环里判断奇数偶数),可以说,掌握了循环,才算把 C 语言"三大结构"中的最后一个大件装齐了,后面学函数、数组、指针,都是在这三大结构的基础上盖楼。
2. while循环:先看门再进门的执行逻辑,以及死循环从哪来
while是三种循环里最"耿直"的一个。它的执行逻辑一句话就能说完:先判断条件,条件为真就执行循环体,执行完再回来判断,直到条件为假为止。就像进小区门,门卫先看你的通行证,有证才放你进去,没证直接拦在外面,一次都别想进。这个"一次都不执行"的特性,是它与do-while最本质的区别。
2.1 语法结构和完整的累加求和案例
最基本的while写法是:
while (条件表达式) { // 循环体:在条件为真时反复执行的语句 }看一个最经典的入门案例:计算 1 加到 100 的和。
#include <stdio.h> int main() { int sum = 0; int i = 1; // ① 初始化循环变量 while (i <= 100) { // ② 循环条件 sum += i; // ③ 循环体:累加 i++; // ④ 更新循环变量,让循环能"前进" } printf("1+2+...+100 = %d\n", sum); return 0; }这个例子我建议初学者亲手敲三遍,不是背,而是感受四个部分之间的关系。sum是累加结果的容器,必须初始化为 0,不然第一次sum += i会把垃圾值加进去;i是循环变量,它决定了循环从哪里开始、到哪里结束;i++是整段代码的灵魂,少了这一行,i永远是 1,i <= 100永远为真,程序就会陷入死循环。
2.2 死循环是怎么产生的,以及怎么防
说到死循环,这是初学者接触while后最先遇到的"大事故"。程序卡住不动,终端疯狂刷输出,只能按 Ctrl+C 强制终止。死循环的根源只有一个:循环条件永远为真,没有变成假的机会。
最常见的两类原因,一类是忘记写更新语句,就像上面例子去掉i++;另一类是更新语句永远无法让条件趋于假,比如:
int i = 1; while (i <= 100) { i--; // i 越减越小,永远 <= 100 }还有一种隐蔽的死循环,是条件表达式本身写得有问题,比如把i <= 100写成i >= 100,同时i从 1 开始递增——1 确实永远不大于等于 100,循环体一次都不会执行,程序跳过循环,结果不是死循环,而是"空转"。所以调试循环时,第一件事永远是问自己两句话:循环变量初始化了吗?每次循环之后循环变量往条件为假的方向走了吗?这两句话能解决 90% 的循环入门问题。
我在实际调试中的一个小技巧是:在循环体开头加一行printf("i = %d\n", i);,先观察i的变化轨迹。只要轨迹和你预期不符,问题立刻暴露;等到逻辑稳定了再删掉这行调试输出。别嫌麻烦,这比对着代码凭空猜快得多。
2.3 while 适合什么场景
while最自然的场景是"循环次数事先不知道,但知道什么时候该停"。比如提示用户输入一个正数,用户可能输入 1 次就对,也可能输入 10 次都不对。这类"读到有效输入为止"的逻辑,用while写起来特别顺,因为条件本身就表达了"什么时候继续要"这个需求:
int score; printf("请输入一个 0-100 的分数:"); scanf("%d", &score); while (score < 0 || score > 100) { printf("输入不合法,请重新输入:"); scanf("%d", &score); }这段代码把"只要输入不合法,就一直问"翻译得非常直接。等到后面学了文件读写,while更是读取未知行数数据的标配——你不知道文件里有多少行,只能"读到末尾才算结束"。
3. do-while循环:先把事做了再谈条件,输入校验场景的最优选
do-while和while像一对双胞胎,但性格完全相反。它先把循环体执行一遍,再去判断条件是否继续。这意味着无论条件一开始是真是假,循环体至少会执行一次。还是举进小区的例子:do-while是那种"不管你有没有证,先让你开进去,进门之后门卫再查证,没证下一趟就不让进了"。
3.1 语法格式和与 while 的核心差异
do { // 循环体 } while (条件表达式); // 注意这里的分号不能丢请注意末尾的分号,这是do-while语法上最容易踩的坑,漏了分号编译直接报错。和while对比一下执行流程:
| 对比项 | while | do-while |
|---|---|---|
| 条件判断时机 | 循环体执行前 | 循环体执行后 |
| 循环体最少执行次数 | 0 次 | 1 次 |
| 典型前提 | 条件不满足时不该执行 | 无论如何都要先执行一次 |
有时候,同一个需求用两种循环都能写,但可读性和边界处理难度不一样。比如"反复读取合法输入"这个问题,用while需要在进入循环前先读一次,然后在循环末尾再读一次,代码里输入语句会重复出现两遍;用do-while则只需出现一遍,因为第一次读取放进了循环体。
3.2 用 do-while 实现"易用版"用户输入校验
回到前面那个"请输入 0-100 分数"的需求,用do-while写出来是:
int score; do { printf("请输入一个 0-100 的分数:"); scanf("%d", &score); if (score < 0 || score > 100) { printf("输入不合法,请重新输入。\n"); } } while (score < 0 || score > 100);这个版本好在哪里?它不需要在读循环前先"预读"一次,也不需要在循环末尾再"补读"一次,输入动作只写在循环体里一次,逻辑非常紧凑。这种"无论怎样都得先问一遍用户"的场景,天然就是do-while的主场。类似的应用还有菜单选择:先把菜单打印出来,读取选项,如果选项不在合法范围内就重新打印菜单再读。用户至少有两次机会操作,而且程序结构非常直观。
3.3 什么时候不建议用 do-while
do-while也不是万能的。当"条件一开始是否为真"会直接决定"循环体该不该执行"时,硬用do-while就得在循环体里加if判断来兜底,代码反而更绕。比如处理数据时,如果数据源本身可能为空,一上来就执行一次处理逻辑就不合适,这种场景应该用while先判断。简单记:"至少要做一次"是你真正需要的结果时才用 do-while;如果"能不做就不做"更好,就用 while 或 for。这个判断标准不是死规则,但足够帮助初学者在具体问题里做选择。
4. for循环:初始化、条件、更新三件套的打包管理
for循环是三种循环里用得最频繁、也是初学者最喜欢的一个。原因很直接:它把循环所需要的"初始化""条件判断""更新操作"三件事全部写在了一行for(...)里,结构一目了然,不会像while那样容易漏写更新语句。
4.1 三个表达式的逐一拆解
语法是:
for (表达式1; 表达式2; 表达式3) { // 循环体 }三个表达式分工明确:
- 表达式1(初始化):循环开始前执行一次。通常用来定义并初始化循环变量,比如
int i = 0。 - 表达式2(循环条件):每次循环体执行前判断。为真就继续执行循环体,为假就退出循环。
- 表达式3(更新):每次循环体执行完后执行一次。通常用来修改循环变量,比如
i++。
它的完整执行顺序是:表达式1 → 判断表达式2 → 循环体 → 表达式3 → 再判断表达式2 → ……,你注意,判断总是发生在循环体之前,所以for和while一样,循环体最少执行 0 次。
用for重写前面的 1 到 100 累加求和:
int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; } printf("sum = %d\n", sum);相比while版本,初始化int i = 1和更新i++都被收纳进了for的头部,循环体里只剩纯逻辑sum += i,阅读时一眼就能看出"这个循环从 1 开始,到 100 结束,每次加 1"。
4.2 for 和 while 的等价转换:看懂一个就懂两个
很多人刚学时有个疑问:既然for这么方便,还要while干嘛?实际上两者完全可以互相转换:
// for 版 for (int i = 1; i <= 100; i++) { sum += i; } // 等价的 while 版 int i = 1; while (i <= 100) { sum += i; i++; }看出对应关系了吗:for的表达式1就是while前的初始化,表达式2就是while的条件,表达式3就是while循环体末尾的更新。所以for并没有发明新东西,它只是把这三件事"打包"放在了一行,强迫你把它们写清楚,减少遗漏。这也是为什么我建议初学者优先从 for 入手,因为它的结构化设计天然引导你把循环的三个关键要素一次写齐。
4.3 省略写法、逗号表达式和常见的"高级用法"
for的三个表达式都可以省略,但两个分号必须保留。比如:
int i = 1; for (; i <= 100; i++) { // 省略初始化:因为 i 在外面已经定义好了 sum += i; }甚至可以把更新语句放进循环体,写成for (;;)——这就是一个经典的隐形死循环"写法",配合break使用(下一节会讲)。但要提醒大家,省略写法虽然语法合法,可读性往往变差,入门阶段不建议刻意追求"写得短",代码首先是写给人的,其次才是给机器跑的。
逗号表达式也常出现在for头部,一次处理多个变量:
for (int i = 0, j = 10; i < j; i++, j--) { printf("i = %d, j = %d\n", i, j); }这个循环同时用两个变量从两端往中间逼近,在很多算法题里都会碰到。记住一个原则:for头部里放"和循环推进相关的操作",别把业务逻辑塞进去,否则代码会变得非常难读。
5. break和continue:一个负责急刹车,一个负责跳台阶
学完三种循环,接下来就是循环控制结构里的两大"辅助开关":break和continue。它们不创建新的循环,而是改变循环的执行流。很多初学者把这两个搞混,其实区分起来特别简单:break是整个循环直接结束(急刹车),continue是只跳过本次循环的剩余部分、进入下一次迭代(跳台阶)。
5.1 break:立即跳出当前循环
break在循环体中出现时,程序会立刻终止整个当前循环,跳到循环后面的第一句代码继续执行。典型的应用场景是"提前结束":循环明明还没跑完,但目标已经达成了,就没必要继续空转。
比如判断某个数是不是素数,不需要真的把 2 到 n-1 全部试一遍,只要找到一个能整除的因子就可以立即下结论:
int n = 37; int isPrime = 1; // 假设是素数 for (int i = 2; i < n; i++) { if (n % i == 0) { isPrime = 0; break; // 找到因子,立刻退出循环 } } if (isPrime) { printf("%d 是素数\n", n); } else { printf("%d 不是素数\n", n); }这个例子里的break价值非常明显:如果 n 很大,你提前在第 2 步就找到了因子,后面成百上千次判断全都可以省掉。
5.2 continue:跳过本次循环的剩余部分,但循环要继续
continue则温和得多。它遇到后直接跳到本次循环体的末尾,不执行循环体里continue后面的语句,但会照常进入下一次迭代(先执行for的表达式3,再判断循环条件)。
一个典型场景是"过滤":输出 1 到 20 之间所有不能被 3 整除的数,遇到能被 3 整除的就跳过,不输出它:
for (int i = 1; i <= 20; i++) { if (i % 3 == 0) { continue; // 跳过本次输出 } printf("%d ", i); }执行结果会是:1 2 4 5 7 8 10 11 13 14 16 17 19 20。continue在这里相当于流水线上的"剔除不合格品":不合格的直接推走,但整条流水线照常运转。这和break的"整个车间停工"是完全不同的逻辑。
5.3 嵌套循环里的 break 和 continue:只对最近的一层生效
这是初学者最容易搞蒙的地方。break和continue都只作用于它们所在的那一层循环,不会跳出外层。比如:
for (int i = 1; i <= 3; i++) { for (int j = 1; j <= 3; j++) { if (j == 2) { break; // 只会跳出内层 for,外层 i 的循环不受影响 } printf("i=%d, j=%d\n", i, j); } }这段代码会输出三行:i=1, j=1、i=2, j=1、i=3, j=1。每次内层循环到j==2就中断,重新回到外层。很多人在这里以为break会把内外两层一起干掉,结果 debug 半天发现外层照常跑。如果你确实需要跳出多层循环,传统做法是使用一个标志变量,在内外层同时判断;更后面也可以学goto,但入门阶段不建议优先用goto,先学会标志变量法更稳妥:
int found = 0; for (int i = 1; i <= 3 && !found; i++) { for (int j = 1; j <= 3; j++) { if (j == 2) { found = 1; break; } } }把标志变量写进外层循环的条件里,break内层退出后,外层条件也变成假,循环就自然结束了。
6. 嵌套循环:九九乘法表背后的两层思维
循环里面再套循环,就是嵌套循环。这是第一次让程序的"维度"从一维变成二维的地方,也是很多初学者的"劝退点"。其实嵌套循环一点都不玄乎,核心就一句话:**外层循环每执行一次,内层循环就完整走一遍。**把握住这个执行顺序,嵌套循环就不再可怕。
6.1 从九九乘法表看嵌套循环的规律
九九乘法表是理解嵌套循环最经典的入门案例。先确定规则:一共 9 行,第 i 行有 i 个式子,每个式子是列号*行号=乘积的格式。用代码实现:
for (int i = 1; i <= 9; i++) { // 外层循环:控制行 for (int j = 1; j <= i; j++) { // 内层循环:控制列,列号不超过行号 printf("%d*%d=%-2d ", j, i, i * j); } printf("\n"); // 每行结束后换行 }运行效果就是标准的左下三角九九乘法表。拆解一下执行流程:当外层i = 1时,内层j从 1 循环到 1,输出 1 个式子后换行;当外层i = 2时,内层j从 1 循环到 2,输出 2 个式子后换行……这样一层一层推进,9 行全部打印完毕。我在黑板上讲这个例子时,会在纸上一行一行模拟执行,把i、j和输出的变化全部列出来,跑完 3 行基本就通了。
6.2 二维遍历的思维顺序:外层管行、内层管列
九九乘法表只是特例,嵌套循环更普遍的应用是处理二维数据,比如一个 3 行 4 列的矩阵,要把每个元素都遍历一遍。这时候外层循环负责"行",内层循环负责"列",是绝大多数二维遍历的固定套路:
int rows = 3, cols = 4; for (int i = 0; i < rows; i++) { // 外层:每一行 for (int j = 0; j < cols; j++) { // 内层:该行的每一列 printf("(%d,%d) ", i, j); } printf("\n"); }你可以在纸上把输出模拟一遍,会发现它严格按照"第 0 行的所有列、第 1 行的所有列、第 2 行的所有列"这个顺序推进。理解了这个顺序,不管是处理图像像素、二维数组还是后面学的矩阵运算,你的脑子都会有清晰的轮廓。
6.3 顺手聊聊嵌套循环的性能意识
嵌套循环看起来简单,但隐藏着一个性能问题:循环层数越多,执行次数增长越恐怖。假设外层 100 次、内层 100 次,循环体就要执行 10000 次;如果再来一层,就是 100 万次。入门阶段写个小程序感觉不出来,但等以后处理真实数据、甚至刷算法题时,这种"循环爆炸"直接关系到程序能不能在合理时间内跑完。所以从学嵌套循环开始,就要养成估算循环次数的习惯,看到for (i = 0; i < n; i++) for (j = 0; j < n; j++)能脱口而出这是 $O(n^2)$ 的操作。不求现在就算复杂度,但心里要有个"这个循环可能要跑多久"的尺子。
6.4 一个小综合练习:输出质数表
把前面所有知识串起来,我自己带学生时最喜欢布置的一个小练习是:输出 2 到 50 之间的所有质数,每行 5 个。这个题目看起来简单,其实要用到嵌套循环(外层遍历数字,内层判断是否为质数)、break(找到一个因子就提前退出)、计数器控制换行。整体写法参考:
#include <stdio.h> int main() { int count = 0; for (int num = 2; num <= 50; num++) { int isPrime = 1; for (int i = 2; i < num; i++) { if (num % i == 0) { isPrime = 0; break; } } if (isPrime) { printf("%-4d", num); count++; if (count % 5 == 0) { printf("\n"); } } } return 0; }练这个题的目的不是炫技,而是让循环思维真正"落地":你能把一个数一个数地检查转化为两层循环的自动推进,说明你已经从"我手动做一遍"过渡到了"我写规则让机器做一遍",这一章的思维换挡就算真正完成了。
7. 循环实战里最容易翻车的地方,我帮你排过雷
循环的语法本身不难,但实际写起来,有几个错误几乎每个初学者都会遇到,而且它们往往编译能过、也不会立刻报错,只在运行时给你一个奇怪的结果。这一节我集中排一下最常见的雷,顺便把调试思路一起给出来。
7.1 分号、空循环体、更新遗漏:三个"看不见"的错误
第一个雷:while 后面多写一个分号,循环体变成空语句。比如:
int i = 1; while (i <= 100); { sum += i; i++; }这里的while (i <= 100);因为分号结束,循环体是那条空语句,而下面的大括号块跟循环完全没关系,只会执行一遍。同时i永远不会更新,程序直接死循环。这种错特别隐蔽,因为编译不会报错,肉眼不仔细看也容易忽略。排查方法很简单:看到while或for那一行结尾,心里默念一下"分号到底属于谁"。
第二个雷:循环体不小心为空或者只执行一次。比如for后面紧跟一个分号:
for (int i = 1; i <= 10; i++); printf("%d", i);你以为会输出 1 到 10,实际for自己空转 10 遍,然后printf只执行一次。这些都是"分号放错位置"惹的祸。
第三个雷:忘记更新循环变量。前面提过,while里容易漏写i++,for里相对不容易漏,因为更新被固定写在了头部。这也是我推荐入门阶段多用for的原因之一——它的语法结构天然地在提醒你:初始化、条件、更新,三件事一个都不能少。
7.2 循环变量的作用域与生命周期:for 循环头的 i 到底能不能在外面用
很多初学者会遇到这个困惑:在for循环头里定义了int i,循环结束后在循环外面想再打印i的值,结果一编译就报错"未定义标识符 i"。原因在于,C99 标准之后,在for的初始化表达式里定义的变量,其作用域仅限于这个 for 循环,循环一结束,i就"消亡"了。需要在循环外面使用循环变量的最终值时,就必须在循环外先定义:
int i; for (i = 1; i <= 10; i++) { // ... } printf("循环结束后 i = %d\n", i); // i 仍然可以使用这个细节看起来小,但涉及变量的作用域和生命周期,是后续学函数、指针时反复要用的基础概念,建议从现在就建立清晰认识。
7.3 调试循环的三种实用手段
遇到循环行为不符合预期,别干瞪眼,按我的习惯依次用下面三招:
第一招:打印中间值。在循环体里加printf,把关键变量每一轮的值打出来。这是最朴素也最有效的办法。我曾经带过一个学生,累加求和结果总是不对,加了打印才发现他每次循环都忘了把sum重新清零,导致多组数据累加时互相污染。
第二招:缩小数据规模。如果你要处理 1 到 1000 的数据,先改成 1 到 5 跑一遍,把执行轨迹在纸上写出来,和代码逻辑对一遍。数据一大,眼就花了;规模一小,规律立刻清晰。
第三招:条件断点。用 IDE 的调试器,在循环体里设断点,并且设置条件(比如i == 50时才停下来)。这样不用循环到几百次断点才拦一次,能精准定位到出问题的那一轮。入门阶段学一下这个,后面排错效率能提升很多。
这三招没有高深技术,但非常管用。循环报错不丢人,丢人的是看到结果不对却只会重启程序,那永远找不到问题在哪。
写到这儿,关于循环控制结构该讲的核心内容基本都覆盖了。我最后再啰嗦一句个人体会:循环这一章真不用急着求快,拿一张纸,把while、do-while、for各写两三道题,手动模拟执行,画一画变量变化表,比盯着屏幕看十遍视频都管用。等循环真正"长"在脑子里了,后面学数组、指针、函数,你会发现很多难点其实都是在循环这一关埋下的伏笔,而现在,地基已经打好了。