Files
scuc-qt-course/docs/teaching/PRETEST.md
T
张宗平 6640baffae 堂前测试重做:C++98 双平行卷(A/B)+ 三级难度梯度
原版 10 题单卷难度近似、缺梯度、无平行卷,区分度不足。新版改为两份等价
平行卷(防抄袭 / 可重测),按 L1 初级 / L2 中级 / L3 进阶三级分布,补齐
对象切片、菱形继承二义、const 与指针等真正能筛出高阶学员的考点;严格限定
C++98,不涉及现代特性。所有代码题输出经 g++ -std=c++98 -pedantic-errors
实测核实,修正了 B 卷第 2 题链式 << 求值顺序未定义的缺陷(改为三行输出)。
2026-07-04 13:31:33 +08:00

18 KiB
Raw Blame History

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 题) 13 记得住、读得懂基础语法 根基明显不足,Day1 需回退到语法讲解
L2 中级4 题) 47 OOP 三大特性、内存模型有概念 概念有但没吃透,讲课时在对应点加时
L3 进阶3 题) 810 多态细节、继承陷阱、能分析真题 达到"高阶"假设,可全速推进

题型:选择题 + 代码阅读(判断输出/找 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 123static 局部变量只初始化一次,之后跨调用保持值;三次调用依次返回 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.dataa.data 指向同一块 new[] 内存;use() 结束时 ~Buffer() 被调用两次,对同一块内存 delete[] 两次 → 行为未定义,通常崩溃。修复:自定义拷贝构造(深拷贝),或禁用拷贝 深拷贝 vs 浅拷贝 · p01/ch04/s03/deep_vs_shallow_copy.cpp
L3·8 输出 Baseprint(Base b) 形参按值接收Derived 对象被切片(slicing:只拷贝了 Base 子对象,派生类新增部分被截断,b 是个货真价实的 Base,虚函数自然调到 Base::name。要让多态生效,形参须为 Base&/Base* 对象切片 · p01/ch04/s08
L3·9 Cchar* const p —— const 修饰 p 本身,故指针不可改指向;*p 未被 const 修饰,数据可改。A/B 是"指向 const 的指针"D 两边都 const const 与指针 · p01/ch03
L3·10 编译失败d.value 二义(ambiguous)。DerivedLeftRight 两条路径各继承了一份 Base::value,编译器无法判断要访问哪一个。修复:①用 d.Left::value 显式限定;②改用虚继承 class Left : virtual public Base,让两条路径共享同一个 Base 子对象 多重继承二义性与虚继承 · p01/ch04/s07

B 卷答案

题号 答案 考查点 · 课程出处
L1·1 Ddelete 只释放指针指向的内存,不改动指针本身——它仍保存原地址,成为"悬空指针(dangling)";继续解引用是未定义行为。这是 C++98 没有 nullptr 时代的经典坑) 指针与内存管理 · p01/ch04/s03
L1·2 三行依次输出 100 / 99 / 98static 局部变量只初始化一次、跨调用保持值;n-- 先返回当前值再自减)。三行分开打印,无求值顺序歧义 static 与存储期 · p01/ch04/s04
L1·3 11(引用传递:xn 的别名,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;析构相反:643 继承+成员的构造析构顺序 · p01/ch04/s07/inheritance_ctor_dtor_order.cpp
L2·7 浅拷贝导致 double free:默认拷贝构造浅拷贝,b.dataa.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 和 Bconst char* pchar const* p 写法不同但完全等价——const 修饰的是 * 的左边,即指向的数据;指针本身可改。C 是 const 指针,D 两边都 const。全选 A、B 才得分 const 与指针 · p01/ch03
L3·10 编译失败f.id 二义(ambiguous)。FinalAB 两条路径各继承一份 Root::id,编译器无法抉择。修复:①f.A::id 显式限定;②虚继承 class A : virtual public Root 多重继承二义性与虚继承 · p01/ch04/s07

关于 B 卷第 2 题的设计说明:原稿曾写成单行链式 std::cout << tick() << tick() << tick(); 但该写法的输出顺序依赖 << 的求值顺序,而 C++ 未规定该顺序unspecified behavior), 不同编译器可能拼出 10099989899100 等不同结果——会让"标准答案"本身不稳定。现稿改为 三行分开打印,输出确定无歧义,更适合做闭卷摸底的客观题。

评分与分层预案

计分规则:每题 1 分,满分 10 分。选择/代码阅读答对即得分;"找 bug / 解释"题(A·7、A·8、A·10、 B·7、B·8、B·10)只要说对核心机制(浅拷贝/double free、对象切片、二义性)即得分,不苛求 措辞。B 卷第 2 题按上述概念核分说明处理。

分层与教学预案(对应 OUTLINE.md §0):

分段 占比解读 指导动作
810 分 达到"已掌握 C++ 基础与 OOP 概念"假设 Day1 模块 B 按原"巡礼式"节奏推进,现代特性(C++11+)正常引入
57 分 部分薄弱——对照上表找出集中失分的层级 L1 失分多Q13):回退到基础语法讲解;L2 失分多(Q4–7):在构造析构/浅拷贝/重载决议处加时 5–10 分钟;L3 失分多(Q8–10):说明学员 OOP 基础尚可,多态/继承陷阱课上点到为止、留自学
04 分 明显达不到假设 OUTLINE.md §0 预案,Day1 模块 B 整体退回语法讲解,练习任务保留但预期完成度下调

逐题正确率用法(大规模班级):统计每题正确率,低于 50% 的题对应的知识点在 Day1 模块 B 里适当加时;无需逐份细看。

与旧版的差异(为什么重做)

此前一版 PRETEST(10 题、单卷)的主要不足:

  1. 缺梯度——10 题难度近似,集中在"概念识别"层面,区分不出"学过"和"真正懂了"
  2. 无平行卷——单卷无法防抄袭、无法平行重测;
  3. 进阶层薄弱——没有对象切片、菱形继承这类真正能筛出"高阶"学员的题,全做对与做对 7 题 的学员水平差异,旧卷测不出来。

新版改进:三级梯度(L1/L2/L3)+ 两份等价平行卷(A/B)+ 每题标注考查点与课程出处, 既能给出每层级单独的掌握情况,又支持防抄袭与平行重测。

使用方式

  • 纸质或线上表单均可,闭卷、不查资料、不讨论,10 分钟收卷
  • 两份平行卷按需使用:相邻座席 A/B 交错防抄袭;或 A 卷当堂测、B 卷补测/作业;
  • 只用于摸底分层,不计入 docs/TRAINING_PLAN_2026.md 的正式考核成绩;
  • 大规模班级可只统计"每题正确率",正确率低于 50% 的题对应知识点在 Day1 模块 B 里适当加时。