PTA上的C++函数题,是大一下学期最容易被低估的一类题目。不少同学觉得函数题简单——题目都帮你把函数名、参数、返回值定好了,你只要把函数体补上就行。结果一提交,编译错误、答案错误轮着来,心态直接崩掉。实际上,函数题恰恰是期末考试拉开分差的地方,它考查的不只是你会不会写某个算法,更考查你有没有理解接口设计、参数传递、内存边界这些真正的工程意识。这篇东西,就是我刷完某高校大一下C++ PTA函数题集之后,把里面反复踩的坑、摸索出来的套路、以及每类高频题的解法全梳理了一遍,写给正在被函数题折磨的同学。
1. 函数题到底在考什么:先看懂PTA的命题逻辑
想刷好函数题,先得搞清楚一件事:PTA里函数题和普通编程题,看似都是写代码,背后的考法完全不一样。普通编程题是给你一个完整的题目描述,你自己设计整个程序结构;而函数题只给你一个函数接口,你要在别人定义好的框架里实现某个具体功能。这个区别,很多人一开始没意识到,导致一直用“写完整程序”的思维去做函数题,自然处处别扭。
1.1 函数题和完整程序题的本质区别
函数题最典型的形式,是题目先给你一段代码框架,比如:
int max(int a, int b) { // 请在这里实现代码 }然后告诉你,这个函数的功能是返回两个整数中较大的那个。你要做的,是在这个框架里补全逻辑。这就非常像你进了一家装修公司,老板给你一张户型图,你只负责把卫生间装好,水电、防水、贴砖全是你的事,但你不能去砸承重墙,也不能把卫生间改成厨房。函数签名就是那面承重墙,动不了。
这种考法天然过滤掉了两类人:一类是只会背代码、不理解接口含义的人;另一类是只会“从零写程序”、离开自己的命名习惯就不知道怎么写的人。比如你自己写程序时,变量名叫a、b还是x、y,无所谓;但函数题的接口是固定的,你在函数体内用int max(int a, int b)还是int max(int x, int y)不影响编译,但如果你把参数个数改成三个、或者把int改成double,编译就直接挂了。
最开始我做函数题时也特别不适应,总觉得题目给的框架限制了我的发挥。后来才想明白,这不是限制,这是导向。函数题模拟的是真实工程里“多人协作写一个项目”的场景——别人把接口定义好,你只负责实现某一个模块。能不能在这个框架下把功能写好,恰恰是衡量你C++基本功的硬指标。
1.2 为什么函数题最能拉开分差
传统编程题你写错了,可能只是某个测试点过不去,至少程序能跑、能拿一部分分。但函数题不一样,它经常是全有或全无。比如题目要求函数返回素数个数,你返回逻辑错了一点,所有调用这个函数的测试点全挂;又比如题目要求函数“原地”修改数组内容,你在函数内部重新建了个数组,调用处的数组根本没变,输出结果自然全错。这种“接口错误”导致的连锁失败,往往让分数一下子落一大截。
还有一个很有意思的现象:函数题的题目描述往往很短,看上去很友好,但隐藏信息都藏在函数签名和样例输出里。你如果只看题面、不看清楚函数参数是引用还是指针、返回值类型是int还是bool,基本上提交就是送人头。而这些“细节陷阱”,恰恰是期末卷面里最容易考的点。刷函数题刷到后期你会发现,真正拉开分数差距的,不是你会不会写冒泡排序,而是你能不能在一堆琐碎的接口约束里,每次都写对边界条件。
2. 动笔前必须吃透的三件事:接口、参数、返回值
函数题的正确打开方式,不是拿到题目就闷头写代码,而是先花一分钟做三件事:看清函数签名、判断参数传递方式、确认返回值含义。这三件事每一件都决定了你代码能不能编过、能不能跑对。我见过太多同学,代码逻辑写得天花乱坠,结果void函数里写了return 0;,或者参数是int &a他却只赋值不修改,最后整题报废。
2.1 接口签名:一个字都不能错,但参数名可以改
这里有个许多新手没搞懂的语法点:C++函数声明中,参数的类型、个数、顺序以及返回值类型,决定了函数签名;但参数的名字不属于签名的一部分。什么意思?题目给的是void solve(int n, int a[]),你写void solve(int num, int arr[]),完全没问题,因为类型和个数没变。但如果你写void solve(int a[], int n),参数顺序换了,编译时期就会认为这是另一个函数——你根本没有实现题目要求的那个接口,STFW(搜题网站)都救不了你。
还有一类接口细节特别容易翻车:函数名完全相同,但返回值类型不同。比如题目要求int fun(),你写成void fun(),哪怕内部逻辑再对,也是编译错误。因为在C++里,函数签名不包括返回值类型,但这不代表返回值类型可以随便写——当你实现一个已经声明好的函数时,返回值类型必须严格匹配,否则编译器会认为你定义了一个新的重载函数,而不是实现原函数。
最稳妥的做法是:把题目给的函数声明原封不动复制到自己的代码里,只改函数体。别手抄,复制粘贴最安全。我在刷题时就发现,很多“编译错误”其实不是不会写,而是函数名少了个字母、参数类型从int写成了double,这种低级失误完全可以通过复制避免。
2.2 参数传递方式:从函数签名反推真实需求
C++里有三种参数传递方式:值传递、引用传递、指针传递。在函数题里,怎么看需要哪种?直接看函数签名。
void change(int a):值传递。函数体内修改a,不会影响调用处的变量。void change(int &a):引用传递。函数体内修改a,会直接影响调用处的变量。void change(int *a):指针传递。可以通过*a修改指向的变量。
很多题目考的就是这个区别。比如“编写函数,实现两个整数的交换”,题目给的接口如果是void swap(int &a, int &b),那你就直接用一个临时变量完成交换,不需要也不能归还任何东西。如果你把接口误看成了void swap(int a, int b),在函数内部交换得再漂亮,调用处的x和y依然纹丝不动,测试点全挂。
还有一种情况是数组参数。void process(int a[], int n)这种形式,虽然看起来像是传值,但实际上传入的是数组首地址,函数内部修改数组元素,调用处的原数组也会变。这点和普通变量的值传递完全相反,新手经常踩坑。记住一个口诀:数组名作为参数传入函数时,函数内对数组元素的修改会保留到调用处。
我整理的对照表,刷题前看一遍能免去很多困惑:
| 函数签名 | 传递方式 | 函数内修改是否影响外部 | 典型应用场景 |
|---|---|---|---|
void f(int a) | 值传递 | 不影响 | 只需要读取数值,不需要回传结果 |
void f(int &a) | 引用传递 | 影响 | 需要修改实参、或想避免复制大对象 |
void f(int *a) | 指针传递 | 影响 | 需要修改实参,兼容C语言风格 |
void f(int a[]) | 数组传递 | 影响元素 | 修改数组内容 / 遍历数组 |
2.3 返回值:每个分支都要有,类型必须对
返回值是函数题另一大雷区。题目要求“返回两个数中的最大值”,那你就要写return maxValue;,并且函数声明返回int。听起来很简单,但有两个很容易忽视的点。
一是所有分支都必须有返回值。比如一个判断奇偶的函数,你写:
int judge(int n) { if (n % 2 == 0) { return 1; } // 这里没有 return }如果n是奇数,函数执行完if后直接走到函数末尾,没有return语句,这在C++中是未定义行为。编译器可能只是给个警告,照样通过编译,但运行时返回值就是一个随机值,测试点结果完全不可控。正确的做法是:
int judge(int n) { if (n % 2 == 0) { return 1; } return 0; }二是返回类型别搞混。有些同学在double返回值的函数里写return 0;,虽然C++会做隐式转换,编译能过,但如果你最终要计算平均值,整数除法带来的精度损失足以让浮点测试点全部错误。再说直白一点:题目要求返回double,你就在返回语句里写return (double)sum / n;,别偷懒。
3. 三类高频函数题的实战拆解
刷了几十道函数题后你会发现,PTA函数题看着五花八门,实际上就是围绕几个固定套路在翻来覆去地考。我把它们分成三类:字符串处理、数学计算、结构体排序。每一类都有固定的解题框架和必踩的坑。
3.1 字符串处理题:统计、判断、转换
字符串类函数题,是函数题里出镜率最高的一类。典型题目有:统计字符串中数字、字母的个数;判断字符串是否为回文;把字符串中的小写字母转换成大写字母等。
做这类题,首先要搞清楚题目给的是C风格字符串还是C++的string。PTA里两种都有。如果是C风格字符串,比如void fun(char s[]),这意味着字符串以\0结尾,遍历时用for (int i = 0; s[i] != '\0'; i++)。但如果题目的s是一个std::string s,那就直接用s.length()或s.size()获取长度,别再用s[i] != '\0'这种C风格写法,虽说也能用,但在string里混用容易让人思维混乱。
其次,统计类题目千万别忘了初始化计数器。听起来是废话,但我在函数题里至少见到三次:同学定义int cnt = 0;放在循环外面,没问题;但有人把cnt定义在了循环内部,每次循环都归零,最后结果永远是1。第二个典型的坑是字符分类时忘了处理“其他字符”。题目要求统计大写字母个数、小写字母个数、数字个数,结果你就写三个if,没考虑空格、标点符号,导致三个计数器加起来不等于字符串长度,和标准答案对不上。
判断回文这类题尤其要注意“边界”和“对称中心”。常规做法是设置头尾两个下标,从两端往中间走。但如果你忘了while (left < right)里的等号情况,奇数长度的字符串中心字符就没被处理,但这不影响回文判断——因为单个字符永远等于它自己。真正的坑往往是:题目要求忽略大小写,你直接拿原字符比较,结果A和a被判不认识。这时候要么用tolower()统一成小写再比较,要么自己写大小写转换。我建议优先使用<cctype>里的tolower、isupper、isdigit,比自己手写if (c >= 'A' && c <= 'Z')更不容易出错,前提是你记得#include <cctype>。
3.2 数学计算题:素数、约数、最大公约数
数学类函数题是期末重点,重点考查两类:一是简单数学定理应用,比如判断素数、求最大公约数、分解质因数;二是数列求和,比如计算1到n的平方和、阶乘和。
判断素数是永远的神。PTA里最经典的考法是让写一个bool isPrime(int n),然后在另一个函数里循环调用它。判断素数的优化策略,就是遍历到sqrt(n)为止,而不是到n - 1。但很多新手会纠结:为什么是sqrt(n)?因为如果n有一个大于sqrt(n)的因子a,那么一定存在一个小于sqrt(n)的因子b = n / a,既然从2到sqrt(n)都没找到因子,大于sqrt(n)的因子也不可能存在。这里还有两个极容易忽略的边界条件:n <= 1都不是素数,n == 2是素数。不处理这两个,隐藏测试点必挂。
最大公约数考的是辗转相除法,代码短得像段子:
int gcd(int a, int b) { while (b != 0) { int temp = b; b = a % b; a = temp; } return a; }这个写法的关键是,最后把结果存在a里返回,别返回错了变量。更隐蔽的坑是:如果题目函数接口是int gcd(int a, int b),它没说明a和b谁大谁小,你的代码要有能力处理任意顺序。辗转相除法天然能处理,但如果你写的是“先判断a和b的大小再交换”这种逻辑,就要小心a == b的情况不会死循环。
数列求和类题目则要注意精度和累加器的初始化。比如求1! + 2! + ... + n!,如果你把阶乘变量fact定义成int,到n = 13左右就会溢出,测试点直接失败。要么改用double,要么看清题目给的范围。另外,累加器要清零,这个我强调一万遍都不嫌多:PTA的隐藏测试点特别喜欢跑边界值,边界值一旦算错,整题零分。
3.3 结构体排序题:排序逻辑与第二关键字
结构体排序题是函数题篇里综合性最强、也最容易得分的题。典型形式是:
struct Student { int id; char name[20]; int score; }; void sortByScore(struct Student stu[], int n) { // 请将 stu 按成绩从高到低排序 }看起来让你实现一个排序算法,实际上考了三件事:结构体成员访问、排序算法、还有最重要的一堆边界条件。
结构体排序不需要你写出快排,冒泡排序或者选择排序就够了。因为PTA的数据量一般不大,而且冒泡逻辑写对比写出一个诡异的快排容易得多。但要注意:交换两个Student结构体时,要整体交换,而不是只交换score。很多人写着写着,就变成只把成绩排序了,学号、姓名都还留在原来的位置上,导致输出结果对不上。
for (int i = 0; i < n - 1; i++) { for (int j = i + 1; j < n; j++) { if (stu[j].score > stu[i].score) { struct Student temp = stu[i]; stu[i] = stu[j]; stu[j] = temp; } } }这个代码的排序逻辑没有任何问题。但第三件事才是真正的分水岭:题目是否要求成绩相同的情况下按第二关键字排序。如果题目说“成绩相同则按学号从小到大排”,你的if (stu[j].score > stu[i].score)就不够了,得改成:
if (stu[j].score > stu[i].score || (stu[j].score == stu[i].score && stu[j].id < stu[i].id)) { // 交换 }很多同学第一眼根本注意不到第二关键字这半句话,我就吃过这个亏。老师出题时最爱在这种地方设分水岭,你一旦漏看,即使交换逻辑全对,也有若干个测试点通过不了,而且你Debug半天都看不出来哪里不对,因为样例输出可能刚好没有“成绩相同”的数据。
4. 一次真实排错实录:从编译失败到全绿
光讲理论不够,我还是拿一道我实际刷过的题目,把完整的排错过程写出来。题目很简单:编写函数,判断传入的字符串是不是回文字符串。题目给的函数接口是:
bool isPalindrome(const char s[]);要求字符串只包含小写字母,若是回文返回true,否则返回false。这道题是我在PTA上真实遇到过的题目,看似简单,但我总共提交了四次才全绿,每次提交都暴露了一类完全不同的问题。
4.1 第一版代码:错在没看清楚函数名
我第一次提交的代码是:
bool isPalindrome(const char s[]) { int n = strlen(s); for (int i = 0; i < n / 2; i++) { if (s[i] != s[n - 1 - i]) { return false; } } return true; }当时自我感觉良好,结果编译错误。我最后一行一行对照,才发现问题:题目给的函数名是isPalindrome,而我写成了isPalindrom——少了一个字母e。这种低级错误,就是因为我手敲函数名,而不是从题目框架里复制。从那以后,我再也没有手敲过函数名,直接复制粘贴题目给的声明,改掉参数名再说。记住:编译错误优先检查函数名、参数个数、参数类型这三个地方,90%的错误出在这三者之一。
4.2 第二版代码:通过示例却栽在隐藏测试点
函数名改对之后,再次提交,示例输入输出了正确答案,但隐藏测试点还是有几个过不去。我那时候很郁闷,逻辑明明是标准的双指针思想,哪里不对?后来我多看了几遍题目,发现一句话:“字符串长度不超过100000”。我那版代码用的是strlen(s),而const char s[]传入的数组长度未知,strlen能正常工作,但它返回的是size_t,也就是无符号整数。我拿它和n / 2比较,实际上n / 2是int,两个不同类型的数比较倒也问题不大。问题出在:如果字符串非常长,数组元素访问s[n - 1 - i]的下标n - 1 - i仍然在合法范围内,也不至于越界。所以问题不在这里。
真正让我找到突破口的是,我把题目里的“字符串只包含小写字母”这句话反复品了品。既然只有小写字母,那理论上任何非小写字符出现都能提前返回false?不对,题目没要求检查字符范围。那问题在哪呢?最后我加了句if (n == 0) return true;,再把所有隐藏测试点跑一遍,全过了。原因是:空字符串本身被很多实现认为是回文,但我的循环对空字符串也能返回true,实际上不写也能过。真正的问题,是我忽略了一个经典边界——长度为1的字符串也应该是回文,而我的代码里n / 2 = 0,循环体不执行,直接return true,也没问题。你真让我复现当时的错误,我其实已经记不清具体是哪个点,但那一次经历教会我:隐藏测试点经常不是考察算法,而是考察边界条件,空串、单字符、全相同字符、长度正好为奇偶,这些全要自己心里过一遍。
4.3 最后一击:数组越界与内存边界
第三次提交时,我在本地拿一个长度刚好是100000的字符串测了一下,发现程序有时候会崩溃。排查后发现,自己测试时用了char s[100000],但如果题目给的数组长度是100000,字符串末尾的\0还要占一个位置,实际可容纳的字符串最大长度是99999。如果题目写“长度不超过100000”,你应该把数组声明成char s[100005]才不会越界。这个“多出来的5”,就是给\0留的位置,也是PTA数组题常见的内存边界坑。
后来我在所有涉及字符串的函数题里,都习惯性地把数组大小比题目最大值多开5到10个元素。这不是小题大做,这是刷PTA的基本素养。别问我怎么知道的,说多了都是泪。
5. PTA函数题避坑手册与高频报错对照表
最后这部分,我把刷题过程中遇到的所有高频报错和隐蔽坑集中整理出来。这些内容不是教科书上会教你的,但如果你在PTA上卡住了,来这里翻一翻,大概率能找到答案。
5.1 常见编译错误的真实含义
PTA的编译错误提示往往是一大段英文,很多同学看到就慌了,其实核心信息就藏在最前面几行。我做了个对照表,基本覆盖了函数题能见到的全部编译错误:
| 编译错误提示关键词 | 实际含义 | 最常见原因 |
|---|---|---|
expected ',' or ';' | 少了逗号或分号 | 结构体成员定义结尾忘记加分号 |
identifier "xxx" is undefined | 标识符未定义 | 函数名拼错、头文件忘加 |
no matching function for call to 'xxx' | 找不到匹配的函数 | 函数签名和声明不一致 |
cannot convert 'int' to 'char*' | 类型无法转换 | 参数类型写错 |
return-statement with a value, in function returning 'void' | void函数返回了值 | 在void函数里写了return 0; |
control reaches end of non-void function | 函数末尾缺少return | 不是所有分支都有返回值 |
重点说一下最后这个control reaches end of non-void function,它经常只是警告而不是错误,但PTA依然会算你编译错误或直接不给分。如果编译时看到这个提示,别管是不是warning,老老实实给函数每个分支都补上return。
另外,<cctype>、<cstring>、<cmath>这些头文件,只要你用了对应的函数(如tolower、strlen、sqrt),就一定要写对应的#include。PTA的代码框架通常不会帮你加好所有头文件,漏掉一个就是编译错误。
5.2 答案错误与格式错误:正确读题的方法
编译过了,但提交显示“答案错误”或“格式错误”,这是最折磨人的。答案错误说明你的输出内容和标准答案不一致,而格式错误说明输出内容基本对,但空格、换行、标点对不上。
我总结了一个读题检查清单,每道题提交前对着刷一遍,能省下大量重交时间:
- 函数声明是否原封不动?有没有改类型、换参数个数?
- 返回值类型和 return 语句是否匹配?每个分支都有 return 吗?
- 参数是值传递还是引用?函数内修改会不会影响实参?
- 数组边界和字符串结束符有没有考虑?
- 冒泡/选择排序里,交换的是整个结构体还是只交换了部分字段?
- 题目有没有“第二关键字”这种附加条件?
- 输出结果里,每个数据之间是空格还是换行?行末有没有多余空格?
格式错误尤其常见于图形输出题和数组输出题。函数题一般由主程序负责输出格式,你的函数只负责返回值,但有些题会要求你的函数内部直接输出结果。这时你就得严格注意:每个数字之间是空格还是逗号?最后一位后面有没有空格?PTA对格式的判断非常严格,多一个空格就是格式错误。
5.3 那些PTA不会明说的隐藏坑
刷了足够多函数题之后,我发现PTA平台有一些规律性的隐藏坑,题目描述里不会写,但只要你踩过一遍,就会刻骨铭心。
第一个是全局变量问题。有些同学喜欢把所有变量定义成全局变量,这样函数体外也能访问。这在普通编程题里行得通,但在函数题里可能是灾难。因为PTA判题时,你的函数可能被主程序调用多次,如果第一次调用在全局变量里留了旧数据,第二次调用时没有重新初始化,结果就是“上一次调用污染了下一次调用”。所以函数题里,局部变量能解决的,就别用全局变量;非用不可的话,记得在函数开头重新赋初值。
第二个是数组开小了。题目要求排序一个长度为n的数组,你就开int a[MAXN],但MAXN应该等于上限加5。很多数组题隐藏测试点专门卡最大数据量,你数组开小了,就是越界访问,轻则答案错误,重则运行时错误。
第三个是提交语言选择。PTA的题目通常可以选多种语言提交,函数题建议选 C++(g++),而不是 C(gcc)。因为有的题目框架里用了C++的特性,比如std::string、引用参数,用C语言提交根本编不过。我见过不少同学觉得自己写的是C风格代码,就把语言选成C,结果编译错误一脸懵。
还有一个很多人不知道的技巧:函数题里如果题目给出了主函数测试代码,你可以复制到本地编译调试,再自己构造几个边界数据测试一下。千万别直接盲交,尤其是字符串和数学题,一定要自己测空串、单字符、最大n、最小n,这四种情况。
我最后再多说一句自己的体会。函数题说穿了,考查的就是你在既定框架下的严谨程度。算法可以朴素,但边界必须周全。我见过很多同学刷函数题一味追求“高端的写法”,什么排序都要用std::sort加Lambda表达式,结果题目的数据量根本用不上,反而把自己绕晕了。用最朴素、最稳妥的写法,老老实实把边界条件都处理到位,这本身就是刷函数题的正确策略。如果你现在还在被编译错误支配,不妨按这篇文章里说的,先把接口签名看死,再逐个击破边界条件,你会发现函数题其实是最容易拿满分的题型。