原版 10 题单卷难度近似、缺梯度、无平行卷,区分度不足。新版改为两份等价 平行卷(防抄袭 / 可重测),按 L1 初级 / L2 中级 / L3 进阶三级分布,补齐 对象切片、菱形继承二义、const 与指针等真正能筛出高阶学员的考点;严格限定 C++98,不涉及现代特性。所有代码题输出经 g++ -std=c++98 -pedantic-errors 实测核实,修正了 B 卷第 2 题链式 << 求值顺序未定义的缺陷(改为三行输出)。
18 KiB
C++ 基础摸底小测验(10 分钟 / 10 题 · 两份等价平行卷)
用途:验证
OUTLINE.md§0 里"学员已掌握 C++ 基础语法与 OOP 概念"这条假设 是否成立。建议在 Day1 模块 B 开始前(C++ 回顾环节之前)发给学员,闭卷、不查资料, 10 分钟内独立完成。本卷语言基线 = C++ 98:刻意不涉及任何 C++11/14/17/20+ 现代特性(auto / 智能指针 / 移动语义 / lambda / override / 列表初始化等)。目的是先把"经典 C++ / OOP 这块根基本身 是否扎实"测清楚——这是 Qt 课程 C++ 部分的真正地基;现代特性的回顾放在测验之后、随堂进行。
两份平行卷(A / B):覆盖相同的知识点梯度、相同的难度分布,题目平行但不雷同, 用于以下场景:
- 防抄袭:相邻座席发不同卷(A/B 交错);
- 重测:对摸底结果存疑的学员,换卷再测一次,两次难度可比;
- 平行对照:A 卷统一当堂测、B 卷留作补测/作业,便于横向比较。
三级难度分布(每份卷 10 题):
层级 题号 能力定位 答错意味着 L1 初级(3 题) 1–3 记得住、读得懂基础语法 根基明显不足,Day1 需回退到语法讲解 L2 中级(4 题) 4–7 OOP 三大特性、内存模型有概念 概念有但没吃透,讲课时在对应点加时 L3 进阶(3 题) 8–10 多态细节、继承陷阱、能分析真题 达到"高阶"假设,可全速推进 题型:选择题 + 代码阅读(判断输出/找 bug)。每题标注建议用时,加起来约 10 分钟。
A 卷
【L1 · 第 1 题】选择(30 秒)· 引用基础
下列关于"引用(reference)"和"指针(pointer)"的说法,错误的是?
- A. 引用必须在定义时初始化
- B. 引用可以绑定到
nullptr - C. 引用一旦绑定到一个对象,就不能再改绑到另一个对象
- D. 函数参数用引用传递,可以避免实参被拷贝
【L1 · 第 2 题】代码阅读(45 秒)· static 局部变量
#include <iostream>
int counter() {
static int n = 0; // 只在第一次进入时初始化一次
return ++n;
}
int main() {
std::cout << counter() << counter() << counter();
}
请写出程序的输出。
【L1 · 第 3 题】代码阅读(45 秒)· 值传递 vs 引用传递
#include <iostream>
void f(int x) { x = x + 1; } // 形参 x 是实参的副本
int main() {
int n = 10;
f(n);
std::cout << n;
}
请写出程序的输出。
【L2 · 第 4 题】代码阅读(1 分钟)· 函数重载决议
#include <iostream>
void h(long) { std::cout << "L"; }
void h(double) { std::cout << "D"; }
int main() { h(5); }
这段代码会发生什么?(编译通过则写出输出;不能编译则说明原因)
【L2 · 第 5 题】选择(45 秒)· 虚函数与多态
关于虚函数,下列说法正确的是?
- A. 只要基类析构函数不加
virtual,通过基类指针delete派生类对象一定没有问题 - B. 构造函数可以声明为虚函数
- C. 含有纯虚函数(
= 0)的抽象类不能被直接实例化 - D. 派生类必须重写基类中的每一个虚函数,否则编译报错
【L2 · 第 6 题】代码阅读(1 分钟)· 构造与析构顺序
#include <iostream>
struct M { M(){ std::cout<<"M"; } ~M(){ std::cout<<"~M"; } };
struct Base { Base(){ std::cout<<"B"; } ~Base(){ std::cout<<"~B"; } };
struct Derived : Base {
M m; // 成员对象
Derived(){ std::cout<<"D"; }
~Derived(){ std::cout<<"~D"; }
};
int main(){ Derived d; }
请写出构造与析构的完整输出顺序。
【L2 · 第 7 题】找 bug(1.5 分钟)· 浅拷贝与 double free
class Buffer {
char* data;
public:
Buffer(int n) { data = new char[n]; }
~Buffer() { delete[] data; }
};
void use() {
Buffer a(10);
Buffer b = a; // 用 a 拷贝构造 b
} // a、b 离开作用域
use() 运行到结束时会发生什么问题?请说明原因。
【L3 · 第 8 题】代码阅读(1 分钟)· 对象切片
#include <iostream>
struct Base {
virtual const char* name() const { return "Base"; }
};
struct Derived : Base {
const char* name() const { return "Derived"; } // 重写虚函数
};
void print(Base b) { // 注意:按值传递,不是引用/指针
std::cout << b.name();
}
int main() {
Derived d;
print(d);
}
请写出程序的输出,并解释为什么不是 Derived。
【L3 · 第 9 题】选择(1 分钟)· const 与指针
下列声明中,表示"指针本身是常量(不能再指向别的地址),但指针指向的数据可以修改"的是?
- A.
const char* p - B.
char const* p - C.
char* const p - D.
const char* const p
【L3 · 第 10 题】代码阅读(1.5 分钟)· 多重继承的二义性
class Base { public: int value; };
class Left : public Base {}; // 普通继承
class Right : public Base {}; // 普通继承
class Derived : public Left, public Right {};
int main() {
Derived d;
d.value = 10; // ?
}
d.value = 10; 这一行能编译通过吗?如果可以,value 来自哪条继承链路?如果不能,请说明原因。
B 卷
与 A 卷一一对应、考点相同、难度相当的平行卷。
【L1 · 第 1 题】选择(30 秒)· 指针基础
下列关于"指针"的说法,错误的是?
- A. 指针可以不初始化(此时其值未定义)
- B. 指针可以在生命周期内先后指向不同的对象
- C. 指针变量本身在内存中也占用存储空间
- D. 用
delete释放一个指针后,该指针自动变成空指针nullptr
【L1 · 第 2 题】代码阅读(45 秒)· static 局部变量
#include <iostream>
int tick() {
static int n = 100; // 只在第一次进入时初始化一次
return n--;
}
int main() {
std::cout << tick() << "\n";
std::cout << tick() << "\n";
std::cout << tick() << "\n";
}
请写出程序的输出(共三行)。
【L1 · 第 3 题】代码阅读(45 秒)· 值传递 vs 引用传递
#include <iostream>
void f(int& x) { x = x + 1; } // 形参 x 是实参的别名
int main() {
int n = 10;
f(n);
std::cout << n;
}
请写出程序的输出。
【L2 · 第 4 题】代码阅读(1 分钟)· 函数重载决议
#include <iostream>
void h(short) { std::cout << "S"; }
void h(float) { std::cout << "F"; }
int main() { h(7); }
这段代码会发生什么?(编译通过则写出输出;不能编译则说明原因)
【L2 · 第 5 题】选择(45 秒)· 虚函数与多态
关于虚函数,下列说法正确的是?
- A. 基类把析构函数声明为
virtual,是为保证delete 基类指针时派生类部分也能被正确析构 - B. 在构造函数里调用虚函数,会触发派生类的虚函数版本
- C. 派生类重写基类虚函数时,可以任意改变返回类型,不受限制
- D. 一个类只要有虚函数,编译器就不会为它生成虚函数表(vtable)
【L2 · 第 6 题】代码阅读(1 分钟)· 构造与析构顺序
#include <iostream>
struct N { N(){ std::cout<<"1"; } ~N(){ std::cout<<"4"; } };
struct A { A(){ std::cout<<"2"; } ~A(){ std::cout<<"3"; } };
struct C : A {
N n; // 成员对象
C(){ std::cout<<"5"; }
~C(){ std::cout<<"6"; }
};
int main(){ C c; }
请写出构造与析构的完整输出顺序。
【L2 · 第 7 题】找 bug(1.5 分钟)· 浅拷贝与 double free
class Stack {
int* data;
int size;
public:
Stack(int n) : size(n) { data = new int[n]; }
~Stack() { delete[] data; }
};
void use() {
Stack a(10);
Stack b = a; // 用 a 拷贝构造 b
} // a、b 离开作用域
use() 运行到结束时会发生什么问题?请说明原因。
【L3 · 第 8 题】代码阅读(1 分钟)· 对象切片
#include <iostream>
struct Animal {
virtual const char* sound() const { return "..."; }
};
struct Dog : Animal {
const char* sound() const { return "Woof"; } // 重写虚函数
};
const char* record(Animal a) { // 注意:按值传递,不是引用/指针
return a.sound();
}
int main() {
Dog d;
std::cout << record(d);
}
请写出程序的输出,并解释为什么不是 Woof。
【L3 · 第 9 题】选择(1 分钟)· const 与指针
下列声明中,表示"指针指向的数据是常量(不能通过该指针改数据),但指针本身可以指向别的地址"的是?(本题有两个等价正确选项,全选才得分)
- A.
const char* p - B.
char const* p - C.
char* const p - D.
const char* const p
【L3 · 第 10 题】代码阅读(1.5 分钟)· 多重继承的二义性
class Root { public: int id; };
class A : public Root {}; // 普通继承
class B : public Root {}; // 普通继承
class Final : public A, public B {};
int main() {
Final f;
f.id = 5; // ?
}
f.id = 5; 这一行能编译通过吗?如果可以,id 来自哪条继承链路?如果不能,请说明原因。
答案与评分(教师用,建议测验时不发给学员)
所有"代码阅读 / 找 bug"题的输出与编译行为,均已用 g++ 13.3 在
-std=c++98 -pedantic-errors -Wall严格模式下编译运行核实,非凭记忆作答。
A 卷答案
| 题号 | 答案 | 考查点 · 课程出处 |
|---|---|---|
| L1·1 | B(引用不能为空,这是它和指针的关键区别之一——引用是已绑定对象的别名,不存在"空引用") | 引用的本质 · p01/ch03/reference_essence.cpp |
| L1·2 | 123(static 局部变量只初始化一次,之后跨调用保持值;三次调用依次返回 1、2、3) |
static 与存储期 · p01/ch04/s04 |
| L1·3 | 10(值传递:f 内改的是 n 的副本,函数返回后副本销毁,n 不变) |
函数参数传递 · p01/ch03 |
| L2·4 | 编译失败:h(5) 中 int 既要转 long 又要转 double,两个都是"标准转换",优先级相同 → 重载二义(ambiguous)。必须显式写 h(5L) 或 h(5.0) 消歧义 |
函数重载决议 · p01/ch03/function_overload_mechanism.cpp |
| L2·5 | C(A 错:非虚析构时派生类部分不会被析构,是经典内存泄漏;B 错:构造函数不能是虚函数;D 错:不重写就继承基类实现,只有纯虚函数才"必须"重写) | 虚函数与抽象类 · p01/ch04/s08/virtual_dtor_purpose.cpp |
| L2·6 | BMD~D~M~B(构造:基类 B → 成员 M → 派生类体 D;析构完全相反:派生类 ~D → 成员 ~M → 基类 ~B) |
继承+成员的构造析构顺序 · p01/ch04/s07/inheritance_ctor_dtor_order.cpp |
| L2·7 | 浅拷贝导致 double free:编译器合成的默认拷贝构造是浅拷贝,b.data 与 a.data 指向同一块 new[] 内存;use() 结束时 ~Buffer() 被调用两次,对同一块内存 delete[] 两次 → 行为未定义,通常崩溃。修复:自定义拷贝构造(深拷贝),或禁用拷贝 |
深拷贝 vs 浅拷贝 · p01/ch04/s03/deep_vs_shallow_copy.cpp |
| L3·8 | 输出 Base。print(Base b) 形参按值接收,Derived 对象被切片(slicing):只拷贝了 Base 子对象,派生类新增部分被截断,b 是个货真价实的 Base,虚函数自然调到 Base::name。要让多态生效,形参须为 Base&/Base* |
对象切片 · p01/ch04/s08 |
| L3·9 | C(char* const p —— const 修饰 p 本身,故指针不可改指向;*p 未被 const 修饰,数据可改。A/B 是"指向 const 的指针",D 两边都 const) |
const 与指针 · p01/ch03 |
| L3·10 | 编译失败:d.value 二义(ambiguous)。Derived 经 Left 和 Right 两条路径各继承了一份 Base::value,编译器无法判断要访问哪一个。修复:①用 d.Left::value 显式限定;②改用虚继承 class Left : virtual public Base,让两条路径共享同一个 Base 子对象 |
多重继承二义性与虚继承 · p01/ch04/s07 |
B 卷答案
| 题号 | 答案 | 考查点 · 课程出处 |
|---|---|---|
| L1·1 | D(delete 只释放指针指向的内存,不改动指针本身——它仍保存原地址,成为"悬空指针(dangling)";继续解引用是未定义行为。这是 C++98 没有 nullptr 时代的经典坑) |
指针与内存管理 · p01/ch04/s03 |
| L1·2 | 三行依次输出 100 / 99 / 98(static 局部变量只初始化一次、跨调用保持值;n-- 先返回当前值再自减)。三行分开打印,无求值顺序歧义 |
static 与存储期 · p01/ch04/s04 |
| L1·3 | 11(引用传递:x 是 n 的别名,x=x+1 直接改 n) |
函数参数传递 · p01/ch03 |
| L2·4 | 编译失败:h(7) 中 int 既要转 short 又要转 float,两个都是标准转换、优先级相同 → 重载二义(ambiguous)。需写 h((short)7) 或 h(7.0f) 消歧义 |
函数重载决议 · p01/ch03/function_overload_mechanism.cpp |
| L2·5 | A(A 正确,这正是虚析构存在的意义;B 错:构造期间虚表尚未就绪/对象类型还是基类,调用基类版本;C 错:返回类型必须协变或相同,不能"任意";D 错:有虚函数才会生成 vtable) | 虚函数与抽象类 · p01/ch04/s08/virtual_dtor_purpose.cpp |
| L2·6 | 215643(构造:基类 2 → 成员 1 → 派生类 5;析构相反:6 → 4 → 3) |
继承+成员的构造析构顺序 · p01/ch04/s07/inheritance_ctor_dtor_order.cpp |
| L2·7 | 浅拷贝导致 double free:默认拷贝构造浅拷贝,b.data 与 a.data 同指一块 new[];use() 结束时析构两次 → double free / 未定义行为。修复同 A 卷第 7 题 |
深拷贝 vs 浅拷贝 · p01/ch04/s03/deep_vs_shallow_copy.cpp |
| L3·8 | 输出 ...。record(Animal a) 按值接收 → 对象切片,Dog 部分被截断,a 是个 Animal,调到 Animal::sound。要听到 Woof,形参须为 Animal&/Animal* |
对象切片 · p01/ch04/s08 |
| L3·9 | A 和 B(const char* p 与 char const* p 写法不同但完全等价——const 修饰的是 * 的左边,即指向的数据;指针本身可改。C 是 const 指针,D 两边都 const。全选 A、B 才得分) |
const 与指针 · p01/ch03 |
| L3·10 | 编译失败:f.id 二义(ambiguous)。Final 经 A、B 两条路径各继承一份 Root::id,编译器无法抉择。修复:①f.A::id 显式限定;②虚继承 class A : virtual public Root |
多重继承二义性与虚继承 · p01/ch04/s07 |
关于 B 卷第 2 题的设计说明:原稿曾写成单行链式
std::cout << tick() << tick() << tick();, 但该写法的输出顺序依赖<<的求值顺序,而 C++ 未规定该顺序(unspecified behavior), 不同编译器可能拼出1009998、9899100等不同结果——会让"标准答案"本身不稳定。现稿改为 三行分开打印,输出确定无歧义,更适合做闭卷摸底的客观题。
评分与分层预案
计分规则:每题 1 分,满分 10 分。选择/代码阅读答对即得分;"找 bug / 解释"题(A·7、A·8、A·10、 B·7、B·8、B·10)只要说对核心机制(浅拷贝/double free、对象切片、二义性)即得分,不苛求 措辞。B 卷第 2 题按上述概念核分说明处理。
分层与教学预案(对应 OUTLINE.md §0):
| 分段 | 占比解读 | 指导动作 |
|---|---|---|
| 8–10 分 | 达到"已掌握 C++ 基础与 OOP 概念"假设 | Day1 模块 B 按原"巡礼式"节奏推进,现代特性(C++11+)正常引入 |
| 5–7 分 | 部分薄弱——对照上表找出集中失分的层级 | L1 失分多(Q1–3):回退到基础语法讲解;L2 失分多(Q4–7):在构造析构/浅拷贝/重载决议处加时 5–10 分钟;L3 失分多(Q8–10):说明学员 OOP 基础尚可,多态/继承陷阱课上点到为止、留自学 |
| 0–4 分 | 明显达不到假设 | 按 OUTLINE.md §0 预案,Day1 模块 B 整体退回语法讲解,练习任务保留但预期完成度下调 |
逐题正确率用法(大规模班级):统计每题正确率,低于 50% 的题对应的知识点在 Day1 模块 B 里适当加时;无需逐份细看。
与旧版的差异(为什么重做)
此前一版 PRETEST(10 题、单卷)的主要不足:
- 缺梯度——10 题难度近似,集中在"概念识别"层面,区分不出"学过"和"真正懂了";
- 无平行卷——单卷无法防抄袭、无法平行重测;
- 进阶层薄弱——没有对象切片、菱形继承这类真正能筛出"高阶"学员的题,全做对与做对 7 题 的学员水平差异,旧卷测不出来。
新版改进:三级梯度(L1/L2/L3)+ 两份等价平行卷(A/B)+ 每题标注考查点与课程出处, 既能给出每层级单独的掌握情况,又支持防抄袭与平行重测。
使用方式
- 纸质或线上表单均可,闭卷、不查资料、不讨论,10 分钟收卷;
- 两份平行卷按需使用:相邻座席 A/B 交错防抄袭;或 A 卷当堂测、B 卷补测/作业;
- 只用于摸底分层,不计入
docs/TRAINING_PLAN_2026.md的正式考核成绩; - 大规模班级可只统计"每题正确率",正确率低于 50% 的题对应知识点在 Day1 模块 B 里适当加时。