ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C语言流程控制全解析:分支、循环与跳转,彻底搞懂执行顺序

C语言流程控制全解析:分支、循环与跳转,彻底搞懂执行顺序 Day 5C语言的流程控制——分支、循环与跳转一篇吃透执行顺序写C语言最怕什么不是语法记不全不是指针绕晕头而是代码明明编译通过了运行结果却完全不是想象的那样。80%的这类问题都出在流程控制上。今天这篇是“Day 5”的实战笔记核心就一块C语言的流程控制。学完这一篇你拿到任何一段带if、for、while的代码都能像看故事线一样把执行顺序捋得清清楚楚自己写循环也不会再莫名其妙地死循环。C语言是结构化的编程语言所谓“结构化”说白了就是程序的执行是有条理的一共就三种基本结构顺序、分支、循环。你写的任何一个程序哪怕再复杂拆到底也就是这三种结构的组合嵌套。今天这篇文章适合刚学完变量和数据类型、正准备开始写真正有逻辑的程序的初学者也适合那些“看得懂每一行、但看不懂整段”的卡壳选手。咱们直接进入正题把分支、循环、跳转三块逐一拆开讲透最后再送你几个我踩过的坑。1. 流程控制到底在控制什么1.1 三种基本结构顺序、分支、循环C语言程序默认是顺序执行的就是你写一句、它跑一句从上往下老老实实。绝大多数脚本、批处理任务就是这么干的。但现实问题哪有这么简单你要判断一个数是正数还是负数要循环叠加一批数据要让程序在某种异常情况下提前退出——这些需求单靠顺序执行根本写不出来。于是有了分支和循环。顺序结构执行完一条语句自动走到下一条没有岔路。分支结构根据条件决定走哪条路典型就是if...else和switch。循环结构条件满足就反复执行同一段代码典型就是while、do...while、for。这个分类不是我随便列的它是“结构化程序设计”理论的基础。你看名字就知道了“结构化”意味着程序可以被拆成清晰的逻辑块每一块都有唯一入口和唯一出口块与块之间不交叉。为什么后来的编程语言都强调这个因为人脑处理复杂逻辑的能力有限一旦程序执行顺序可以乱跳代码就完全没法维护了。1.2 为什么说流程控制是C语言的“承重墙”你可以把变量、数据类型、运算符理解为C语言的“砖瓦”那流程控制就是“承重墙”。砖瓦再多没有承重墙房子立不起来。函数、指针、结构体这些进阶内容之所以能成立前提就是你能用分支和循环把数据加工的逻辑写出来。一个特别直观的例子你以后学数组、学字符串处理几乎每个算法冒泡排序、查找、统计都离不开循环嵌套分支。哪怕是打印一个九九乘法表本质上也是两层循环加一个换行判断。可以说流程控制这一章如果没学扎实后面每学一个知识点都会觉得使不上劲——不是新知识点难是你连“让代码按你想要的顺序跑”这个基本功都没拿到手。1.3 学习流程控制的正确姿势手动跟一遍执行轨迹这一章的学习方法和前面完全不一样。学变量时你记住“int是整型float是浮点型”就行。但学流程控制你必须练一个能力在脑子里或纸上模拟程序执行。我的建议是拿到一段带分支或循环的代码先在旁边画个表列三列当前行号、变量值、输出结果。然后手动走一遍每执行一行就更新三列数据。这个过程我管它叫“人肉调试”。这个习惯越早养成越好。很多同学一遇到程序跑出来不对就慌了到处乱改。其实90%的情况你只要老老实实从第一行跟到出错的位置问题马上就能看出来。这不是浪费时间这是程序员的底层直觉训练。2. 分支结构实操if...else 与 switch 的核心细节2.1 条件表达式非零即真if后面那个括号里可以放“关系表达式”比如a b、“逻辑表达式”比如a 0 a 10也可以直接放一个变量或者一个数值。C语言里的规则非常简单表达式的值为非0条件就成立为0条件就不成立。这跟其他一些语言里必须是布尔类型的规则不太一样刚接触时容易犯迷糊。比如int a 5; if (a) { printf(a 不等于 0所以这条会打印); }这个代码没有任何问题。a的值是5非0条件成立。刚开始学习时你可以这么记if 判断的不是“真/假”而是“0与非0”。这个区别后面学位运算、学嵌入式编程时还会反复遇到现在记住省得以后反应不过来。2.2 赋值等于号与判断等于号的“幽灵坑”这是C语言初学者翻车率最高的一个操作没有之一。int a 5; if (a 3) { // 注意这里是一个等号 printf(条件成立); }你本意是“a 是否等于 3”但写成了一个等号实际上是“把 3 赋给 a然后拿 a 的值3做判断”。3非0条件成立所以这条竟然能打印。你以为写了判断其实做了赋值而且条件往往莫名其妙地成立。我见过太多这样的案例了尤其是刚学指针那会儿一边写代码一边心里还在想内存布局手一抖就多写少写一个等号。我的习惯是凡是写判断时如果左边是常量就写成常量 变量比如if (3 a)。这样万一少写一个等号变成if (3 a)编译器直接报错就不会留下那个暧昧的“能编译但行为诡异”的隐患了。2.3 悬空 elseelse 到底跟谁配对if...else可以嵌套但嵌套多了容易出问题。看这个例子int a 5, b 10; if (a 0) if (b 0) printf(两个都大于0); else printf(a 不大于0);你可能会觉得else和第一个if配对但C语言的实际规则是else 与它最近的、尚未配对的 if 配对。也就是说上面这串代码里else实际配的是if (b 0)这个内层条件。因为b 0成立所以else根本不会执行你那个“a 不大于0”的提示也就永远打印不出来。解决方案很简单要么用花括号把内层 if 明确括起来要么干脆给每个 if 都配上花括号。别嫌多打两个字符这点字数为你节省的是半小时的抓狂时间。2.4 switch 的贯穿fall-through现象与 break 的意义switch在处理“多选一”的场景下比一串if...else if清爽不少但它有个让新手懵逼的特性——贯穿。int score 85; switch (score / 10) { case 9: case 8: printf(优秀); break; case 7: printf(良好); break; default: printf(继续努力); }注意case 9:下面没有break如果score / 10 9程序会进入case 9后继续向下执行case 8的代码也就是说“优秀”照样打印。这就是贯穿。利用这个特性你可以把多个值合并到同一个执行逻辑里比如上面的case 9和case 8合并为“优秀”。但如果没有意识到贯穿的存在忘记写break就会发生“为什么我选了AB也跟着执行了”的诡异情况。经验写switch时先想好每个 case 的“范围”和“交集”再动手。每写一个 case先问自己一句这里该不该停该停就写break。default建议每次都不偷懒写上哪怕里面什么都不干也能帮你兜住那些没考虑到的值。3. 循环结构实操while、do-while 与 for 的选型3.1 三种循环语法对比先判断还是先执行C语言提供三种循环语法很多人第一反应是“既然都能实现循环为什么不统一留一个”。确实你完全可以用while写所有循环但另外两种在某些场景下更自然、更不容易写错。先看语法形状循环类型判断时机典型应用场景while先判断再执行不知道循环次数可能一次都不执行do...while先执行再判断至少要执行一次比如菜单显示for先初始化、再判断、再更新知道循环次数比如遍历数组do...while是很多初学者最容易忽略的但它其实非常实用。举个例子你要写一个“用户输入密码不对就重新输入”的交互逻辑这种场景天然就是“先执行一次输入动作再判断是否继续”——用do...while写出来非常直观用while反而要先给变量赋个初值去骗过第一轮判断。3.2 循环三要素初始化、条件、更新一个循环能不能正确结束取决于三要素是否配齐初始化循环变量初值、条件什么时候继续、更新每轮结束后怎么改变循环变量。比如for的写法就是长这样for (i 1; i 100; i) { sum i; }初始化是i 1条件是i 100更新是i。三个回合都清楚循环就不会出大问题。用while写同样的功能就得自己把这三个动作拆到不同的位置i 1; // 初始化 while (i 100) { // 条件 sum i; i; // 更新 }很多死循环的根源就是只写了初始化和条件忘记更新或者更新语句被某种条件分支跳过了。比如i 1; while (i 100) { if (i % 2 0) { i; // 只有在偶数时更新 } }奇数时i永远不增加循环卡死。这种问题在你用“人肉调试”一步步跟代码时一眼就能看穿。3.3 实战一九九乘法表双重循环的经典入门九九乘法表到了 Day 5 这个进度正好是练习双重循环的绝佳素材。它的逻辑是外层循环控制行数1到9内层循环控制每行打印几个算式第几行就打印几个。关键设计点是换行位置和对齐。#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保证了每行算式数等于当前行号也就是第一行 1 个、第二行 2 个。%-2d是左对齐占两位让排列整齐。第一次写双重循环的同学经常把printf(\n)放到内层循环里面结果每个算式后面都换行左边成了一个竖排这就是典型的没有想清楚“外层管行、内管列”的分工。3.4 实战二字符串逆序循环与数组的第一场协同作战字符串逆序是各类练习平台比如 PTA上出现率极高的题。本质上它考的是利用循环把数组元素前后对调。这个题完美演示了流程控制和数组怎么配合。#include stdio.h #include string.h int main() { char str[] hello; 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; } printf(%s\n, str); return 0; }用两个变量i和j分别指向字符串的头和尾每轮交换一个字符然后i往右走一步、j往左走一步直到两个指针相遇i j。这个“双指针”思路在 C 语言里非常经典你以后学数组倒置、链表反转、快排划分都会看到它的影子。而这个循环能够正确终止正是靠i和j--这两个更新操作在for括号里“各就各位”。4. 跳转控制break、continue 与 goto 的使用边界4.1 break 与 continue 的核心区别在循环里break和continue都能改变执行流程但方向完全不同。break是立即终止整个循环跑到循环外面continue是跳过本轮循环中 continue 后面的代码直接进入下一轮。举个例子打印 1 到 10 里第一个能被 7 整除的数。for (int i 1; i 10; i) { if (i % 7 0) { printf(%d\n, i); break; // 找到就收工循环结束 } }再比如打印 1 到 10 里所有奇数。for (int i 1; i 10; i) { if (i % 2 0) { continue; // 偶数直接跳过不打印 } printf(%d , i); }break是“我受够了整个循环我都不要了”continue是“这一轮我不玩了下一轮继续”。搞混这两个典型症状是你想跳过某次处理结果整个循环都退出了或者你想提前退出结果它还在那跑个没完。4.2 嵌套循环里 break 只退出一层这个坑我大二时踩过一次印象极深。当时写一个矩阵搜索逻辑两层循环里用了break本意是“找到目标元素就停止全部搜索”。结果程序跑了很久因为break 只跳出它所在的那一层循环外层循环继续跑内层又重新开始。问题不严重但性能被拖垮了。正确的做法有三种用一个标志变量检测到后内外层都判断退出把搜索封装成一个函数用return直接返回结果了解并使用goto但这招要慎重见下文。我建议刚开始学习时优先掌握“标志变量”这个方法因为它在任何语言里都是通用的不会染上依赖goto的习惯。4.3 goto 到底能不能用能但收着用goto在 C 语言里可以实现任意跳转威力极强也因此被很多老师直接禁掉了。但说实话在“从多重嵌套中统一跳出”这个场景下goto 是非常清晰的解决方案。for (int i 0; i 10; i) { for (int j 0; j 10; j) { if (arr[i][j] target) { found 1; goto out; // 直接跳到 out 标签 } } } out: if (found) { printf(找到了\n); }这种“用一次跳转替代一连串标志判断”的写法在 Linux 内核源码里也比较常见主要用在错误处理。初学者如果要用我只有两个约束第一goto 只能往同一个函数内的标签跳绝不能跨函数第二一段代码里 goto 不要超过一两个跳来跳去多了代码就成了意大利面条没人看得懂。你现阶段的主要任务是把if和循环用熟练goto 知道有这么个东西就够了。5. 常见问题速查表与调试心得5.1 C语言流程控制相关的常见报错与逻辑问题下面这张表是我这几年帮学弟学妹排查问题时的总结覆盖了初学者在流程控制阶段最常遇到的几类错误建议收藏起来对照自查。现象可能原因解决思路程序卡住不动CPU 100%循环条件永远成立且循环变量没有更新检查三要素用打印机法观察循环变量是否在变if 没执行但条件明明“看起来”成立写了单等号float 直接比较相等变量值出乎意料检查等号加打印确认类型不要拿浮点直接switch 一个 case 执行后其他 case 也跟着执行缺少break每个 case 末尾检查 break循环只执行了一次更新条件写在 break/continue 后面被跳过把更新写在循环体最末尾或用 for 括号自带的更新内层 break 后外层还在跑忽略了 break 只退出当前层用标志变量或封装函数返回scanf 后循环行为异常输入缓冲区残留了换行符被后续 scanf 读到用空格吃掉换行比如scanf( %c, ch)5.2 条件判断中的边界值错误与差的不是一点点初学者在做循环时还有一个高频翻车点边界条件写反。比如要遍历 1 到 10写了i 10结果少算一次写了i 10结果多算一次。这种差一错误在二分查找、数组遍历里影响很大而且极其隐蔽。我的建议是写循环条件时先在纸上写清楚“循环什么时候继续”而不是“循环什么时候结束”然后用具体值代进去试。比如要执行 5 次你就试i1时进不进、i5时进不进、i6时进不进。三个点一试边界就不会错了。用for (i 0; i n; i)这个“从0开始、小于n”的模式遍历数组今天就可以养成习惯因为 C 语言的数组下标就是 0 到 n-1跟它天然契合。5.3 调试三板斧打印、注释、人肉走查这里分享一个我屡试不爽的调试方法适用于 Day 5 这个阶段的所有代码问题打印观察在关键位置插入printf查看变量的变化轨迹。比如怀疑循环没执行就在循环开头打一行怀疑分支判断不对就把条件里的每个变量都打出来。二分注释法程序逻辑太长先注释掉一部分确认“是这边的问题还是那边的问题”再逐步缩小范围。人肉走查拿张纸代码一行一行过变量值一个个记直到你发现跟预期不符的那一行——那就是 bug 的所在。这三招不要嫌笨它们是一切调试工具gdb、IDE断点的基本原理。会用自己的眼睛跟代码的人以后用调试器也只会更快上手。等你学到后面的章节再引入 gdb 这样专业的逐行调试工具会发现那时你已经能“猜”出问题位置了。6. 两个经典练习题的思路拆解日期计算与鞍点6.1 输入年月日判断这一天是当年的第几天这个题是各类C语言练习册的常客热词里出现了不止一次主要是因为它把分支、循环和数组全串起来了。思路分三步第一步判断闰年。闰年的规则是能被4整除但不能被100整除或者能被400整除。写成条件的优先级要小心int isLeap (year % 4 0 year % 100 ! 0) || (year % 400 0);第二步用一个数组存储每个月1到12月的天数注意2月的天数要根据isLeap加1。int daysInMonth[] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (isLeap) { daysInMonth[1] 29; // 2月 }第三步循环累加前面月份的天数最后加上当月的日。int dayOfYear 0; for (int i 0; i month - 1; i) { dayOfYear daysInMonth[i]; } dayOfYear day;这个题完美检验了你对“数组下标从0开始”的理解数组里存的是“第1月到第12月”但下标是0到11所以累加时要循环到month - 1。很多同学在这里写错多算了一个月或者少算了一个月此时不妨回到第1.3节说的人肉走查法把month 1和month 12的代进去看数组访问范围。6.2 5乘5鞍点问题双重循环里的标志位“鞍点”是指矩阵中某一行最大、同时又是该列最小的元素。这个问题适合学完流程控制和二维数组之后做综合练习思路是遍历矩阵的每一行找到该行的最大值再检查这个最大值是不是它所在列的最小值。核心的“检查”环节是一个双重循环套判断遍历列找最大值再遍历行检查最小。如果找到了鞍点往往需要用一个标志变量记录并提前退出此时正好用上咱们在 4.2 里学的“标志变量跳出嵌套循环”。这道题啃下来你对“嵌套循环中变量作用范围”和“标志位的生命周期”的理解会有一个实质提升。这个题留给你自己动手写一遍写完再对照网上各种解法重点比较一下“别人是怎么组织循环的”“别人在找到鞍点之后是怎么停止的”。最好能写出两三种不同思路的版本这对建立代码手感非常有帮助。写在最后的一点体会其实流程控制这一章真要说难度并不在语法本身而在“把脑子里的逻辑准确翻译成代码”。我练习初期最常犯的毛病是脑子想的是一套手上写的却是另一套。后来养成了一个习惯——写循环前先在注释里写出你的执行计划比如“遍历1到9每行打印当前行号个算式”然后再落代码。这个习惯让我少写了无数遍 printf 调试。另外Day 5 的练习量很重要。我当时基本是每天把课上例题重新默写一遍再独立把作业题的思路写出来跑通。流程控制不像别的知识点光看书不动手是绝对学不会的。你花一晚上把九九乘法表、日期计算、字符串逆序这三道题从头到尾练透第二天再看循环会有一种“原来就这么回事”的轻松感。C语言的承重墙今天就算砌上了。
返回列表