十二月的GESP成绩出来之后,好几个学生跑来问我同一件事:判断题明明感觉都会,怎么一对答案错了四五道?我让他们把错题复盘发过来一看,发现错的点高度集中——不是不会,而是概念记得太“死”,换个说法就认不出来了。
C++四级的判断题1到10,考的从来不是某个偏门语法,而是把教材里最常见、最基础的结论,用“换一种表达”“加一个前提”“改一个顺序”的方式拿出来考你。这篇复盘我按照考后回忆整理了题面,个别文字和原卷会有出入,但考点、答案逻辑都是对齐的。如果你准备参加下一次的四级认证,把这10道题吃透,比刷十套模拟卷都管用。
1. 判断题为什么是四级的“隐形拉分项”
1.1 一张卷子里判断题的定位:不是送分题
很多学生觉得判断题就是“二选一”,蒙也有50%概率,所以复习时基本不花时间。这个想法在四级相当危险。
四级卷面的判断和单选混在一起,每题分值不算高,但整张卷子的通过线卡得很死。判断题一旦错得多,后面的大题压力会翻倍。更关键的是,判断题的命题方式非常“阴”——它不考你会不会写代码,它考你对概念的边界清不清楚。四级考纲里那些“一句话能说完”的知识点,比如指针退化、内存对齐、静态变量生命周期,全都能变成判断题的素材。
我见过太多学生,写代码能写出来,但让他判断“一维数组做函数参数时等价于指针”这句话对不对,他反而犹豫。代码是肌肉记忆,判断是概念校验,两者不是一回事。
1.2 从这10道题反推四级考点覆盖
把2025年12月这套判断题1到10过一遍,你会发现考点分布相当均匀,几乎没有重复:
| 题号 | 核心考点 | 所属模块 |
|---|---|---|
| 1 | 间接递归 | 函数与递归 |
| 2 | 结构体内存对齐 | 结构体与联合体 |
| 3 | 数组参数指针退化 | 指针与数组 |
| 4 | 指针减法语义 | 指针运算 |
| 5 | 位运算与运算符优先级 | 位运算 |
| 6 | 静态局部变量生命周期 | 作用域与存储期 |
| 7 | 默认构造函数的生成条件 | 类与对象 |
| 8 | 单链表头部插入顺序 | 链表 |
| 9 | 引用传参特性 | 引用 |
| 10 | 枚举法的基本思想 | 简单算法 |
这正好对应四级考纲的七大模块。判断题是“覆盖面最广”的题型,这一点很多人没意识到。单选题还能靠排除法猜,判断题如果知识点没覆盖到,连猜的方向都没有。
1.3 判题得分规则与“宁缺毋滥”策略
GESP机考环境下,判断题选完就能看到对错吗?不是,最终成绩统一出。所以考场上的策略是:不空题,但也别乱蒙。
判断题总共就那么几道,每道都值得认真对待。我的建议是每题控制在60秒以内,超过90秒还拿不准,先标记跳过去,做完后面的题再回头。机考系统通常支持题号跳转。千万别在某一题上死磕五分钟,耽误的是后面程序阅读题的时间。
还有一个很多人不知道的细节:GESP的判断题基本是“非黑即白”的知识点考察,很少出“一半对一半错”的表述。如果你发现一个判断句里既有对的部分又有错的部分,那它的答案大概率是“错误”——因为命题人就是在等你抓不住那个漏洞。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 2025年12月C++四级判断题(1-10)逐题复盘
2.1 第1题:函数A调B、B又调A——间接递归
题干:函数A在函数体中调用函数B,函数B在函数体中又调用了函数A,这种现象称为间接递归。
答案:正确。
递归分两种:直接递归是函数在函数体内直接调用自己;间接递归是函数没有直接调用自己,而是通过调用链绕了一圈又回到自己。题目中A调用B、B调用A,调用链是A→B→A,形成了一个环,这当然属于递归,而且是教科书标准的间接递归定义。
这道题错的学生,多半是脑子里只装了“函数自己调用自己”这半句话。看到A调用B、B调用A,觉得“没有自己调用自己啊”,于是判断成错误。这个理解太窄了。
我给学生讲递归时,习惯让他们把调用过程画成栈帧图。间接递归画出来之后非常直观:A的栈帧还没弹出,B的栈帧入栈,然后B又调A,A的新栈帧再入栈。栈的深度一层层增加,这就是递归的特征。能画出这个图,就不会被“间接”两个字绕晕。
真正值得多想一步的是:间接递归在实际代码里很少见,但在编译原理、状态机这类场景中会出现。四级考这个点,考的不是你会不会写间接递归,而是你知不知道递归的完整定义。以后写代码遇到函数互相调用,要能意识到这可能构成递归,避免无意识的死循环。
2.2 第2题:struct Node的sizeof是5吗——内存对齐
题干:在默认对齐规则下,声明struct Node { char c; int x; };,则sizeof(struct Node)的值是5。
答案:错误。sizeof(struct Node)的值是8,不是5。
这是四级结构体板块最经典的陷阱之一,我几乎每年都能看到有学生栽在这里。很多人一看到结构体里有1个char和1个int,想当然就是1加4等于5,完全忽略了编译器的内存对齐机制。
内存对齐规则的核心是:每个成员的存放地址要能被它自身大小整除。char占1字节,随便放;int占4字节,地址必须从4的倍数开始。所以char c先放在0号字节,int x不能紧跟着放1号字节,而要跳到位移4的位置,中间的1到3号字节被填充(padding),最终int x占4到7号字节,整个结构体占8字节。
| 成员 | 类型 | 自身大小 | 对齐要求 | 起始偏移 | 占用范围 |
|---|---|---|---|---|---|
| c | char | 1 | 1 | 0 | 0 |
| x | int | 4 | 4 | 4 | 4~7 |
对齐不是C++编译器随便加的,而是CPU读取内存的效率决定的。CPU按4字节或8字节一块去读内存,如果int x放在一个不是4倍数的地址上,CPU要分两次才能读完,效率太低。编译器牺牲了一点空间,换来访问速度的提升。这就是“对齐”存在的底层原因。
考试时遇到这种题,别口算,在草稿纸上画格子。画8个格子出来,一格一格往里填,马上就能看到中间空出来的那几格。填完你会牢牢记住:结构体的大小不是成员大小的简单相加。
2.3 第3题:一维数组传参后还是数组吗——指针退化
题干:将一维数组作为参数传递给函数时,形参中得到的实际是该数组首元素的指针。
答案:正确。
这是C++指针与数组关系里最核心的结论之一。你写void f(int a[10]),在编译器眼里和void f(int *a)是等价的。数组作为函数参数时,不会把整个数组拷贝一份传进去,而是退化为指向首元素的指针。注意,这是个单向的过程:数组名在表达式中会隐式转换为指针,但指针不能反推成数组。
为什么说这是四级重点?因为很多学生在这个地方有一个根深蒂固的误解:以为在函数内部写sizeof(a) / sizeof(a[0])能得到数组长度。我见过无数学生在函数里这么算长度,结果算出来的值完全不对。原因就是a已经是指针了,不是数组。
cpp复制#include <iostream>
using namespace std;
void test(int a[]) {
cout << "函数内 sizeof(a) = " << sizeof(a) << endl; // 4或8,取决于系统位数
}
int main() {
int arr[10];
cout << "main中 sizeof(arr) = " << sizeof(arr) << endl; // 40
test(arr); // 数组名退化为指针传入
return 0;
}
这段代码的输出会非常直观:sizeof(arr)是40,因为它是整整10个int;sizeof(a)是4或8,因为它是地址。这就是“退化”一词的含义——数组的长度信息在传参那一刻就丢了。
延伸一步:二维数组作参数时也是一样的逻辑,但退化的只是第一维,第二维及以后必须把维度写清楚,比如void f(int a[][5]),因为编译器需要知道每行有多少个元素才能算地址偏移。这些在四级后面的多维数组与指针部分经常连着考。
2.4 第4题:两个int指针相减等于字节差吗——指针算术
题干:若p和q是同一数组中两个int型指针,则p - q的值等于两个地址之间相差的字节数。
答案:错误。p - q的值是元素个数,不是字节数。
这是指针运算里最容易出错的点之一,错的原因是很多人把指针当成普通整数来做减法。普通整数相减,结果是数值差,比如100减88等于12。但指针不是整数,指针相减的结果被C++定义为两个指针之间的元素个数,类型是ptrdiff_t。
cpp复制#include <iostream>
using namespace std;
int main() {
int a[5] = {10, 20, 30, 40, 50};
int* p = &a[3]; // 指向40
int* q = &a[1]; // 指向20
cout << p - q << endl; // 输出2,中间隔了2个元素
cout << (char*)p - (char*)q << endl; // 输出8,按字节算才会得到8
return 0;
}
p和q的地址确实相差8字节(在int为4字节的机器上),但p - q的结果不是8,而是2。如果你强行转成char*再相减,结果才是8,因为char是1字节单位。
这道题错误率高的原因,是学生没有建立“指针运算以元素为单位”的直觉。记住一条:指针加1,不是地址加1,而是跳过整个元素。int指针加1,地址加4;double指针加1,地址加8。这是C++地址运算的基础逻辑。
另外提醒一句:两个指针相减有前提,必须指向同一个数组或同一块动态分配的内存。如果指向完全无关的两个变量,相减是未定义行为,结果没意义。考试如果看到这种选项,直接判断为错误。
2.5 第5题:0x0F & 0xF0 == 0 到底等不等于真——运算符优先级
题干:if (0x0F & 0xF0 == 0) 的条件表达式值为真。
答案:错误。这个表达式会被解析为0x0F & (0xF0 == 0),最终结果是0,条件为假。
这题考的是位运算和运算符优先级的结合,陷阱藏得非常深。很多人看到0x0F & 0xF0,本能地先做按位与,得到0,然后拿0和0比较,觉得成立,于是判断为真。但C++的运算符优先级表告诉我们:==的优先级高于&。也就是说,这个表达式的真正解析顺序是先算0xF0 == 0。
0xF0等于240,不等于0,所以0xF0 == 0的结果是false,对应整数0。再用0x0F去按位与0,得到0。0作为if条件,结果为假。
这里有个实用的记忆方法:如果拿不准优先级,就全部加括号。if ((0x0F & 0xF0) == 0) 才是你想表达的意思,结果也确实为真。可题目不给括号,就是要考你运算符优先级是不是真的记住了,而不是靠“看起来对”来猜。
这类问题在真实编码中同样常见,尤其是判断奇偶的时候。很多人写if (x & 1 == 0),以为自己在判断x是否是偶数,实际上编译器解析成x & (1 == 0),等于x & 0,永远为假。正确的写法必须是if ((x & 1) == 0)。
复习时可以把常见运算符的优先级默写一遍,重点记住这几组容易混淆的:==高于&高于^高于|,<<、>>高于关系运算符,逻辑与&&高于逻辑或||。默写一遍再对照表检查,基本就不会再踩坑了。
2.6 第6题:函数里的static变量每次调用都重置吗——静态局部变量
题干:在函数内定义的静态局部变量,只会在第一次执行到该变量声明语句时被初始化一次,后续函数调用会保留上一次的值。
答案:正确。
静态局部变量是四级作用域与存储期部分的常客,这道题考的是它的两个核心特征:只会初始化一次、跨函数调用保留值。
cpp复制#include <iostream>
using namespace std;
void countCalls() {
static int cnt = 0;
cnt++;
cout << cnt << endl;
}
int main() {
countCalls(); // 输出1
countCalls(); // 输出2
countCalls(); // 输出3
return 0;
}
cnt是一个典型的静态局部变量。第一次调用countCalls时,cnt被初始化为0,然后自增为1。第二次调用时,cnt不会重新初始化为0,而是保留上一次的1,自增为2。每次调用值都在累加,这就是“保留上一次的值”的含义。
很多学生在这里有认知偏差,以为函数调用结束后所有局部变量都被销毁。普通局部变量确实如此,但被static修饰后,变量从“自动存储期”变成了“静态存储期”,生存期延长到整个程序结束。但注意,作用域没变——它仍然只在函数内部可见,函数外访问不到。这一点经常被混淆:生存期长不等于作用域大。
另一个容易忽略的细节是零初始化。普通局部变量不初始化就是垃圾值,但静态局部变量如果没写初始化,会被自动零初始化。这个特性在某些计数场景下很实用,也是考试爱挖的细节。
2.7 第7题:带参构造函数都定义了,编译器还自动生成无参构造吗
题干:若一个类中已经定义了一个带参数的构造函数,但没有定义无参构造函数,此时编译器会自动生成一个无参构造函数。
答案:错误。编译器不会自动生成无参构造函数。
这个结论一定要记牢:编译器自动生成默认构造函数的前提是——类中没有定义任何构造函数。只要你自己定义了哪怕一个构造函数,不管带不带参数,编译器都不会再帮你生成默认构造函数。
举个例子,类里只有一个Book(string title)构造函数,没有Book(),然后你在main里写Book b;,编译器会直接报错,提示找不到匹配的构造函数。
cpp复制#include <iostream>
#include <string>
using namespace std;
class Book {
public:
// 只定义了带参构造函数
Book(string t) {
title = t;
}
private:
string title;
};
int main() {
// Book b; // 编译错误:no matching function for call to 'Book::Book()'
Book b("C++ Primer"); // 正确
return 0;
}
这个知识点在四级的类与对象部分非常基础,但特别容易被“想当然”带偏。很多学生背结论时只记住“编译器会自动生成默认构造”,漏掉了“前提是没有定义任何构造函数”这个大前提。题目把这个前提改掉,答案就完全反过来了。
我在课上讲这个点时,喜欢让学生做一个延伸思考:如果有个类既需要带参构造,又需要无参构造,怎么办?答案是手动把两个构造函数都写出来,编译器不会帮你补。这个习惯在以后做面向对象设计时非常重要,别指望编译器替你做决定。
2.8 第8题:单链表头部插入,两步操作的顺序能反吗
题干:在单链表头部插入一个新结点时,正确顺序是先让新结点的next指针指向当前头结点,再将链表头指针指向新结点。
答案:正确。
链表是四级数据结构的重点,头插法是其中最基础的操作。正确顺序是:
- 新结点
newNode的next指向原来的头结点:newNode->next = head; - 把链表头指针更新为新结点:
head = newNode;
这个顺序不能反。如果你先执行head = newNode;,原来链表的头结点就找不到了,因为head已经指向了新结点,而新结点的next还没有指向任何东西。整个链表的剩余部分就丢了,这在链表操作里叫“断链”。
cpp复制struct Node {
int data;
Node* next;
};
// 头插法
void insertAtHead(Node*& head, int value) {
Node* newNode = new Node();
newNode->data = value;
newNode->next = head; // 第一步:先接上原链表
head = newNode; // 第二步:再更新头指针
}
我给学生的记忆方法是“先伸手、再换人”:新人要先伸出手抓住原来队首的人,才能让整个队伍排到新人后面。如果先宣布新人当队首,原来的队伍早就散了,新人抓了个寂寞。
类似的操作陷阱在删除头结点时也会出现。删除头结点的正确顺序是:先用临时指针保存头结点,把head移到head->next,再释放临时指针指向的旧头。如果先释放头结点再移动head,head就指向一块已经释放的内存,变成悬空指针。这俩放在一起记,效果最好。
2.9 第9题:引用传参会复制实参吗
题干:用引用(reference)作为函数参数传递时,函数内对形参的修改会直接作用于实参,并且不会产生实参的副本。
答案:正确。
引用是C++区别于C语言的一个重要特性,本质上是给一个已存在的对象起了个别名。用引用传参时,形参和实参是同一个对象,你在函数里修改形参,实参跟着变;同时因为没有发生拷贝,所以不产生副本。
为了看清楚引用和指针传参的区别,可以列个表对比:
| 传参方式 | 是否复制对象 | 函数内修改是否影响实参 | 典型写法 |
|---|---|---|---|
| 值传递 | 是 | 否 | void f(int a) |
| 指针传递 | 复制指针本身 | 是(通过解引用) | void f(int* a) |
| 引用传递 | 否 | 是(直接生效) | void f(int& a) |
很多学生觉得“引用和指针没区别”,实际上区别很大。指针传参时,指针变量本身被复制了一份,只是它们指向同一个对象;引用传参时,连指针变量都不用复制,形参就是实参的别名。另外,指针可以为空,引用必须在定义时初始化,不存在“空引用”。
这个知识点在四级里只是引入,到五级、六级会大量用于类的拷贝构造、运算符重载、容器传参等场景。尤其是const T&这种写法,既能避免拷贝大对象带来的性能开销,又保证了函数内不会意外修改实参,是C++里性价比最高的传参方式。从现在开始养成写const T&的习惯,后面会少走很多弯路。
2.10 第10题:枚举法的“逐一列举”为什么是核心
题干:枚举法的基本思想是逐一枚举所有候选答案,依次检验是否满足条件,在所有可能情况中找到符合要求的解。
答案:正确。
枚举法,也叫穷举法,是四级简单算法部分的核心思想。它不绕弯子:把问题所有可能的情况罗列出来,逐个验证,留下满足条件的。真题里的“小猫分鱼”这类题目就是典型的枚举思路——从小规模开始试,逐步推导。
为什么这道题要专门拿出来说?因为很多学生把“枚举法”理解成“碰运气”。看到题目描述里有“试”“猜”这类字眼,就以为枚举法和随机尝试是一回事,判断成错误。但枚举法的关键词是“逐一”“不重不漏”——它要求把候选答案的集合完整地遍历一遍,而不是随机挑几个碰运气。
枚举法看起来笨,但它是很多聪明算法的起点。比如三重循环枚举配对,再通过剪枝降低复杂度,本质上都是“先枚举,再优化”。搞懂了枚举法的思想,后面学贪心、动态规划时才有对照物。学算法的人常说的“暴力出奇迹”,指的就是枚举法。
这道判断题本身不难,但如果你在“枚举法是否只适用于小规模问题”这种细节上纠结,反而容易出错。记住:枚举法是思想,时间复杂度高不代表思想有错,只是应用时要注意数据范围。
3. 判断题60秒拿分技巧:从读题到落笔的完整流程
3.1 先划绝对化关键词,再想反例
判断题里最值得警惕的是“一定”“总是”“所有”“只要就”这类绝对化表述。大部分情况下,一个带绝对化关键词的命题,只要你能找出一个反例,它就是个假命题。
这次的第7题就是典型。题干里说“此时编译器会自动生成一个无参构造函数”,这里面暗含了一个“自动”的绝对判断。你只要回想起“只有类中没有定义任何构造函数时才会自动生成”这个条件,反例立刻浮出来——定义带参构造后就不会自动生成了,那题干就是错的。
反过来,表述相对保守、带“通常”“可能”“在某些情况下”这类限定词的命题,往往更可能是真命题。这不是绝对规律,但可以作为你拿不准时的参考信号。
3.2 计算型判断:草稿纸上的三步验算法
涉及计算的判断题,比如第2题的sizeof、第4题的指针减法、第5题的位运算结果,一定不要靠心算拍板。我建议在草稿纸上走三步:
- 写出完整的表达式或计算步骤,不跳步;
- 画出内存布局或运算符优先级分析,一行一行算;
- 把计算结果带回题干,检查它符不符合题干描述。
以第5题为例,如果你心算,很容易被0x0F & 0xF0的直觉带跑。但你在草稿纸上写出“先算0xF0 == 0,再算按位与”之后,答案就很明确了。计算型判断题的失分大多不是不会算,而是算得太急、跳步太多。
3.3 真不会的题怎么办:概念边界法
考试时遇到完全没思路的判断题,别瞎蒙,用“概念边界法”兜底:把题干涉及的核心概念在脑子里过一遍定义,然后问自己三个问题——这个概念的定义里有没有前提条件?题干的说法是不是这个定义的准确表述?有没有一个反例能推翻它?
举个例子,如果某个题考到了多维数组和指针,你可以在脑子里快速过一遍三级、四级学过的定义:数组名在表达式里会退化为指针,所以a[i][j]和*(*(a + i) + j)是等价的。题干如果把这个等价关系说反了,答案就是错误。
蒙题不是乱选,而是用概念边界去卡选项。哪怕最后真的不确定,至少你的选择是有逻辑依据的,而不是纯靠运气。
4. 考后复盘:这10道题暴露的四级薄弱点
4.1 指针+内存是四级的分水岭
把这10道题放在一起看,你会发现指针相关内容占了相当大的比重:第2题的内存对齐、第3题的指针退化、第4题的指针减法,全是指针与内存范畴。这印证了一个规律:C++四级的核心就是“指针”,能不能理解内存中数据的组织和访问方式,决定了你能不能跨过四级这道坎。
很多学生的复习误区是猛刷“写代码”题,忽略了对指针底层逻辑的理解。我建议在备考阶段专门花一个下午做这几件事:画一画各种变量在内存中的布局,写几个用指针操作数组的小程序,练习用sizeof验证不同类型的长度。这些基础工作看着不起眼,但对判断题的命中率提升非常明显。
4.2 “背结论”容易翻车,要能举出反例
四级判断题错的另一大原因,是学生习惯背结论而不是理解结论。背结论的问题是,换个说法你就认不出来了。比如“编译器会自动生成默认构造函数”这句,很多人都会背,但题目加了个前提“已经定义了带参构造函数”,就有人翻车了。
理解一个结论和记住一个结论的区别,在于你能不能举出反例。复习每个考点时,试着问自己:这个结论在什么情况下不成立?比如“数组名可以当作指针”,那什么时候不是指针?答案是sizeof运算符作用在数组名上时,返回的是整个数组的大小,而不是指针大小。能举出这种反例,判断才不会翻车。
4.3 下一阶段备考建议:从四级到五级/六级的衔接
四级之后,GESP会进入新的难度:五级开始引入类与对象的高级特性、标准模板库、排序与查找算法,六级则会接触到树、图、贪心和动态规划。这些内容不是凭空冒出来的,它们的根基都埋在四级的知识点里——链表是图的基础,引用是运算符重载的基础,枚举法是动态规划的基础。
所以这次判断题暴露出的薄弱点,不只是影响四级成绩,更会拖慢后面五级、六级的学习进度。我的建议是:考完四级不要急着刷五级题,先把指针、结构体、链表这些基础概念盘扎实。地基没打牢,楼盖得越高越危险。就拿六级常见的图论来说,邻接表本质上是链表数组,如果你链表操作不熟,写邻接表会非常痛苦。
我个人在实际带学生复盘时发现,把判断题的错题整理成一个“一句话考点表”,考前快速过一遍,效果出奇地好。比如“指针相减=元素个数不是字节数”“默认构造函数自动生成的前提是没有定义任何构造函数”“头插法必须先接链再换头”。这些一句话考点看着简单,但都是历年考试反复挖坑的位置。你要是能把这份表整理出来并真正理解每一条后面的原理,四级的判断题基本就稳了。
