如果你是哈工大软件学院或计算机相关专业的新生,第一次打开SSE平台,看到“C语言编程练习3”这个练习名称,心情大概很复杂——前两次练手还停留在printf("Hello World")和简单分支循环,突然题目列表里出现了数组、字符串、指针相关的描述,英文题目读起来也费劲。我当年就是在这个节点上第一次意识到,原来代码“能跑”和“通过评测”是两回事。SSE是学院自建的在线程序训练与评测平台,课程内的练习和实验都在这里提交,判题逻辑跟机考完全一致。练习3通常出现在开课第三到第四周,表面上是几道数组与字符串题目,实际上它是整个C语言课程里第一次真正考验“数据处理能力”的关卡。这篇文章面向正在做练习3的同学,也会照顾到刚接触在线评测平台的新手,把平台机制、常见题型、本地调试方法和典型报错一次性梳理清楚,你可以把它当备课笔记,也可以当排查手册用。
1. 练习3到底考什么:从课程定位看核心考点
1.1 为什么说练习3是分水岭
练习1和练习2的任务大多是熟悉语法:输入输出、if-else、while/for、switch,题目套路清晰,无外乎“读两个数比大小”“打印几行规律图形”这类。到了练习3,题目要求开始从“单个数值处理”转向“批量数据操作”,数组和字符串就是第一批工具。你在课堂上听老师讲数组还觉得简单:不就是连续内存、按下标访问?可一旦题目变成“把字符串逆序”“把整型数按位拆开”“把一组数排序后输出”,你需要在脑子里同时维护多个下标和一个正在变化的数据集合,很多人的逻辑能力在这里第一次接受压力测试。
另外,练习3往往也是指针概念第一次正式登场的时间点。指针不像数组那样可以直接拿下标“看见”数据,它描述的是地址关系,对很多新手来说非常抽象。“为什么函数里改了数组的值,main里也变了,而改int局部变量却不变?”这类问题基本从这一周开始密集出现。你如果能在练习3阶段把数组和指针的关系理清,后边的链表、结构体、动态内存,就是水到渠成的事;要是这周囫囵过去了,到练习5、练习6会连题目都读不懂。从这个角度讲,练习3是整个课程前期真正意义上的分水岭,值得投入比前两次练习多一倍的时间。
1.2 SSE评测机制:AC到底是什么意思
哈工大软件学院的SSE平台本质上是一个在线评测系统,你提交代码后,后台会经历编译、运行、比对输出三个阶段。每道题都有多组测试数据,包括普通样例、边界样例和极端样例,只有所有测试点全部通过,状态才会变成Accepted,也就是大家常说的AC。如果只有部分数据通过,你会看到Wrong Answer或Partial Correct,评测端通常不会明确告诉你是哪组数据挂了,这导致很多新手只测题面样例就觉得代码没问题,结果反复WA。
练习3阶段最常见的三个评测状态是Compile Error、Runtime Error和Wrong Answer。Compile Error多是语法问题,平台会给出编译器输出,细心看就能找到是缺了头文件还是括号不匹配;Runtime Error多半是数组越界、除零、非法指针访问这类运行时问题;Wrong Answer则复杂一些,可能出在逻辑边界、输入读取方式或输出格式上。你要建立的第一个习惯是:评价程序好坏的标准不是“我本机跑出来对”,而是“所有测试点都过了”。这个认知转变得越早,后面做练习时越省力。
1.3 练习3题目的源头与风格
哈工大的练习题目不完全是自己原创的,很多类型你会在其他评测平台上看到类似版本。比如PAT乙级、杭电OJ、北大POJ、西工大NOJ,都有大量同款改编题。练习3最有代表性的题型集中在数组处理和字符串处理:字符串逆序、进制转换、日期间隔天数、数组循环移位、冒泡排序、筛选完数。题目描述通常用英文或中英混合,参数名用得很简洁,读题时需要特别小心输出格式的要求,例如“每个数字后用空格隔开”“每行末尾不得有空格”“输出时保留两位小数”。这些细节恰恰是评测比对的重点,题目不会因为你思路正确就放你一马。
这也是为什么我建议你练习3之外可以顺手去PTA或者NOJ刷几道同考点题目,尤其是翁恺老师C语言课程里的配套习题,题型和SSE高度相似。不同平台虽然界面不同,但底层的判题逻辑都一样,练多了手感会迁移,后面在SSE上做题会快很多。
2. 本地开发环境怎么搭:VS Code与GDB调试实战
2.1 VS Code跑C语言代码的最小配置
很多同学觉得练习3需要先装一个庞大的IDE,其实完全不必。我在日常中推荐VS Code加gcc的方式:轻量,跨平台,调试还算顺手。Windows下最简单粗暴的方案是安装MinGW-w64,把gcc加入PATH;macOS和Linux自带的clang/gcc直接用。装好之后用VS Code打开一个文件夹,写好hello.c,配置一个编译任务,按Ctrl+Shift+B就能编译,再装个C/C++插件就能按F5调试。我实测下来,这套组合应付SSE练习完全够用,加载速度比IDE快得多。
VS Code新手常报“无法打开源文件”,原因无非三种:一是编译器没配置好,头文件搜索路径不对;二是文件名或目录里带中文、空格,导致编译命令被截断;三是tasks.json里的args写错了,比如把${file}写成了${fileDirname}。遇到报错先不要重装软件,打开终端手动执行gcc hello.c -o hello,如果手动能过而编辑器报错,问题一定出在配置;如果手动也报错,再去检查环境变量。这个排查顺序能帮你在练习3阶段少走很多弯路,因为平台上面的编译错误信息和本地是一致的,本地能过,平台大概率也能过。
2.2 GDB调试:崩溃之后别急着重写
练习3阶段的程序运行错误,用printf大法其实也能对付,但效率太低。GDB能让你在程序崩溃的那一刻看到变量值和调用栈,这比蒙头猜要快得多。具体到练习场景,我只推荐记四个高频命令:break设置断点,next单步执行,print观察变量,bt查看调用栈。调试前用gcc -g -o test test.c编译,然后运行gdb ./test,输入break main给main函数设置断点,再输入run开始运行,程序会停在main入口;接着用next逐行执行,print i或者print arr[i]查看内容。当程序报了Segmentation Fault,直接输入bt,就能看到崩溃发生在哪个函数、哪一行,八九不离十是数组越界或空指针。
我见过很多同学遇到段错误的第一反应是“在代码里加几个printf定位”,其实改用gdb之后,一次就能定位到具体行。建议你花半小时把GDB的这几个命令练熟,练习3后半程的指针题会非常需要它。比如一个空指针调用,程序崩在a->value那一行,你在gdb里print a就能看到0x0,立刻意识到指针没有分配内存,而不是在那里改半天的printf格式。
2.3 提交前固定自查的五个问题
每次写练习3的题目,不管简单还是难,提交前我都会按固定顺序过一遍:第一,输入格式是否处理正确,多个数据用空格还是换行分隔,是否有多组输入直到EOF;第二,数组长度是否开得足够大,题目说最多100个元素,你开了50,边界数据一上来就越界;第三,循环边界是否准确,是i < n还是i <= n,差一个往往就是程序能过样例但WA的原因;第四,输出格式是否严格一致,要不要换行,要不要保留小数位,末尾是否允许空格;第五,变量类型是否匹配,int够不够存,要不要用long long。这五个问题能过滤掉练习3里至少七成错误,相当于一份无形的checklist,做完一道题提交前看一遍,比报错之后反复试错稳妥。
3. 高频题型逐个拆解:从字符串逆序到完数查找
3.1 字符串逆序:下标法和指针法都练一遍
字符串逆序是练习3里出现概率极高的题目,常见形式有两种:一是给定一行字符串,要求逆序输出;二是把原字符串就地逆序,再打印。第一种比较简单,从尾往头输出即可;第二种需要对数组做前后交换。基础写法如下:
#include <stdio.h> #include <string.h> void reverse(char str[]) { int n = strlen(str); for (int i = 0; i < n / 2; i++) { char temp = str[i]; str[i] = str[n - 1 - i]; str[n - 1 - i] = temp; } } int main() { char line[101]; fgets(line, sizeof(line), stdin); line[strcspn(line, "\n")] = '\0'; reverse(line); puts(line); return 0; }这里有两个点要提醒。第一,gets函数在C11标准里已经废弃,SSE如果用严格编译选项,建议用fgets代替。fgets会把换行符也读进来,所以最好手动去掉末尾的\n,否则逆序后换行符会跑到了字符串开头,输出结果看起来就是多了一个空行。第二,交换次数是strlen(str)/2,不是strlen(str)。很多同学写成n次,结果字符串被换回原样,样例输出看起来一模一样但就是不对。
指针版本其实更接近C语言的本心:两个指针,一个指向头,一个指向尾,头小于尾就交换,然后头指针向后移动、尾指针向前移动,直到相遇。指针版本的循环条件和数组版本等价,但写起来会让你更直观地理解“数组名就是首元素地址”这句话。建议两个版本都写一遍,写完之后在纸上画出内存布局,之后再做指针题会顺手很多。
3.2 进制转换与金额拆分:核心都是“短除法”
进制转换同样是练习3的高频题型,最常见的是十进制转二进制/十六进制,或者反过来。思路其实就一个:反复除目标进制,余数逆序排列。比如十进制转二进制:
void decToBin(int n) { int bin[32], len = 0; if (n == 0) { printf("0"); return; } while (n > 0) { bin[len++] = n % 2; n /= 2; } for (int i = len - 1; i >= 0; i--) { printf("%d", bin[i]); } }这份代码里值得琢磨的是“余数先存下来,最后倒序输出”这个模式。它本质上和栈的思路一样:后进先出。练习3里另一道经典题“计算某日期是当年的第几天”,用的也是类似思想,只是不再除进制,而是把每个月的天数存成数组,然后循环累加。这类题最隐蔽的坑是闰年判断:能被4整除但不能被100整除,或者能被400整除。很多同学只写了“能被4整除就是闰年”,结果2月29日的用例直接WA。
还有一类题把进制拆成了“不同单位之间的换算”,PAT乙级的经典题《在霍格沃茨找零钱》就是这种:钱由加隆、西可、纳特三部分组成,每个部分进制不同。解法可以统一换算成最小单位再计算,也可以拆开做进位借位。练习3如果碰到这种题目,你可以先自己算一遍样例,体会一下“先统一单位,再算差值,最后分位还原”的思路。另外,涉及字符判断时,isdigit()和isalpha()这类函数会比手动比较ASCII码更清晰,也没那么容易写错。
3.3 数组循环移位与排序:算法基本功从这里起步
“把数组循环左移k位”或“右移k位”这类题目,练习3里基本必有一道。最容易犯的错是忽略k可能大于数组长度,比如数组长度5,k=7,实际只需要移动2位,于是第一步应该是k %= n。另一种常见错误是写了一个O(n*k)的双重循环,虽然小数据能过,但数据量一大就会Time Limit Exceeded。这里我建议至少掌握一种“三次反转”的写法,它很简洁:
void reverseRange(int arr[], int start, int end) { while (start < end) { int t = arr[start]; arr[start++] = arr[end]; arr[end--] = t; } } // 循环右移k位 void rightRotate(int arr[], int n, int k) { k = k % n; reverseRange(arr, 0, n - 1); reverseRange(arr, 0, k - 1); reverseRange(arr, k, n - 1); }这种解法背后的逻辑是:先整体反转,再分别反转两个子区间,就能得到最终结果。如果第一次看不太明白,可以在纸上拿一个长度为5的数组手动走一遍,很快就能理解为什么三次反转可以代替一次次移动。冒泡排序也是练习3的常客,虽然现在大家都知道有qsort,但课程阶段就是想让你手写一遍排序,理解比较和交换的本质。写冒泡时注意内层循环是j < n-1-i,i每多一轮,最后i个元素已经有序,没必要再比较,漏掉这个减i条件并不会报错,只是白白多跑几趟,严格一点的评测会卡时间。
3.4 看似简单的题最考验输出格式:九九乘法表与完数
九九乘法表是所有C语言教材里的经典练习,SSE里也有。难点不在乘法计算,而在格式。题目如果没有特殊说明,通常要求等号右边右对齐、宽度为2,这就要写成printf("%d*%d=%2d", i, j, i*j);如果题目要求左对齐,则加一个负号写成%-2d。评测对空格宽度很敏感,搞错方向就是Presentation Error。Presentation Error是个很磨人的状态:你的计算结果全对,但输出格式跟期望输出差一个空格或空行,评测端就认为你没过。遇到PE,先别急着改逻辑,回到题目描述里对照样例,一个字符一个字符地比,重点检查行尾空格、左对齐右对齐、是否需要空行。
完数这道题也类似。题目会给你一个范围n,要求输出该范围内所有“完数”,并统计个数。完数指一个数恰好等于它的真因子之和,比如6 = 1+2+3,28 = 1+4+7+14。暴力做法是每个数从1遍历到n/2求和,数据范围小没有问题,数据范围大就会TLE。稍微好一点的做法是只遍历到sqrt(i),同时把成对的因子都加上,复杂度从O(n^2)级别降到O(n*sqrt(n))级别。这类题给我们的启发是:练习3不只是练语法,也是在教你如何在约束条件下选对方法,哪怕只是把因子枚举范围缩小,也能明显改善运行时间。
4. 提交过不去的真实原因:运行时错误与评测端问题排查
4.1 提交后提示stream disconnected before completion: idle timeout waiting for sse
有同学提交练习3代码后,页面停留很久,然后弹出一句stream disconnected before completion: idle timeout waiting for sse,以为是自己代码把服务器搞崩了,非常紧张。实际上这句话描述的是浏览器与评测服务之间的事件连接超时,不是编译错误,也不是你的程序打印出来的内容。SSE在这里指的是一种服务器向浏览器推送消息的机制,当浏览器长时间没有收到推送数据,网关会判定空闲超时并断开连接。出现这个提示常见有两种情况:一是提交高峰期,评测任务排队太久,前端等推送等超时了;二是你的程序陷入了死循环或者没有正确读取完输入,评测端迟迟无法结束运行,连接被上游掐断。
判断方法很简单:刷新页面看看这道题的评测状态,如果状态显示Waiting或者Pending,过一会儿再看,通常是自己更新出结果,这种情况基本是全平台或高负载导致,跟代码无关。如果刷新后状态一直是Running,或者别人同时提交都正常而你一直超时,那就要怀疑代码死循环了。我最常见到的死循环是while(scanf("%d", &n))这种写法,输入流结束后scanf返回EOF,而EOF的值是-1,-1在条件判断里是非0真值,循环根本不会停,评测机只能等它超时。正确写法应该是while(scanf("%d", &n) == 1)或while(scanf("%d", &n) != EOF)。这个修法看着简单,但每到考试总有同学中招。
4.2 缓冲区问题:scanf、printf和换行符残留
练习3中后段,题目开始混用格式串,比如先读一个整数,再读一个字符,很多同学会突然发现第二个输入读到的不是自己输入的字符。原因是整数输入后,缓冲区里还留着换行符\n,后面的scanf("%c", &ch)直接把它读走了。解决方案是在%c前面加空格,写成scanf(" %c", &ch),让它先跳过空白字符。还有一个更隐蔽的场景:printf之后程序不结束,屏幕上就是看不见输出。这是因为标准输出一般是有缓冲的,只有当缓冲区满了、程序正常结束、或者显式刷新时才会真正写到终端。调试等待输入时,printf的内容可能还躺在缓冲区里,给人一种“程序卡住”的错觉。
做题时如果确实需要在等待输入前看到提示,可以加上fflush(stdout),或者直接用fprintf(stderr, ...)打印,stderr是不带缓冲的。这些缓冲区知识在练习3阶段不一定直接出题,但排查“为什么程序好像没反应”时会非常有用。你可以做个简单实验:在一个循环里printf一个字符,然后sleep一秒,如果不加刷新,屏幕可能会很晚才出现内容;一旦改成输出到stderr,立刻就能看到。这个小实验能让你真正理解“文件缓冲区 c语言程序”这些搜索结果到底在讲什么。
4.3 fscanf/fprintf与文件读写题目
有些版本的练习3会加入文件读写题,要求从指定文件读数据、把结果写到另一个文件。fscanf和fprintf的用法与scanf/printf几乎一样,只是多一个FILE*参数。核心要注意的有三点。第一,fopen之后必须检查返回值是否为NULL,文件打不开说明路径或权限有问题,后续操作全是空指针操作。第二,读完写完都要记着fclose,fclose不只是释放资源,还会触发缓冲区刷新,忘记fclose的结果可能是文件里什么都没写,或者写了一半。第三,用fscanf循环读文件时,不要拿feof()作为循环结束条件,因为feof只有在尝试读取越过文件末尾之后才会置位,你可能会多读一条旧数据。
稳妥的写法是while(fscanf(fp, "%d", &val) == 1),读到一个返回值就处理一个,读不到就结束。这个写法跟你处理stdin的scanf完全一致,逻辑上也好理解。练习3如果碰到这类题,先想清楚输入文件的格式,再决定用几个变量读取,千万不要在没看清格式的情况下直接上手写循环,那样很容易把分隔符也当成数据读进去。
4.4 语句语义的细节:while、do-while与自增运算符
练习3里有很多题会让选“以下代码输出什么”,或者在你自己写的循环里因为误用while和do-while而出错。两者的本质区别是一个先判断后执行,一个先执行后判断。最典型的误用是“读入一串以0结尾的数字并求和”:如果坚持用do-while,就要在循环体里先判断再累加,否则0也被加进去了;如果换成while(scanf("%d", &n) == 1 && n != 0),逻辑就顺很多。所以我的建议是,除了那些确定至少执行一次的菜单交互场景,普通数据处理循环尽量用while,少用do-while,能少很多边界问题。
类似地,++b和b++这种表达式辨析是笔试常客,练习阶段写代码时也会遇到。++b是先把b加1,整个表达式的值是加完之后的值;b++是先把b当前值作为表达式的值,然后再把b加1。比如int b=5; int a = ++b;之后a和b都是6;换成int a = b++;之后a是5,b是6。在循环里写while(i++ < n)和while(++i < n)的结果完全不同,前者先判断再加,后者先加再判断。这些语义如果理解不透,调试起来非常痛苦,因为程序不会报错,只是结果莫名其妙地差一位。
5. 常见问题速查与练习后的进阶方向
5.1 高频报错速查表
我把练习3阶段最容易遇到的评测结果和常见成因整理成一张速查表,你可以提交前对照着看:
| 评测结果/现象 | 常见成因 | 优先排查方向 |
|---|---|---|
| Compile Error | 缺少头文件、括号不匹配、C标准不兼容 | 看编译器输出,从第一行开始读 |
| Wrong Answer | 边界条件错误、输入读取方式不对 | 多跑几个边界用例,检查while截止条件 |
| Runtime Error | 数组越界、除零、空指针访问 | 用gdb定位崩溃行号 |
| Time Limit Exceeded | 死循环、算法复杂度过高 | 查while条件、减少不必要循环层数 |
| Presentation Error | 输出多了空格、少换行、格式宽度不一致 | 对照样例逐字符比对 |
| stream disconnected ... idle timeout | 排队超时或服务器端运行卡住 | 刷新看状态,排除死循环后重试 |
这张表里的条目绝大多数都来自我自己的WA经历和帮同学排查时的记录。练习3阶段很锻炼这种“根据状态猜测原因”的能力,你如果能把每次报错的原因记在备忘录里,后面遇到同样的问题时两分钟就能定位。不要觉得记这些琐碎事情浪费时间,很多老手之所以写代码快,不是因为语法记得牢,而是因为常见错误的模式见得足够多。
5.2 练习3之后的进阶路线
练习3做完并不代表数组和字符串就通关了,它只是让你第一次有机会把这些语法用熟。接下来你可以把每道题用两种方式重写:字符串逆序用下标和指针各写一版,进制转换用递归和循环各写一版,数组移位用普通循环和三次反转各写一版。这种“一题多解”的训练能让你对C语言的多个侧面都保持敏感,后续学到链表、结构体、文件操作时会发现很多模式是相通的。
更重要的建议是,不要局限在SSE这一个平台,去PTA、POJ、NOJ等在线评测系统刷同考点的题目,感受不同平台的出题风格。西北工业大学NOJ的100道基础题、翁恺老师课程配套练习,质量都很高。如果之后想接触更大的项目,练习3打下的基础是课程设计中做贪吃蛇、网吧计费管理这类小系统的前提;再往后,虚拟存储器管理、最优二叉搜索树这些操作系统和算法课的大作业,本质上也是从“用C语言准确表达逻辑”起步。方向可以后来再选,基础必须现在打好。
最后分享一个我自己养成的习惯。每次练习3的提交挂了WA,我都不会急着改代码,而是先把题面样例在纸上演算一遍,再用几个边界值替换参数,比如数组长度取1、取最大值、字符串全空或全满,看看程序会输出什么。这套笨办法帮我从练习3一直到期末机考都少走了很多弯路。说句实在话,SSE的评测系统严格到近乎苛刻,但它恰恰逼着你在每一次提交前都对自己的边界条件负责。这种习惯比一次AC值钱得多,因为等你做到链表和动态内存的练习时,会明白绝大多数崩溃都源于边界。希望这篇内容能让你在练习3上少熬几个夜,也能让你把C语言的第一道坎迈得比别人更稳。