简介:C++期末考试复习题.docx 面向高校计算机相关专业、正在备考C++语言程序设计期末考试的本科生,聚焦面向对象编程核心考点的系统梳理与自测。内容以选择题和填空题为骨架,覆盖类的声明与数据成员、访问修饰符的任意顺序与默认权限、构造函数与析构函数、new与delete的内存管理、运算符重载与友元函数、继承与多态、虚基类与纯虚函数、重载函数的匹配依据、引用的初始化、静态成员、内联函数以及fstream文件操作等章节,题目后附有参考答案,便于对照查漏补缺。整包仅1个docx文件,体积约136KB,可直接打开打印或批注,适合考前集中刷题与知识点串联。目前已有126人学习下载,可用来快速定位薄弱环节,配合教材复习形成完整闭环。
1. 一份 C++期末考试复习题.docx,真正卡住人的地方在哪
期末前一周,拿到一份 C++期末考试复习题.docx,多数人的第一反应是打开从头刷到尾。刷完两轮,遇到读程序写结果还是靠猜,遇到编程题还是写不出能编译通过的完整代码——问题不在题量,在于这份文档只给了题目和答案,没给「这条语法到底在机器上怎么表现」的验证路径。
我一般把这份复习题当成三个东西用:一份考纲(题目分布就是老师的重点分布)、一份语法清单(每道题对应一到两个语言点)、一份待验证断言(所有关于输出结果的判断,都必须扔进编译器跑一遍)。指针用法、字符串数组初始化、结构体链表、流 I/O、冒泡排序、二分查找这些考点,在纸面上看都眼熟,只有跑起来才知道哪里会越界、哪里会读到空串。
这篇按复习顺序推:先拆考点、再配环境把每道题跑起来、然后把五类高频题型的可抄模板写死、最后落到错题驱动和自动对拍。适合正在备考的人,也适合带课设、带实验的助教。
2. 把 C++期末考试复习题拆成可打卡的知识点清单
拿到文档先别做题,花四十分钟做一次「分类」,后面能省下十倍时间。做法是只看题干关键词,不看答案,把每道题归到五类里。
2.1 按题型分类:复习题的分数其实是不均匀的
| 题型 | 文档里的典型问法 | 复习动作 | 建议时间占比 |
|---|---|---|---|
| 概念判断题 | 引用与指针的区别、const 放在 * 左右 | 只记结论 + 一个反例 | 10% |
| 读程序写结果 | 循环嵌套、指针自增、输出格式 | 手算后必须编译核对 | 30% |
| 改错题 | 数组越界、忘 delete、缺分号、头文件 | 建错误清单,按错因归类 | 15% |
| 编程题 | 冒泡排序、二分查找、链表、判断质数 | 默写模板到能一次编译通过 | 35% |
| 简答/概念题 | 三大特性、重载与重写的区别 | 关键词答题,按点给分 | 10% |
读程序写结果这一类最值得投资。它同时考察运算符优先级、变量作用域、内存布局三件事,而这三件事恰好是编程题出错的主要来源。
2.2 知识点到题号模式的映射表
建立下面这张表,左边是考点,右边是你在复习题里应该能一眼认出的提问特征。填不满的行,就是你还没复习到位的部分。
| 考点 | 识别特征 | 最小验证手段 |
|---|---|---|
| 指针用法 | *p++、*(p+2)、p = a | 打印地址和值,看移动步长 |
| 全局变量 / 局部变量 | 同名变量在两个作用域内 | 加块作用域{}后重新观察 |
| 字符串数组初始化 | char s[]="abc"、char s[3]="abc" | sizeof与strlen对比 |
| 字符串转数组 | std::string与char[]混用 | s.c_str()与s.data() |
| 结构体链表 | Node *next、new Node | 打印每个节点的地址 |
| 流 I/O | cin >>与getline连用 | 输入带空格的名字 |
| 冒泡排序 | 双层循环、交换相邻元素 | 逆序数组跑一遍看比较次数 |
| 二分查找 | 有序数组、mid、lo/hi | 找不存在的值看返回什么 |
| sort 引入库 | 要求用标准库排序 | 检查#include <algorithm> |
| 判断质数优化 | 大数判断、超时提示 | 试到sqrt(n)的耗时对比 |
注意:表里任何一行只要你在复习题里找不到对应题目,说明这份文档不是全覆盖,需要拿教材目录补齐。
2.3 用脚本把复习题.docx 拆成可勾选的任务队列
文档很长时,手工划题号容易漏。用 python-docx 把段落按题号切开,导出成 JSON,再逐条打勾。
from docx import Document import re, json doc = Document("C++期末考试复习题.docx") # 匹配 "1." "2、" "3)" "(4)" 这类题号开头的段落 pat = re.compile(r"^\s*[((]?(\d{1,3})[)).、]\s*") items, cur = [], None for p in doc.paragraphs: # paragraphs 不含表格单元格 t = p.text.strip() if not t: continue if pat.match(t): if cur: items.append(cur) cur = {"no": int(pat.match(t).group(1)), "text": t, "done": False} elif cur is not None: cur["text"] += "\n" + t # 题干换行后续行拼回去 if cur: items.append(cur) for tbl in doc.tables: # 表格里的题目单独取,避免漏题 for row in tbl.rows: cells = [c.text.strip() for c in row.cells] if cells and pat.match(cells[0]): items.append({"no": int(pat.match(cells[0]).group(1)), "text": " | ".join(cells), "done": False}) items.sort(key=lambda x: x["no"]) json.dump(items, open("questions.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2) print("题量:", len(items))正则pat决定题号识别能力,如果你的文档用「第 3 题」这种写法,把正则改成r"^\s*第\s*(\d+)\s*题"。遍历doc.paragraphs拿不到表格内容,所以额外扫一遍doc.tables,两边合并后按题号排序,done字段留作后续勾选状态。题量打印出来和文档目录对一下,数量对不上说明有题号格式不统一,回去补正则而不是手工补题。
2.4 编译期错误和运行期错误必须分开记
改错题里最常见的陷阱是把两类错误混在一起。编译期错误有明确报错行号和英文提示,运行期错误往往什么都不报,或者干脆段错误。复习时给每道改错题标一个标签:CE(编译不过)、RE(运行崩溃)、WA(结果不对)、UB(能跑但行为未定义)。第四类最坑,比如delete之后再用指针、越界读一个元素,考试时可能刚好「跑对了」,一问原理就露馅。
3. 用 VS Code 与 g++ 把每道复习题跑成可观察的程序
纸上推演的结论,只有跑起来才算数。这一章给一套最小可用流程:单文件编译、VS Code 配置、gdb 单步、以及常见报错的处理。
3.1 单文件编译:g++ 和 cl 两条路
复习题的每题单独存成一个文件,比如q12_ptr.cpp,编译命令固定成下面这行,别省编译选项。
g++ -std=c++17 -Wall -Wextra -g -O0 -fsanitize=address,undefined q12_ptr.cpp -o q12 ./q12参数逐个说明:-std=c++17锁定语言标准,避免不同机器行为不一致;-Wall -Wextra打开警告,很多「能跑但有隐患」的写法会在这里被点出来;-g生成调试信息,供 gdb 使用;-O0关闭优化,优化会让单步调试的变量值对不上源码;-fsanitize=address,undefined在运行时捕获越界和未定义行为,排错阶段必开,交卷前关了再测一次看是否有警告被优化掉。
Windows 上走 MSVC 的话,命令是:
cl /std:c++17 /EHsc /W4 /Zi /Od q12_ptr.cpp q12_ptr.exe/EHsc打开标准 C++ 异常模型,/W4是高警告等级,对应 g++ 的-Wall -Wextra。
3.2 VS Code 里三份配置文件的写法与路径优先级
在题目文件夹下建.vscode,放三份文件。c_cpp_properties.json只影响智能提示和跳转,不影响编译结果,写错的表现是红线乱标、跳转跳到别的头文件。
{ "version": 4, "configurations": [ { "name": "gcc-x64", "compilerPath": "C:/msys64/ucrt64/bin/g++.exe", "includePath": [ "${workspaceFolder}/**", "C:/msys64/ucrt64/include/**" ], "cStandard": "c17", "cppStandard": "c++17", "intelliSenseMode": "windows-gcc-x64" } ] }路径优先级按includePath数组顺序从上往下找,命中即停,所以把自己写的头文件目录放在最前面,系统目录放后面;compilerPath一旦填对,编译器自带的系统头路径会自动补上,比手工在includePath里堆路径可靠得多。改完配置执行一次命令面板里的C/C++: Reset IntelliSense Database,否则旧缓存会继续生效。
tasks.json负责编译当前文件:
{ "version": "2.0.0", "tasks": [ { "label": "build-active-file", "type": "shell", "command": "g++", "args": [ "-std=c++17", "-Wall", "-Wextra", "-g", "-O0", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}.exe" ], "group": { "kind": "build", "isDefault": true }, "problemMatcher": ["$gcc"] } ] }${file}是当前打开的文件,${fileBasenameNoExtension}去掉扩展名做输出名,所以一个文件夹里几十道复习题互不干扰,按Ctrl+Shift+B就编译当前那道。problemMatcher填$gcc后,编译器报错会直接标在源码行上,跳转到出错位置只要按 F8。
调试用launch.json把 gdb 接上:
{ "version": "0.2.0", "configurations": [ { "name": "gdb-launch", "type": "cppdbg", "request": "launch", "program": "${fileDirname}/${fileBasenameNoExtension}.exe", "preLaunchTask": "build-active-file", "MIMode": "gdb", "miDebuggerPath": "C:/msys64/ucrt64/bin/gdb.exe", "externalConsole": true } ] }preLaunchTask指向tasks.json里的 label,保证每次调试前先重新编译,避免调试的是上一版二进制。
3.3 一道指针题从手算到 gdb 核对
复习题里出现*p++之类表达式时,光靠口诀很容易记反。写一个最小程序,然后用 gdb 看每一步。
#include <iostream> int main() { int a[4] = {10, 20, 30, 40}; int *p = a; // 数组名退化为首元素地址 std::cout << *p << "\n"; // 10 (*p)++; // 先解引用再自增:a[0] 变 11 std::cout << *p << "\n"; // 11 int x = *p++; // 先取值 11,再让 p 指向 a[1] std::cout << x << " " << *p << "\n"; // 11 20 std::cout << sizeof(a) / sizeof(a[0]) << "\n"; // 4 return 0; }gdb 里逐条验证:
g++ -std=c++17 -g -O0 q_ptr.cpp -o q_ptr gdb -q ./q_ptr (gdb) break main (gdb) run (gdb) next (gdb) print a[0] (gdb) print p (gdb) print *p (gdb) x/4dw a # 以 4 字节十进制查看整个数组 (gdb) display *p (gdb) nextx/4dw a的意思是:从地址a开始,按 4 字节一组、以十进制显示 4 组。用它比对数组内容比一个个print a[i]快。display *p每次停下都自动打印*p,观察指针移动,值的变化过程一目了然。括号的位置决定结果:(*p)++改的是数组元素,*p++改的是指针本身,这两者在复习题里几乎是必考项。
3.4 复习题附带工程跑不起来时的几类报错
| 现象 | 常见原因 | 处理 |
|---|---|---|
undefined reference to 'foo()' | 只有声明没有定义,或多文件没一起编译 | 把定义补齐,或写成g++ a.cpp b.cpp |
| 运行时报缺少运行库 | 程序依赖的运行时组件没装 | 安装对应版本的 Microsoft Visual C++ Redistributable |
装 Python 包时报error: Microsoft Visual C++ 14.0 or greater is required | 缺 C++ 编译器工具链 | 装 Visual Studio Build Tools,勾选「使用 C++ 的桌面开发」 |
expected ';'指向前一行 | 前一行少分号、括号没闭合 | 往上一行找,别看报错行 |
| 中文输出乱码 | 源文件编码与控制台代码页不一致 | 统一保存为 UTF-8,或改用英文输出 |
那个14.0指的是编译器工具集版本号,代表 VS 2015 这一代,后续版本向下兼容,装更新的版本同样满足要求。判断顺序是先看编译期报错,再看运行期依赖,最后才怀疑代码逻辑——很多人一上来就改代码,结果白改半小时。
4. 复习题里最容易丢分的五类题型与可抄模板
这一章把五类高频题型的代码模板固定下来,目标是看到题目能默写、写完能一次编译通过。
4.1 指针与内存:数组名退化与函数传参
| 写法 | 实参类型 | 形参里sizeof的值 |
|---|---|---|
void f(int a[], int n) | 数组 | 指针大小(8 或 4 字节) |
void f(int *a, int n) | 数组或指针 | 指针大小 |
void f(int (&a)[5]) | 长度恰为 5 的数组 | 20 字节 |
template<int N> void f(int (&a)[N]) | 任意长度数组 | 4*N 字节 |
#include <iostream> void byPointer(int *a, int n) { // 数组名退化为指针 std::cout << sizeof(a) << "\n"; // 8(64 位) } template <int N> void byRef(int (&a)[N]) { // 真正的数组引用 std::cout << sizeof(a) << "\n"; // 4*N } int main() { int a[5] = {1, 2, 3, 4, 5}; byPointer(a, 5); byRef(a); std::cout << (&a)[1] - a << "\n"; // &a 是数组指针,+1 跨 20 字节 return 0; }byPointer拿到的只是一个地址,长度必须额外传参,这是复习题里改错题的常见考点。byRef用模板推导数组长度,sizeof在函数内仍然有效,代价是每个长度生成一份实例。&a的类型是「指向长度为 5 的 int 数组的指针」,加一跨过整个数组,(&a)[1] - a结果是 5,这个表达式在概念题里出现频率很高。
函数指针做回调也常在复习题里冒头,结构通常是「遍历函数 + 对每个元素执行的操作」:
void forEach(int *a, int n, void (*action)(int&)) { for (int i = 0; i < n; ++i) action(a[i]); // 通过回调修改元素 } void twice(int &v) { v *= 2; } // 调用:forEach(arr, n, twice);action的参数用引用而不是值,回调才能改到原数组,这是判断回调写得对不对的关键。
4.2 字符串与字符数组初始化:sizeof 和 strlen 必须分清
char s1[] = "hello"; // 6 字节:5 个字符 + '\0' char s2[10] = "hi"; // 10 字节,后 8 个字节全补 0 const char *s3 = "hello"; // 指向字面量,不能改内容 char s4[5] = "hello"; // 错误:没有空间存放 '\0's1的sizeof是 6,strlen是 5;如果换成char *p = s1;,sizeof(p)变成指针大小,和strlen(p)无关。s4这种写法在新标准下直接编译不过,能通过的老编译器会把'\0'挤掉,后面凡是当字符串用的地方都可能越界读——这是复习题最爱设的陷阱。
std::string转字符数组的两种方式:
#include <string> #include <cstring> #include <vector> std::string s = "a,b,c"; const char *p = s.c_str(); // 只读访问,s 改动后 p 可能失效 std::vector<char> buf(s.begin(), s.end()); buf.push_back('\0'); // 想要可写副本就自己补结束符s.c_str()返回的指针生命周期跟着s走,s被修改或销毁后不能再用。需要可写副本时用vector<char>拷一份,别直接const_cast去改字面量,那是未定义行为。
4.3 冒泡排序、二分查找与两个常用小算法模板
冒泡排序背加法而不是背代码:
void bubbleSort(int a[], int n) { for (int i = 0; i < n - 1; ++i) { bool swapped = false; // 本轮是否发生交换 for (int j = 0; j < n - 1 - i; ++j) { // 末尾 i 个已就位 if (a[j] > a[j + 1]) { int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t; swapped = true; } } if (!swapped) break; // 已经有序,提前退出 } }内层上界是n-1-i,因为每完成一轮最大值就被推到末尾;swapped让最好情况降到 O(n)。考试时写j < n-1也能出正确结果,只是多做无用比较,不算错但会丢优化分。
二分查找统一用闭区间写法,不容易错:
int lowerBound(const int a[], int n, int target) { int lo = 0, hi = n - 1, ans = -1; while (lo <= hi) { int mid = lo + (hi - lo) / 2; // 防 (lo+hi) 溢出 if (a[mid] >= target) { ans = mid; hi = mid - 1; } // 找左边界 else lo = mid + 1; } return ans; }四个必查点:数组必须有序;mid用减法算避免溢出;区间开闭与循环条件必须配套,闭区间配lo <= hi;重复元素时返回的是哪一个要按题目要求定,找左边界和找任意一个的写法不同。
排序本身可以直接用标准库,别忘引入头文件:
#include <algorithm> // std::sort 在这里 #include <functional> // std::greater std::sort(a, a + n); // 升序 std::sort(a, a + n, std::greater<int>()); // 降序判断质数按试除到平方根写,快速幂按二进制拆分写:
bool isPrime(long long n) { if (n < 2) return false; if (n % 2 == 0) return n == 2; // 2 是唯一的偶质数 for (long long d = 3; d * d <= n; d += 2) // 只试奇数,到 sqrt(n) if (n % d == 0) return false; return true; } long long qpow(long long a, long long e, long long mod) { long long r = 1 % mod; // mod 为 1 时结果为 0 a %= mod; while (e) { if (e & 1) r = r * a % mod; a = a * a % mod; e >>= 1; } return r; }d * d <= n写成d <= n / d可以进一步避免溢出;1 % mod而不是直接写 1,是为了处理模数为 1 的边界。这两个模板在填空题里大概率以「补全循环条件」的形式出现。
4.4 结构体与链表:new/delete 配对与销毁顺序
struct Node { int val; Node *next; Node(int v) : val(v), next(nullptr) {} }; Node *pushFront(Node *head, int v) { // 头插,返回新的头 Node *p = new Node(v); p->next = head; return p; } void destroy(Node *head) { while (head) { Node *nxt = head->next; // 先存下一个节点 delete head; head = nxt; // 再移动,顺序不能颠倒 } }destroy里两行的顺序是最常见的改错点:先delete再取head->next,就是访问已释放内存。pushFront返回新头指针,调用方必须接住返回值,写pushFront(head, v);而不赋值,链表就白改了。
遍历和计数要区分空链表:
int length(Node *head) { int cnt = 0; for (Node *p = head; p != nullptr; p = p->next) ++cnt; return cnt; }循环条件写p != nullptr而不是p->next != nullptr,后者遇到空链表会直接解引用空指针。链表题如果要求返回中间节点或倒数第 k 个,用快慢指针,别先求长度再走一遍。
4.5 流 I/O:cin 与 getline 混用的那个换行符
这是复习题里出错率最高的一处细节:
#include <iostream> #include <iomanip> #include <string> int main() { int n; std::string name; std::cin >> n; // 只读走数字,'\n' 留在缓冲区 std::cin.ignore(1, '\n'); // 关键一行:吃掉残留换行 std::getline(std::cin, name); // 否则这里拿到空串 std::cout << name << " " << n << "\n"; double pi = 3.14159265; std::cout << std::fixed << std::setprecision(2) << pi << "\n"; // 3.14 std::cout << std::setw(6) << std::setfill('0') << 42 << "\n"; // 000042 return 0; }operator>>会跳过前导空白并在读到不匹配字符时停止,不消费那个分隔符;getline默认以'\n'结束并把它吃掉,所以中间必须有一次ignore。格式控制里std::fixed和std::setprecision是持续生效的,std::setw只影响紧接着的一个输出项,这两者的作用范围差别是概念题的常客。读整行含空格的字符串时统一用getline,读单个单词用>>,混用时永远记得补ignore。
5. 进阶:用自动对拍脚本和错题本把复习效率拉满
到这一步,题目都会做了,剩下的是稳定性和速度。手工一题一题编译运行,五十道题要花掉两小时,而且容易漏。把程序题统一命名成q01.cpp、q02.cpp,输入输出放在cases/下,用脚本批量跑。
#!/usr/bin/env bash # run_all.sh:批量编译复习题里的程序题,并与期望输出对拍 set -u mkdir -p out pass=0; fail=0 for src in q*.cpp; do name="${src%.cpp}" if ! g++ -std=c++17 -Wall -Wextra -g -O0 "$src" -o "out/$name" 2> "out/$name.build.log"; then echo "[BUILD-FAIL] $src"; fail=$((fail + 1)); continue fi if [ -f "cases/$name.in" ]; then timeout 3 "out/$name" < "cases/$name.in" > "out/$name.actual" 2>&1 else timeout 3 "out/$name" > "out/$name.actual" 2>&1 fi if [ -f "cases/$name.expected" ] && ! diff -q "cases/$name.expected" "out/$name.actual" > /dev/null; then echo "[WA] $src" diff -u "cases/$name.expected" "out/$name.actual" | head -20 fail=$((fail + 1)) else echo "[OK] $src"; pass=$((pass + 1)) fi done echo "pass=$pass fail=$fail"几个参数的意义:set -u让引用未定义变量时直接报错退出,避免路径写错却静默跑完;timeout 3给每道题三秒上限,链表或二分题一旦写出死循环不会把整个脚本挂住;2>&1把标准错误合并进实际输出,段错误和未定义行为触发的 sanitizer 提示都会落到.actual文件里,与期望输出一比对就暴露出来;diff -u加head -20只显示前 20 行差异,够定位又不刷屏。期望输出用记事本存成q07.expected,注意行末不要留多余空格。
错题本用一张表维护,字段固定下来:
| 日期 | 题号 | 类型 | 错因(一句话) | 修正要点 | 复做轮次 |
|---|---|---|---|---|---|
| 第 1 天 | q07 | WA | 二分边界写成开区间 | lo <= hi配闭区间 | 1/3/7 |
| 第 1 天 | q15 | RE | 销毁链表先 delete 后取 next | 先存 next | 1/3/7 |
| 第 3 天 | q22 | UB | c_str 指针在 string 改动后继续用 | 改用 vector 副本 | 7 |
复做轮次按 1、3、7 天间隔安排,第二遍只做第一遍错的题,第三遍只做第二遍还错的题,五轮下来剩下的通常不超过三道。每道错题处理完,把diff -u输出的那几行差异原文粘进错因栏,比写「粗心」有用得多,因为下次翻到这张表时你能立刻想起当时到底错在哪一行、错在哪个符号上。
本文还有配套的精品资源,点击获取