火龙信奥
  • 分享
  • 课程
  • 题库
  • CSES
    • GESP
    • CSP
  • 打卡
    • 代码对战
    • 快速对战
  • 题单
  • e课堂
  • 团队
  • 荣誉墙
  • 商城
  • 登录 / 注册

附录 A 四级考前速查与真题训练安排

作者: 作者的头像   huolong , 时间:2026-09-08 10:15:02 , 所有人可见, 阅读  14

附录 A 四级考前速查与真题训练安排

四级讲义主体知识已经覆盖函数、作用域、三种传参、指针、二维数组、结构体、递推、排序、复杂度、文件读写和异常处理。考前阶段不适合再继续扩展新知识,更适合把已经学过的内容压缩成速查表和训练清单。

这一部分用于最后复习。目标有三个: - 快速回顾四级高频概念; - 形成选择题、判断题、代码跟踪题的检查顺序; - 安排真题训练和错题复盘。


1. 函数与作用域速查

1.1 函数声明与定义

函数调用前,编译器必须已经知道函数的返回值类型、函数名和参数列表。 可以先声明,再定义:

int add(int a, int b);
int main() {
    cout << add(1, 2);
    return 0;
}
int add(int a, int b) {
    return a + b;
}
  • 函数声明末尾有分号:int add(int a, int b);
  • 函数定义有函数体: cpp int add(int a, int b) { return a + b; }

常见判断: | 说法 | 判断 | | :--- | :--- | | 函数必须定义在调用之前 | 错 | | 函数调用前必须已有声明或定义 | 对 | | 函数声明末尾要写分号 | 对 | | void 函数可以返回具体值 | 错 |

1.2 形参与实参

  • 形参写在函数定义或声明中:int add(int a, int b)
  • 实参写在调用中:add(x, y)
  • 参数按位置对应,不按名字对应。
void f(int a, int b) {
    cout << a << " " << b;
}
int main() {
    int a = 1, b = 2;
    f(b, a);
}

输出:2 1

1.3 作用域

变量查找顺序:从当前位置向外层查找,优先使用最近的定义。

int x = 10;
int main() {
    int x = 20;
    cout << x << " " << ::x;
    return 0;
}

输出:20 10 (::x 表示全局变量 x。)

常见判断: | 场景 | 结果 | | :--- | :--- | | 函数内定义局部变量 | 只在函数内有效 | | {} 内定义变量 | 只在当前代码块内有效 | | 局部变量与全局变量同名 | 局部变量遮蔽全局变量 | | 使用 ::x | 访问全局变量 x |


2. 三种传参方式速查

2.1 值传递

void f(int x) {
    x += 10;
}

函数内修改的是副本,不影响实参。

int a = 5;
f(a);
cout << a;

输出:5

2.2 引用传递

void f(int &x) {
    x += 10;
}

函数内修改的是原变量。

int a = 5;
f(a);
cout << a;

输出:15

2.3 指针传递

void f(int *p) {
    *p += 10;
}

调用时传地址:

int a = 5;
f(&a);
cout << a;

输出:15

2.4 三种传参对比表

形参写法 调用写法 函数内修改 是否影响原变量
int x f(a) x = 10 否
int &x f(a) x = 10 是
int *p f(&a) *p = 10 是

2.5 混合传参题读法

void fun(int a, int &b, int *c) {
    a += 1;
    b += 2;
    *c += 3;
}
int x = 1, y = 1, z = 1;
fun(x, y, &z);
cout << x << " " << y << " " << z;

输出:1 3 4

判断顺序: - a 是值传递,x 不变; - b 是引用传递,y 被修改; - c 是指针传递,*c 修改 z。


3. 指针速查

3.1 & 和 *

int a = 10;
int *p = &a;
写法 含义
a 变量 a 的值
&a 变量 a 的地址
p 指针变量,保存 a 的地址
*p p 指向的变量,也就是 a
&p 指针变量 p 自己的地址

执行:

*p = 20;

等价于:a = 20;

3.2 二级指针

int a = 5;
int *p = &a;
int **q = &p;

