C语言第一次编程作业1-6题:从循环累加到二分查找的拆解与调试
1. 拆题比写代码更重要我拿到题目后的前十分钟坦白说第一次编程作业给我留下的最深印象不是“难”而是“无从下手”。题目不多一共六道从1排到6可我盯着屏幕看了很长时间脑子里一直纠结同一个问题第一行代码到底该写什么。后来我才想明白题量少的作业并不是在考你会不会背语法而是在逼着你建立一套完整的做题流程读题、拆解、编码、测试、修改。这套流程一旦形成之后的每一次项目开发、每一道算法题都会跟着受益。如果你也正面对一份类似的“第一次作业1-6题”这篇内容就是我按自己当时的做法一步一步拆给你看的。不只有六道题的答案还有每个考点背后踩过的坑以及我事后复盘总结出来的通用方法。我把拿到题目后的前十分钟花在了“拆题”上而不是马上开编辑器。具体做法很简单打开一个表格把六道题的信息逐行填进去每个标题记四列内容——题号、需求摘要、核心考点、可能埋下的坑。做完之后这张表长这样题号需求摘要核心考点容易踩的坑1输入n求1到n的和for循环、累加变量结果溢出、循环边界写错2输入字符串输出逆序字符数组、双指针/遍历忘记数组末尾的\0、空格读入3输出99乘法表双层循环、格式化输出列格式对不齐、乘数顺序颠倒4输入年份判断闰年分支判断、逻辑表达式漏掉“百年不闰、四百年再闰”5在有序数组中二分查找边界控制、循环不变量mid更新导致死循环6统计若干成绩的平均分、最高分、最低分并降序输出数组、函数、综合运用n为0、下标边界、排序算法写错这张表最大的价值是把“我可能不会做”变成“我至少可以先做哪一部分”。拆完之后我给自己定了一条做题顺序先做第1题热身再做第2、3题巩固基础最后集中火力打第4、5题的难点和第6题的综合应用。拆题这个动作本身不产生代码但它让我对整份作业的工作量和优先级有了清晰判断后面的编码过程基本没有回头路。2. 六道题逐题拆解从循环累加到综合应用2.1 第一题循环累加别被“简单”骗了第一题的需求通常长这样输入一个正整数n计算1 2 3 …… n的结果并输出。表面上看确实简单无非一个for循环加一个变量累加。但我当时差点在这道题上犯一个经典错误用int存放结果。先说结论我最后写出来的核心代码是这样#include stdio.h int main() { int n; long long sum 0; scanf(%d, n); for (int i 1; i n; i) { sum i; } printf(%lld\n, sum); return 0; }为什么这里要特意强调数据类型因为在多数入门教材里题目给出的测试用例都是很小的数字比如n等于100int完全装得下。但如果你把n换成100000甚至更大int的极限值大约是21亿而1到100000的和是50亿已经超出了int的表示范围。我在本地测试时故意把n输入到100000程序输出负数才意识到是溢出。这道题还有一个容易被忽略的点循环条件到底写i n还是i n。第一次写的时候我写成了结果n等于5时只加到了4输出了10而不是15。这类边界问题在视觉上很难发现最简单的验证办法就是拿最小的n——也就是1——去跑一遍。n等于1时如果输出不是1那边界一定有问题。2.2 第二题字符串逆序不要急着“原地交换”第二题是输入一个字符串输出它的逆序。我一开始的想法很直白从后往前把所有字符打印一遍不就行了。这么做确实能通过一部分测试但它没有真正改变字符串本身严格来说只是“反向输出”。如果后面的改造需要用到已经反转后的字符串这种写法就废了。更稳妥的做法是直接在字符数组里做交换把字符串原地反转。核心代码如下#include stdio.h #include string.h void reverseString(char s[]) { int left 0; int right strlen(s) - 1; while (left right) { char temp s[left]; s[left] s[right]; s[right] temp; left; right--; } } int main() { char str[100] {0}; scanf(%s, str); reverseString(str); printf(%s\n, str); return 0; }这道题真正让我学到东西的地方是字符数组的定义。刚开始我觉得char str[100]足够了确实足够装99个字符加一个结尾符。但问题是scanf(%s, str)遇到空格会截断输入如果测试数据是“hello world”程序只能读入“hello”反转输出“olleh”。题目如果规定字符串中可能包含空格就必须改用fgets读一整行并且在反转前去掉末尾换行符。这一点是很多第一次交作业的人会踩的板子。调试这个反转逻辑时还有个小技巧在纸上写下hello的数组下标从0到4然后用两个指针模拟交换过程。模拟两轮之后你会发现right的初始值必须是strlen(s) - 1而不是strlen(s)。因为数组最后一个有效字符的下标比长度小1多写一位交换时就会用到字符串结尾的\0结果会变得非常奇怪。2.3 第三题99乘法表真正的考点是输出格式第三题是输出一个99乘法表。逻辑层面无非两层循环外层控制行数内层控制每行表达式个数。可我第一次交上去的版本数字和等号之间没有对齐评审反馈说看着像“乱码表格”。问题就出在printf的格式控制上。我最终采用的是这样一段代码#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; }这里的核心在于%-2d。平时我们写%d系统会自动按实际宽度输出数字位数不同时整体看起来会错位。%-2d的意思是左对齐并且至少占2个字符宽度。这样一来4和16的起始位置就都能对齐。两个表达式之间我多加两个空格每一行的最后一个表达式后面也会带多余空格不过对于这道题来说不会判错想更严谨可以加一个判断来控制是否输出尾部空格。我在乘法表这道题上反复出过的问题是循环变量的顺序。题目里的表达式通常是“左小右大”也就是1*11、1*22、2*24内层变量应该从1循环到外层变量。但有些同学为了省事外层层内层都从1到9输出的乘法表整个变成长方形缺少了熟悉的三角结构。做这类题目一定要先想清楚需求里的“行”和“列”分别对应什么而不是只看代码能不能跑。2.4 第四题闰年判断一条表达式胜过四个if第四题是输入年份输出它是否为闰年。这个需求看起来只是两个分支但隐藏着一个非常典型的条件组合问题。闰年的判断规则是能被4整除但不能被100整除或者能被400整除。我第一次看到这个规则第一反应是写多重ifif (year % 4 0) { if (year % 100 ! 0) { printf(闰年\n); } else { printf(平年\n); } }这种写法在2000年这种年份上就会出错。因为2000能被4整除也能被100整除但也能被400整除按规则它应该是闰年。上层的if被“能被4整除”放行之后内层直接按“能被100整除”把它判成平年结论就反了。后来我改成用一条逻辑表达式#include stdio.h int isLeap(int year) { return (year % 4 0 year % 100 ! 0) || (year % 400 0); } int main() { int year; scanf(%d, year); if (isLeap(year)) { printf(闰年\n); } else { printf(平年\n); } return 0; }这条表达式既简洁又不容易漏条件。很多人写错是把两个条件用成写成“能被4整除且能被400整除”那几乎所有闰年都会被判错。我自己的测试用例一般固定取4个年份1900年应该是平年2000年应该是闰年2024年应该是闰年2021年应该是平年。这四个值覆盖了既能被100整除又不能被400整除、能被400整除、普通能被4整除、完全不能被4整除这四种分支。这道题给我的启示是遇到带规则的知识点先把它翻译成数学条件再翻译成逻辑表达式最后才动手写分支结构。顺序反过来的话很容易越写越绕。2.5 第五题二分查找死循环和边界是同一个问题第五题是给定一个升序排列的数组和一个目标值找到目标值的下标找不到则返回-1。这是六道题里算法味道最浓的一道也是我第一次真正理解“边界”这个词的题目。网上关于二分查找的写法很多我最终参考用的是这样#include stdio.h int binarySearch(int arr[], int n, int target) { int left 0; int right n - 1; while (left right) { int mid left (right - left) / 2; if (arr[mid] target) { return mid; } else if (arr[mid] target) { left mid 1; } else { right mid - 1; } } return -1; } int main() { int a[] {1, 5, 8, 12, 15, 20}; int n sizeof(a) / sizeof(a[0]); printf(%d\n, binarySearch(a, n, 8)); // 输出2 printf(%d\n, binarySearch(a, n, 21)); // 输出-1 return 0; }我最开始写mid的时候用的是(left right) / 2。乍一看没问题但如果left和right都是很大的数相加之后可能超过int上限导致溢出。用left (right - left) / 2就可以避开这个问题。这个细节在数组很大的时候才体现出来但作为第一次作业养成好习惯比碰运气重要得多。二分查找更隐蔽的坑在left mid 1和right mid - 1。如果写成left mid或right mid当目标值不在数组中时两个指针可能永远无法收拢程序就卡死在循环里。这本质上是一个“区间收缩”问题左闭右闭区间里已经排除mid那么下一次搜索范围就不该再包含mid。判断标准是始终保证搜索区间为空时循环结束。我把三种边界写法放在一起对比过感受更加明显写法循环条件left更新right更新是否容易死循环左闭右闭left rightleft mid 1right mid - 1不易左闭右开left rightleft mid 1right mid需要配合约定一致左开右开left 1 rightleft midright mid对初学者复杂题目如果只要求返回是否存在左闭右开也是常用选择但第一次做作业我建议始终盯住一行字你定义的搜索区间是哪一段你就要保证每一次循环都让这段区间真的变小。能做到这一点边界题基本都能扫清。2.6 第六题成绩统计综合题把前五题打通第六题是一个典型的综合应用输入学生人数n以及n个成绩输出平均分、最高分、最低分并把成绩按从高到低排序。这题把前五题的数组操作、循环累加、比较分支和格式化输出全部串起来了。我当时写出来的完整程序大致是#include stdio.h int main() { int n; scanf(%d, n); if (n 0 || n 1000) { printf(输入不合法\n); return 0; } int scores[1000]; for (int i 0; i n; i) { scanf(%d, scores[i]); } int sum 0, max scores[0], min scores[0]; for (int i 0; i n; i) { sum scores[i]; if (scores[i] max) max scores[i]; if (scores[i] min) min scores[i]; } for (int i 1; i n; i) { int key scores[i]; int j i - 1; while (j 0 scores[j] key) { scores[j 1] scores[j]; j--; } scores[j 1] key; } printf(avg%.2f max%d min%d\n, (double)sum / n, max, min); for (int i 0; i n; i) { printf(%d , scores[i]); } printf(\n); return 0; }我在这个题目上卡了三处。第一处是没有考虑n非法输入n等于0时直接访问scores[0]程序直接崩溃。第二处是平均分输出忘记转换成浮点数sum / n是整数除法90和100平均出来永远是90而不是95。第三处是排序循环里判断条件写成了向后移动的升序逻辑导致结果反过来了。关于排序这个版本使用的是插入排序思想是从第二个元素开始逐个往前比较遇到比它小的就往后挪。排序算法本身不是第一次作业的考核终点但通过这道题我真正体会到“一段正确但顺序不够好的代码会带来多大调试成本”。如果只是应付作业可以先用选择排序或冒泡排序代码更直白出错后也更好排查。3. 调试期间最容易踩的三个坑编译报错、下标越界、格式错位写完六道题的初版大概只花了一半时间剩下时间全部耗在调试上。现在复盘有三个坑几乎每个人第一次写作业都会遇到而且它们有个共同特点编译器和运行结果会给你各种暗示但你未必看得懂。第一个坑是scanf漏写取地址符号。比如scanf(%d, n)你在学生时代大概率写过。这段代码在部分编译器里居然能编译通过直到运行时崩溃才暴露。编译器的警告信息会提示“format specifies type int * but the argument has type int”如果开着警告选项就能看到。处理方式很简单看到变量就条件反射式地确认是否带尤其是数组名不需要取地址但整型和浮点型变量都要。第二个坑是数组下标越界。二分查找那题我测试时只试了奇数个元素的数组没试偶数个结果有一次mid计算出的下标让arr[mid]访问到不存在的区域。这次经历让我意识到越界不一定让程序立刻崩溃它可能只是给出一个乱值然后把整个判断逻辑带偏。可以用编译器自带的调试工具或者在关键循环里输出当前下标值来定位。加了打印之后数组越界几乎都能在五次运行内找到。第三个坑是格式输出错位。99乘法表不用说了第六题里也有个类似的坑输出avg90.00时我一开始用了%f结果输出了90.000000。题目的预期往往只要两位小数所以必须写成%.2f。这类问题在题目样例中特别容易忽略因为样例未必覆盖精度要求。提交前把每个输出都当成文本格式去核对一遍而不要只关心数字对不对。我一般会在编译时打开全部警告来快速暴露第一类问题。在Linux或macOS下是gcc -Wall -g main.c -o mainWindows下如果用的是常见的IDE项目属性里也会有一项“警告级别”尽量调到最大。-g选项用于保留调试信息配合断点调试六个题目的bug基本上都能在一个小时内清完。调试不是天赋问题只是熟练问题你看到的报错越多下一次定位越快。4. 提交代码前我会用这张自检清单从头到尾过一遍写完整份作业我最开始的想法是“能跑就是胜利”后来吃过亏才知道能跑和能交是两回事。这里说的“能交”指的是按题目要求重新读一遍边界用例全部通过输出格式完全一致代码结构和命名不乱。我给自己总结了一份提交前清单大概包括下面的项目每次交作业前都会过一遍。第一是重新读需求。这个看起来废话但实际很关键作业要求是“从大到小输出成绩”你实现了“从小到大”这就是典型的顾头不顾尾。第二是边界测试。每道题挑两三个异常情况跑一遍第1题n等于1和很大的值第2题空字符串和含空格字符串第4题1900和2000第5题目标值在开头和不在数组中第6题n等于0和成绩全相等。边界测试不要求写得很复杂关键是逼自己把默认情况之外的可能性想一遍。第三是输出格式。留意空格、换行、小数点位数以及表达式对齐。格式类题目永远以评审的预期为准不以自己本地的结果为准。第四是代码风格。缩进是否一致、变量名是否看得懂这些虽然不影响1到6题的得分但会潜移默化地影响你的编程习惯。第五是备份。当时我把源文件在本地路径和保存网盘上都保留了一份还顺手压缩归档了一下。看起来小题大做但如果你经历过写程序中途电脑崩溃全盘重来的情况就知道备份的意义了。检查完这些之后我会把作业当成别人写的代码重新看一遍。这个“陌生化视角”是最后的防呆手段自己写的代码很容易默认哪里都对但换个格式再看经常能发现变量名拼错、多余语句、甚至循环条件写反等低级错误。5. 复盘的第一步把做题流程沉淀成可复用的方法论六道题全部完成并提交后我做的最后一件事不是庆祝而是把整个做题过程重新回放了一遍。我发现自己最早的错误集中在两处一是不读题就动手二是测试不充分就认为程序正确。这两个问题不分领域、不分难度哪怕之后去做大型项目也会反复出现。所以我给自己定了一条做题流程之后几次作业一直沿用效果明显拿到任务先拆出需求清单再对每个需求写出最小可验证的代码接着跑正常值和边界值最后回看格式与风格。这套流程用一句话概括就是“小步快跑边写边验证”。不要在脑子里完整想好整个程序再一次性敲出来那种做法只对简单任务有效遇到综合题很容易在某个角落出现一个你看不见的逻辑窟窿。从这份第一次作业里得到的东西可能比六道题本身更持久。每道题都对应一个基本功循环、数组、字符串、分支、边界、综合组织。我第一次把它们串在一起做花了很长时间可做完之后后面写课程设计时就没有再对“开头第一行写什么”有过犹豫。代码能力的建立过程其实就是这样一份作业一份作业叠出来的你以为自己在完成题目其实是在完善自己的思维方式。如果你正在面对自己的第一份编程作业我的建议很简单不要急着让代码跑通第一步先拆题最后一步一定要自测。中间遇到报错记录一下报错信息不要改一个变量就重新跑一圈。等六道题全部结束回头把踩过的坑整理成几句话你会发现第一次作业带给你的东西远远超过一道题的分数。