C语言选择结构三题拆解:闰年、计费、排序的常见坑与正确写法
每年讲到第三章的选择结构我的班级群就会准时炸开锅。不是没人写得出代码而是每个人都写出了一版“看似对、一跑就错”的代码。尤其是教材3.2节那三道编程题——在不少C语言入门课本里它们被排成“分支结构小节练习”题目本身一句话就能读完但交上来的作业里能一遍跑通的不到三分之一。这三道题分别是闰年判断、出租车分段计费、三个整数排序。听着都眼熟吧很多高校的基础编程题库里都有它们的变体比如“小玉买文具”“分段函数求值”都是同一个套路。今天我就把这三道题从头到尾拆一遍从错误写法到正确写法从运算符优先级到边界值测试把我这几年批作业时攒下的翻车案例全摆出来。无论你是在校学生、自学编程的初学者还是偶尔需要带新人的老手这篇文章都值得你花十分钟慢慢读。1. 这三道题为什么是新手的分水岭1.1 三道题的本来面目先说清楚题目我用的是最经典的一版C语言描述其他语言思路一样题目一输入一个年份判断它是否为闰年。闰年条件是“能被4整除但不能被100整除或者能被400整除”。题目二某市出租车计价规则为起步价8元包含3公里超过3公里但不超过10公里的部分每公里1.6元超过10公里的部分每公里2.4元每趟另加1元燃油附加费。输入行驶里程输出总费用。题目三输入三个整数按从小到大的顺序输出。这三道题没有任何算法难度没有数组没有循环没有函数。它们唯一用到的语法就是if、else if、else、关系运算符、逻辑运算符外加一个用来交换两个变量的临时变量。可恰恰是这些最基础的语法细节把初学者按在地上摩擦。1.2 真正的考点不在“会不会”而在“熟不熟”我带过的学生里能把if语句语法背得滚瓜烂熟的人很多但一写这三道题就露馅。问题出在哪第一他们对运算符优先级只有“大概印象”没有精确记忆。和||谁先谁后%和谁先谁后括号该加还是不该加这些判断慢半拍代码就写不对。第二他们对边界值没有敏感度。3公里整算哪一档10公里整算哪一档年份等于0怎么办这些细节教材上不会专门讲只能靠实战摔出来。第三他们不习惯做“验证”。写完了输入一个正常值输出对了就觉得自己会了。很少有人会专门拿边界值、特殊值去测试自己的程序。所以我说这三道题是分水岭——跨过去你对选择结构的理解就真正落地了跨不过去后面学到循环、数组、函数时每节课都会因为这里埋下的坑而反复踩雷。2. 第一道闰年判断——短路求值与优先级先后2.1 题目与第一版常见错误先看题目要求的完整条件能被4整除但不能被100整除或者能被400整除。用数学语言拆一下就两句话条件A能被4整除 且 不能被100整除条件B能被400整除最终结果条件A 或 条件B很多新生的第一版代码长这样#include stdio.h int main(void) { int year; scanf(%d, year); if (year % 4 0 year % 100 ! 0 || year % 400 0) printf(%d是闰年\n, year); else printf(%d不是闰年\n, year); return 0; }你把这代码跑一遍2000年、2024年、1900年结果居然全对。于是学生很高兴地交上来我却在旁边看得后背发凉。为什么因为这行if条件根本没加括号。它之所以能“碰巧”跑对完全依赖于C语言中优先级高于||这个规则——条件被解析成(year % 4 0 year % 100 ! 0) || (year % 400 0)。如果把题目改一下或者换一种写法这个不假思索的“碰巧”下一秒就会翻车。2.2 运算符优先级是最大的坑闰年判断这个题目真正想考的就是你把条件和运算符搅在一起时还能不能保持头脑清醒。先列一下这题涉及的运算符优先级从高到低运算符含义优先级!逻辑非最高*/%算术运算符次高-算术运算符再次关系运算符中!等于/不等于较低逻辑与更低||逻辑或最低也就是说year % 4 0会先算year % 4再比较 0这个没问题。和||混用时先结合这也是对的。真正怕的是学生的脑子没跟上这个顺序以为自己写的是(year % 4 0 year % 100 ! 0) || (year % 400 0)实际编译器也是这么理解的但换个上下文比如把两个条件互换位置或者误写一个结果就完全不是那么回事了。经验之谈只要和||同时出现就老老实实加括号。不是C语言不允许不加括号而是不加括号的代码读代码的人包括三天后的你自己要在脑子里做一次运算符优先级分析而人脑做这种分析是最容易出错的。代码是写给人读的顺便给机器执行。2.3 正确写法与验证用例我给学生的参考写法是这样#include stdio.h int main(void) { int year; scanf(%d, year); if ((year % 4 0 year % 100 ! 0) || (year % 400 0)) printf(%d是闰年\n, year); else printf(%d不是闰年\n, year); return 0; }括号一加逻辑一目了然。条件部分的意思和题目要求完全一一对应左边括号是“能被4整除但不能被100整除”右边括号是“能被400整除”两者是或的关系。写完这道题我还要求学生在本地跑一组验证数据输入年份期望结果说明2000闰年能被400整除2024闰年能被4整除且不能被100整除1900不是闰年能被4整除但也能被100整除且不能被400整除2023不是闰年普通年份4闰年小年份边界这组用例覆盖了闰年公式里的所有分支。如果只看一个2000年就交作业等于只验证了一个||后面的分支前面的分支到底写没写对完全不知道。还有一个进阶问题值得想一想如果年份是0程序会输出什么闰年的定义是对公历年份而言的0年、负数年份在历史上没有意义但程序不会管这些。所以严谨一点可以在条件里加上year 0的校验不过入门阶段不强求知道有这回事就行。3. 第二道出租车分段计费——边界值和浮点精度决定成败3.1 题目说明与计费逻辑拆解第二道题是分段计费这类题目在编程题里出镜率极高变体包括水电费阶梯计价、快递运费、商场打折。核心都是同一个根据输入落在哪个区间应用不同的计算公式。先把题目规则翻译成数学分段函数当里程 d 3 时费用 8 1 9 元当 3 d 10 时费用 8 (d - 3) * 1.6 1当 d 10 时费用 8 (10 - 3) * 1.6 (d - 10) * 2.4 1注意起步价8元包含的是“3公里以内”所以3公里整的时候仍然是9元跟2.5公里一样绝不能给3公里整算成8 0 * 1.6 1再来一遍——虽然结果一样但逻辑上是错的。然后把燃料附加费这1元放到最后加或者放进起步价里变成9元都可以只要全程保持一致。3.2 四个高频翻车现场这个题目我在作业里见过的高频错误列出来给各位避雷。错误一等于号边界丢失。有人写if (d 3)没问题但紧接着写else if (d 3 d 10)这里把10公里整漏掉了。输入10的时候既不进第一档也不进第二档直接掉进最后的else按超过10公里计费结果多收了一档差价。错误二区间条件写反或重复。有人写else if (d 3 d 10)看起来没问题但前提是前面的if必须是d 3而不是d 3否则3公里整会匹配到第一个分支后面的 3分支永远没机会执行。这类错误隐蔽就隐蔽在程序不报错输入小数字时结果也对只有当你测试恰好等于3的数字时才会发现。错误三浮点直接比较相等。里程是里程表读出来的计算机里以double存储。3.0这个值在浮点数里并不是精确的3.0而是多少带一点误差。如果你写出if (d 3.0)这样的判断最后能不能进这个分支完全看运气。分段计费场景下不要用去比较浮点数用区间判断比如d 3.0。错误四燃油附加费忘加或者加错位置。这个属于粗心。但更有意思的是有人把1元加到每一档的公式里结果超过10公里的部分多加了两次。3.3 正确实现与测试用例表这是我的参考写法#include stdio.h int main(void) { double d; double cost; printf(请输入行驶里程公里: ); scanf(%lf, d); if (d 3) { cost 8 1; } else if (d 10) { cost 8 (d - 3) * 1.6 1; } else { cost 8 (10 - 3) * 1.6 (d - 10) * 2.4 1; } printf(总费用: %.2f 元\n, cost); return 0; }注意这里第二档的边界条件我写的是d 10而没有写d 3 d 10。因为能走到else if这一步说明d 3已经隐含成立了。这就是多分支结构的顺序优势——前面的条件把后面的条件简化掉了。写else if (d 10)可读性更好也不容易写错。测试用例我建议至少跑这五组输入里程期望费用测试目的29.00起步价内39.00恰好起步价边界1020.20第二档右边界10.521.40刚进入第三档09.00等价于起步价为什么10公里整是20.20因为8 (10 - 3) * 1.6 1 8 11.2 1 20.2。如果程序输出21.4说明10公里整被错误地算进了第三档。我还见过有人把double和printf(%.2f)配合出错——输入时用%lf输出时用%.2f这没问题但有人输入用%d读里程读进去的全是垃圾值。真遇到这种问题先检查scanf的格式控制符和变量类型是否匹配通常能解决一半的玄学Bug。提示分段的题目不要在一开始就把所有公式写完再判断。先判断区间再写公式顺序别乱。如果后面想改成“超过15公里每公里3元”只需要改第三档公式和第二个else if的边界其余代码不用动。4. 第三道三个数排序——交换法的直觉与其边界4.1 题目、通行错误与一个直觉陷阱第三道题是“输入三个整数按从小到大输出”。很多学生的第一反应是两两比较if (a b b c) printf(%d %d %d\n, a, b, c); else if (a c c b) printf(%d %d %d\n, a, c, b); // ... 还有四个分支这是一种写法但极其繁琐而且极容易漏分支。三个数的排列一共有6种你得写6个else if每个里面还要把三个数按照对应顺序打印出来。写的时候脑子稍微短路一下就漏掉一种情况。程序跑起来通常不会报错只是某些输入下什么都不输出——因为6种排列没有覆盖完整。比这更常见的错误是“想当然交换”if (a b) { a b; b a; }你以为这是在交换实际上执行完a b之后原来的a值已经被覆盖了再执行b a时b拿到的是a被覆盖后的值也就是原来的b。两个变量变成了同一个值原来的a彻底丢了。这是交换变量最经典的翻车现场。4.2 手动跟踪一遍排序过程先看正确解法——比较交换法核心思想是“让最小值依次沉淀到最前面”。int a, b, c, t; if (a b) { t a; a b; b t; } if (a c) { t a; a c; c t; } if (b c) { t b; b c; c t; } printf(%d %d %d\n, a, b, c);为什么这样三次比较就能完成排序我拿3 1 2手动走一遍初始 a3, b1, c2第一轮if (a b)3 1交换得到 a1, b3。现在a是前两个数里较小的那个。第二轮if (a c)1 2 不成立不交换。a保持1。现在a是三个数里最小的。第三轮if (b c)3 2交换得到 b2, c3。b比c小。输出 1 2 3。关键套路是第一轮保证a是a和b中的较小者第二轮保证a是三者中的最小者第三轮保证b和c有序。三轮做完全局有序。很多学生犯迷糊的地方在于第一轮之后a和b已经变了第二轮拿新的a去和c比较会不会有问题不会这正是思路的精妙之处。你只需要记住一句话每一轮比较都让当前“最左边的位置”变成剩余数字里的最小值。4.3 三种解法横向对比与扩展建议除了比较交换法还有另外两种常见方案方案一枚举所有排列前面提到的6个else if分支。优点是逻辑直白缺点是代码量大、容易漏分支而且一旦改成4个数排序就彻底爆炸。方案二两两比较交换法上面推荐的。优点是对三个数来说最简洁也可以推广到更多个数——想排四个数就再加一轮if (c d)之类的比较逻辑模式完全一样。缺点是需要临时变量初学者容易在交换步骤上栽跟头。方案三用库函数如C语言里qsort、Python里sorted。优点是杀鸡用牛刀绝对正确缺点是如果这题的教学目标是练习if分支用库函数等于跳过了练习目标。入门阶段不建议。我的建议很明确写排序题能不用库函数就尽量不用。不是库函数不好而是排序过程中的交换、比较、覆盖这些都是理解变量和内存的基础动作亲手拆一遍和自己调sort函数得到的经验完全不是一个量级。另外提一句这三个数的类型是整数。如果把题目改成三个浮点数代码逻辑一模一样但scanf里的%lf、printf里的%f千万别写成%d否则又是经典的“输入一堆乱码、输出一堆鬼”现场。5. 三道题刷完我写给学生们的自检测试5.1 三道题共同暴露的编程习惯问题每年批完这三道题我都会发现一个规律能一次写对的学生不是因为天赋而是因为他们有以下几个习惯——而这些习惯跟语法知识本身没多大关系。第一个习惯写代码之前先在草稿纸上画清楚逻辑。特别是分段计费这种题先画数轴标出3和10两个分界点再写上每一段对应的公式。画完再写代码出错的概率下降一半。第二个习惯写完以后不急着跑“正常值”而先跑“边界值”。边界值就是分界点本身闰年题里的1900和2000计费题里的3和10排序题里相等的数和负数。只要边界值对了正常值基本不会出错。第三个习惯把“能编译”和“正确”分开看。很多学生一看到编译通过就长舒一口气以为万事大吉。其实编译通过只是开始逻辑对不对完全是另一回事。C语言里很多逻辑错误不报错比如漏分支、覆盖变量、区间漏掉等号这些编译器通通不管只能靠测试用例来暴露。5.2 一个随手可用的“边界值清单元”综合这三道题我整理了一个通用的测试清单不限于这题任何分段、判断、排序类题目都能沿用测试类型测试值示例要观察什么最小值边界年份0、里程0、排序中的负数程序会不会崩逻辑是否合理区间左端点里程3公里整该进上一档还是这一档区间右端点里程10公里整会不会漏进下一档区间内普通值里程5公里公式本身是否正确跨越区间的值里程10.01第三档是否正常接棒相等值排序两个或三个数相等排序是否会重复输出或丢失数据逆序数据排序输入3 2 1三轮交换是否全部触发这个清单非常朴素但每次检查都能救回一两道题。养成习惯之后写任何程序都会顺手过一遍边界值出错率肉眼可见地下降。5.3 个人教学经验的补充最后分享一点我自己的体会。有不少学生来问我“这三道题这么基础是不是会了就行不用太较真”我的回答通常是基础题不要求快要求稳。这三道题虽然简单却涵盖了选择结构里最容易踩的所有坑。能把它们写得每一步都经得起追问后面学到循环嵌套、函数递归时你才不会一边写一边怀疑自己是不是哪里又漏了边界。如果你现在还处于刚学编程的阶段我建议你把这3道题用自己的语言重新实现一遍不要看任何参考答案。写完之后再对着上面的测试用例逐条验证。等你能做到“不加括号就知道优先级”“看到浮点数就想边界”“排序前先想到临时变量”你这关就算真正过了。这三道题并不难难的是你愿不愿意在简单的东西上多花一点笨功夫。编程这个手艺本质上就是靠这种扎实的小练习一点一点堆出来的。