可以画成:q -> p -> a 所以: | 表达式 | 含义 | | :--- | :--- | | *q | p | | **q | a |

执行:**q += 7; 会把 a 从 5 改成 12。

3.3 空指针

int *p = nullptr;

p 当前不指向合法对象。 不能写:cout << *p; (空指针不能解引用。)

3.4 未初始化指针

int *p;
*p = 10;

这种写法不安全。p 没有指向合法变量。 正确写法:

int a = 0;
int *p = &a;
*p = 10;

3.5 指针与数组

int a[5] = {1, 2, 3, 4, 5};
int *p = a;

此时: | 表达式 | 对应元素 | | :--- | :--- | | *p | a[0] | | *(p + 1) | a[1] | | *(p + 2) | a[2] |

数组名可以表示首元素地址,但不能自增: - a++; // 错误 - p++; // 可以


4. 二维数组速查

4.1 定义与访问

int a[3][4]; 表示 3 行 4 列。 访问第 2 行第 3 列:a[1][2] (因为数组下标从 0 开始。)

4.2 初始化

int a[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

a[1][2] 的值是 6。

4.3 按行连续存储

int a[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

内存顺序可以看成:1 2 3 4 5 6 7 8 9 10 11 12 若:int *p = &a[0][0]; 则 *(p + 5) 对应 a[1][1],值为 6。

4.4 *(*(a+i)+j)

*(*(a + i) + j) 等价于 a[i][j]。 例如:*(*(a + 1) + 2) 等价于 a[1][2]。

4.5 二维数组作为函数参数

第二维必须写出:void f(int a[][4], int n) 也可以写:void f(int (*a)[4], int n) 不能直接写:void f(int a[][]) 也不能把普通二维数组直接当成 int **a。


5. 结构体速查

5.1 定义

struct Student {
    string name;
    int score;
};

结构体定义末尾要写分号。

5.2 变量与成员访问

Student s;
s.name = "Yang";
s.score = 90;

结构体变量访问成员用点号:s.score

5.3 结构体指针

Student *p = &s;

访问成员:p‐>score 等价于 (*p).score。

5.4 结构体传参

参数写法 是否修改原对象
Student s 否
Student &s 是
Student *s 是(通过 s‐>成员 修改)

5.5 结构体数组排序

结构体排序时要交换整个对象:swap(a[i], a[pos]); 不能只交换某个成员:swap(a[i].score, a[pos].score); (会导致信息错乱。)


6. 递推速查

6.1 递推四步

  1. 定义状态;
  2. 写初值;
  3. 写递推关系;
  4. 写循环范围。

6.2 斐波那契数列

若:

f[1] = 1;
f[2] = 1;

递推:f[i] = f[i ‐ 1] + f[i ‐ 2]; 循环:for (int i = 3; i <= n; i++)

6.3 爬楼梯

每次爬 1 阶或 2 阶:

f[1] = 1;
f[2] = 2;
f[i] = f[i ‐ 1] + f[i ‐ 2];

6.4 滚动变量

int a = 1, b = 2, c = 0;
for (int i = 3; i <= n; i++) {
    c = a + b;
    a = b;
    b = c;
}

更新顺序不能乱。先算新值,再移动旧值。

6.5 阶乘

long long ans = 1;
for (int i = 2; i <= n; i++)
    ans *= i;

7. 排序速查

7.1 三种排序对比

排序 核心思想 稳定性 平均复杂度
冒泡排序 相邻比较交换 稳定 $O(n^2)$
选择排序 每轮选最小值 通常不稳定 $O(n^2)$
插入排序 插入到有序部分 稳定 $O(n^2)$

7.2 冒泡排序

for (int i = n ‐ 1; i > 0; i‐‐)
    for (int j = 0; j < i; j++)
        if (a[j] > a[j + 1])
            swap(a[j], a[j + 1]);

特点: - 每轮把最大值放到后面; - 相等时不交换,稳定; - 可用 flag 提前结束。

7.3 选择排序

for (int i = 0; i < n ‐ 1; i++) {
    int pos = i;
    for (int j = i + 1; j < n; j++)
        if (a[j] < a[pos]) pos = j;
    swap(a[i], a[pos]);
}

特点: - 每轮选最小值; - 普通写法不稳定; - 即使已经有序,也要大量比较。

7.4 插入排序

for (int i = 1; i < n; i++) {
    int key = a[i];
    int j = i ‐ 1;
    while (j >= 0 && a[j] > key) {
        a[j + 1] = a[j];
        j‐‐;
    }
    a[j + 1] = key;
}

特点: - 前面部分保持有序; - 当前元素插入到合适位置; - 相等时不移动,稳定; - 接近有序时效率高。

7.5 稳定性判断

稳定排序要求:关键字相同的元素,排序后相对顺序不变。 例如原来 {3, 'A'}, {3, 'B'},排序后仍是 A 在 B 前,才保持稳定性。


8. 复杂度速查

8.1 常见复杂度

代码形式 复杂度
顺序语句 $O(1)$
一层循环到 $n$ $O(n)$
两层都到 $n$ 的嵌套循环 $O(n^2)$
三层都到 $n$ 的嵌套循环 $O(n^3)$
每次乘 2 或除 2 $O(\log n)$
枚举所有子集 $O(2^n)$
枚举子集并扫描元素 $O(n \cdot 2^n)$

8.2 三角形循环

for (int i = 1; i <= n; i++)
    for (int j = 1; j <= i; j++)
        cnt++;

总次数:$1 + 2 + \dots + n$,复杂度:$O(n^2)$。

8.3 连续循环

for (int i = 1; i <= n; i++) cnt++;
for (int j = 1; j <= n; j++) cnt++;

复杂度是 $O(n)$,不是 $O(n^2)$。

8.4 常数内层循环

for (int i = 1; i <= n; i++)
    for (int j = 1; j <= 10; j++)
        cnt++;

复杂度是 $O(n)$。

8.5 排序复杂度

排序 最好 最坏 额外空间
冒泡排序 有 flag 时 $O(n)$ $O(n^2)$ $O(1)$
选择排序 $O(n^2)$ $O(n^2)$ $O(1)$
插入排序 $O(n)$ $O(n^2)$ $O(1)$

9. 文件操作速查

9.1 读文件

ifstream fin("data.txt");
int x;
fin >> x;
fin.close();

9.2 写文件

ofstream fout("out.txt");
fout << "Hello" << endl;
fout.close();

9.3 追加写文件

ofstream fout("log.txt", ios::app);
fout << "new line" << endl;
fout.close();

9.4 freopen

  • 输入重定向:freopen("input.txt", "r", stdin);
  • 输出重定向:freopen("output.txt", "w", stdout);

9.5 cout.rdbuf

ofstream logFile("log.txt");
streambuf *oldCout = cout.rdbuf();
cout.rdbuf(logFile.rdbuf());
cout << "file" << endl;
cout.rdbuf(oldCout);
cout << "screen" << endl;
logFile.close();

9.6 文件操作常见陷阱

ofstream fout("log.txt");
cout << "Hello" << endl;
fout.close();

这不会把 Hello 写入 log.txt,因为输出写给了 cout,不是 fout。 正确写法:fout << "Hello" << endl;


10. 异常处理速查

10.1 基本结构

try {
    throw 1;
} catch (int x) {
    cout << x;
}

输出:1

10.2 执行规则

  • try 中放可能抛出异常的代码;
  • throw 抛出异常;
  • catch 捕获异常;
  • throw 后,同一个 try 块中后面的语句不执行;
  • 多个 catch 从上到下匹配;
  • catch(...) 可以捕获任意异常;
  • 异常被捕获后,继续执行 try-catch 后面的代码。

10.3 输出题示例

try {
    cout << "A";
    throw 1;
    cout << "B";
} catch (int x) {
    cout << "C";
}
cout << "D";

输出:ACD (B 不执行。)


11. 选择题检查顺序

做选择题时,不要急着选答案。可以按下面顺序检查:

11.1 语法题

先看: - 函数有没有返回值类型; - 函数声明有没有分号; - void 函数是否返回了具体值; - 结构体定义末尾有没有分号; - 二维数组第二维是否写出; - 指针是否初始化; - 引用参数调用是否传入了变量。

11.2 输出题

按顺序模拟: 1. 从 main 开始; 2. 遇到函数调用,进入函数; 3. 给形参建立变量表; 4. 判断值传递、引用传递、指针传递; 5. 遇到同名变量,从内到外找; 6. 遇到指针,画箭头; 7. 遇到数组,注意下标; 8. 遇到异常,跳到匹配 catch。

11.3 排序题

先判断是哪种排序: - 相邻比较交换:冒泡; - 选最小值位置:选择; - key 和元素后移:插入。 再看:问一轮还是完整排序、升序还是降序、稳定性是否与相等元素有关、循环边界是否越界。

11.4 复杂度题

按规则:嵌套相乘;连续相加;去掉常数;保留最高阶;常数循环不增加阶数;三角形循环是 $O(n^2)$;递归看分支数量。


12. 判断题常见陷阱

说法 判断
函数定义必须写在调用前 错
函数调用前必须已有声明或定义 对
普通形参能修改实参 错
引用形参能修改实参 对
指针保存的是地址 对
未初始化指针可以直接解引用 错
空指针可以安全解引用 错
数组名可以自增 错
二维数组按行连续存储 对
普通二维数组可以直接传给 int** 错
结构体指针访问成员用 ‐> 对
结构体值传递会修改原对象 错
递推就是函数自己调用自己 错
标准冒泡排序稳定 对
标准插入排序稳定 对
普通选择排序稳定 错
选择排序最好情况是 $O(n)$ 错
插入排序接近有序时较快 对
"GESP.txt" 是绝对路径 错
创建 ofstream 后,cout 会自动写入该文件 错
catch(...) 可以捕获任意异常 对
throw 后同一 try 块剩余语句继续执行 错

13. 编程题检查清单

13.1 输入

  • 是否读入了所有数据;
  • 多组数据是否循环处理;
  • 字符串是否包含空格;
  • 二维数组行列是否读反;
  • 文件输入和标准输入是否混用。

13.2 存储

  • 一维数组是否开够;
  • 二维数组是否开够;
  • 是否需要 long long;
  • 结构体成员是否完整;
  • 是否需要记录出现状态。

13.3 下标

  • 从 0 开始还是从 1 开始;
  • 循环边界是否统一;
  • 访问 a[i-1] 时,i 是否从 2 开始;
  • 二维数组第 $r$ 行第 $c$ 列是否对应 a[r-1][c-1] 或 a[r][c]。

13.4 函数

  • 返回值类型是否正确;
  • 每条路径是否都有返回值;
  • 参数是否需要引用或指针;
  • 二维数组参数第二维是否写出;
  • 函数名是否与调用一致。

13.5 条件

  • 是否处理相等情况;
  • 是否区分 “至多一次” 和 “恰好一次”;
  • 排序升序和降序是否写反;
  • 并列时是否按题目要求处理;
  • 是否处理 $n = 1$ 等边界。

13.6 输出

  • 输出内容是否符合题目;
  • 是否多输出调试内容;
  • 是否需要换行;
  • 大小写是否一致;
  • 小数是否按要求保留位数。

14. 真题训练安排

14.1 第一轮:按知识点刷题

第一轮不追求速度,按章节对应知识点做题。建议顺序: 1. 函数、作用域、传参; 2. 指针与二维数组; 3. 结构体; 4. 递推; 5. 排序与稳定性; 6. 复杂度; 7. 文件读写; 8. 异常处理; 9. 编程题模型。

每做完一题,写下它考的是哪一个知识点。不要只写 “粗心”,要写清楚错因: - 错因:把值传递当成引用传递。 - 错因:二维数组函数参数漏写第二维。 - 错因:插入排序最后位置应为 j + 1。 - 错因:throw 后面的语句不会执行。

14.2 第二轮:整套限时训练

第二轮按完整试卷训练。建议:严格计时;先做会做的题;代码跟踪题写变量表;排序题手动模拟;编程题写完后至少检查一组小样例。限时训练的目标是熟悉考试节奏。四级题目并不需要抢时间,但要避免在某一道代码跟踪题上停太久。

14.3 第三轮:错题复盘

第三轮只看错题和不确定题。每道错题至少回答三个问题: 1. 当时为什么选错; 2. 正确规则是什么; 3. 下次看到什么特征要警惕。

14.4 考前最后一天

考前最后一天不建议再学新内容。建议做三件事:看速查表、复盘错题、写 2 到 3 道编程题保持手感。重点看高频点: - 函数声明与默认参数; - 作用域和同名变量; - 三种传参; - 指针 p、*p、&a; - 二维数组参数; - 结构体指针 ‐>; - 递推初值和更新顺序; - 三种排序稳定性; - 插入排序 a[j+1] = key; - 冒泡第一轮结果; - 三角形循环复杂度; - ofstream 与 cout 的区别; - throw 后的执行流程。


15. 考场答题建议

15.1 选择题

选择题不要凭第一眼感觉。看代码题时,建议在草稿上写变量表。 - 函数题写出:main: a = ?, f: x = ? - 指针题画出:p -> a, q -> p -> a - 二维数组题写出:a[i][j] 的线性位置 = i * 列数 + j - 排序题只模拟题目要求的轮数,不要多算。

15.2 判断题

判断题要警惕绝对化表述(如:一定、必须、只能、所有、总是)。很多概念有条件: - 函数定义不一定要在调用前,但调用前要有声明; - 冒泡排序标准写法稳定,但相等也交换的写法会不稳定; - ofstream 可以写文件,但 cout 不会自动写入它; - 递归不一定指数级,直接斐波那契递归才是指数级常见例子。

15.3 编程题

编程题先写清楚主流程,再补函数。推荐顺序: 1. 写输入; 2. 写存储; 3. 写核心函数; 4. 写主循环; 5. 写输出; 6. 用样例手动跑一遍; 7. 检查边界。

不要在没有想清楚下标的情况下直接写复杂循环。四级编程题的主要错误通常是边界和条件,不是算法本身。


16. 最后一张总表

模块 必会内容 高频错误
函数 声明、定义、调用、返回值 调用前无声明,void 返回具体值
作用域 全局、局部、块作用域 同名变量看错
传参 值、引用、指针 把值传递当成能改原变量
指针 &、*、二级指针、空指针 未初始化或空指针解引用
二维数组 下标、连续存储、函数参数 第二维省略,误用 int**
结构体 成员访问、数组、指针、嵌套 指针用点号,只交换成员
递推 初值、关系、循环范围 更新顺序错,边界未处理
排序 冒泡、选择、插入、稳定性 选择排序稳定性误判
复杂度 循环、三角形循环、指数复杂度 连续循环误判为嵌套
文件 ifstream、ofstream、freopen 打开文件却仍写 cout
异常 try、throw、catch throw 后语句误认为会执行
编程题 数位、字符串、网格、结构体排序 下标、数据类型、输出格式错误

四级考试的核心不是记很多高级算法,而是把基础 C++ 语法、数组结构、函数调用、简单算法过程读准、写稳。考前复习应围绕真题和错题反复确认这些细节。

—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com

关于火龙

  • 关于我们
  • 学员获奖
  • 预约试听
  • ACM课程
  • CSP课程
  • 学习指南

帮助中心

  • 用户协议
  • 打字练习 HOT
  • 在线画图
  • DevC++下载
  • CSP报名
  • GESP官网

推荐课程

  • C++零基础入门(可试看)
  • C++进阶提升
  • GESP考级辅导
  • GESP打卡
  • CSP-J/S打卡

公众号

火龙信奥公众号二维码

© 2017-2026 义乌市睿码科技有限公司版权所有 浙ICP备2021013995号

火龙信奥
请输入登录信息


请完成安全验证
验证码底图 滑块
向右拖动滑块完成验证
请输入用户名 / 绑定的手机号码



请输入注册信息(手机号验证码注册)





验证码5分钟有效,60秒内不可重复获取,每日最多3次

微信登录

微信登录二维码

正在生成二维码...

账号已过期,请续期。
去续期

绑定手机号

📱

为了更好地保护您的账号安全,享受完整的平台服务

请您尽快绑定手机号码