简介:华南理工大学《C++程序设计基础》期末试卷A卷(16-17学年)及配套知识点详解,面向正在复习C++基础语法的高校生与自学者。试卷覆盖常量与变量、逻辑表达式、自增/自减与条件运算符、for/while/switch流程控制、函数原型与重载、const引用、指针与数组、结构体等核心考点,代表性选择与程序阅读题均配有解析,如--a && --b的短路求值、switch无break穿透、strlen对\0的截断等易错点均有说明。资源为1个PDF文件,大小288KB,排版清晰,可直接打印模拟自测。已有116人学习,适合考前突击、查漏补缺或作为C++入门配套练习。通过对照解析梳理知识盲区,能更扎实地掌握对象类型判断、循环边界、指针运算等关键内容,提升期末应试与编码基本功。
1. 一份C++程序设计基础A卷能告诉你的六件事
期末复习最怕的不是不会,而是不知道从哪里开始。前几天群里传了一份标题写“包打听分享”的C++程序设计基础A卷1,对应的应该是2016-2017学年第一学期,文件名写得很明白,就是某高校计算机专业本科一学期的期末正卷。很多人拿到这种卷子的第一反应是赶紧从头到尾刷一遍,这是最可惜的用法。
我做过几年助教,也在一线写 C++ 写了不短时间,看这个标题第一反应是:这份卷子不是用来“刷”的,是用来“拆”的。卷面结构会直接告诉你这门课的重心放在哪——选择题考语法细节,程序阅读题考调用关系与作用域,编程题固定落在数组、类和文件流上。这篇内容就是教你按题型拆卷、把错题转成速查表、用最小代码把拿不准的语法验证掉,再避开五个高频踩坑点。适合正在备考C++的学生、带班助教和想拿往年卷反推考核重点的自学者;新手按步骤走,熟手可以直接跳到第4章和第5章对照避坑。
2. 先看懂A卷1的题型分布:选择题、阅读题与编程题各自在考什么
拿到一份往年卷,我不会先看题目,而是先翻页面结构。C++程序设计基础的期末A卷,题型基本固定为选择、填空、程序阅读、程序填空和编程题。先搞清每一类题在考哪个层次的能力,复习才不会出现“选择全会、编程全空”的极端情况。
选择题考的是“认不认识这个语法”,程序阅读题考的是“能不能跟踪程序执行”,编程题考的是“能不能自己组织代码”。三个层次对应三种复习方法,刷题策略完全不同。下面按题型拆开讲。
2.1 选择题与填空:常量、类型转换、运算符优先级是最稳的考点
A卷的选择题一般有15到20道,覆盖的语法点看似很散,其实高度集中在六类内容。我按常见程度整理了一张表,复习权重就是选择题里出现频率的参考。
| 考点 | 常见考法 | 复习权重 |
|---|---|---|
| 常量与字面量 | const 与 #define 的区别、枚举值、字符串字面量 | 高 |
| 隐式类型转换 | int 与 double 混合运算、char 输出与强转 | 高 |
| 运算符优先级 | ++i 与 i++、逗号表达式、位运算 | 高 |
| 数组定义与初始化 | 缺省初始化值、二维数组行列数 | 中 |
| 流程控制 | if/switch 条件真假、for/while 循环次数 | 高 |
| 函数与默认参数 | 函数声明位置、默认实参从右往左规则 | 中 |
这里最容易被扣分的是 const 与 #define 的区别。很多初学的人以为二者只是写法不同,实际上 #define 是预处理阶段的文本替换,不参与类型检查,而 const 是一个有类型的对象,编译器会检查它的使用是否合法。选择题里常给一段代码,问哪一行会报错,考的就是这个点。
// 基础语法易错点复现:类型转换与自增自减 #include <iostream> using namespace std; int main() { int a = 3; double b = 2.5; cout << a + b << endl; // int 先隐式转成 double,结果是 5.5 int i = 5; cout << i++ << endl; // 后自增,表达式取旧值 5,然后 i 变成 6 cout << ++i << endl; // 前自增,先变成 7,再输出 7 char c = 65; // char 是整型 cout << c << endl; // 直接输出时按 ASCII 码转成字符 A return 0; }逻辑说明:整型和浮点型混合运算时,int 会隐式转成 double,所以 a + b 的结果类型是 double,输出 5.5 而不是 5。i++ 和 ++i 的区别在于表达式的值取自增前还是自增后,这是每年选择题必考的自增陷阱。char 类型本质是 1 字节整型,直接输出会被解释成 ASCII 字符,这题错的人比想象中多。
参数说明:如果想把 char 按数字输出,不能直接 cout << c,要写成 static_cast (c)。考试里如果看到强制转换,优先想到 static_cast,而不是 C 风格的 (int),C++ 的复习题里更认可前者。
2.2 程序阅读题:先标调用关系,再跟踪变量,最后猜输出
程序阅读题是 A 卷的必考题型,通常给一段包含一个或两个函数的完整代码,让你写出程序输出。这种题考的不是背语法,而是两条能力线:函数调用时参数的传递方式,以及静态变量、全局变量在多次调用之间的状态保持。
我读这种题的固定习惯是四步。第一步,圈出所有函数签名,在参数旁边标出是传值、传引用还是传指针;第二步,标出全局变量和函数内部的 static 变量;第三步,从 main 函数开始逐行展开,每遇到一次函数调用就跳到函数体里执行;第四步,把每一行输出按顺序写在草稿纸上,最后再誊到答题区。这一步顺序经常被忽略,程序阅读题丢分主要原因不是不会,而是漏写某一行输出。
// 程序阅读题常见套路:静态局部变量 #include <iostream> using namespace std; void counter() { static int n = 0; // 静态局部变量,只初始化一次 n++; cout << n << " "; } int main() { for (int i = 0; i < 3; i++) { counter(); // 连续三次调用,输出 1 2 3 } return 0; }逻辑说明:static 局部变量存放在静态存储区,第一次执行到声明时才初始化,之后每次进入函数都会跳过初始化语句,保留上一次调用结束时的值。去掉 static 后,每次调用 n 都会重新赋成 0,输出就会变成 1 1 1,这是程序阅读题最喜欢埋的差异点。
参数说明:函数调用时,传值参数是实参的拷贝,函数内任何修改都不影响实参;传引用参数是实参的别名,函数内修改会直接作用到实参。程序阅读题里如果出现 void change(int &x),基本就是在考引用传参,答题时要在草稿上写出实参的初始地址和变化过程。
2.3 程序填空题与编程题:三类题型的出题套路
程序填空题一般把函数调用、循环边界、数组下标挖空,难度不大,但非常考边界意识。比如排序的内层循环 j < n - 1 - i,很多人填错成 j < n - 1,题目就会在运行结果上出差异。复习时对这类题,重点看循环变量从几开始、到几结束、步长是正是负。
编程题是分值最大的一块,通常是 2 到 3 道。我根据多份 A 卷的共性,整理出下面这张复习优先级表。
| 编程题类型 | 核心考察点 | 复习优先级 |
|---|---|---|
| 数组处理 | 排序、求最值、统计频次、逆序 | 高 |
| 字符串处理 | 字符数组、单词计数、反转、去空格 | 高 |
| 类与对象 | 构造函数、成员函数、运算符重载 | 高 |
| 文件流 | ifstream/ofstream 读写 | 中 |
复习顺序上,我一般建议先把数组和函数确保能完整写出来,再攻类和文件流。原因是多数 A 卷的编程题按步骤给分,函数定义写对、循环框架搭对,就算中间有一行写错也能拿一半分。反过来,如果思路不清,整道题空着,就一分都没有。字符串处理题要特别注意字符数组末尾的 '\0',这是字符串题最常见的扣分项。
3. 用往年A卷做考前自查:三个步骤把试卷变成复习清单
把一份A卷变成复习清单,核心思路是:不要完整重刷,而是用它做一次“知识体检”。这三步做完,基本就能知道考前最后两天该把时间花在哪。
3.1 第一步:用两小时做卷面标记,分清“会”与“不会”
找一整块时间,大约两小时,把这份A卷从头到尾做一遍,但不是为了得分,而是做标记。我常用的标记是三色符号:能秒答的题打对勾;要想一会才能下笔的题打问号;完全没思路的题打叉。
做完之后,把每道题对应的章节填进下面这张表里,统计每个章节的问号加叉号数量。数量最多的章节就是复习重点,而不是按教材目录从第一章开始平摊时间。
| 章节 | 对勾 | 问号 | 叉号 | 处理顺序 |
|---|---|---|---|---|
| 指针与数组 | 2 | 4 | 1 | 第一优先 |
| 函数与作用域 | 3 | 2 | 0 | 第二优先 |
| 类与对象 | 1 | 3 | 2 | 第一优先 |
| 文件流 | 2 | 1 | 1 | 第三优先 |
这一步的关键是诚实。很多人做题时遇到不会的就翻书,标记就会失真,最后统计出来的重点全是假象。不会的题先跳过,等整卷标记完再统一处理。
3.2 第二步:把错题对应的语法点整理成速查表
整理错题不是在错题本上抄一遍题目,而是把题目背后的语法点压成一行结论。比如指针题错得多,就整理一张指针运算速查表,考前半小时只看这个。
| 表达式 | 含义 | 结果 |
|---|---|---|
| p[i] | 等价于 *(p + i) | 下标 i 的元素 |
| *p++ | 先取 *p 的值,再把指针后移 | 返回当前元素,指针指向下一元素 |
| (*p)++ | 把指针指向的元素自增 | 元素值加 1 |
| ++*p | 先取 *p,再给值自增 | 元素值加 1 |
表格下面配一段最小复现代码,验证这些规则,比背书可靠得多。
// 指针运算速查:p[i]、*p++、(*p)++ 的区别 #include <iostream> using namespace std; int main() { int arr[5] = {10, 20, 30, 40, 50}; int *p = arr; // p 指向数组首元素 cout << p[1] << endl; // 20,等价于 *(p + 1) cout << *p++ << endl; // 先取 *p 的 10,然后 p 指向 arr[1] cout << *p << endl; // 20,此时 p 已经后移 (*p)++; // 把 arr[1] 从 20 改成 21 cout << arr[1] << endl; // 21 return 0; }逻辑说明:指针自增移动的是一个元素大小的地址偏移,int 类型就是 4 字节,不是 1 字节。*p++ 的优先级是后缀自增高于解引用,所以先取 p 指向的值,再移动指针。(*p)++ 里的括号强制先解引用,再给元素值自增。
参数说明:在数组遍历里 *p++ 是常见写法,等价于先处理 p[i] 再 i++,但要注意它改变了指针本身。如果后续还要用数组首地址,提前用另一个指针变量保存,否则会追丢数组头。
3.3 第三步:用最小复现代码验证拿不准的语法行为
C++ 里很多行为不要靠猜,直接写一个 10 行的小程序编译运行,答案就摆在眼前。这一步能治很多“这事儿我只能靠背”的玄学问题。比如默认参数到底写在函数声明还是函数定义里,背十遍不如亲自验证一遍。
// 验证默认参数的正确写法 #include <iostream> using namespace std; void printNum(int n, int times = 2); // 声明处给默认参数 void printNum(int n, int times) { // 定义处不能再给默认值 for (int i = 0; i < times; i++) { cout << n << " "; } cout << endl; } int main() { printNum(7); // times 用默认值 2,输出 7 7 printNum(7, 3); // 显式传 3,输出 7 7 7 return 0; }逻辑说明:默认参数只能在函数声明和函数定义的一个位置给出,常见做法是写在声明处,定义处保持普通形参列表。如果两边都写,编译器会报重复默认参数错误。
参数说明:默认参数必须从右往左连续排列。void f(int a, int b = 1, int c = 2) 合法,void f(int a = 1, int b) 非法,因为调用 f(5) 无法确定 b 该取什么值。
提示:最小复现代码的唯一标准是“只保留能触发问题的最小片段”。不要带上一堆无关的类定义和业务逻辑,代码越少,排错越容易。
4. 六道典型题的解法拆解:类、重载与文件流
这一章从编程题里挑出六道最常见的题,分成三组来拆解。每道题我都给完整代码,再讲清三个问题:为什么这样写、参数为什么这么设、踩坑点在哪。
4.1 类与对象:构造函数顺序与拷贝构造的浅拷贝陷阱
第一类必考题是“设计一个类并实现基本功能”。A 卷里最常见的版本是设计一个学生类,包含姓名和成绩字段,构造函数初始化,成员函数显示信息。这道题本身的难度不大,但衍生出的拷贝构造和析构问题才是真正拉开分差的地方。
#include <iostream> #include <cstring> using namespace std; class Student { public: char *name; // 指针成员 int score; Student(const char *n, int s) : score(s) { name = new char[strlen(n) + 1]; // 动态分配内存 strcpy(name, n); } // 自定义拷贝构造,避免浅拷贝 Student(const Student &other) : score(other.score) { name = new char[strlen(other.name) + 1]; strcpy(name, other.name); } ~Student() { delete[] name; } }; int main() { Student s1("stu01", 90); Student s2 = s1; // 调用拷贝构造,name 指向独立空间 cout << s2.name << " " << s2.score << endl; return 0; }逻辑说明:默认拷贝构造函数只是逐成员复制,指针成员 name 会被直接复制地址,导致两个对象的 name 指向同一块堆内存。程序结束析构时,同一块内存被 delete 两次,直接崩溃。自定义拷贝构造为 s2 重新分配一块内存,让两个对象各持有一份数据。
参数说明:拷贝构造函数的参数必须是 const 引用。如果写成 Student(Student other),传参会再次触发拷贝构造,造成无限递归,编译器会直接报错。另外,构造函数初始化列表里 score(s) 是在函数体执行前完成成员初始化,比函数体内赋值更高效,考试时写这个细节能看出功底。
4.2 运算符重载:成员函数与友元函数怎么选
第二类高频题是实现一个类并重载运算符,最常见的是复数类。复数类的 + 和 << 是必考的两种重载类型,但这两者的写法完全不同,选错方式就编译不过。
#include <iostream> using namespace std; class Complex { public: double real, imag; Complex(double r = 0, double i = 0) : real(r), imag(i) {} // 成员函数重载 +:左操作数是调用对象 Complex operator+(const Complex &other) const { return Complex(real + other.real, imag + other.imag); } // 友元函数重载 <<:左操作数是 ostream friend ostream &operator<<(ostream &os, const Complex &c) { os << "(" << c.real << ", " << c.imag << ")"; return os; } }; int main() { Complex a(1, 2), b(3, 4); Complex c = a + b; // 等价于 a.operator+(b) cout << c << endl; // (4, 6) return 0; }逻辑说明:operator+ 作为成员函数重载,左操作数 a 被绑定到 this 指针,右操作数 b 作为参数传入。而 operator<< 的左操作数是 ostream 对象,它是标准库类型,不能往里面加成员函数,所以必须定义为友元函数,把 Complex 对象作为第二个参数。
参数说明:operator+ 的参数用 const Complex&,避免值传递拷贝;函数末尾 const 表示该成员函数不修改 this 指向的对象,这样 a + b 就不会意外改变 a。operator<< 必须返回 ostream&,才能支持 a << b << c 这样的连续输出;如果返回 ostream 值,流的拷贝已被删除,编译直接失败。
4.3 文件流:文本文件求平均分与二进制读写长度坑
第三类题是文件流操作。文本文件读写和二进制文件读写各有一道典型的,两道题的坑不一样。
// 文本文件:读入姓名和成绩,求平均分 #include <iostream> #include <fstream> using namespace std; int main() { ifstream fin("scores.txt"); // 相对路径是相对于当前工作目录 if (!fin.is_open()) { cout << "文件打开失败" << endl; return 1; } int cnt = 0; double sum = 0, score; string name; while (fin >> name >> score) { // 每次读一个姓名和一个成绩 sum += score; cnt++; } cout << "平均分: " << (cnt ? sum / cnt : 0) << endl; fin.close(); return 0; }逻辑说明:fin >> name >> score 会跳过空白符,以空格或换行分隔输入,循环条件直接判断读入操作本身。不要用 while (!fin.eof()) 作为循环条件,因为 eof 标志在读取到文件末尾之后才置位,循环体会多执行一次,最后一次读入的是无效数据。
参数说明:如果把读入顺序写成 fin >> score >> name,而文件里第一列是姓名,第二列是成绩,那么所有数据都会错位。写文件读取代码前,先看清楚文件每一列的排列顺序,这是文件题最便宜也最常丢的分。
// 二进制文件:读写结构体 #include <iostream> #include <fstream> using namespace std; struct Record { int id; char name[20]; double score; }; int main() { Record r = {1, "stu01", 92.5}; ofstream fout("record.bin", ios::binary); fout.write((char *)&r, sizeof(r)); // 按字节写入 fout.close(); Record r2; ifstream fin("record.bin", ios::binary); fin.read((char *)&r2, sizeof(r2)); cout << r2.id << " " << r2.name << " " << r2.score << endl; fin.close(); return 0; }逻辑说明:二进制读写用 write 和 read,两个函数的参数都是 char* 和字节数。结构体按内存布局直接写入文件,没有文本编码问题,也不需要字符串结束符的判断。读回来时用另一个同类型结构体变量接收,只要字节数一致就能还原。
参数说明:sizeof(Record) 不是简单把字段长度相加,编译器会按对齐规则填充空字节,跨编译器、跨平台时内存布局可能不同。如果文件需要在不同程序之间交换,按字段逐个读写文本格式比二进制更稳;二进制格式适合追求读写速度、不跨平台的场景。
5. 避坑排查:C++期末复习最容易翻车的五个点
下面五个坑,是我看平时作业和期末卷时翻车率最高、最集中的地方。每条都按“现象、原因、解决”写,复习时直接对照。
5.1 数组越界不报错:程序跑一半才崩
现象:定义 int a[5],循环里写 a[5] = 10,编译不报错,程序运行到 return 0 附近才崩溃。
原因:C++ 数组不检查下标越界,越界写入破坏的是相邻的栈内存,编译器无法发现,崩溃位置离实际错误位置往往很远,排查起来像玄学。
解决:写循环时严格用 i < 数组长度;更稳妥的做法是用 vector 代替裸数组,定位越界时用 at() 代替方括号访问。
#include <iostream> #include <vector> using namespace std; int main() { vector<int> v(5, 0); try { v.at(5) = 10; // 越界会抛异常,[] 不会 } catch (const out_of_range &e) { cout << "越界访问: " << e.what() << endl; } return 0; }逻辑说明:vector 的 at() 方法在越界时会抛出 out_of_range 异常,而方括号运算符不做任何检查。考试时用裸数组没问题,但平时练习和课程设计里优先用 vector,能让这类问题在运行前暴露出来。
5.2 析构函数没写 virtual:多态删除时只调了基类析构
现象:基类指针指向派生类对象,delete 指针时只执行了基类析构函数,派生类里 new 出来的内存没释放,程序内存泄漏。
原因:基类析构函数不是虚函数时,通过基类指针 delete 对象采用静态绑定,只调用基类析构版本。
解决:只要类有 virtual 成员函数,析构函数就应该声明为 virtual。
#include <iostream> using namespace std; class Base { public: virtual ~Base() { cout << "Base析构" << endl; } // 关键 }; class Derived : public Base { public: int *data = new int[10]; ~Derived() override { cout << "Derived析构" << endl; delete[] data; } }; int main() { Base *b = new Derived(); delete b; // 虚析构保证派生类析构也被调用 return 0; }逻辑说明:基类析构加了 virtual 后,delete 基类指针会先调用派生类析构,再调用基类析构,两块资源都释放。C++11 之后可以在派生类析构后加 override,写错时编译器会提示。
5.3 文件读不出来:相对路径与编码各背一半锅
现象:scores.txt 明明和 .cpp 文件在同一目录,但 ifstream 打开失败,返回“文件打开失败”。
原因:IDE 运行时的工作目录通常是工程目录,不是源文件所在目录。另外,UTF-8 带 BOM 的文本文件,读出来第一个字符是乱码,也会让解析逻辑出错。
解决:先打印当前工作目录,确认程序实际在哪里找文件;文本文件统一另存为 UTF-8 无 BOM 或 ANSI 编码。
#include <iostream> #include <filesystem> namespace fs = std::filesystem; int main() { cout << "当前目录: " << fs::current_path() << endl; // 看到输出后,把数据文件放到该目录下,或改用绝对路径 return 0; }逻辑说明:这段代码要求编译器支持 C++17,如果不支持,就在命令行执行 pwd 或 cd 命令查看当前目录。路径问题排除后,若文件能打开但输出乱码,就是编码问题,优先检查 BOM。
5.4 cin/cout 与 scanf/printf 混用:输出顺序错乱
现象:代码里先 printf("a") 再 cout << "b",运行结果却是 b 先显示,a 后显示。
原因:C 的 stdio 流和 C++ 的 iostream 流各自独立缓冲,输出时机不同,混用会导致顺序错乱。
解决:统一使用 cin/cout,或者在 main 开头执行 ios::sync_with_stdio(false),然后彻底别再用 scanf 和 printf。
#include <iostream> using namespace std; int main() { ios::sync_with_stdio(false); // 关闭与 stdio 的同步 cin.tie(nullptr); // 解除 cin 与 cout 的绑定 cout << "hello" << endl; return 0; }逻辑说明:sync_with_stdio(false) 关闭两套流的同步机制,C++ 流不再等待 stdio 流刷新,速度变快,但代价是不能混用 printf。cin.tie(nullptr) 让 cin 不再在读取前强制刷新 cout,适合大量读写场景。考试代码量小,不设置也行,但课程设计或比赛里这是标准配置。
5.5 函数没声明就用:“was not declared”报错
现象:在 main 函数里调用一个定义在 main 后面的函数,编译报错,提示函数名 not declared。
原因:C++ 要求函数在使用前必须已有声明,不能像部分语言那样自动识别后文定义。
解决:main 函数之前加函数声明,或者直接把函数定义整体放在 main 之前。
#include <iostream> using namespace std; int add(int a, int b); // 函数声明 int main() { cout << add(3, 4) << endl; // 7 return 0; } int add(int a, int b) { // 函数定义 return a + b; }逻辑说明:声明告诉编译器函数的返回类型和参数类型,定义提供函数体。声明可以重复出现,定义只能有一次。如果声明和定义的参数类型不一致,也会报类似错误。
注意:报错信息里如果出现“no matching function”,优先检查参数类型是否匹配。int 与 double 混传时,隐式转换可能匹配到错误的函数重载,这类错误在选择题里也经常出现。
6. 把A卷当模拟考:十五分钟自测法与考前最后一天的用法
离考试只剩一天时,把整份A卷重新完整做一遍是低效的,因为会做的题还是在会做,不会的题还是会卡。我自己的习惯是改用十五分钟自测法:随手翻开A卷,随便抽一道题,用手机或手表倒计时十五分钟,用纸笔写出完整思路和关键代码骨架。时间一到就停笔,能写出来就过,写不出来就翻速查表,把对应语法点再看一遍。
考前最后一天的时间安排可以按下面的表来走。
| 时间段 | 动作 | 目标 |
|---|---|---|
| 上午 | 十五分钟自测法过一遍选择题和程序阅读题 | 找出仍然卡壳的语法点 |
| 下午 | 只翻阅速查表和卷面上的错题标注 | 强化短时记忆 |
| 晚上 | 手写两道编程题:数组题和类设计题 | 保持手写代码的手感 |
这个节奏的关键是:晚上手写代码后,必须把代码敲进电脑编译一遍。手写时容易忽略分号、大括号、类型不匹配这类细节,编译一次能暴露所有低级错误。这一步做完,第二天进考场时,至少不会在语法层面丢分。
我记得有一年期末,我把三份往年卷各刷了两遍,分数反而不如一个只刷了一遍但认真整理错题的同学。后来想明白了:刷题遍数再多,不解决“这个点我为什么错”的问题,就只是重复熟练。从那以后,我复习就只做一遍题,但把每道错题对应到教材的章节,复习的是考点而不是题面。这套方法每次考试前我都拿出来用,希望帮到你。
本文还有配套的精品资源,点击获取