大纲按「3天C++强化(含OOP)+1天Qt桌面」组织,讲:练≈1:2.7;每个知识点直接 链接仓库真实代码文件与 ERRATA/VERSION_NOTES 里记录的具体坑。幻灯片 vendor 了 reveal.js 5.2.1(核心+highlight插件),零 CDN 依赖,配 serve.py 本地起服务(仅绑定 127.0.0.1)供离线播放,代码链接可点开查看源码。
16 KiB
教案大纲 —「Qt 方向」4 天实训
配套材料:
docs/teaching/slides/day1.html…day4.html(reveal.js 交互幻灯片,已 vendor 进仓库,离线可用,见文末「材料与播放方式」)。
0. 受众假设(显式声明,供随时纠偏)
本教案假定学员已掌握 C++ 基础语法与面向对象概念(类、继承、多态等术语不陌生), 但未接触过 Qt。 这是本次问答中用户明确选择的组合:
- 先修水平:「已熟悉 C++/OOP,只是不了解 Qt」
- 4 天主线:「3 天 C++ 强化(含 OOP)+ 1 天 Qt 桌面」
两者合起来的落地方式是:Part1(C++/OOP)3 天不讲入门语法,改为「强化 / 纠错 /
深挖」定位——重点讲仓库里已经踩过并记录在 docs/ERRATA.md 的真实坑、
docs/VERSION_NOTES.md 里的跨版本反直觉结论,以及对象模型、虚函数表这类「学过但
未必讲透」的机制;Part3(Qt)压缩进 1 天,定位是「快速建立骨架认知 + 挑一个
方向动手做出一个可跑的小工具」,不追求覆盖全部 29 个目标。
如果实际学员水平达不到「已熟悉 C++/OOP」,Day1–3 的讲授部分需要退回到语法讲解, 练习任务保持不变但预期完成度应下调——教师可据此现场调整,无需改讲义结构。
1. 总体目标
- 学员能解释 C++ 相对 C 的关键扩展机制的原理(不是背语法),并能指出这些语法在 哪个标准版本才成立/失效。
- 学员能独立分析并修正一段有缺陷的 C++/Qt 代码(本仓库的
ERRATA*.md就是活教材)。 - 学员能读懂并扩展一个 Qt 桌面小程序(信号槽、控件、事件、绘图、文件 IO 至少覆盖一类)。
- 每天结束都有一个可运行、可验证的产出(不是抄书面练习)。
2. 课时结构(讲:练 ≈ 1:3 的落地方式)
每天 6 课时(45 分钟/课时 ×6 = 270 分钟),分两个模块,模块内部再拆讲/练/答疑:
| 模块 | 时长 | 讲授 | 练习 | 小结/答疑 | 讲:练 |
|---|---|---|---|---|---|
| 模块 A(上午,3 课时) | 135min | 35min | 95min | 5min | ≈1:2.7 |
| 模块 B(下午,3 课时) | 135min | 35min | 95min | 5min | ≈1:2.7 |
全天讲授 70min / 练习 190min,比例 ≈1:2.7,接近题目要求的 1:3;docs/teaching/slides/
每天的 HTML 幻灯片按「模块 A 讲 → 模块 A 练习任务卡 → 模块 B 讲 → 模块 B 练习任务卡」
组织,练习任务卡本身就是幻灯片的一部分(放投影上,学员对着做),教师在 95min 内巡场答疑。
3. 四天总览
| Day | 主题 | 模块 A | 模块 B |
|---|---|---|---|
| 1 | C++ 对 C 的扩展 + 类和对象基础 | p01/ch02 p01/ch03 强化精讲 |
p01/ch04/s01–s03 封装/构造析构 |
| 2 | OOP 进阶 | p01/ch04/s04–s06 对象模型/友元/运算符重载 |
p01/ch04/s07–s08 继承/多态 |
| 3 | 泛型与标准库 | p01/ch05 模板 + p01/ch06 类型转换 |
p01/ch07 异常 + p01/ch08 IO流 + p01/ch09 STL |
| 4 | Qt 桌面全景 | p03/ch02–ch05 项目/信号槽/主窗口 |
p03/ch06 p03/ch08–ch11 对话框/控件/事件/绘图/文件(速览+分组选做) |
标注约定:下文表格中 讲 = 课堂演示讲解,练 = 学员必须动手完成,参考 = 不 在课堂展开、留作课后自学(文件仍在仓库里,链接照给)。24 课时不可能覆盖全部 173 个 目标,所有清单都是精选而非全集,未点名文件默认按「参考」处理。
Day 1 — C++ 对 C 的扩展 + 类和对象基础
模块 A:C++ 对 C 的扩展(强化,非入门)
讲(35min)——只挑「学过但容易讲不透」的点,每点配 ERRATA/VERSION_NOTES 出处:
| 知识点 | 文件 | 强化角度 |
|---|---|---|
| namespace 在函数体内定义为什么非法 | p01/ch03/namespace_syntax.cpp(对照 docs/ERRATA.md [58954439:3/6/7]) |
wiki 原文的错误写法 + 编译器报错原文,讲“作用域”不是“文件顺序” |
| 函数重载的决议机制 | p01/ch03/function_overload_mechanism.cpp、function_overload_basics_2.cpp(ERRATA [58954439:42] ambiguous-overload) |
名字改写(name mangling)与重载集合,为什么会出现二义性 |
const 的本质 vs #define |
p01/ch03/const_diff_summary.cpp、const_vs_define.cpp |
类型检查、作用域、编译期替换的本质差异 |
| 引用的本质 | p01/ch03/reference_essence.cpp |
“引用是自动解引用的 const 指针”这一实现视角 |
| 三目运算符可否作左值——纯版本差异 | p01/ch03/ternary_operator.c vs ternary_operator.cpp |
现场用 tools/std_probe.py 演示,对照 docs/VERSION_NOTES.md §2 |
练(95min):
- 任务卡 1:在
p01/ch03挑 8–10 个文件(namespace / reference / overload / const 系列), 逐个tools/run.sh <target>跑通,对照docs/ERRATA.md里的对应条目,写一句话解释 “wiki 原文错在哪、为什么现在的写法是对的”。 - 任务卡 2(版本探针实操):
记录哪个标准报错、哪个不报错,对照
tools/std_probe.py --lang c --file p01/ch03/ternary_operator.c \ --standards c89,c99,c11,c17 --pedantic-errorsdocs/VERSION_NOTES.md§2 的结论。 - 任务卡 3(拔高,选做):阅读
ERRATA.md[58954439:42],解释 dedupe 脚本为什么会把 重载函数改名导致二义性——这是一个「工具误伤正确代码」的真实案例,训练读 diff 的能力。
模块 B:类和对象基础
讲(35min):
| 知识点 | 文件 | 强化角度 |
|---|---|---|
| 封装:C 结构体+函数 vs C++ 类 | p01/ch04/s01/class_encapsulation.c 对比 class_encapsulation.cpp |
同一问题两种语言写法并排看 |
| 构造/析构时机 | p01/ch04/s03/ctor_and_dtor.cpp |
强调 ERRATA 里一串 missing-include([58954451:0..13])——wiki 原文常见疏漏,编译器不报错但运行期用到 strcpy/strlen 会炸 |
| 深拷贝 vs 浅拷贝 | p01/ch04/s03/deep_vs_shallow_copy.cpp |
用析构函数里的打印现场演示悬垂指针/double free |
| 静态成员与单例 | p01/ch04/s03/static_singleton.cpp |
静态成员的生命周期 |
练(95min):
- 任务卡 1:以
p01/ch04/s02/point_and_circle.cpp或cube_class_design.cpp为起点, 新增一个成员函数(如计算面积/体积),编译运行验证。 - 任务卡 2:修改
deep_vs_shallow_copy.cpp,故意触发浅拷贝析构两次释放同一块内存, 用run.sh观察崩溃或输出异常,再改回深拷贝对比。 - 任务卡 3:参照
static_singleton.cpp独立写一个单例类(禁止拷贝构造)。
当日产出验证:cmake --build build --target all_p01_ch03 与 all_p01_ch04_s03
全绿;至少完成 1 个浅拷贝崩溃复现 + 修复对比。
Day 2 — OOP 进阶
模块 A:对象模型 / 友元 / 运算符重载
讲(35min):
| 知识点 | 文件 |
|---|---|
| this 指针与常量成员函数 | p01/ch04/s04/this_pointer.cpp、const_member_function.cpp |
| 友元破坏封装的取舍 | p01/ch04/s05/friend_syntax.cpp |
| 运算符重载全景(=, ==, [], (), ++/--) | p01/ch04/s06/overload_assignment.cpp、overload_equality.cpp、overload_function_call.cpp、overload_increment_decrement.cpp |
练(95min):
- 任务卡 1:给一个自定义类实现
operator=(配合深拷贝)+operator==,参照overload_assignment.cpp/overload_equality.cpp。 - 任务卡 2:实现一个重载了
operator[]的简单容器类(参照overload_pointer_operator.cpp的思路),做边界检查。 - 任务卡 3(选做):
overload_with_friend.cpp——用友元函数实现operator<<, 讨论“为什么流输出运算符通常不能是成员函数”。
模块 B:继承与多态
讲(35min):
| 知识点 | 文件 | 强化角度 |
|---|---|---|
| 继承中构造/析构的顺序 | p01/ch04/s07/inheritance_ctor_dtor_order.cpp |
先基类后派生类构造,析构相反 |
| 同名成员遮蔽 | p01/ch04/s07/same_name_members.cpp |
作用域遮蔽 vs 重写 |
| 虚继承解决菱形问题 | p01/ch04/s07/diamond_virtual_inheritance.cpp |
内存模型图示(vbptr) |
| 纯虚函数/抽象类 | p01/ch04/s08/pure_virtual_dtor.cpp |
ERRATA [58954469:5] abstract-instantiation——wiki 原文误实例化了抽象类 |
| 虚析构的意义 + 裁切问题 | p01/ch04/s08/virtual_dtor_purpose.cpp、upcast_problem.cpp |
用基类指针 delete 派生类对象不加 virtual 析构的内存泄漏演示 |
练(95min):
- 任务卡 1:跑
upcast_problem.cpp,观察未加virtual时的行为,然后手动加上virtual关键字重新编译对比输出差异。 - 任务卡 2:设计一个含虚析构的基类 + 2 个派生类,用基类指针数组管理对象,验证
析构顺序正确、无内存泄漏(可选
valgrind或简单打印计数)。 - 任务卡 3(选做):
diamond_virtual_inheritance.cpp与diamond_virtual_inheritance_2.cpp对比普通继承 vs 虚继承的成员地址/构造次数。
当日产出验证:一个「基类指针管理派生类对象、正确释放」的可运行小程序。
Day 3 — 泛型与标准库
模块 A:模板 + 类型转换
讲(35min):
| 知识点 | 文件 |
|---|---|
| 函数模板 vs 类模板 | p01/ch05/function_template.cpp、class_template_basics.cpp |
| 模板的局限性 | p01/ch05/template_limitations.cpp、template_vs_function.cpp |
| 四种显式类型转换 | p01/ch06/static_cast_demo.cpp、dynamic_cast_demo.cpp、const_cast_demo.cpp |
练(95min):
- 任务卡 1:仿照
class_template_basics.cpp写一个通用Pair<T1, T2>或简易Stack<T>类模板。 - 任务卡 2:用
dynamic_cast对一个多态类层次做安全向下转型判断(失败返回nullptr的分支要写出来), 参照dynamic_cast_demo.cpp。 - 任务卡 3(选做):
class_template_derives_class.cpp/class_template_derives_template.cpp—— 类模板的派生关系。
模块 B:异常 + IO 流 + STL
讲(35min,三块内容合并速览,各分配约 12min):
| 知识点 | 文件 |
|---|---|
| 异常语法与栈展开 | p01/ch07/exception_syntax.cpp、stack_unwinding.cpp |
自定义异常类 / std::exception 体系 |
p01/ch07/custom_exception_class.cpp、std_exception_library.cpp |
| 文件 IO(文本/二进制) | p01/ch08/ascii_file_io.cpp、binary_file_io.cpp |
| STL 三大组件速览 | p01/ch09/s02/stl_components_case.cpp |
| 常用容器 + 迭代器 | p01/ch09/s03/vector_iterator.cpp、list_data_structure.cpp |
| 算法 + 函数对象 | p01/ch09/s04/predicate.cpp、function_object.cpp、traversal_algorithms.cpp |
练(95min,任选其二完成,第三个留参考):
- 任务卡 1:给 Day2 写的类加异常处理——非法参数抛自定义异常,
catch后打印信息不崩溃。 - 任务卡 2:用
vector+<algorithm>(sort/find_if)完成一个小任务(如“找出成绩表中不及格的学生”)。 - 任务卡 3:参照
binary_file_io.cpp把一个结构体数组写入二进制文件再读出校验。
当日产出验证:至少 1 个模板类 + 1 个使用 STL 容器/算法解决实际小问题的程序。
Day 4 — Qt 桌面全景(1 天,快速建立骨架认知 + 动手做一个可跑小工具)
定位声明:Day1–3 是「强化已知」,Day4 是「Qt 零基础入门」——讲授压缩为骨架级 讲解,练习是「跟着例子改」而不是从零写。9 个章节、29 个目标不追求全覆盖, 模块 B 采用分组选做。所有 Qt 目标运行前需先执行
tools/install_qt_host.sh(一次性)与cmake -S . -B build -G Ninja -DBUILD_QT_PART=ON -DCMAKE_PREFIX_PATH=$PWD/.qt514/5.14.2/gcc_64。
模块 A:项目结构 / 第一个程序 / 信号槽 / 主窗口
讲(35min):
| 知识点 | 文件 |
|---|---|
最小 Qt 程序结构(QApplication + exec()) |
p03/ch02/minimal_qt_app.cpp |
| 控件创建与显示 | p03/ch03/button_creation.cpp |
| 内置信号槽 / 自定义信号槽 / lambda 槽 | p03/ch04/builtin_signal_slot.cpp、custom_signal_slot.cpp、lambda_signal_slot.cpp |
QMainWindow 中心部件与停靠窗口 |
p03/ch05/central_widget.cpp、dock_widget.cpp |
练(95min):
- 任务卡 1:编译运行
minimal_qt_app与button_creation(先用离屏模式验证再选做真实窗口):QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app - 任务卡 2:仿
custom_signal_slot.cpp给按钮连接一个自定义信号槽(点击后改变一个QLabel文本)。 - 任务卡 3:用
QMainWindow+dock_widget.cpp搭一个「中心区+侧边栏」的窗口骨架。
模块 B:对话框 / 控件 / 事件 / 绘图 / 文件(速览 + 分组选做)
讲(35min,五个方向各约 7min 速览,只给「这个方向能做什么」的骨架认知):
| 方向 | 文件 |
|---|---|
| 对话框(模态/非模态/文件/消息框) | p03/ch06/modal_dialog.cpp、file_dialog.cpp、message_box_question.cpp |
| 常用控件(图片/文本标签、自定义控件) | p03/ch08/label_pixmap.cpp、label_text_html.cpp、custom_widget.cpp |
| 事件机制(鼠标事件、事件过滤器) | p03/ch09/mouse_event.cpp、event_filter_mainwindow.cpp |
绘图(QPainter、绘图设备) |
p03/ch10/qpainter_basic.cpp、paint_device_qimage.cpp |
文件系统(QFile/QTextStream/QDataStream) |
p03/ch11/qfile_basic.cpp、qtextstream_io.cpp、qdatastream_io.cpp |
练(95min,三选一,分组完成,组内可互相帮忙):
- 方案 A:图片查看器雏形 ——
label_pixmap.cpp+file_dialog.cpp:用文件对话框选图片, 加载到QLabel显示。 - 方案 B:简易绘图板 ——
qpainter_basic.cpp+mouse_event.cpp:监听鼠标拖动事件, 用QPainter画出轨迹。 - 方案 C:文本文件小工具 ——
qfile_basic.cpp+qtextstream_io.cpp:读一个文本文件、 统计行数/字符数,写回一个新文件。
当日产出验证:每组现场用 QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target>(或有
X 环境时直接运行)演示自己选做的方案,教师验收「编译通过 + 交互符合预期」。
4. 验证与考核方式
- 每天结束前 10 分钟(占用小结时间的延伸,或作为课后作业):学员提交当天练习任务卡的
产出文件,用
cmake --build build验证全绿。 - Day4 结束:三选一小工具的现场演示,作为本次实训的最终考核点。
- 教师侧参考答案:所有练习任务的「正确写法」在仓库对应文件里已经存在(生成器产出即
正确版本),可直接用作评分参照;
docs/ERRATA*.md记录了每一处修正,可用于讲解 “错在哪、对在哪”。
5. 材料与播放方式
- 大纲:本文件
docs/teaching/OUTLINE.md。 - 交互幻灯片:
docs/teaching/slides/day1.html…day4.html,index.html为入口页。 - 技术方案:reveal.js 5.2.1(仅
dist/核心 +plugin/highlight)已 vendor 进docs/teaching/slides/reveal/,零 CDN 依赖,断网教室也能播放。 - 播放方式(代码链接用相对路径指向仓库源文件,需要 HTTP 而非
file://才能正常跳转):cd /home/charles/workspaces/qt-course python3 -m http.server 8000 # 浏览器打开 http://localhost:8000/docs/teaching/slides/index.html - 幻灯片内所有「文件」链接均为相对本仓库路径的可点击链接,点开后由浏览器直接展示 源码(配合上面的 http.server);教师也可直接在编辑器里打开同一路径带学员精读。