2026年9月 GESP C++ 四级真题解析:指针、引用、排序"三件套"全上阵

四季读书网 5 0

2026年9月 GESP C++ 四级真题解析:指针、引用、排序"三件套"全上阵

四级这次把指针、引用、结构体、排序、文件、异常全部塞进了一张卷子,综合度是近几年最高的一次。好消息是:没有偏题,全是核心考点


一、答案速查

题号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
单选
C
B
B
C
A
D
C
C
C
C
B
D
B
C
A

判断题官方未公布答案,以下为按题意推算的参考答案。


二、单选题

板块 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 *p
(= int 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);                 // 传引用 → 改的是局部 score    cout << score << ” ” << ::score;   // 85 65}

两个关键点:

  1. update(int &score)
     是引用传参,函数内改的就是 main 里的 score → 80 + 5 = 85
  2. 全局变量用 ::score 访问 → 仍是 60(函数改的不是它)。

输出 85 65

第 6 题 → D:值传递 vs 引用传递

void reset(Device d)  { d.state = 0; }   // 值传递:改的是副本,无效void start(Device &d) { d.state += 1; }  // 引用传递:真改Device d{72};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(选"正确的")

选项
判断
A 三种排序最坏都是 O(n log n)
✘ 冒泡/插入/选择最坏都是 O(n²)
B 冒泡只能从小到大
✘ 把比较符号反过来即可降序
插入排序每次把一个元素插入前面已有序的序列
✔ 正确
D 选择排序每轮只比较一次
✘ 每轮要扫描全部未排序元素才能找到最小值

第 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;        // ← 第一处:每轮开始先置 false    for (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 < 0throw age;      // 抛出的是 int    cout << ”A”;catch (const char *msg) { cout << ”B”; }   // 类型不匹配,跳过  catch (int value)       { cout << ”C” << value; }  // 匹配!

输出C-1

关键点:① throw 之后 try 块内代码不再执行(不会输出 A);② catch 按书写顺序匹配类型,int 异常会跳过 const char * 分支。


三、判断题(10 题)

参考答案:√ × √ × √ × √ √ √ ×

关键依据
1
*p += 5
 就是 a += 5,10 → 15
2
×
函数可以只声明不定义(定义可放在别处);且"必须在调用前既声明又定义"过于绝对
3
二维数组按行优先连续存储,*(*(a+1)+0) = a[1][0] = 5
4
×
void change(int x)
 是值传递,函数内 x = 20 不影响外部,输出仍是 10
5
Point p{3, 4};
 是合法的列表初始化
6
×
升序稳定插入排序应写 a[j] > key;写成 a[j] >= key 会破坏稳定性
7
阶乘 4! = 24
8
外层 n 次,内层 j *= 2 为 log₂n 次 → O(n log n)
9
ofstream
 打开 log.txt 并写入 "Welcome"
10
×
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)

i
f[i]
g[i]
1
2×0+1 = 1
2×0+0+2 = 2
2
2×2+1 = 5
2×2+1+2 = 7 ← n=2 的答案
3
2×7+1 = 15
2×7+5+2 = 21 ← n=3 的答案

与题面样例(n=2 → 7、n=3 → 21)完全吻合

三个关键点

  1. 一定要先想清"方向是环状的",不能逆向移动——这是本题与经典汉诺塔唯一的区别,也是全部难度所在。
  2. 需要两个递推量f 和 g)。只用一个数组推不出来,这是本题的分水岭。不要试图套用经典汉诺塔的 2ⁿ − 1,那是错误方向。
  3. 手推前 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 6          4 3  →  3 4          2 1  →  1 2列排序:  5 3 1  →  1 3 5   (第 1 列)          6 4 2  →  2 4 6   (第 2 列)结果:    1 2          3 4          5 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);   // 再把每列排好    // 输出}

四个关键点

  1. 顺序不能颠倒。题目明确"先行后列",先列后行结果完全不同。
  2. 排序方向要看清:行是"左右"比较 a[n][j] 与 a[n][j+1],列是"上下"比较 a[j][m] 与 a[j+1][m]下标访问方式完全不同,这是最容易写错的地方。
  3. 封装成两个函数,避免在同一段代码里混淆行列下标(四级考纲明确要求函数的使用)。
  4. 数据范围很小(n、m ≤ 15),O(n·m² ) 的冒泡排序完全够用,不需要写更复杂的排序算法。

五、四级划重点

必须掌握
说明
p
 与 *p
*p += 4
 改的是 p 所指的变量
const int *p
 vs int * const p
const 在 * 左管值,在 * 右管指针
行指针
int (*p)[m]
p + 1 跨一整行
二维数组作形参
列数必须写出:int arr[][5]
值传递 / 引用传递
只差一个 &,结果完全不同
全局变量与作用域
局部同名会遮蔽全局,用 ::name 访问全局
结构体 + 指针
p->member
 等价于 (*p).member
三种 O(n²) 排序
冒泡、插入、选择;前两者稳定
插入排序条件
a[j] > key
 才稳定,>= 会破坏稳定性
冒泡优化
flag
 标记本轮是否交换,已有序则提前退出
复杂度估算
双重循环 j *= 2 → O(n log n);j++ → O(n²)
文件读写
ifstream
 / ofstream>> 按空白分隔
异常处理
throw
 后 try 内代码不执行;catch 按类型和书写顺序匹配

抱歉,评论功能暂时关闭!