2026年9月 GESP C++ 四级真题解析:指针、引用、排序"三件套"全上阵
四级这次把指针、引用、结构体、排序、文件、异常全部塞进了一张卷子,综合度是近几年最高的一次。好消息是:没有偏题,全是核心考点。
一、答案速查
判断题官方未公布答案,以下为按题意推算的参考答案。
二、单选题
板块 1:指针(第 1、2、3 题)
第 1 题 → C
int count = 8;int *p = &count; // p 指向 count*p += 4; // 等价于 count += 4cout << count << ” ” << *p; // 12 12
*p 就是 count 本身,改 *p 就是改 count,所以两者同时是 12。
一句话:
p是地址,*p是那个变量。
第 2 题 → B:const int *p = &a;
记住这条口诀:const 在左边,管的是"指向的值";const 在右边,管的是"指针本身"。
*p | p 的指向 | |
|---|---|---|
const int *pint const *p) | ||
int * const p |
本题 const int *p 属于第一种 → 不能改 *p,但可以 p = &b → 选 B。
第 3 题 → B
int goods[3][4] = {{2,4,6,8},{10,12,14,16},{18,20,22,24}};int (*p)[4] = goods; // p 是”行指针”,每行 4 个 intint x = *(*(p + 1) + 2);
拆解 ((p+1) + 2):
p + 1→ 指向第 2 行( goods[1])*(p + 1)→ 第 2 行的首地址(退化为 int *)*(p+1) + 2→ 第 2 行第 3 个元素的地址 *( ... )→ goods[1][2] =14
当心:
int (*p)[4]是行指针(p 一次跨 4 个 int),int *p[4]是指针数组。括号位置决定含义。
板块 2:二维数组作函数参数(第 4 题)→ C
voidaddOne(________, int r) { for (int j = 0; j < 5; j++) arr[r][j]++; }二维数组作形参时,列数(第二维)必须写出来,行数可以省:
int arr[][5] | |
int arr[3][5] | |
int **arr | |
int arr[][]int arr[5][] |
板块 3:引用与作用域(第 5、6 题)
第 5 题 → A
int score = 60; // 全局变量voidupdate(int &score){ score += 5; }intmain(){int score = 80; // 局部变量,遮蔽全局update(score); // 传引用 → 改的是局部 scorecout << score << ” ” << ::score; // 85 65}
两个关键点:
update(int &score)是引用传参,函数内改的就是 main里的score→ 80 + 5 = 85;全局变量用 ::score访问 → 仍是 60(函数改的不是它)。
输出
85 65。
第 6 题 → D:值传递 vs 引用传递
void reset(Device d) { d.state = 0; } // 值传递:改的是副本,无效void start(Device &d) { d.state += 1; } // 引用传递:真改Device d{7, 2};reset(d); // 状态不变,仍为 2start(d); // 状态 2 + 1 = 3cout << d.id << ” ” << d.state; // 7 3
这是"值传递/引用传递"最经典的对比考法:结构体传值不会改原对象,传引用才会。
板块 4:结构体与指针(第 7 题)→ C
Book books[2] = {{”C++”, 120}, {”Math”, 150}};Book *p = books + 1; // p 指向 books[1]p->pages += 10; // 等价于 books[1].pages += 10cout << books[1].name << ” ” << books[1].pages; // Math 160
books + 1不是"前进 1 个字节",而是前进一个 Book对象的长度;p->pages等价于 (*p).pages;150 + 10 = 160。
板块 5:排序(第 8、10、11、12、15 题)
第 8 题 → C(选"正确的")
第 10 题 → C:排序稳定性判断
排序前:(90,'A') (80,'B') (90,'C') (80,'D')排序后:(80,'B') (80,'D') (90,'C') (90,'A')
两个 90 分:原本 'A' 在前、'C' 在后;排序后变成了 'C' 前、'A' 后 ——相对顺序改变 → 不稳定。
稳定性定义:关键字相同的元素,排序前后相对次序是否保持。只要有一对变了,就是不稳定的。
第 11 题 → B:插入排序的移动条件
while (j >= 0 && a[j] > key) { // 升序:把比 key 大的元素往后挪a[j + 1] = a[j];j--;}a[j + 1] = key;
为什么用 > 而不是 >=?
用 >:遇到与key相等的元素就停下,把key放在它后面 → 相等元素保持原顺序 → 稳定;用 >=:相等元素也会被挪到后面 → 破坏稳定性。
(这也是判断题第 6 题的考点。)
第 12 题 → O(n²)
for (int i = 0; i < n; i++)for (int j = i + 1; j < n; j++) // 内层执行 n-1-i 次if (a[i] + a[j] == 100) cnt++;
总次数 = (n−1) + (n−2) + … + 1 = n(n−1)/2 →O(n²)。
第 15 题 → A:冒泡排序的提前退出
for (int i = n - 1; i > 0; i--) {bool changed = false; // ← 第一处:每轮开始先置 falsefor (int j = 0; j < i; j++) {if (a[j] > a[j + 1]) {swap(a[j], a[j + 1]);changed = true; // ← 第二处:发生了交换则置 true}}if (!changed) break; // 一轮下来没交换过 → 已经有序,提前结束}
答案:第一处false,第二处true。
优化后,已有序数组的最好情况复杂度从 O(n²) 降到 O(n)。
板块 6:文件与异常(第 13、14 题)
第 13 题 → B:文件输入
// data.txt 内容:Blue Skyifstream fin(”data.txt”);string a, b;fin >> a >> b; // >> 按空白分隔:a = ”Blue”,b = ”Sky”cout << b << ”-” << a; // Sky-Blue
>>读到空格就停,所以读完第一次后,第二次会自动跳过空白继续读。
第 14 题 → C:异常类型匹配
try {int age = -1;if (age < 0) throw age; // 抛出的是 intcout << ”A”;} catch (const char *msg) { cout << ”B”; } // 类型不匹配,跳过catch (int value) { cout << ”C” << value; } // 匹配!
输出C-1。
关键点:①
throw之后 try 块内代码不再执行(不会输出 A);② catch 按书写顺序匹配类型,int异常会跳过const char *分支。
三、判断题(10 题)
参考答案:√ × √ × √ × √ √ √ ×
*p += 5a += 5,10 → 15 | ||
*(*(a+1)+0) = a[1][0] = 5 | ||
void change(int x)x = 20 不影响外部,输出仍是 10 | ||
Point p{3, 4}; | ||
a[j] > key;写成 a[j] >= key 会破坏稳定性 | ||
j *= 2 为 log₂n 次 → O(n log n) | ||
ofstreamlog.txt 并写入 "Welcome" | ||
throw "Error"const char * 类型,catch (int e)类型不匹配,无法捕获 |
四处最容易错的
第 2 题:"声明"和"定义"是两回事。只要在调用处之前有声明(原型)即可,定义可以在文件后面甚至别的文件里。 第 4 题: void change(int x)与void change(int &x)只差一个&,但结果一个是 10、一个是 20。第 6 题: >与>=之差,就是"稳定"与"不稳定"之差。第 10 题:异常按类型匹配。 "Error"是字符串字面量(const char *),不是int。未被捕获的异常会导致程序调用terminate()终止。
四、编程题
4.1 新汉诺塔
题意:在经典汉诺塔规则之上增加限制——每次移动只能 A→B、B→C 或 C→A(单向循环,不能逆向)。求把 n 个圆盘从 A 全部移到 C 的最少步数。
样例:n = 2 → 7 步;n = 3 → 21 步。
参考实现(双状态递推)
int f[22], g[22]; // f[i]:i 个盘从 A 移到 B 的最少步数// g[i]:i 个盘从 A 移到 C 的最少步数for (int i = 1; i <= n; ++i) {f[i] = 2 * g[i - 1] + 1;g[i] = 2 * g[i - 1] + f[i - 1] + 2;}cout << g[n] << endl;
怎么想到这两个式子?
先明确环状方向:A → B → C → A。于是"从一根柱子到相邻柱"只需 1 步,而"跨到隔一根的柱子"必须走 2 步(例如 A→C 只能 A→B→C)。
由于移动方向受限,最大的盘无法一步到位,必须拆成两小步,因此需要两个递推量:
f[i]:把 i 个盘从一根柱子移到相邻柱子的最少步数; g[i]:把 i 个盘从一根柱子移到隔一根柱子的最少步数。 f[i] = 2 * g[i-1] + 1:最大盘只需 1 步(相邻),但之前要先把它上面的 i−1 个盘"挪开",而挪开的位置恰好是隔一根的柱子,所以要花 g[i-1],之后还要再挪回来一次。g[i] = 2 * g[i-1] + f[i-1] + 2:最大盘要跨一根柱子,至少 2 步;过程中 i−1 个盘要来回腾挪两次(一次 g型、一次f型)。
验证样例(f[0] = g[0] = 0)
与题面样例(n=2 → 7、n=3 → 21)完全吻合。
三个关键点
一定要先想清"方向是环状的",不能逆向移动——这是本题与经典汉诺塔唯一的区别,也是全部难度所在。 需要两个递推量( f和g)。只用一个数组推不出来,这是本题的分水岭。不要试图套用经典汉诺塔的2ⁿ − 1,那是错误方向。手推前 2~3 项验证递推式:如果推出的 g[2]不是 7,说明递推方向错了,立刻回头检查。
提示:结果随 n 增长很快(3 的量级),参考程序使用
int且数组只开到 22,说明 n 的范围不大。考试时先按数据范围确认是否需要long long。
4.2 有序网格
题意:给一个 n 行 m 列的二维网格,先对每一行从左到右升序排序,再对每一列从上到下升序排序,输出最终结果。
样例一:3 2 / 6 5 / 4 3 / 2 1
行排序: 6 5 → 5 64 3 → 3 42 1 → 1 2列排序: 5 3 1 → 1 3 5 (第 1 列)6 4 2 → 2 4 6 (第 2 列)结果: 1 23 45 6
参考实现
voidsort_row(int n){ // 对第 n 行排序for (int i = 1; i <= m; i++)for (int j = 1; j < m; j++)if (a[n][j] > a[n][j + 1]) swap(a[n][j], a[n][j + 1]);}voidsort_col(int m){ // 对第 m 列排序for (int i = 1; i <= n; i++)for (int j = 1; j < n; j++)if (a[j][m] > a[j + 1][m]) swap(a[j][m], a[j + 1][m]);}intmain(){// 读入后:for (int i = 1; i <= n; i++) sort_row(i); // 先把每行排好for (int i = 1; i <= m; i++) sort_col(i); // 再把每列排好// 输出}
四个关键点
顺序不能颠倒。题目明确"先行后列",先列后行结果完全不同。 排序方向要看清:行是"左右"比较 a[n][j]与a[n][j+1],列是"上下"比较a[j][m]与a[j+1][m],下标访问方式完全不同,这是最容易写错的地方。封装成两个函数,避免在同一段代码里混淆行列下标(四级考纲明确要求函数的使用)。 数据范围很小(n、m ≤ 15),O(n·m² ) 的冒泡排序完全够用,不需要写更复杂的排序算法。
五、四级划重点
p*p | *p += 4p 所指的变量 |
const int *pint * const p | * 左管值,在 * 右管指针 |
int (*p)[m]p + 1 跨一整行 | |
int arr[][5] | |
&,结果完全不同 | |
::name 访问全局 | |
p->member(*p).member | |
a[j] > key>= 会破坏稳定性 | |
flag | |
j *= 2 → O(n log n);j++ → O(n²) | |
ifstreamofstream,>> 按空白分隔 | |
throw |