附录 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 递推四步
- 定义状态;
- 写初值;
- 写递推关系;
- 写循环范围。
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