学C语言绕不过去的一个坎就是分支与循环。分支让程序在岔路口自己选路循环让程序把重复劳动交给机器这两个东西一旦掌握你写的代码才算真正有了逻辑而不是从上到下平铺直叙。不管你是刚接触编程的大学生、自学C语言准备走嵌入式方向的初学者还是已经在写单片机固件想回头补基础的人分支与循环都是绕不开的基本功。我见过太多人背了一堆语法一写程序就卡壳其实问题就出在这两块没有真正吃透。所以这次我不讲虚的直接带着分支和循环的完整用法、代码示例、实战项目和踩坑记录从头到尾过一遍。1. 先搞懂这两个概念到底在解决什么问题很多人学分支与循环一上来就背if、for的语法格式背得挺熟但拿到题目还是不知道怎么写。原因很简单你只记住了“怎么用”却不知道“为什么用”。1.1 分支程序在岔路口做决定什么叫分支就是程序执行到某个位置时遇到了一个岔路口需要根据条件判断往哪边走。比如你写了个登录程序用户输入密码密码对了进入主界面密码错了提示重新输入。这种“如果……否则……”的判断逻辑就是分支。有人可能会说这不就是if-else吗没错但你要换个角度看分支不是语法而是程序的“决策机制”。一个小程序可能只有三五个分支但一个真实项目里分支的数量可能多到几百上千条。你写的每一句if都是在帮程序建立一套行为准则。顺带说一句Git 里也有个词叫“分支”那是版本管理里的概念跟 C 语言里的分支语句不是一回事。Git 分支指的是代码仓库里分出来的开发线而 C 语言的分支指的是程序在运行时的执行路径分岔一个是文件管理层面的东西一个是代码逻辑层面的东西别搞混。1.2 循环把重复劳动交给机器循环解决的是另一类问题重复。比如你要输出 1 到 100 这 100 个数字如果不用循环就得写 100 条printf这是人干的事吗显然不是。循环就是让计算机反复执行同一段代码直到满足某个条件才停下来。循环的价值我当年是在写单片机程序时真正体会到的。一个流水灯程序8 个 LED 依次点亮本质上就是“亮灯、延时、灭灯、下一盏”这个动作被循环执行。没有循环这种程序只能无限重复复制粘贴代码量直接爆炸。所以嵌入式、单片机领域里循环几乎是命根子每个控制程序里都泡在while(1)里。另外说个容易混淆的点深度学习领域有个“循环神经网络RNN”听起来也有“循环”俩字但那个循环指的是网络结构里带反馈连接跟 C 语言里控制流程的循环语句完全是两个层面的概念。搜索资料的时候别被带偏了。1.3 为什么“分支”和“循环”总被放在一起讲因为在实际编程里它们是配套出现的。循环负责“重复做”分支负责“在重复中做选择”。比如你写一段遍历数组的代码循环让 i 从 0 走到 n-1循环体里用分支判断当前元素是正是负。没有分支的循环是“傻循环”没有循环的分支是“孤立判断”。所以你在学习时不要把这两个知识点拆开看而要把它们当作一套组合拳来练。下面我会分别拆解各自的细节最后再用一个完整的实战项目把两者串起来。2. 分支语句if-else与switch的实战细节分支在 C 语言里主要就是两套工具if-else系列和switch-case系列。两套工具各有各的适用场景平时写得多了你自然会形成手感。2.1 if-else的完整形态与悬空elseif的完整形态有三个层次单独的ifif-else配对以及if-else if-else的多条件判断。if (score 60) { printf(及格\n); } else if (score 75) { printf(良好\n); } else if (score 90) { printf(优秀\n); } else { printf(不及格\n); }上面这段代码有一个经典陷阱如果用户考了 95 分程序会输出什么答案是“及格”。因为程序执行顺序是从上往下的score 60已经被满足了后面的else if根本不会执行。这种逻辑上的“顺序错误”比语法错误更难发现。所以写多层分支的时候我有个习惯先写最严格的从上往下的约束要么从高到低排要么从低到高排但每个条件里都要带上界或下界的限制。比如上面这个例子改成score 90排最前面或者写成score 60 score 75这样带区间的写法就不会踩坑。还有一个绕不开的“悬空 else”问题if (a 0) if (b 0) printf(positive\n); else printf(negative?\n);这段代码的else到底跟哪个if配对答案是跟离它最近的if也就是if (b 0)配对。很多人一眼看上去以为else属于第一个if结果程序逻辑完全不是自己想的。避免悬空 else 的唯一可靠办法就是永远把if和else的大括号写完整不要省。2.2 switch的穿透机制break一个都不能少switch-case适合做“多选一”的场景尤其是当判断条件是一个整数或者单个字符的时候。switch (grade) { case A: printf(优秀\n); break; case B: printf(良好\n); break; case C: printf(及格\n); break; default: printf(成绩无效\n); break; }这里最坑的就是break。如果你在某一个case后面忘了写break程序会直接“穿透”到下一个case继续执行这叫 fall-through。有时候这是故意的比如下面这种场景switch (day) { case 1: case 2: case 3: case 4: case 5: printf(工作日\n); break; case 6: case 7: printf(周末\n); break; }这里故意不写break让多个case共享同一段逻辑这是合法的用法。但如果不是刻意为之的穿透而是忘了写break那程序结果就会莫名其妙。我排查过很多次这种 bug最后的结论都是同一个case后面漏了break。新手写switch前三个月老老实实每个case都写break不会吃亏。2.3 写条件时容易踩的三个坑分支的核心是条件表达式条件表达式写错分支逻辑必错。常见的坑有三个。第一个是赋值符号当等号用。C 语言里判断相等要写但很多人手一滑写成比如if (x 5)。这在 C 语言里是合法的因为赋值表达式有值x 5的值就是 5非零即为真所以这个if永远成立而且还会顺手把 x 改成 5。这种 bug 特别隐蔽编译器最多给个警告。我的建议是凡是比较相等的地方把常量写在左边比如if (5 x)这样万一写成if (5 x)编译器会直接报错。第二个坑是优先级混乱。比如if (a 1 0)你以为是在判断 a 按位与 1 之后等不等于 0实际上的优先级比高程序先执行1 0结果是 0然后执行a 0结果永远是 0。整个表达式就废了。这种问题没什么好说的真正靠谱的方案只有一个拿不准优先级就加括号不要怕括号多。第三个坑是浮点数直接用比较。浮点数在内存里是近似存储的0.1 0.2算出来不是精确的0.3。你直接写if (x 0.3)往往会得到 false。正确做法是判断两个数的差绝对值小于一个很小的数if (fabs(x - 0.3) 1e-6) { printf(相等\n); }2.4 分支语句的选型建议说了这么多到底什么时候用if-else什么时候用switch-case我个人的习惯是条件少的、判断逻辑复杂的、需要比较大小或者判断范围的用if-else条件是整数或字符、选项固定且数量较多的用switch-case。另外switch只能做“相等判断”不能像if那样写范围判断和组合条件所以不要指望用switch包打天下。3. 循环语句for、while、do-while到底怎么选C 语言里有while、do-while、for三大循环语句。语法格式大多数人都背得下来但“怎么选”才是真正体现功力的地方。3.1 while循环适合“不知道循环次数”的场景while的特点是先判断条件条件成立才执行循环体条件不成立就跳过整个循环。int score; printf(请输入成绩输入-1结束\n); scanf(%d, score); while (score ! -1) { // 处理成绩 printf(成绩%d\n, score); scanf(%d, score); }这类叫“哨兵循环”用一个特殊值这里是 -1来标记结束。它的最大特点是循环次数在进入循环之前根本不知道只知道“什么时候该停”。处理用户输入、读取文件直到结尾、等待某个标志位变化这些场景都是while的主场。注意while可能一次都不执行。如果用户第一次输入的就是 -1那循环体直接被跳过。这个特性有时候是好事有时候会变成坑下面第 6 节我会专门讲。3.2 do-while循环先做一次再说do-while和while的唯一区别是它先执行一次循环体再判断条件。这意味着do-while至少会执行一次。int num; do { printf(请输入一个正数); scanf(%d, num); } while (num 0);这种“先做一次不满意就再来”的模式特别适合做菜单程序。比如电子词典的界面先显示菜单、读取用户选择、根据选择执行功能然后回到菜单这就是一个典型的do-while循环。如果你用while写就得在循环外面先初始化一个假条件进去写起来别扭得多。3.3 for循环循环变量三件套for循环把循环变量初始化、条件判断、变量更新这三件事集中压在一行里for (int i 0; i n; i) { // 循环体 }为什么很多时候优先用for因为它把循环的“骨架”摆在最显眼的位置你看一眼for (int i 0; i n; i)就知道循环从哪里开始、到哪里结束、每一步怎么走。而while的循环变量更新往往藏在循环体末尾容易漏写漏写就是死循环。在循环变量的选择上我的习惯是计数循环用i、j、k语义循环用更有意义的名字。比如遍历数组元素用i遍历字符串用pos或idx这样代码读起来清楚得多。另外循环变量的边界也很有讲究i n和i n是两种完全不同的遍历范围数学里叫左闭右开和左闭右闭。C 语言社区的主流习惯是左闭右开也就是i n因为数组下标从 0 开始最后一个元素的访问要写在n-1。3.4 break与continue循环里的两把控制刀break的作用是跳出整个循环continue的作用是跳过本次循环的剩余部分、立即进入下一次迭代。两者看着相似实际用起来差别巨大。for (int i 0; i 100; i) { if (i % 10 0) { continue; // 跳过10的倍数 } if (i 50) { break; // i超过50直接结束循环 } printf(%d\n, i); }这段代码输出的数字是 1 到 49 之间所有不是 10 的倍数的数。continue让程序跳过i等于 10、20、30、40 这些数而break在i 51的时候结束了整个循环。有一个使用建议break和continue虽然好用但嵌套循环里容易出问题。break只能跳出最里面那一层循环如果你想从多层嵌套里直接跳到最外层要么加一个标志变量要么用goto对就是你们教材上不让用的那个goto在某些场合它反而最清晰。我的建议是循环嵌套超过两层尽量把逻辑抽成函数用return代替break代码可读性会好很多。4. 综合实战做一个成绩统计器光说不练假把式。下面我带你把分支和循环串起来写一个完整的小项目成绩统计器。4.1 需求拆解这个程序需要什么这个程序要完成的功能很明确从键盘持续输入学生成绩每输入一个成绩就记录一次当用户输入 -1 时结束输入最后输出总人数、平均分、最高分、最低分、不及格人数。这个需求本身并不复杂但它同时用上了循环持续输入、分支判断合法输入、比较最大值最小值、以及几个经典的计数器技巧非常适合练手。设计思路是这样的程序需要一个while循环持续读入成绩因为循环次数是由用户决定的符合“哨兵循环”的典型特征。循环体内部要判断成绩是否在 0 到 100 范围内还要维护max和min两个变量的实时更新。循环结束后根据累计的count计算平均值。4.2 完整代码与逐段解析直接上代码#include stdio.h int main(void) { int score; int count 0; int sum 0; int max 0; int min 100; int fail 0; printf(请输入成绩0~100输入-1结束\n); scanf(%d, score); while (score ! -1) { if (score 0 || score 100) { printf(成绩不合法请重新输入\n); } else { count; sum score; if (score max) { max score; } if (score min) { min score; } if (score 60) { fail; } } scanf(%d, score); } if (count 0) { printf(总人数%d\n, count); printf(平均分%.2f\n, (double)sum / count); printf(最高分%d\n, max); printf(最低分%d\n, min); printf(不及格人数%d\n, fail); } else { printf(没有输入任何有效成绩。\n); } return 0; }逐段说几个关键点。为什么循环开始前要先scanf一次因为while (score ! -1)需要先知道score的值才能判断。循环体末尾再scanf一次是为了读取下一个成绩。这就是“先读再判断、处理完再读”的标准哨兵循环模式。如果你把循环体里的scanf漏了程序就会用同一个成绩无限循环直接卡死。max 0和min 100这两个初始值是有讲究的。成绩范围是 0 到 100所以max起始设为 0只要成绩比 0 大就会被更新min起始设为 100只要成绩比 100 小就会被更新。这是用“极值初始化”的思路能保证第一次比较就能正确赋值。相比之下有些人习惯把max和min都设成 0那min就永远不会更新最后最低分会算出 0这是新手常犯的错误。平均分那里我写了(double)sum / count这个细节后面还会再提。sum和count都是int如果直接写sum / countC 语言会做整型除法小数部分全部丢掉。加上(double)强制转换之后除法才得到浮点数结果配合%.2f输出两位小数。这一行是最能体现“基础知识漏洞”的地方。输入非法成绩时程序不会计入任何统计只提示用户重新输入。这里我是把非法分支和合法分支分开处理的逻辑上更清晰。你也可以把score 0 || score 100的判断直接放在循环条件里但那种写法对新手不太友好我建议先写清楚再优化。4.3 边界测试与参数验证写完代码不是终点测边界才是关键。我通常会把程序跑几遍专门测试极端情况。第一遍直接输入 -1什么都不输。程序应该输出“没有输入任何有效成绩”说明count 0的分支被正确触发。这里count 0的判断是必须的否则sum / count会是“除以 0”的未定义行为程序直接崩。第二遍输入60、60、60、-1。平均分应该是 60.00最高分最低分都是 60不及格人数 0。这是用“全相同数值”验证比较逻辑是否正常。第三遍输入100、0、59、-1。最高分 100最低分 0不及格人数 1平均分 53.00。这一步验证最值更新和不及格统计是否可靠。第四遍输入85、abc、90、-1。注意如果用户输入了abcscanf读取失败score里保存的是上一次的值程序行为可能不可控。这在真实程序里要专门处理但作为入门练习我一般不会在第一个版本里加scanf返回值检查先把主流程跑通再说。想在更严谨的程序里用可以检查scanf的返回值是不是 1不是的话就清理输入缓冲区再重试。这四轮测完这个成绩统计器基本就稳了。你会发现真正让程序出问题的往往不是核心逻辑而是边界条件和非法输入这两个角落这两个角落恰好就是分支和循环的交叉地带。5. 高手的嵌套玩法冒泡排序与循环嵌套分支和循环都掌握之后下一个台阶就是循环嵌套。这个环节我用冒泡排序来演示因为它是所有教科书和新手练习里最经典的嵌套循环案例。5.1 冒泡排序双循环的最经典入门冒泡排序的思路特别直观从头到尾比较相邻的两个元素如果前一个比后一个大就交换位置。这样每趟跑完后最大的元素会像气泡一样“冒”到数组末尾。重复 n-1 趟整个数组就排好了。#include stdio.h int main(void) { int arr[] {5, 2, 9, 1, 7}; int n 5; 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; } } } for (int i 0; i n; i) { printf(%d , arr[i]); } printf(\n); return 0; }这里有两个细节值得反复琢磨。第一内层循环j n - 1 - i为什么减i因为外层已经跑了 i 趟数组末尾的 i 个元素已经是排好序的最大值不用再比较了。减去i可以省掉无谓的比较。如果你写j n - 1排序结果也不错但循环次数明显增加了。第二交换三个变量值的写法temp a; a b; b temp这是 C 语言里必须熟练到肌肉记忆的基础操作。有些人喜欢用异或交换写成a ^ b; b ^ a; a ^ b;那样确实能省一个临时变量但可读性差而且如果a和b指向同一个变量异或交换会直接把值变成 0属于自己给自己挖坑。我的建议始终是老老实实用temp。5.2 嵌套循环的时间复杂度估算冒泡排序里双层循环的比较次数是固定的第一趟比较 n-1 次第二趟比较 n-2 次一直到最后 1 次。总次数是等差数列求和结果是 n(n-1)/2。代入 5 个元素就是 5×4/2 10 次比较。这个公式你可以自己验证上面代码里外层循环跑 4 趟内层循环分别跑 4、3、2、1 次一共 10 次。当 n 很大的时候n(n-1)/2 约等于 n²/2所以冒泡排序的时间复杂度是 O(n²)。为什么要知道这些因为写程序不只是“让它跑起来”还要考虑数据量变大之后程序会不会变慢。一旦你会用时间复杂度估算循环嵌套的开销写代码的时候就会主动去想“这个循环能不能少一层”“这个条件能不能提前跳出”。这种“优化直觉”就是从循环嵌套练习里培养出来的。网上那些所谓的“C语言必背100代码”里面很大一部分就是这种经典算法题背不背另说但至少每一道都应该亲手敲一遍体会一下循环在里面是怎么转的。6. 常见问题排查与避坑实录这一节是真正的实战总结。我把平时答疑和写代码时最常碰到的问题汇总成下面这些条目每条背后都有真实案例。6.1 死循环最经典的几种死法死循环是循环里最让人头疼的问题。最常见的原因是循环变量的更新被遗漏。比如int i 0; while (i 10) { printf(hello\n); // 忘写 i; }这个程序会一直打印 hello因为 i 永远等于 0循环条件永远成立。排查死循环时我第一个检查点就是循环变量有没有更新、代码路径上会不会跳过更新语句。比如循环体里有一个if分支continue写在i前面那你一旦满足条件就会永远跳过后面的更新直接死循环。第二种死法是用浮点数做循环计数器。比如for (float x 0.0; x ! 1.0; x 0.1)因为浮点数的存储误差x 加 10 次 0.1 后不会精确等于 1.0这个循环大概率停不下来。解决方案很简单循环计数器永远用整数需要浮点值的时候临时算一个出来。第三种死法是条件写反了比如while (score ! -1)被改成while (score -1)那只有输入 -1 的时候才进循环其他情况直接跳过。这种 bug 不算是死循环但逻辑完全颠倒排查时也容易被忽略。我一般会写一句注释在循环上方把出口条件描述清楚比如“当 score 等于 -1 时结束输入”这样下次读代码的时候不会把条件看反。6.2 循环边界差一错误差一错误是数组遍历里最经典的坑症状通常是“数组越界访问”。比如int arr[5] {1, 2, 3, 4, 5}; for (int i 0; i 5; i) { printf(%d\n, arr[i]); }当i等于 5 时访问的是arr[5]但这个数组只有 0 到 4 共 5 个下标arr[5]已经越界了。C 语言不检查数组下标越界访问读取到的是内存里的垃圾数据甚至可能修改别的变量程序有时候会在运行很久之后才崩这时候根本想不到是这里的问题。我排查这类问题时有个习惯凡是数组遍历先数一遍下标范围再用几个特殊下标心里过一遍。比如 5 个元素的数组有效下标是 0 到 4循环条件应该是i 5不是i 5。宁可多写一次循环也不要越界访问。6.3 浮点数比较与整型除法第 2 节提到浮点数不能直接这里再补充一个整型除法的例子。很多初学者写平均数都是int sum 89; int count 5; double avg sum / count;你以为 avg 会是 17.8实际上它是 17.0。因为sum / count在 C 语言里是整型除法89 除以 5 得 17余数 4 直接被丢掉然后才转换成浮点数赋值给 avg。解决方案有三种(double)sum / count、sum * 1.0 / count、或者把count定义成double类型。我推荐写sum * 1.0 / count因为不需要额外强制转换代码读起来更自然也能顺便提醒自己这里要的是浮点结果。6.4 运算符优先级与括号优先级这个坑我在第 2 节已经提过和的问题。这里再给一个更日常的例子if (a 0 b 0 || c 0) { // 这段代码很容易让人误解 }的优先级比||高所以程序实际执行的是(a 0 b 0) || (c 0)。如果你本意是a 0 (b 0 || c 0)那逻辑就完全错了。这种问题没有任何取巧的办法只能靠加括号。我写条件表达式有一条铁律超过一个运算符就必须加括号哪怕我知道优先级也加。因为代码不只是给编译器看的更是给人看的少让读代码的人猜就是减少 bug。6.5 scanf与字符输入的那些坑这个坑出现过太多次了。scanf(%d, n)读取整数时会在输入缓冲区里留下一个换行符。紧接着如果你用getchar()读取字符读到的往往不是你想输入的字符而是那个残留的换行符。int n; char ch; printf(输入数字); scanf(%d, n); printf(输入字符); ch getchar(); // 这里读到的其实是换行符解决方案是在scanf后加上一句getchar()把残留的换行符吃掉或者用scanf( %c, ch)在%c前面加一个空格让scanf自动跳过空白字符。这个坑尤其让人抓狂因为程序看起来完全没错输出就是不对。我在开发嵌入式调试菜单时用过无数次scanf配合getchar每次都记得清一下缓冲否则菜单界面会像吃了毒药一样跳过输入。6.6 排查工具和技巧没有一个能拿来打天下的调试手段但有一个特别适合新手的习惯在可疑的位置加printf打印中间变量。比如循环里打印每次更新的i、sum、max程序跑一遍你马上能看出是哪里没按预期走。等确认逻辑对了再把调试用的printf删掉就行。这个方法看起来土但效率其实很高尤其是处理分支和循环这种流程类问题打印变量值比看代码猜一百遍都直观。我以前带过不少学员他们问我怎么调试我的回答永远是先printf再想别的。等代码熟练到一定程度自然就能接受断点调试、单步执行这些更高级的工具但初学阶段最缺的不是工具而是“程序是跑出来的不是看出来的”这种意识。踩过几次坑之后我现在写循环的步骤已经很固定先确定循环条件再确定循环变量怎么更新最后才写循环体里的业务逻辑。每次写完循环我都会问自己三个问题这个循环会不会一次都不执行会不会执行太多次循环变量会不会永远不更新这三个问题想清楚分支与循环这一关基本就算真正过了。