
学习C语言的过程里分支和循环是绕不开的两座山头。很多刚入门的朋友觉得这两个东西没什么难度无非就是if判断、for循环写几个例子就完了。但实际等我带过不少新人、自己也踩过无数坑之后回头再看真正能把这两个基础语法用明白的人并不多。标题里提到的“分支和循环上”我想把这块内容掰开揉碎讲清楚核心的语法结构是什么、为什么要这样设计、实际写代码时怎么用才顺手以及那些教科书上很少讲的细节和经验。这篇文章适合刚开始学C语言的人也适合那些会写if和for但总觉得代码不够干净的朋友。先说结论分支和循环不是你学会写就完了而是你建立编程逻辑思维的第一块基石。1. 分支结构让程序学会做决定1.1 为什么分支是编程的第一步思维转变很多初学者面对的第一道坎不是语法而是思维方式的转变。写一个顺序结构的程序从上到下一条路走到黑这很像我们照着菜谱做菜按部就班就能完成。但真实世界的需求没有这么线性比如一个登录系统用户输入密码后密码正确进首页密码错误提示重试一个温度监测程序温度超过阈值就报警否则继续监测。这些场景都有一个共同点——程序要根据条件决定走哪条路。分支结构回答的就是“如果……否则……”这个问题。C语言里实现分支的核心工具是if和switch这两者对应的是两种不同的决策场景一个是基于“条件满足与否”的二元判断另一个是基于“值匹配哪个选项”的多路选择。理解了这个区别你就知道什么时候用if什么时候用switch。很多初学者会陷入一个误区觉得只要会写if就算会分支了但实际上分支结构有三个层级需要逐步掌握第一层是基本的if判断第二层是if-else if-else的多条件判断第三层是分支嵌套也就是在分支里面再写分支。每一层都有容易出错的地方。1.2 if-else 的结构细节先看最简单的if它的语法长这样if (条件表达式) { // 条件成立时执行的代码 }括号里的条件表达式是一个关键。C语言里没有专门的布尔类型至少标准C99之前没有它规定表达式值为0时表示假非0值表示真。这意味着if (1)是成立的if (-1)也是成立的只有if (0)不成立。这在初学阶段非常容易让人困惑尤其是从Python等语言转过来的朋友在Python里if 1:可以正常判断但C语言里如果你忘记写括号写成if 1编译器会直接报错。再看if-elseif (条件表达式) { // 条件成立时执行 } else { // 条件不成立时执行 }这里有一个新手经常忽略的点if和else后面如果没有花括号默认只控制后面第一条语句。比如if (a 0) printf(a是正数\n); printf(这条语句无论如何都会执行\n);第二行printf不在if的控制范围内因为没有花括号包裹。这种写法不是不能跑但非常容易出问题。我自己带项目时见过太多就是因为少写一对花括号导致逻辑错乱排查半天找不到原因。还有一类经典错误是悬空else问题。看下面这段代码if (a 0) if (b 0) printf(两个都为正); else printf(a不大于0);这个else到底跟哪个if配对很多初学者以为是和第一个if配对但实际上C语言规定else总是和它上面最近的、未配对的if配对所以这个else实际上配对的是if (b 0)。也就是说当a 0且b 0时会进入else分支这就完全偏离了你原本想要的逻辑。解决办法只有一个写代码时不要省花括号不论多么简单的分支都加上{}。1.3 else if 的多条件判断与顺序陷阱当条件分支超过两个时就需要else if了。这里有一个非常重要的经验条件判断的顺序决定了程序的正确性。举一个经典的例子根据分数划分等级90分以上为A80到89为B60到79为C60以下为D。if (score 90) { grade A; } else if (score 80) { grade B; } else if (score 60) { grade C; } else { grade D; }这样写是正确的因为判断是从高到低逐层递减的。但如果你把顺序反过来先写if (score 60)再写else if (score 90)那一个95分的同学就会被误判为C。原因是score 60这个条件先被满足程序直接进入了这个分支后面的else if根本没机会执行。所以else if链的关键在于条件的顺序必须符合你的逻辑优先级。如果你想写成任意顺序也能正确就必须把条件写完整比如score 90写成score 90 score 100score 60写成score 60 score 80。这样写虽然能避免顺序问题但代码会冗余很多所以更推荐的做法是主动设计条件的先后顺序。1.4 switch 分支的适用场景和注意事项switch解决的是另一个维度的问题。当你要判断的是一个变量的值等于某个选项时用switch会比if-else链更清晰。比如星期几的转换、菜单选项的处理、状态机的跳转。switch (day) { case 1: printf(星期一\n); break; case 2: printf(星期二\n); break; default: printf(无效输入\n); break; }switch有几个细节必须强调第一case后面必须是整型常量表达式不能是变量也不能是浮点数。case 12可以case a不行case 3.14不行。第二每个case块的结尾一般都要加break否则会发生穿透fall-through。所谓穿透就是匹配到某个case之后如果没有break程序会继续执行下面所有case的代码直到遇到break或整个switch结束。穿透并不总是坏事有时反而可以利用。比如你想把多个选项统一处理switch (ch) { case a: case e: case i: case o: case u: printf(是元音\n); break; default: printf(是辅音或其他字符\n); break; }这里就把五个元音字母合并成了一个处理逻辑干净利落。第三default分支放在哪里没有强制规定但习惯上放在最后。它相当于if-else里的那个else用来处理所有未匹配到的情况。写上default是个好习惯哪怕里面只写个注释也方便后来的人理解你没有遗漏任何选项。从实际经验来看如果你的分支条件多于三个且都是判断变量等于某个值优先考虑switch如果是判断范围比如score 90这种可以用if-else if也可以用switch配合区间判断的技巧但后者可读性稍差不太推荐新手使用。2. 循环结构让重复的事情交给机器2.1 循环的本质和三种基本形态如果说分支是让程序学会“思考”那循环就是让程序学会“重复”。计算机最擅长做的事情就是重复比如打印一篇文章的一千行内容、计算1到100的和、遍历数组里的所有元素。代码里如果让你手动写一千行printf程序员会疯掉所以需要循环。C语言提供了三种循环结构while、do-while、for。它们的共同点是需要一个循环条件不同点在于条件判断的时机和循环变量的管理方式。while循环的语法是while (条件表达式) { // 循环体 }它在每次进入循环体之前先判断条件条件为真才执行所以循环可能一次都不执行。适合“不知道要循环多少次只知道满足什么条件就继续”的场景。do-while循环的语法是do { // 循环体 } while (条件表达式);它的特点是先执行一次循环体再判断条件。所以无论条件是否成立循环体至少会执行一次。这个特性在有些场景下很好用比如菜单的循环显示总要显示一次菜单让用户选择然后根据用户的选择决定是否继续。for循环的语法是for (初始化表达式; 条件表达式; 更新表达式) { // 循环体 }for把循环变量初始化、循环条件、变量更新这三件事在语法层面固定在同一行结构清晰。当你明确知道循环次数时优先用for。比如遍历一个数组数组长度已知循环变量从0递增到n-1用for写起来最直观。2.2 for 循环三步走的背后逻辑for循环括号里用分号隔开的三段表达式对应了循环生命周期的三个阶段初始化、判断、更新。很多初学者不理解为什么中间的条件表达式要用分号而不是逗号或者容易在更新表达式上犯错。我举个例子计算1到100的和int sum 0; for (int i 1; i 100; i) { sum i; } printf(1到100的和是%d\n, sum);这个写法里包含几个需要在实践中理解的点初始化表达式int i 1只在循环开始前执行一次。条件表达式i 100在每次循环开始前都会判断。更新表达式i在每次循环体执行完之后执行。所以这个循环的执行顺序是初始化i 1判断i小于等于100吗是执行sum i然后i变成2再次判断……直到i变成101条件不成立循环结束。有个细节值得注意i和i在for循环的更新表达式里效果完全一样因为它们的区别在于返回值不同而更新表达式的值是被丢弃的。但在其他场景里这两个运算符的区别就很重要了这个后面讲循环嵌套时再展开。还有一个很多教材里没讲透的点for循环的三个表达式都可以省略但两个分号不能省略。如果省略条件表达式C语言默认条件永远为真循环会一直执行。这就是一种死循环写法。你在实际项目中可能会看到这样的代码for (;;) { // 相当于 while (1)死循环 }这种写法不是错误反而在嵌入式或服务器编程里很常见比如让程序持续检测某个状态。但要注意搭配break语句使用否则就是真的死循环了。2.3 while 和 do-while 的使用场景有人会问既然for这么好用是不是只需要学for就够了不是的因为有些场景用while更自然有些场景甚至只能用while。while最常见的场景是“我不知道循环次数但我知道循环终止条件”。比如从键盘读入字符直到用户输入换行符为止char ch; while ((ch getchar()) ! \n) { // 处理每个字符 }这个循环在读取之前并不知道会读多少个字符完全由用户的输入决定。再比如文件读取直到读到文件末尾while (fscanf(fp, %d, num) ! EOF) { // 处理每个数字 }这些场景如果非要用for你会发现循环次数的上限根本写不出来硬写一个很大的数也违背了代码的清晰性。do-while的价值在于“先做一次再判断要不要继续”。最典型的例子是显示菜单并接收用户选择int choice; do { printf(请选择功能); scanf(%d, choice); // 处理选择 } while (choice ! 0);这个逻辑如果用while写代码会变成先初始化choice为一个特殊值进入循环然后询问用户……但你仔细想这个循环无论如何都会执行一次询问操作用do-while直接表达“至少执行一次”的语义逻辑上更贴合实际需求。我在实战中还有一个经验用do-while处理需要先验证再重试的操作。比如C语言里用scanf读入数据如果格式不匹配输入缓冲区会残留无用字符导致后续读取全部出错。此时可以用do-while配合检查返回值确保用户输入的是合法数据int num; do { printf(请输入一个整数); } while (scanf(%d, num) ! 1);这个代码的逻辑是不断提示用户输入直到scanf成功读入一个整数才退出循环。它利用了do-while至少执行一次的特性非常优雅。2.4 break 和 continue循环里的交警循环体内部有两个改变循环流程的关键字break和continue。这两个字的用法必须彻底分清因为它们的管理范围完全不同。break的作用是立即退出当前所在的循环或switch。注意“当前所在”这个限定break只能跳出一层循环如果循环嵌套在外面还有一层break只能跳出内层外层循环照常运行。continue的作用是跳过当前这一次循环的剩余部分直接进入下一次循环。它不会退出循环只是跳过“本轮”而已。看个实际例子要打印0到10之间所有的偶数for (int i 0; i 10; i) { if (i % 2 ! 0) { continue; } printf(%d\n, i); }当i是奇数时continue跳过后面的printf直接进入i再判断。这个逻辑也可以用其他方式实现但continue会让代码在你不想深入处理某些情况时提前收工逻辑更集中。break的典型场景是提前退出。比如在数组中查找某个元素int target 25; int arr[5] {10, 20, 30, 40, 50}; int found 0; // 标志变量 for (int i 0; i 5; i) { if (arr[i] target) { found 1; break; // 找到了不需要继续遍历 } } if (found) { printf(找到了 target\n); } else { printf(没找到\n); }这里如果没有break循环会继续把后面的元素也都遍历一遍。对于长度为5的数组无所谓但如果是在一个十万个元素的数组里查找一个排在前面的元素break能省下大量无意义的遍历操作。我还想强调标志变量这个常用技巧。found这个变量在循环外定义循环内修改循环结束后再根据它的值做判断。这种模式在你需要在循环里捕获某种状态时非常常见类似“哨兵”的作用。3. 分支与循环的组合实战从九九乘法表到字符串逆序3.1 九九乘法表嵌套循环的入门学会分支和循环的基本语法后接下来就是组合应用。九九乘法表是训练嵌套循环最经典的题目几乎所有C语言课程都会用到。原因很简单它只需要一张循环嵌套的二维结构同时又能直观地展示行列关系。先看最直白的双层循环实现for (int i 1; i 9; i) { for (int j 1; j 9; j) { printf(%d*%d%-4d, j, i, i * j); } printf(\n); }这个代码会输出一个完整的9乘9的矩形乘法表。但通常我们想要的九九乘法表是下三角形状的也就是每行只输出到i为止。把内层循环的条件改成j i就行for (int i 1; i 9; i) { for (int j 1; j i; j) { printf(%d*%d%-4d, j, i, i * j); } printf(\n); }这里%-4d是格式化输出表示左对齐、占4个字符宽度。不加这个控制符乘法表会因为位数不同而对不齐输出很凌乱。这是实践里很容易忽略的细节写代码时一定要养成对齐的习惯不然看输出结果大脑会混乱。外层循环控制行数内层循环控制列数这种“行与列”的思维在后续很多编程场景里都会遇到比如矩阵操作、图像数据处理本质上都是嵌套循环的运用。3.2 字符串逆序结合数组与循环字符串逆序这道题在PTA和浙大翁恺的练习题里经常看到。它的核心思路是一个字符串本质上是一个字符数组逆序就是交换首尾对应的字符。先看用循环和数组下标实现的版本#include stdio.h #include string.h int main() { char str[100]; gets(str); // 注意gets有安全隐患面试题里可以用实战推荐fgets int len strlen(str); for (int i 0; i len / 2; i) { char temp str[i]; str[i] str[len - 1 - i]; str[len - 1 - i] temp; } printf(%s\n, str); return 0; }这段代码有几个细节值得说。第一len / 2这个上界是逆序算法的关键。如果字符串长度为奇数中间的字符不需要交换如果长度为偶数正好交换所有对称位置。所以循环只需要执行长度的一半次数。第二交换两个字符要用临时变量temp这是基础但重要的操作。后面讲冒泡排序时这个交换操作还会再次出现。第三现在写代码不建议用gets因为它无法限制输入长度容易造成缓冲区溢出编译器通常也会报warning。合理的做法是用fgets(str, 100, stdin)。不过使用fgets时要注意它会把换行符也读进来需要手动去掉。如果把分支也融入这个题目还可以加一个判断如果输入的字符串含有数字则不参与交换只交换字母。这就是一个很典型的“循环分支”综合题能同时训练两个核心语法。实际写下来你会更直观地理解分支和循环是如何配合工作的。3.3 冒泡排序分支是循环的灵魂冒泡排序是很多人接触的第一个排序算法也是“循环分支”的最佳组合演练场。它的核心思想很简单从头到尾两两比较相邻元素如果顺序错误就交换每一轮结束后最大的元素就像气泡一样浮到末尾。#include stdio.h int main() { int arr[10] {5, 3, 8, 1, 9, 6, 2, 7, 4, 0}; int n 10; for (int i 0; i n - 1; i) { // 外层循环进行 n-1 轮比较 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; } } } for (int i 0; i n; i) { printf(%d , arr[i]); } printf(\n); return 0; }这里两个循环各自承担不同职责外层循环控制“轮数”总共需要比较n-1轮因为最后一轮只剩一个元素不需要比较内层循环控制“每一轮中比较的位置范围”随着轮数增加已经排好的元素从末尾开始增多内层比较范围相应缩短所以上限是n - 1 - i。分支的作用体现在if (arr[j] arr[j 1])。很多人排序写完但没有真正理解这个if的含义它不只是判断大小而是在决定要不要交换。如果你把改成排序结果就会从升序变成降序。你可能会发现这个算法即使数组已经排好序也会继续执行完所有轮次。这里可以加一个优化用一个标志变量记录本轮是否有交换发生如果没有说明数组已经有序直接break跳出外层循环。这个优化是理解break使用场景的绝佳案例。4. 常见问题与调试技巧实录4.1 死循环和无限循环的几种触发原因死循环是初学者最常遇到也最容易绝望的bug。最大特点是程序运行后光标一直闪烁不打印结果或者疯狂打印同一行内容你只能按CtrlC强制终止。死循环的几个常见成因我以实际经验列一下第一种是条件永不变化。最典型的是忘了在循环体里更新循环变量int i 1; while (i 10) { printf(i %d\n, i); // 忘了写 i; }循环变量i永远是1条件永远成立程序永远不会退出。解决思路很简单在循环体内更新循环变量或者改用for循环把更新语句写在语法里从结构上防止遗忘。第二种是条件写反了。比如你想当输入值不为0时继续循环结果把while (num 0)写成while (num ! 0)逻辑直接翻转原本该退出的时候继续循环该继续循环的时候退出了。第三种是边界条件写成开区间导致永远达不到退出条件。比如while (i 0)而i从1开始递增i永远不会小于0循环体永远不会执行——这不算死循环但逻辑上也是错的。反过来while (i 0)配ii从-5开始倒也能退出但循环次数可能超出预期。第四种是浮点数的比较问题。比如while (x ! 1.0)x每次加0.1理论上应该到1.0但浮点数的二进制表示不精确可能永远不会精确等于1.0导致死循环。正确的做法是使用精度范围判断while (x 1.0)或者while (fabs(x - 1.0) 1e-6)。死循环不是不可挽救的错误关键是养成好习惯每次写循环前先想清楚循环变量的初始值、退出条件和更新方式三件事缺一不可。4.2 循环变量作用域的坑C99标准之后可以在for的初始化表达式里直接定义变量for (int i 0; i 10; i) { // 循环体 }这个i的作用域仅限于这个for循环内部循环结束后i就不可见了。这种做法我强烈推荐因为它能避免变量污染。但有一个坑必须提醒如果你在循环外还需要知道循环结束时循环变量是多少就不能把变量定义在for里。比如int i 0; for (; i 10; i) { if (某些条件) { break; } } printf(循环结束时 i %d\n, i); // 这里可以正常使用 i如果把int i 0写进for的括号里循环结束后i就没了作用域你只能用printf打不出来。在写一些查找、匹配类的算法时经常需要在循环结束后使用循环变量的值此时务必把变量定义在循环外。还有一个相关的坑是循环体内重新定义同名变量。比如在循环体内部写了一个int count 0;然后又在外层使用count编译器会报重复定义或者意外阴影警告。对于刚入门的同学建议保持变量命名的统一和清晰。4.3 一个实用的调试思路打印中间结果学分支和循环时最有效的调试手段不是调试器而是最原始的printf。我并不是说调试器没用而是对于刚接触这两个语法的同学printf能更直观地帮你理解程序执行过程。比如你调试一个嵌套循环不确定内层执行了几次可以加上for (int i 0; i 3; i) { for (int j 0; j 3; j) { printf(i %d, j %d\n, i, j); } }运行之后你会看到一共输出9行i和j的取值分别是(0,0)、(0,1)、(0,2)、(1,0)……这个输出结果能帮你验证自己对循环执行顺序的理解。我见过很多同学写嵌套循环时根本分不清内外层执行顺序打印一遍中间变量之后瞬间就通了。排查死循环时也在循环体第一行加一个printf观察条件成立时的变量值变化。看看循环变量有没有更新更新之后有没有逼近期望的退出条件。还有一个小技巧在调试条件分支时可以在每个分支入口加上一个打印语句if (score 90) { printf(进入A分支score %d\n, score); grade A; }这样一旦逻辑不符合预期你能立刻知道程序到底进入了哪个分支是条件判断错了还是分支内的代码写错了。加日志调试不是“笨办法”而是工程上快速定位问题的基本功。只要把打印语句在调试完成之后清理干净就没有什么副作用。4.4 代码风格和命名习惯从现在开始养成最后聊一个很多人初学时不重视、但影响深远的话题代码风格。分支和循环的代码量虽然不大但如果不注意风格很容易写成一团乱麻。我推荐几条经验第一缩进要统一。每进入一层大括号缩进4个空格要么就统一用Tab。同一个文件里不要混用。缩进是给别人看的更是给三天后的自己看的。我看过太多因为缩进混乱而看错逻辑归属的案例。第二大括号的位置。C语言社区里常见的两种风格一种是KR风格左大括号跟在语句后面不换行另一种是Allman风格左大括号单独占一行。两者各有拥趸没有绝对优劣但请你在一个项目里统一不要今天写这个明天写那个。第三条件表达式里不要写过于复杂的逻辑。如果一个if条件里塞了五六个和||一般人根本读不懂。比如if ((a 0 b 10) || (c 5 d ! 3) || e 2) { // 这是想干什么 }更好的做法是提取成有意义的变量或函数int isValidInput (a 0 b 10); int isSpecialCase (c 5 d ! 3); int isEmergency (e 2); if (isValidInput || isSpecialCase || isEmergency) { // 逻辑就清晰多了 }第四写注释不是废话但注释要说“为什么”不要说“是什么”。// 当输入合法时执行比// 进入if分支有意义得多。具体到这个场景注释可以写在分支逻辑比较绕的地方比如悬空else、循环退出条件的边界值这些地方很容易让人怀疑人生。5. 一些我踩过或者看别人踩过的坑顺便做个速查5.1 常见错误速查表把知识点整理成表方便你以后排查问题时快速定位。错误类型错误示例正确写法排查思路条件表达式忘记括号if a 0if (a 0)检查if、while、for括号是否齐全赋值代替判断if (a 1)if (a 1)编译器一般会有warning认真读warning悬空elseif (a) if (b) ... else ...写清花括号明确配对关系检查代码缩进与else距离最近的ifswitch漏写breakcase 1: ... case 2: ...每个case后加break检查每个case结尾while条件永真while (i 10)但 i 不更新循环体内给i赋值或更新打印循环变量确认变化浮点相等比较while (x ! 1.0)while (fabs(x - 1.0) 1e-6)理解浮点数精度边界条件写错for (i 0; i n; i)但只应到n-1for (i 0; i n; i)用printf打印循环过程尤其要提一下if (a 1)这个坑。C语言允许在条件里赋值所以a 1不是语法错误表达式的值是11又代表真所以这个if永远成立。这种错误非常隐蔽因为它不会报警告有些编译器在新标准下会提示但逻辑完全错了。有些人会故意用if (a 1)来显式赋值并判断真值但我个人强烈不推荐这种写法。如果你要判断a是否等于1只写if (a 1)就够了。还有一个习惯把常量写在比较运算符左边if (1 a)这样万一写成if (1 a)编译器会直接报错因为不能给常量赋值。这种写法叫Yoda条件观感因人而异但确实能防错。5.2 用实际题目练手一个综合小案例纸上谈兵再多不如实际敲一遍。我写一个综合运用分支和循环的小程序统计用户输入的一串正整数中有多少个偶数和奇数输入0结束。#include stdio.h int main() { int num; int evenCount 0; int oddCount 0; printf(请输入正整数输入0结束\n); while (1) { scanf(%d, num); if (num 0) { break; } if (num % 2 0) { evenCount; } else { oddCount; } } printf(偶数个数%d\n, evenCount); printf(奇数个数%d\n, oddCount); return 0; }这个案例把while(1)死循环、break退出、if-else分支、累加计数器这几个核心点全部串起来了。你读这段代码时可以试着把每一步的流程在脑子里走出来用户输入55不等于05%2是1进入elseoddCount变成1继续循环。用户输入0if命中break退出最后打印结果。如果你能把这段程序每执行一步、每个变量的变化都讲清楚说明你对分支和循环的理解已经过关了。在此基础上再往哈希表、链表、树这些数据结构上走它们的基本遍历逻辑都离不开这些基础语法。5.3 分支和循环上之后下一部该学什么这篇文章聚焦在分支和循环的语法基础、编写习惯和常见错误上。接下来自然要往两个方向延伸一是函数把分块逻辑封装成可复用的单元二是数组用循环遍历数组元素把批量数据处理的威力发挥出来。从我自己带人的经验来看分支和循环学得扎实的人学函数和数组时会非常轻松反过来如果前面只是背语法、抄代码后面遇到指针和动态内存时就会格外痛苦。编程能力的提升不是靠堆砌概念而是靠把基础结构用到肌肉记忆的程度。等你能随手写出清晰的循环逻辑、能一眼看出死循环和漏break的原因再学什么都不会觉得吃力。我在实际写代码和Code Review时有个很深的体会很多看起来高大上的程序比如操作系统里的进程调度、网络协议里的状态机剥开外层封装底层的骨架依然是分支和循环的组合。你掌握的不是两个语法结构而是所有程序逻辑的底层通用语言。本篇文章先聊到这里下一篇我们再深入函数和数组相关的内容。