☰
C语言选择结构三题拆解:闰年、计费、排序的常见坑与正确写法
2026/10/10 12:43:34 网站建设 项目流程

每年讲到第三章的选择结构,我的班级群就会准时炸开锅。不是没人写得出代码,而是每个人都写出了一版“看似对、一跑就错”的代码。尤其是教材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手动走一遍:

  1. 初始 a=3, b=1, c=2
  2. 第一轮if (a > b):3 > 1,交换,得到 a=1, b=3。现在a是前两个数里较小的那个。
  3. 第二轮if (a > c):1 > 2 不成立,不交换。a保持1。现在a是三个数里最小的。
  4. 第三轮if (b > c):3 > 2,交换,得到 b=2, c=3。b比c小。
  5. 输出 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道题用自己的语言重新实现一遍,不要看任何参考答案。写完之后,再对着上面的测试用例逐条验证。等你能做到“不加括号就知道优先级”“看到浮点数就想边界”“排序前先想到临时变量”,你这关就算真正过了。

这三道题并不难,难的是你愿不愿意在简单的东西上多花一点笨功夫。编程这个手艺,本质上就是靠这种扎实的小练习一点一点堆出来的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询