
1. 循环结构入门为什么你的程序需要“重复”的能力写程序这件事做了几年之后回头看,最大的感悟之一是程序里真正难的不是那些只执行一次的代码而是那些需要反复执行、反复判断、反复处理的逻辑。你让计算机帮你算100个数的平均值、打印一张九九乘法表、或者从一堆数据里找出最大值本质上都是在做同一件事——重复。而C语言里承担“重复”这个核心工作的就是循环结构。我刚带新人那会儿发现很多初学者对循环的理解停留在“背语法”的阶段知道for循环要写三个表达式知道while后面要跟条件但真到写程序的时候——比如让用户输入一串数字直到输入0为止——就开始卡壳了。原因很简单没搞清楚循环的本质是什么也没弄明白for和do-while到底有什么区别。这篇文章我把C语言里两个最常用的循环结构——for循环和do-while循环——从头到尾拆一遍。不光是语法怎么写更重要的是每种循环背后的适用场景、常见误区、以及我在实际项目中总结出来的选型经验。适合正在学C语言基础的学生、准备计算机二级考试的考生、以及自学编程想打好底子的朋友。看完之后你会发现循环真的不难难的是你之前可能一直没找到对的切入点。先说一个最核心的观点循环就是让一段代码在满足条件的情况下重复执行。就这么简单。但“满足条件”这四个字里藏着无数细节——条件在什么时候检查至少执行一次还是可能一次都不执行循环变量怎么变化这些细节直接决定了你该选for还是do-while。2. for循环深度拆解最常用的循环结构没有之一2.1 for循环的语法本质三个表达式各司其职for循环在C语言里的标准写法是这样的for (初始化表达式; 循环条件表达式; 循环变量更新表达式) { // 循环体 }很多教材喜欢画流程图来讲解但我觉得用生活化的类比更容易理解for循环就像你制定的一份跑步计划。初始化表达式是“我从第1圈开始跑”循环条件是“只要还没跑满10圈就一直跑”循环变量更新是“每跑完一圈计数加1”。三个部分各管一段共同控制循环的推进节奏。这里要特别强调一个很多人忽略的细节分号是必须的逗号不是。我在代码评审的时候经常看到新手把for循环写成for (i 0, i 10, i)编译直接报错。记住for后面的括号里一定是用两个分号隔开三个部分三个部分都可以省略但两个分号一个都不能少——哪怕你写for(;;)这种死循环分号也得给我留着。第二个经常被忽略的细节是循环变量更新表达式不一定是i。你可以写i 2跳着走也可以写i--倒着走甚至可以写i i * 2。关键是你要清楚这个更新操作会让循环变量往哪个方向变以及它跟循环条件之间是什么关系。我举个例子下面的代码用for循环计算1到100之间所有奇数的和#include stdio.h int main() { int sum 0; for (int i 1; i 100; i 2) { sum i; } printf(1到100所有奇数之和 %d\n, sum); return 0; }这里初始化表达式是int i 1循环条件是i 100更新是i 2。三个表达式配合起来i的取值序列就是1、3、5、7……99到101的时候条件不成立了循环结束。如果你把条件改成i 100结果一样因为i根本不会取到100如果你把更新改成i那就变成计算1到100所有整数之和了。循环条件的边界值是新手出错率最高的地方之一后面我专门讲。2.2 for循环的三种典型场景计数、遍历、累加实际开发里for循环的应用场景基本可以归为三类理解了这三类场景你就知道什么时候该用for。第一类是固定次数的重复操作比如打印10行输出、让一个动画播放20帧、生成一个长度为n的数组。这类场景的核心特征是你事先就知道循环要执行多少次。选型逻辑很简单——知道次数就用for。第二类是遍历数据结构。虽然C语言没有Python那种for x in list的语法但通过下标访问数组就是for循环的主场。比如遍历一个字符数组找某个字符、遍历整型数组求平均值都是标准的for (int i 0; i n; i)结构。这个写法你以后学指针、学链表、学排序算法的时候还会反复见到务必形成肌肉记忆。第三类是累加和累乘。统计总和、计算阶乘、求平均值、连乘算组合数——这类问题天然适合用for循环配合一个“累加器”变量解决。累加器的核心思想是定义一个变量保存中间结果每循环一次就把新的值叠加上去。看一个阶乘的经典例子#include stdio.h int main() { int n 6; int result 1; for (int i 2; i n; i) { result * i; } printf(%d的阶乘 %d\n, n, result); return 0; }注意我这里的累加器变量result初始化为1而不是0——因为乘法跟加法不一样乘法的单位元是1加法的单位元才是0。这个细节看起来微不足道但很多人求阶乘习惯性地把result初始化为0结果算出来全是0然后一脸蒙圈。写代码之前先问自己一句我这个累加/累乘操作的“起点”应该是什么2.3 for循环的嵌套九九乘法表的实战拆解for循环嵌套是C语言里绕不过去的一道坎而九九乘法表就是最经典的练习题目。我记得大学时候老师布置这道题班上有一半人写不出来。其实嵌套循环的核心逻辑就一句话外层循环控制行内层循环控制列。九九乘法表的标准代码如下#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; }拆解一下外层循环i从1到9代表乘法表的第几行内层循环j从1到i代表每一行打印多少个算式。为什么内层循环条件是j i而不是j 9因为九九乘法表是个三角形第一行只有1个算式第二行2个第九行9个。内层循环的次数依赖于外层循环的当前值这是嵌套循环最核心的联动关系。%-2d这个格式控制符也值得说一下-表示左对齐2表示占两个字符宽度。不加这个的话乘法表的列会参差不齐打印出来歪歪扭扭的。C语言的printf格式控制非常灵活学循环的时候顺便把格式控制练熟后面打印各种表格数据都用得上。嵌套循环还有一个经典应用是冒泡排序——这个题目正好在你的热搜词里。冒泡排序的外层循环控制排序趟数内层循环控制每一趟的比较次数#include stdio.h void bubbleSort(int arr[], int n) { for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } }这里n - 1 - i就是内外层循环联动的体现第一趟需要比较n-1次第二趟n-2次依次递减。如果你把内层循环的条件写成j n - 1程序不会崩但会多做很多无意义的比较白浪费CPU。说到排序可能有人会想到“循环不变量”——这是《算法导论》里证明算法正确性的工具核心思想是在循环的每一次迭代之前和之后某个性质始终成立。比如冒泡排序里每次外层循环结束后数组末尾的i个元素就已经是排好序的最大i个。理解这个概念对以后进阶算法非常有帮助这也是CLRS选择排序那题考察的核心。3. do-while循环被低估的“至少执行一次”利器3.1 do-while的语法结构先执行后判断说完了for循环再来看看do-while。它的语法长这样do { // 循环体 } while (循环条件表达式);这里有个特别容易踩的坑do-while的while后面是有分号的。很多初学者写完do {...} while (x 10)忘了加分号编译直接报错。这个分号意味着do-while语句的结束你不能省略。跟for循环相比do-while最本质的区别是它先执行一次循环体再判断条件。这意味着什么呢意味着do-while的循环体至少会执行一次而for和while可能一次都不执行——只要最开始条件就不成立。用一个生活化的类比来解释for循环像是“先看天气预报再决定要不要出门跑步”do-while循环则像是“先跑出去再说跑完一步发现下雨了再回来”——反正你最少得迈出那一步。这个特性在什么场景下有价值呢我给你举三个最典型的。场景一菜单选择。很多管理系统的交互逻辑是先弹出菜单让用户选选完执行功能然后再次弹出菜单直到用户选择“退出”。这个流程里菜单至少要显示一次吧总不能让用户一进程序就面对一个空屏。用do-while写这个逻辑再自然不过。场景二密码验证。要求用户输入密码如果不对就重新输入直到输入正确为止。用户第一次输入密码这个动作是必须发生的不可能先判断“你输入得对不对”再让你输入——你还没输呢怎么判断所以这也是do-while的标准应用场景。场景三数据校验。从标准输入读取一个正整数如果用户输成负数或零就提示重输。同样的道理读取操作本身至少要做一次。我写一个密码验证的完整代码这个代码在很多C语言课程设计里都会用到#include stdio.h #include string.h int main() { char password[20]; const char *correct 123456; do { printf(请输入密码); scanf(%s, password); if (strcmp(password, correct) ! 0) { printf(密码错误请重新输入\n); } } while (strcmp(password, correct) ! 0); printf(登录成功\n); return 0; }这个程序里strcmp(password, correct) ! 0就是循环条件字符串比较相等返回0不相等返回非0。只要密码不对循环就继续输对了循环结束程序往下走。仔细体会一下如果这里用for或while写你得先把输入的动作放在循环外面做一次然后再在循环体里做第二次——不仅代码变长逻辑也别扭。do-while天生就是为这种“先做后判断”的场景设计的。3.2 do-while与while的经典对比差一次还是差很多为了让你更直观地理解do-while和while的区别我做一个对比实验。同样一个简单的累加逻辑分别用while和do-while实现// while版本一开始条件就不成立 int i 100; int sum 0; while (i 10) { sum i; i; } // 结果sum 0循环体一次都没执行 // do-while版本同样的初始条件 int i 100; int sum 0; do { sum i; i; } while (i 10); // 结果sum 100循环体执行了一次看到没有同样的条件i 10和同样的初始值i 100两个循环的结果完全不同。while是“零容忍”条件不满足就干脆不干do-while是“先斩后奏”不管条件如何先把第一次干了再说。这个对比理解透了你就明白怎么写代码才能避免bug如果你确定循环体至少要执行一次用do-while否则用while或for。另外还有一层关系容易被忽视do-while循环体内对条件的修改会影响下一次迭代的判断。以上面的密码验证为例strcmp(password, correct)是在循环体末尾判断的此时用户已经重新输入过密码了。如果你把这个判断放到循环体中间那后面的代码就可能在条件已不满足的情况下继续执行——这就是逻辑顺序的微妙之处。写do-while循环的时候心里始终要有这根弦循环体里所有语句执行完才会去检查条件。4. 循环选型实战指南到底该用for、while还是do-while4.1 三个维度的决策标准照着选就对了很多初学者最喜欢问的问题就是“老师这题到底该用for还是while”我的回答通常是先从需求出发不要从语法出发。具体来说做决定的时候看三个维度。维度一是否知道循环次数。知道确切次数——比如“打印1到100”“遍历长度为n的数组”这种——首选for循环。不知道次数要靠某个条件来终止——比如“直到用户输入0”“直到文件读到末尾”这种——用while或do-while。维度二循环体是否必须至少执行一次。如果答案是“必须”比如菜单至少显示一次、密码至少验证一次——你就别无选择只能用do-while。在C语言里这个逻辑没有其他更优雅的写法硬要用for或while写的话你得额外加标志变量或者把第一次执行单独拎出来代码会变得很别扭。维度三循环变量的使用方式。如果循环变量只在计数时使用循环结束后你完全不关心它的值——那for循环是最标准的写法因为初始化、条件、更新都集中在括号里可读性最好。如果你需要在循环结束后拿到循环变量的最终值用while或者for都可以但要注意C89标准里for循环内部声明的变量在循环外不可见的问题——不过现代C编译器一般都支持C99或更高标准了这个问题影响不大。我把选型逻辑整理成一个速查表方便你查阅判断依据推荐选择说明知道确切循环次数for初始化、条件、更新集中管理不知道次数靠条件终止while条件先行先判断后执行循环体必须至少执行一次do-while先执行后判断保证至少一次遍历数组/字符串for配合下标操作最自然菜单交互/输入校验do-while交互动作天然需要先做一次死循环服务器监听等while(1) / for(;;)两者等价看团队风格最后多说一句while(1)和for(;;)在C语言里都可以实现死循环而且编译器通常会生成相同的机器码性能没有差别。选哪个纯看个人习惯和团队规范。但死循环内部一定要有退出机制——break或者return否则就真的“死”在那里了。4.2 一道训练题用三种循环实现同一个需求检验你有没有真正理解循环选型最好的方式是同一个需求用三种循环各写一遍。我用一个经典题目演示计算1到n之间所有能被3整除的数的和。先来for版本这个最简单直接#include stdio.h int main() { int n 100; int sum 0; for (int i 1; i n; i) { if (i % 3 0) { sum i; } } printf(1到%d之间能被3整除的数之和 %d\n, n, sum); return 0; }再来while版本注意while版本里循环变量i必须定义在循环外面而且要在循环体内手动更新#include stdio.h int main() { int n 100; int sum 0; int i 1; while (i n) { if (i % 3 0) { sum i; } i; } printf(1到%d之间能被3整除的数之和 %d\n, n, sum); return 0; }最后是do-while版本。说实话这个场景用do-while并不合适——因为n如果给一个0或负数循环体还是会执行一次多做一次无意义的运算。我写在这里纯粹是为了展示三种写法的区别也顺带让你看看do-while在这个场景下的“笨拙”#include stdio.h int main() { int n 100; int sum 0; int i 1; do { if (i % 3 0) { sum i; } i; } while (i n); printf(1到%d之间能被3整除的数之和 %d\n, n, sum); return 0; }对比三份代码你会发现for版本把所有跟循环控制相关的语句收拢在一起阅读的时候一眼就能看清循环的“全貌”while版本的控制语句分散在循环外初始化和循环体末尾更新阅读时需要上下扫视do-while版本在n可能不满足条件的情况下会多做一次无用功。这就是为什么我说“知道次数用for”的原因——不是别的循环不能写而是for的可读性和安全性最好。4.3 初学者最容易犯的5个循环错误我自己全踩过这部分是我最想跟你分享的。我刚开始学C语言的时候犯过的错误带项目时看到新人反复出现的错误我一起整理出来你对照着自查一遍。错误一循环条件边界搞错。想遍历1到10写了i 10结果少算了一个10。想遍历0到9写了i 9没毛病但写成i 10就多了一次越界访问——数组下标越界在C语言里是个大坑它不一定会立刻崩溃但可能悄悄破坏内存里的其他数据。经验法则数组的下标从0开始遍历时条件写成i n遍历到n-1为止如果从1开始计数条件写成i n。错误二死循环不知道怎么退出来。最常见的死循环写法是int i 0; while (i 10) { printf(%d\n, i); // 忘了写 i }输出0、0、0、0……无限循环。屏幕上的数字疯狂翻滚程序根本停不下来。遇到死循环CtrlC强制终止进程然后检查循环变量有没有更新。我在给新人讲这个错误的时候总是用一句话总结循环要前进变量要更新两者缺一不可。错误三循环体内修改循环变量导致逻辑混乱。有些新手会在循环体里对循环变量做意外修改比如for (int i 0; i 10; i) { printf(%d\n, i); i--; // 这行会把循环变成死循环 }每次循环结束i先减1再被for的更新表达式加1结果i原地踏步死循环形成。循环变量一般只在for的更新表达式中修改循环体内部尽量不要动它除非你非常清楚自己在做什么。错误四分号位置不对逻辑被拆分。看这段代码for (int i 0; i 10; i); { printf(%d\n, i); }for后面多了一个分号循环体变成空语句程序会连续执行10次“什么都不做”然后printf打印一次i的值。而且i的作用域已经结束了这段代码可能编译都过不了。记住for循环头后面直接跟循环体不要跟分号。如果循环体是空语句那也要显式写成for(...) ;让别人能看懂你的意图而不是错误地把分号放在那里。错误五逻辑与/或条件写反。比如想“循环直到输入的数大于0且小于100”写成while (num 0 || num 100)——这实际上是“只要输入的数不合法就继续循环”是对的。但如果写成while (num 0 num 100)意思就反了输入的数是合法的才继续循环。循环条件的逻辑跟你的语言描述是反着的你要的是“继续循环的条件”而不是“退出循环的条件”——这两个语义经常混淆每次写while/do-while条件的时候多读两遍确认一下。提示排查循环逻辑问题的时候可以在循环体最前面加一句printf(当前i的值 %d\n, i)打印日志。循环出了bug不要靠脑子硬猜让程序自己告诉你它走到哪一步了。这是所有排查手段里最朴素也最有效的一种。5. 经典循环练习题精讲从PTA到课程设计全覆盖5.1 求素数循环配标志变量的标准打法素数问题是循环结构的经典考题PTA平台和很多学校的OJ上都有。判断一个数n是不是素数思路是从2到sqrt(n)逐个尝试整除只要有一个能整除就说明不是素数。完整代码如下#include stdio.h #include math.h int isPrime(int num) { if (num 1) return 0; for (int i 2; i sqrt(num); i) { if (num % i 0) { return 0; } } return 1; } int main() { for (int i 1; i 100; i) { if (isPrime(i)) { printf(%d , i); } } printf(\n); return 0; }这里有两个技巧值得说。第一判断条件只要循环到sqrt(num)就行不需要到num/2更不需要到num-1。一个合数n必然有一个小于等于sqrt(n)的因子这是数论的基础结论能省掉大量的无意义循环。第二标志变量的思想——虽然我这个写法用了提前return但如果你不想拆函数可以定义一个int flag 1一旦发现能整除就置为0循环结束后检查flag的值。这种“标志变量”配合循环的用法在C语言里非常常见比如查找数组中是否存在某个元素、检查字符串是否是回文。你的热词里有一条“用Python写出用循环找出1~100以内的所有素数并计算总和”——其实C语言的解法逻辑跟Python完全一样区别只在语法。这个问题用C实现的话只需在外层循环里累加素数即可。学会一种语言里的循环逻辑迁移到另一种语言是很快的因为循环的核心思想在所有语言里都是共通的。5.2 字符串逆序循环和下标切换的艺术字符串逆序是另一个高频考题PTA上有好几个变体。核心思路是定义两个指针或两个下标一个指向字符串头一个指向字符串尾交换它们指向的字符然后头指针后移、尾指针前移直到两者相遇。我用一个纯下标的写法#include stdio.h #include string.h void reverseString(char str[]) { int len strlen(str); for (int i 0, j len - 1; i j; i, j--) { char temp str[i]; str[i] str[j]; str[j] temp; } } int main() { char str[100]; printf(请输入字符串); scanf(%s, str); reverseString(str); printf(逆序后%s\n, str); return 0; }注意for循环的初始化表达式里我同时定义了两个变量i和j——C语言的for循环初始化表达式可以用逗号分隔多个操作这在涉及“双向处理”的场景里非常好用。同时更新表达式也是i, j--一次循环同时移动两个下标。循环条件i j意味着当i和j相遇或交错时循环结束——对于一个长度为偶数的字符串i和j会交错长度为奇数时i和j会在正中间的那个字符上相遇。这两种情况都能正确处理。这个题目背后是“双指针”思想的雏形。以后你学链表反转、学快速排序、学滑动窗口算法都会反复用到类似的双端处理逻辑。从一个简单的字符串逆序里提早建立“双指针”的意识对你后面进阶的帮助是非常大的。5.3 循环队列与进阶从PTA题到实际项目热词里有一条关于“循环队列”的内容用数组q[m]存放循环队列元素用rear和length指示队尾和长度。很多初学者看到循环队列就头大其实它就是用数组模拟一个首尾相接的队列结构核心操作全靠取模运算%来实现“绕圈”的效果。循环队列入队的核心操作是rear (rear 1) % m判断队列满的条件是length m。这里面用到了取模运算——取模运算和循环是天生的搭档因为它会把一个不断增长的数值“圈”在固定的范围内。这一点在循环队列、循环缓冲区、哈希表等数据结构里都用得非常频繁。为什么我在这里提循环队列因为你的热词里出现了“PTA乙级”“霍格沃茨找零钱”这类题目——说实话那题本身不涉及循环队列但PTA题库是分梯度的你练完基础的循环结构接下来会接触数组和数据结构相关的题目循环队列就是绕不开的一关。从for循环到循环队列中间其实就是“用循环思想操作数组下标”的进阶难度是渐进的不用怕。另外循环队列里那个% m的操作本质上是在“边界处折返”。这跟接下来要讲的循环嵌套里的“周期性问题”是一家人。写程序时间长了你会慢慢发现很多看似不相关的技术点底层的思想是同源的。6. 循环的运行细节与性能优化为什么你的循环比别人慢6.1 循环条件里的函数调用隐藏的性能杀手有一类性能问题平时写小程序根本看不出来但一旦处理的数据量上去了差距就非常明显。看这段代码for (int i 0; i strlen(str); i) { // 处理str[i] }问题出在strlen(str)被放在了循环条件里。如果字符串长度是1000那么每循环一次都要调用一次strlen而strlen本身又是遍历字符串求长度的——这意味着你的程序在循环体之外又额外做了1000次完整的字符串扫描。时间复杂度从O(n)悄悄变成了O(n²)数据量一大直接卡死。正确的写法是把长度提前存到变量里int len strlen(str); for (int i 0; i len; i) { // 处理str[i] }这个优化做一次就能感受到差距。我跟你说一个实际场景之前处理一个日志解析程序日志文件有几万行每行要遍历一遍提取关键信息。原来代码在循环条件里调了一个字符串处理函数处理整个文件要6秒多。改成提前计算长度之后直接降到1秒以内。循环体是性能的放大器——循环体里任何一点“多余的操作”都会被放大n倍。6.2 循环展开与编译器优化适度了解即可初学者可能听过“循环展开”这个优化技巧——把循环体里的内容复制好几份减少循环次数的开销。比如// 原始版本 for (int i 0; i 1000; i) { a[i] i * 2; } // 展开2次 for (int i 0; i 1000; i 2) { a[i] i * 2; a[i 1] (i 1) * 2; }这样做的理由是每次循环都有判断和跳转的开销展开后循环次数减半判断次数也减半。但现在编译器都很聪明了开启-O2优化选项后编译器会自动做循环展开不需要你手动干预。我的建议是初学阶段完全不用管这个把循环写清楚、写正确才是第一位的。等以后真正遇到性能瓶颈再用profiler工具定位问题那时候再考虑手写优化也不迟。还有一点值得注意循环体内的变量声明和计算尽量提取到循环外。比如循环体里用到了某个不会变化的值提前算好再进循环。这也是“消除循环不变量”的思想跟编译器优化的方向一致有助于编译器生成更好的代码。6.3 注意循环后的分号陷阱for循环的空循环体这个问题在2.3节提了一句这里展开说。看这段代码#include stdio.h int main() { int sum 0; for (int i 1; i 100; i); { sum i; } printf(sum %d\n, sum); return 0; }如果你把这段代码丢到编译器里大概率会得到一个“i未定义”的编译错误或者sum的值完全不对。原因就是for循环那一行末尾多了一个分号导致循环体是一个空语句循环执行100次什么都没干后面那个花括号代码块中的i在循环外早就不存在了。我见过不止一个新手在for循环后随手打了个分号排查了半天才发现问题。怎么避免养成一个习惯写完for循环之后下一行如果是{开头就回头检查一遍for那一行有没有误加分号。另外代码风格上建议始终为for循环写花括号哪怕是单行循环体。虽然C语言允许不写花括号但省略花括号会让“悬空分号”这类bug更加难以察觉。7. 常见问题排查与调试技巧实录7.1 死循环的四种典型形态每种都能一眼识别死循环是循环结构里最让人头疼的问题但好消息是常见的死循环就那几种形态记住了就能快速识别。形态一循环变量不更新。前面说过的while(i 10)里面忘了i。特征输出重复的值程序卡死。形态二循环条件恒为真。比如while (1)、for(;;)但内部没有break。特征程序一直运行不退出。形态三浮点数精度问题。看这个代码float i 0.0; while (i ! 1.0) { i 0.1; }你可能会以为i会精确地从0.0加到1.0然后退出但实际上浮点数在计算机里是二进制表示的0.1无法被精确表示连续累加后i可能变成0.9999999或者1.0000001永远不等于1.0死循环。特征循环次数比预期多很多或者永远不结束。经验法则循环变量永远不要用浮点数做相等比较要用范围判断i 0.9999 i 1.0001或者直接用整数计数。形态四条件的副作用导致反转。循环条件里意外修改了变量。比如int i 0; while (i 10) { printf(%d\n, i); if (i 5) { i 0; // i被重置永远到不了10 } i; }特征循环到某一轮突然“回到原点”反复执行同一段逻辑。排查死循环最有效的方法是打印日志。在循环体的第一行打印循环变量的当前值观察它的变化轨迹。你能看到它是一步步走到原本不该走的位置还是一直在原地踏步还是一开始就跳进了条件陷阱。不要靠猜靠看。7.2 循环嵌套的常见坑内层循环变量与外层循环变量混淆嵌套循环的bug主要集中在内外层变量混用上。最常见的错误是for (int i 0; i 5; i) { for (int j 0; j 5; i) { // 误写成i printf((i%d, j%d)\n, i, j); } }内层循环错误地更新了外层循环变量i结果内层循环的j永远为0而i一路猛涨到5外层循环立刻退出。程序不会报错但输出完全不符合预期。写嵌套循环的时候内层循环的更新表达式一定要检查清楚操作的是哪个变量。另一个常见坑是break的语义。break语句在嵌套循环中只能跳出最近的一层循环。如果你想一次性跳出所有嵌套循环要么用标志变量配合每层循环的条件判断要么直接把代码封装到函数里用return出去。比如int found 0; for (int i 0; i n !found; i) { for (int j 0; j m; j) { if (arr[i][j] target) { found 1; break; // 跳出内层循环外层循环条件发现found为1也会退出 } } }这个模式值得背下来用标志变量配合循环条件实现“多重循环提前退出”。比goto语句优雅得多也更容易理解。7.3 用printf定位循环问题一个最简单的调试手段最后分享一个我一直在用的调试方法。我知道现在有很多IDE都带断点调试功能但对于循环这类逻辑问题我依然觉得printf大法是最直接有效的。在循环体里适当地加打印语句比如for (int i 0; i 10; i) { printf([调试] i %d\n, i); // ... 其他代码 }输出结果会让你瞬间明白循环变量的取值轨迹。观察变量是否如你预期的那样变化——是跳过了某个值还是到了某个值之后没有按预期退出或者是条件判断的方向反了调试完记得把打印语句删掉或者注释掉不然提交OJ的时候会因为多余输出被判“格式错误”。另外还有一个技巧适合排查循环里数组越界的问题在访问数组元素之前打印下标值。比如for (int i 0; i n; i) { printf([调试] 访问 arr[%d]\n, i); printf(值 %d\n, arr[i]); }如果程序输出到某个下标之后崩溃了那问题就锁定了。C语言的数组越界不会像Java那样抛异常它可能静默地返回一个垃圾值也可能直接让程序崩溃。用调试打印找到越界的位置是最省力的方案。8. 从循环到算法思维循环之后你该学什么8.1 循环与递归两种“重复”的对照学完循环下一个跟你打交道的就是递归。递归其实也是一种“重复”——函数自己调用自己直到满足某个终止条件。两者有个有趣的对照循环靠变量更新来控制重复递归靠参数传递来控制重复。拿阶乘来对比。循环版本你已经见过了递归版本长这样int factorial(int n) { if (n 1) return 1; return n * factorial(n - 1); }循环是“跑步绕圈”递归是“俄罗斯套娃”——每一层套娃里又藏着一个更小的套娃直到最小的那个终止。很多用循环能解决的问题递归也能解决反之亦然。但有些问题比如树的遍历、目录的递归删除用递归写起来简洁得多而有些问题比如大数值的累加循环更直观也更省内存。理解循环与递归的关系对你后面学数据结构——特别是二叉树的遍历——会有很大帮助。8.2 循环与数组、指针的配合C语言进阶的必由之路C语言的循环几乎总是和数组、指针联系在一起。你学完循环后做的下一个练习大概率是“用循环遍历数组”“用指针遍历字符串”这类题目。这里有一个核心转变你要做好准备循环从“单纯控制重复次数”的工具变成“遍历数据集合”的工具。当你写下for (int i 0; i n; i)的时候i不再只是一个计数器它代表的是数组的下标、是数据的位置。这个思路转变过来之后你就真正开始用“数据结构”的视角写代码了。热词里提到的“选择排序循环不变量证明”“冒泡排序C语言”这类内容本质上就是把循环和数组结合在一起的应用。排序算法考的不是你会不会写循环语法而是你能不能设计出巧妙的循环嵌套逻辑——什么时候该比较、什么时候该交换、循环的边界怎么划、每一趟循环结束后数组处于什么状态。建议你学完循环之后花一周时间把选择排序和冒泡排序的代码自己手写三遍第一遍照着抄第二遍凭记忆写第三遍不看任何参考独立实现。三轮下来你对循环的理解会上一个台阶。9. 写在最后的实操建议按照我的经验学了循环结构之后接下来一周的练习计划可以这么安排前三天把for、while、do-while的语法练到闭着眼睛能写出来用同一个题目比如累加1到100分别用三种循环实现然后打印循环变量的每一步变化彻底搞懂执行流程。中间两天做九九乘法表、素数判断、字符串逆序这几个经典题每题都尝试用不同的循环实现方式对比它们的代码风格和运行结果。最后两天尝试不看任何参考资料独立完成一个简单的小项目——比如“用户登录次数限制”或者“猜数字游戏”——把循环、分支、输入输出串起来用。我个人实际带人的时候最深的体会是循环是C语言里第一个真正要求你“用计算机的思维方式思考”的知识点。前面学的变量、数据类型、运算符都是“死”的知识记住就能用。但循环不一样它要求你思考“如何让代码自己重复地做事情”这开始有“算法设计”的意味了。所以花再多时间吃透循环都不亏——它不仅是C语言的地基也是你编程思维的地基。最后一个小技巧收尾如果你在写循环的时候犹豫不决不知道该用哪种——我的建议是先写出一个能跑的版本哪怕选错了循环类型能跑就比不能跑强。代码只有先“跑起来”才有资格谈“优化”写循环也一样。先用最顺手的方式实现功能再回头琢磨有没有更优雅的写法这比憋在原地纠结半天强得多。