新增 4 天教案大纲 + reveal.js 交互幻灯片(docs/teaching/)

大纲按「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)供离线播放,代码链接可点开查看源码。
This commit is contained in:
张宗平
2026-07-01 16:51:12 +08:00
parent 8e44203929
commit b89819d5db
13 changed files with 2093 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
# 教案大纲 —「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. 总体目标
1. 学员能解释 C++ 相对 C 的关键扩展机制的**原理**(不是背语法),并能指出这些语法在
哪个标准版本才成立/失效。
2. 学员能独立分析并修正一段有缺陷的 C++/Qt 代码(本仓库的 `ERRATA*.md` 就是活教材)。
3. 学员能读懂并扩展一个 Qt 桌面小程序(信号槽、控件、事件、绘图、文件 IO 至少覆盖一类)。
4. 每天结束都有一个**可运行、可验证**的产出(不是抄书面练习)。
## 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/s01s03` 封装/构造析构 |
| 2 | OOP 进阶 | `p01/ch04/s04s06` 对象模型/友元/运算符重载 | `p01/ch04/s07s08` 继承/多态 |
| 3 | 泛型与标准库 | `p01/ch05` 模板 + `p01/ch06` 类型转换 | `p01/ch07` 异常 + `p01/ch08` IO流 + `p01/ch09` STL |
| 4 | Qt 桌面全景 | `p03/ch02ch05` 项目/信号槽/主窗口 | `p03/ch06` `p03/ch08ch11` 对话框/控件/事件/绘图/文件(速览+分组选做) |
标注约定:下文表格中 **讲** = 课堂演示讲解,**练** = 学员必须动手完成,**参考** = 不
在课堂展开、留作课后自学(文件仍在仓库里,链接照给)。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` 挑 810 个文件(namespace / reference / overload / const 系列),
逐个 `tools/run.sh <target>` 跑通,对照 `docs/ERRATA.md` 里的对应条目,写一句话解释
“wiki 原文错在哪、为什么现在的写法是对的”。
- 任务卡 2(版本探针实操):
```bash
tools/std_probe.py --lang c --file p01/ch03/ternary_operator.c \
--standards c89,c99,c11,c17 --pedantic-errors
```
记录哪个标准报错、哪个不报错,对照 `docs/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`(先用离屏模式验证再选做真实窗口):
```bash
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://` 才能正常跳转):
```bash
cd /home/charles/workspaces/qt-course
python3 -m http.server 8000
# 浏览器打开 http://localhost:8000/docs/teaching/slides/index.html
```
- 幻灯片内所有「文件」链接均为相对本仓库路径的可点击链接,点开后由浏览器直接展示
源码(配合上面的 http.server);教师也可直接在编辑器里打开同一路径带学员精读。
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
"""本地起 HTTP 服务播放教案幻灯片(离线可用)。
用法:python3 docs/teaching/serve.py [端口,默认 8000]
浏览器打开 http://localhost:<端口>/docs/teaching/slides/index.html
只绑定 127.0.0.1(不对局域网暴露)——仓库根目录含 .env(wiki 登录凭据),
必须避免整个仓库被同一 Wi-Fi/局域网上的其他设备访问到。
"""
import http.server
import mimetypes
import os
import socketserver
import sys
# 幻灯片里点击的源码链接(.c/.cpp/.py/.md 等)需要以纯文本显示,而不是被当成
# 未知二进制触发下载——默认 mimetypes 对 .cpp/.c 等会猜成 text/x-csrc 之类,
# 浏览器不会内联渲染,这里统一覆盖成 text/plain。
for ext in (".c", ".cpp", ".h", ".hpp", ".cc", ".qrc", ".py", ".md"):
mimetypes.add_type("text/plain", ext)
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.chdir(repo_root)
try:
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
except ValueError:
print(f"用法:python3 {sys.argv[0]} [端口号]")
sys.exit(1)
class ReuseAddrTCPServer(socketserver.TCPServer):
allow_reuse_address = True
with ReuseAddrTCPServer(("127.0.0.1", port), http.server.SimpleHTTPRequestHandler) as httpd:
print(f"Serving {repo_root} at http://localhost:{port}/docs/teaching/slides/index.html")
httpd.serve_forever()
+64
View File
@@ -0,0 +1,64 @@
/* 共享样式:讲/练/参考标签、任务卡片、代码文件链接 —— 覆盖在 reveal white 主题之上 */
.reveal { font-size: 32px; }
.reveal h1 { font-size: 1.6em; }
.reveal h2 { font-size: 1.15em; }
.reveal h3 { font-size: 0.9em; }
.reveal section img { border: none; box-shadow: none; }
.tag {
display: inline-block;
padding: 1px 12px;
border-radius: 12px;
font-size: 0.5em;
font-weight: 600;
vertical-align: middle;
margin-left: 12px;
letter-spacing: 1px;
}
.tag-lecture { background: #2b6cb0; color: #fff; } /* 讲 */
.tag-practice { background: #2f855a; color: #fff; } /* 练 */
.task-card {
border-left: 6px solid #2f855a;
background: #f4f9f5;
padding: 14px 24px;
margin: 14px 0;
border-radius: 4px;
text-align: left;
font-size: 0.62em;
line-height: 1.35;
}
.task-card h4 { margin: 0 0 6px 0; color: #2f855a; text-transform: none; font-size: 1.05em; }
.filelink, code.filelink {
font-family: 'Courier New', monospace;
background: #eef2f7;
padding: 1px 8px;
border-radius: 3px;
color: #1a4d8f;
text-decoration: none;
border: 1px solid #d3dce6;
}
.filelink:hover { background: #dbe6f5; }
.footer-note { margin-top: 30px; }
.verify-box {
border: 2px dashed #b7791f;
background: #fffaf0;
padding: 12px 20px;
border-radius: 6px;
font-size: 0.6em;
text-align: left;
}
.verify-box h4 { margin: 0 0 6px 0; color: #b7791f; }
.reveal pre code { font-size: 0.68em; max-height: 480px; }
.module-badge {
font-size: 0.4em;
color: #a0aec0;
letter-spacing: 2px;
text-transform: uppercase;
}
+353
View File
@@ -0,0 +1,353 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 1 · C++ 对 C 的扩展 + 类和对象基础</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="reveal/reveal.css">
<link rel="stylesheet" href="reveal/theme.css">
<link rel="stylesheet" href="reveal/plugin/highlight/monokai.css">
<link rel="stylesheet" href="custom.css">
</head>
<body>
<div class="reveal">
<div class="slides">
<section>
<h1>Day 1</h1>
<h3>C++ 对 C 的扩展 &nbsp;+&nbsp; 类和对象基础</h3>
<p class="module-badge">Qt 方向实训 · 第 1 / 4 天 · 讲:练 ≈ 1:2.7</p>
<ul>
<li>模块 A(3 课时):C++ 对 C 的扩展 —— 强化,非入门</li>
<li>模块 B3 课时):类和对象基础</li>
</ul>
<p><small>大纲:<a class="filelink" href="../OUTLINE.md">docs/teaching/OUTLINE.md</a></small></p>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>C++ 对 C 的扩展 <span class="tag tag-lecture"></span></h2>
<p>已默认你熟悉这些语法——今天讲「为什么」,不讲「怎么写」:</p>
<ul>
<li>namespace 的作用域本质</li>
<li>函数重载的决议机制</li>
<li>const 的本质 vs #define</li>
<li>引用的本质(语法糖视角)</li>
<li>三目运算符可否作左值 —— 纯版本差异</li>
</ul>
</section>
<section>
<p class="module-badge">强化点 1 / 5</p>
<h3>namespace 为什么不能定义在函数体内 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
namespace A{
int a = 10;
}
namespace B{
int a = 20;
}
void test(){
cout &lt;&lt; "A::a : " &lt;&lt; A::a &lt;&lt; endl;
cout &lt;&lt; "B::a : " &lt;&lt; B::a &lt;&lt; endl;
}
</code></pre>
<p><small>作用域是「文件级」概念,不是「语句顺序」——wiki 原文把 namespace
写进了函数体内,编译不过;出处:<a class="filelink" target="_blank"
href="../../../p01/ch03/namespace_syntax.cpp">p01/ch03/namespace_syntax.cpp</a>
修正记录 <span class="filelink">ERRATA.md [58954439:3/6/7]</span></small></p>
</section>
<section>
<p class="module-badge">强化点 2 / 5</p>
<h3>函数重载:二义性从哪来 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
void MyFunc(string b){ cout &lt;&lt; "b: " &lt;&lt; b &lt;&lt; endl; }
// 函数重载碰上默认参数
void MyFunc(string b, int a = 10){ cout &lt;&lt; "a: " &lt;&lt; a &lt;&lt; " b:" &lt;&lt; b &lt;&lt; endl; }
int main(){
// MyFunc("hello"); // 二义:两个重载同等匹配
MyFunc("hello", 20); // 显式给第二参才能消歧义
}
</code></pre>
<p><small>重载决议只看「能否唯一确定最佳匹配」,默认参数不参与决议——
出处:<a class="filelink" target="_blank"
href="../../../p01/ch03/function_overload_basics_2.cpp">p01/ch03/function_overload_basics_2.cpp</a>
<span class="filelink">ERRATA.md [58954439:42] ambiguous-overload</span></small></p>
</section>
<section>
<p class="module-badge">强化点 3 / 5</p>
<h3>const 的本质 vs #define <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
const int constA = 10;
int main(){
int* p = (int*)&constA;
*p = 200; // 绕过 const 的“建议性”检查——编译期常量 ≠ 只读内存
}
</code></pre>
<p><small>const 是「编译期类型检查」,不是「运行期内存保护」;#define 只是文本替换,
没有类型、没有作用域。出处:<a class="filelink" target="_blank"
href="../../../p01/ch03/const_diff_summary.cpp">p01/ch03/const_diff_summary.cpp</a></small></p>
</section>
<section>
<p class="module-badge">强化点 4 / 5</p>
<h3>引用的本质 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
void testFunc(int& ref){
ref = 100; // 编译器内部转换为 *ref = 100
}
int main(){
int a = 10;
int& aRef = a; // 自动转换为 int* const aRef = &a —— 这也是引用必须初始化的原因
aRef = 20; // 内部转换为 *aRef = 20
}
</code></pre>
<p><small>把引用当成「自动解引用的 const 指针」来理解,很多“为什么引用不能重新绑定”
的疑惑会自然消失。出处:<a class="filelink" target="_blank"
href="../../../p01/ch03/reference_essence.cpp">p01/ch03/reference_essence.cpp</a></small></p>
</section>
<section>
<p class="module-badge">强化点 5 / 5 · 版本差异</p>
<h3>三目运算符能否作左值 —— 纯版本问题 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
// Cp01/ch03/ternary_operator.c):三目表达式返回右值
(a &gt; b ? a : b) = 100; // C 中非法
// C++p01/ch03/ternary_operator.cpp):三目表达式返回左值引用
(a &gt; b ? a : b) = 100; // C++ 中合法,b 会被改写
</code></pre>
<p><small>现场演示:<code class="filelink">tools/std_probe.py --lang c --file
p01/ch03/ternary_operator.c --standards c89,c99,c11,c17 --pedantic-errors</code>
—— 对照 <span class="filelink">docs/VERSION_NOTES.md §2</span></small></p>
</section>
</section>
<!-- ===================== 模块 A:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 练习 95min</p>
<h2>动手:ch03 强化练习 <span class="tag tag-practice"></span></h2>
<p>目录:<a class="filelink" target="_blank" href="../../../p01/ch03">p01/ch03/</a>
(47 个源文件,本节任务只挑一部分,其余留作参考自学)</p>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 逐一验证 <span class="tag tag-practice"></span></h4>
<p><code class="filelink">p01/ch03</code> 挑 810 个文件(namespace / reference /
overload / const 系列),逐个执行:</p>
<pre><code class="bash" data-trim>tools/run.sh p1c03_03 # namespace_syntax
tools/run.sh p1c03_33 # reference_essence
tools/run.sh p1c03_43 # function_overload_basics_2
# ……自行在 build/ 里用 ninja -t targets 或 cmake --build 找到其余目标名</code></pre>
<p>对照 <code class="filelink">docs/ERRATA.md</code> 对应条目,写一句话解释
「wiki 原文错在哪、现在为什么对」。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · 版本探针实操 <span class="tag tag-practice"></span></h4>
<pre><code class="bash" data-trim>tools/std_probe.py --lang c --file p01/ch03/ternary_operator.c \
--standards c89,c99,c11,c17 --pedantic-errors</code></pre>
<p>记录哪个标准报错、哪个不报错,对照
<code class="filelink">docs/VERSION_NOTES.md §2</code> 的结论,理解“同一段代码
在不同标准下含义不同”。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3 · 拔高(选做) <span class="tag tag-practice"></span></h4>
<p>阅读 <code class="filelink">docs/ERRATA.md [58954439:42]</code>,解释生成脚本的
去重逻辑为什么会把重载函数改名导致二义性——这是一次「工具误伤正确代码」的真实案例,
训练读 diff、读生成器逻辑的能力。</p>
<p><small>参考:<a class="filelink" target="_blank"
href="../../../tools/gen_part1.py">tools/gen_part1.py</a><code>dedupe</code> 相关逻辑</small></p>
</div>
</section>
</section>
<!-- ===================== 模块 B:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min</p>
<h2>类和对象基础 <span class="tag tag-lecture"></span></h2>
<ul>
<li>封装:C 结构体 vs C++ 类</li>
<li>构造 / 析构的调用时机</li>
<li>深拷贝 vs 浅拷贝</li>
<li>静态成员与单例</li>
</ul>
</section>
<section>
<h3>封装:同一个问题,两种语言写法 <span class="tag tag-lecture"></span></h3>
<pre><code class="c" data-trim data-noescape>
// C:结构体没有访问控制,甚至类型都保护不了
typedef struct _Person{ char name[64]; int age; } Person;
typedef struct _Aninal{ char name[64]; int age; int type; } Ainmal;
void AnimalEat(Ainmal* animal){ printf("%s在吃动物吃的饭!\n", animal->name); }
int main(){
Person person; strcpy(person.name, "小明"); person.age = 30;
AnimalEat(&person); // 把 Person* 传给期望 Ainmal* 的函数——编译器毫无察觉
}
</code></pre>
<p><small>对照:<a class="filelink" target="_blank"
href="../../../p01/ch04/s01/class_encapsulation.c">class_encapsulation.c</a> vs
<a class="filelink" target="_blank"
href="../../../p01/ch04/s01/class_encapsulation.cpp">class_encapsulation.cpp</a>
public/protected/private 三级访问控制)</small></p>
</section>
<section>
<h3>构造 / 析构的调用时机 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Person{
public:
Person(){
cout &lt;&lt; "构造函数调用!" &lt;&lt; endl;
pName = (char*)malloc(sizeof("John"));
strcpy(pName, "John");
}
~Person(){
cout &lt;&lt; "析构函数调用!" &lt;&lt; endl;
if (pName != NULL){ free(pName); pName = NULL; }
}
private:
char* pName;
};
</code></pre>
<p><small>强化点:wiki 原文这一串例子普遍漏写 <code>#include &lt;cstring&gt;</code>
(用到 strcpy 却不声明)——出处 <a class="filelink" target="_blank"
href="../../../p01/ch04/s03/ctor_and_dtor.cpp">p01/ch04/s03/ctor_and_dtor.cpp</a>
<code class="filelink">ERRATA.md [58954451:0..13] missing-include</code>:编译器不报错,
但换个平台/换个头文件包含顺序就可能炸——这类隐式依赖正是“强化”该抓的细节。</small></p>
</section>
<section>
<h3>深拷贝 vs 浅拷贝 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Person{
public:
Person(char* name, int age){
pName = (char*)malloc(strlen(name) + 1);
strcpy(pName, name);
}
// 自己实现拷贝构造函数 —— 深拷贝
Person(const Person& person){
pName = (char*)malloc(strlen(person.pName) + 1);
strcpy(pName, person.pName);
}
~Person(){ if (pName != NULL) free(pName); }
private:
char* pName;
};
void test(){
Person p1("Edward", 30);
Person p2 = p1; // 不写拷贝构造 → 默认浅拷贝 → 两个指针指向同一块内存 → 析构两次 double free
}
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s03/deep_vs_shallow_copy.cpp">p01/ch04/s03/deep_vs_shallow_copy.cpp</a></small></p>
</section>
<section>
<h3>静态成员与单例 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Printer{
public:
static Printer* getInstance(){ return pPrinter; }
void PrintText(string text){ cout &lt;&lt; text &lt;&lt; endl; mTimes++; }
private:
Printer(){ mTimes = 0; }
Printer(const Printer&){} // 禁止拷贝
private:
static Printer* pPrinter;
int mTimes;
};
Printer* Printer::pPrinter = new Printer; // 类外初始化静态成员
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s03/static_singleton.cpp">p01/ch04/s03/static_singleton.cpp</a></small></p>
</section>
</section>
<!-- ===================== 模块 B:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 练习 95min</p>
<h2>动手:类和对象 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 扩展一个类 <span class="tag tag-practice"></span></h4>
<p><a class="filelink" target="_blank"
href="../../../p01/ch04/s02/point_and_circle.cpp">point_and_circle.cpp</a>
<a class="filelink" target="_blank"
href="../../../p01/ch04/s02/cube_class_design.cpp">cube_class_design.cpp</a> 为起点,
新增一个成员函数(如计算面积 / 体积),编译运行验证输出正确。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · 复现并修复浅拷贝崩溃 <span class="tag tag-practice"></span></h4>
<p>修改 <a class="filelink" target="_blank"
href="../../../p01/ch04/s03/deep_vs_shallow_copy.cpp">deep_vs_shallow_copy.cpp</a>
故意删掉拷贝构造函数触发浅拷贝,用 <code class="filelink">tools/run.sh</code>
观察 double free / 崩溃或异常输出,再恢复深拷贝对比。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3 · 独立实现单例 <span class="tag tag-practice"></span></h4>
<p>参照 <a class="filelink" target="_blank"
href="../../../p01/ch04/s03/static_singleton.cpp">static_singleton.cpp</a>
独立写一个单例类(拷贝构造设为私有 / delete,验证多次 <code>getInstance()</code>
返回同一地址)。</p>
</div>
</section>
</section>
<!-- ===================== 收尾 ===================== -->
<section>
<h2>当日产出验证</h2>
<div class="verify-box">
<h4>验收标准</h4>
<pre><code class="bash" data-trim>cmake --build build --target all_p01_ch03
cmake --build build --target all_p01_ch04_s03</code></pre>
<p>全绿;至少完成 1 个「浅拷贝崩溃复现 + 修复对比」。</p>
</div>
<p class="footer-note"><small>明日预告:Day 2 —— OOP 进阶(对象模型 / 友元 /
运算符重载 / 继承 / 多态)</small></p>
<p><a class="filelink" href="index.html">返回目录</a></p>
</section>
</div>
</div>
<script src="reveal/reveal.js"></script>
<script src="reveal/plugin/highlight/highlight.js"></script>
<script>
Reveal.initialize({
hash: true,
controls: true,
progress: true,
center: true,
slideNumber: true,
plugins: [ RevealHighlight ]
});
</script>
</body>
</html>
+311
View File
@@ -0,0 +1,311 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 2 · OOP 进阶</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="reveal/reveal.css">
<link rel="stylesheet" href="reveal/theme.css">
<link rel="stylesheet" href="reveal/plugin/highlight/monokai.css">
<link rel="stylesheet" href="custom.css">
</head>
<body>
<div class="reveal">
<div class="slides">
<section>
<h1>Day 2</h1>
<h3>OOP 进阶</h3>
<p class="module-badge">Qt 方向实训 · 第 2 / 4 天 · 讲:练 ≈ 1:2.7</p>
<ul>
<li>模块 A(3 课时):对象模型 / 友元 / 运算符重载</li>
<li>模块 B3 课时):继承与多态</li>
</ul>
<p><small>回顾:Day1 已建立类/构造析构/深浅拷贝的直觉——今天在这个基础上深挖机制</small></p>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>对象模型 / 友元 / 运算符重载 <span class="tag tag-lecture"></span></h2>
<ul>
<li>this 指针到底解决什么问题</li>
<li>友元:突破封装的代价与取舍</li>
<li>运算符重载:=、==、[]、()、++/-- 全景</li>
</ul>
</section>
<section>
<h3>this 指针:区分同名 + 隐式参数 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Person{
public:
Person(string name, int age){
this->name = name; // 形参名与成员名相同,靠 this-> 区分
this->age = age;
}
// 成员函数隐含的第一个参数就是 this —— operator= 也是这样实现的
Person PersonPlusPerson(Person& person){
string newname = this->name + person.name;
int newage = this->age + person.age;
return Person(newname, newage);
}
};
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s04/this_pointer.cpp">p01/ch04/s04/this_pointer.cpp</a></small></p>
</section>
<section>
<h3>友元:全局函数 / 成员函数 / 整个类 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Building{
friend void CleanBedRoom(Building& building); // 友元全局函数
friend class MyFriend; // 友元类——MyFriend 的所有成员都能访问私有区
public:
Building();
private:
string mBedroom;
};
</code></pre>
<p><small>友元是「单向」「不可传递」「不可继承」的特权授予——用得越多,封装边界越模糊,
权衡取舍要讲清楚。出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s05/friend_syntax.cpp">p01/ch04/s05/friend_syntax.cpp</a></small></p>
</section>
<section>
<h3>赋值运算符重载:与拷贝构造的分工 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
Person2& operator=(const Person2& person){
// 关键:当前对象已存在,pName 可能已指向堆内存——必须先释放,否则内存泄漏
if (this->pName != NULL){ delete[] this->pName; }
this->pName = new char[strlen(person.pName) + 1];
strcpy(this->pName, person.pName);
return *this; // 返回引用才能支持 a = b = c 连续赋值
}
</code></pre>
<p><small>“初始化用拷贝构造,已存在对象再赋值用 operator=”——这条分界线是本节强化重点。
出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s06/overload_assignment.cpp">p01/ch04/s06/overload_assignment.cpp</a></small></p>
</section>
<section>
<h3>前置/后置 ++ 的重载差异 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
Complex& operator++(){ // 前置++:改自身,返回引用
mA++; mB++;
return *this;
}
Complex operator++(int){ // 后置++:占位 int 参数只是为了和前置区分重载签名
Complex temp = *this; // 必须先拷贝旧值
mA++; mB++;
return temp; // 返回值(而非引用)——所以后置++开销更大
}
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s06/overload_increment_decrement.cpp">p01/ch04/s06/overload_increment_decrement.cpp</a></small></p>
</section>
</section>
<!-- ===================== 模块 A:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 练习 95min</p>
<h2>动手:运算符重载 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · operator= + operator== <span class="tag tag-practice"></span></h4>
<p>给 Day1 写的类(或新写一个含指针成员的类)实现 <code>operator=</code>(注意先
释放旧资源)与 <code>operator==</code>,参照
<a class="filelink" target="_blank" href="../../../p01/ch04/s06/overload_assignment.cpp">overload_assignment.cpp</a>
<a class="filelink" target="_blank" href="../../../p01/ch04/s06/overload_equality.cpp">overload_equality.cpp</a></p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · operator[] 边界检查 <span class="tag tag-practice"></span></h4>
<p>实现一个重载了 <code>operator[]</code> 的简单容器类(固定大小数组包装),
越界访问要有明确行为(断言或抛异常),参照
<a class="filelink" target="_blank" href="../../../p01/ch04/s06/overload_pointer_operator.cpp">overload_pointer_operator.cpp</a> 的思路。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3 · 选做:友元实现 operator&lt;&lt; <span class="tag tag-practice"></span></h4>
<p>参照 <a class="filelink" target="_blank"
href="../../../p01/ch04/s06/overload_with_friend.cpp">overload_with_friend.cpp</a>
为你的类实现 <code>friend ostream&amp; operator&lt;&lt;</code>,讨论「为什么流输出运算符
通常不能写成成员函数」(提示:左操作数是 ostream,不是本类对象)。</p>
</div>
</section>
</section>
<!-- ===================== 模块 B:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min</p>
<h2>继承与多态 <span class="tag tag-lecture"></span></h2>
<ul>
<li>继承中构造/析构的顺序</li>
<li>同名成员遮蔽 vs 重写</li>
<li>虚继承解决菱形问题</li>
<li>upcast 裁切问题 → virtual 解决</li>
<li>纯虚函数/抽象类、虚析构的意义</li>
</ul>
</section>
<section>
<h3>构造先基类后派生类,析构相反 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class A{ public: A(){cout&lt;&lt;"A构造\n";} ~A(){cout&lt;&lt;"A析构\n";} };
class B : public A{ public: B(){cout&lt;&lt;"B构造\n";} ~B(){cout&lt;&lt;"B析构\n";} };
class C : public B{ public: C(){cout&lt;&lt;"C构造\n";} ~C(){cout&lt;&lt;"C析构\n";} };
int main(){ C c; }
// 输出顺序:A构造 → B构造 → C构造 → C析构 → B析构 → A析构
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s07/inheritance_ctor_dtor_order.cpp">p01/ch04/s07/inheritance_ctor_dtor_order.cpp</a></small></p>
</section>
<section>
<h3>同名成员:默认遮蔽,需类名限定 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Base{ public: Base():mParam(0){} int mParam; };
class Derived : public Base{
public:
Derived():mParam(10){}
void Print(){
cout &lt;&lt; Base::mParam &lt;&lt; endl; // 必须类名限定才能访问被遮蔽的基类成员
cout &lt;&lt; mParam &lt;&lt; endl; // 默认访问派生类自己的
}
int mParam;
};
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s07/same_name_members.cpp">p01/ch04/s07/same_name_members.cpp</a></small></p>
</section>
<section>
<h3>菱形继承 → 虚继承 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class BigBase{ public: int mParam; };
class Base1 : public BigBase{};
class Base2 : public BigBase{};
class Derived : public Base1, public Base2{};
// derived.mParam 二义——两份 BigBase 子对象;须写 derived.Base1::mParam 消歧义
// 虚继承(public virtual BigBase)能让两条路径共享同一份 BigBase 子对象
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s07/diamond_virtual_inheritance.cpp">diamond_virtual_inheritance.cpp</a>
<a class="filelink" target="_blank"
href="../../../p01/ch04/s07/diamond_virtual_inheritance_2.cpp">_2.cpp</a>(虚继承版本对比)</small></p>
</section>
<section>
<h3>upcast 裁切问题:不加 virtual 就没有多态 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
// 问题版:Animal::speak_1 不是 virtual
class Animal{ public: void speak_1(){ cout&lt;&lt;"动物在唱歌...\n"; } };
class Dog : public Animal{ public: void speak_1(){ cout&lt;&lt;"小狗在唱歌...\n"; } };
void DoBussiness(Animal& animal){ animal.speak_1(); }
void test(){ Dog dog; DoBussiness(dog); } // 输出「动物在唱歌」——静态绑定,不是想要的结果
// 解决版:都加 virtual → 动态绑定,输出「小狗在唱歌」
</code></pre>
<p><small>问题版出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s08/upcast_problem.cpp">upcast_problem.cpp</a>;解决版:
<a class="filelink" target="_blank"
href="../../../p01/ch04/s08/virtual_function_solution.cpp">virtual_function_solution.cpp</a></small></p>
</section>
<section>
<h3>纯虚函数、抽象类、虚析构的意义 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class People{
public:
virtual void showName() = 0; // 纯虚函数 → People 是抽象类,不能实例化
virtual ~People(){ cout&lt;&lt;"析构 People\n"; } // 虚析构:基类指针 delete 派生类对象时能正确调用派生类析构
};
class Worker : public People{
public:
virtual void showName(){ /* ... */ }
~Worker(){ cout&lt;&lt;"析构 Worker\n"; delete[] pName; }
private:
char* pName;
};
People* p = new Worker;
delete p; // 若 ~People() 不是 virtual,只会调用 ~PeopleWorker::pName 泄漏
</code></pre>
<p><small>强化点:<code class="filelink">ERRATA.md [58954469:5] abstract-instantiation</code>
——wiki 原文一度把带纯虚析构的类直接实例化,这是编译错误。出处:
<a class="filelink" target="_blank" href="../../../p01/ch04/s08/pure_virtual_dtor.cpp">pure_virtual_dtor.cpp</a>
<a class="filelink" target="_blank" href="../../../p01/ch04/s08/virtual_dtor_purpose.cpp">virtual_dtor_purpose.cpp</a></small></p>
</section>
</section>
<!-- ===================== 模块 B:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 练习 95min</p>
<h2>动手:继承与多态 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 复现并修复 upcast 裁切 <span class="tag tag-practice"></span></h4>
<p>先跑 <a class="filelink" target="_blank"
href="../../../p01/ch04/s08/upcast_problem.cpp">upcast_problem.cpp</a> 观察输出,
再手动加 <code>virtual</code> 关键字重新编译,对比两次输出差异,写清楚“静态绑定 vs 动态绑定”
各自发生在什么时候。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · 基类指针管理派生类对象 <span class="tag tag-practice"></span></h4>
<p>设计一个含虚析构的基类 + 2 个派生类(各自持有一个堆内存成员),用基类指针数组
管理多个对象,逐个 <code>delete</code>,通过析构函数里的打印验证:(1) 调用的是派生类
析构 (2) 没有内存泄漏(可用打印计数或 valgrind 验证)。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3 · 选做:普通继承 vs 虚继承对比 <span class="tag tag-practice"></span></h4>
<p>对照 <a class="filelink" target="_blank"
href="../../../p01/ch04/s07/diamond_virtual_inheritance.cpp">diamond_virtual_inheritance.cpp</a>
<a class="filelink" target="_blank"
href="../../../p01/ch04/s07/diamond_virtual_inheritance_2.cpp">_2.cpp</a>
打印 <code>sizeof(Derived)</code> 并解释两种继承方式下对象体积差异的原因。</p>
</div>
</section>
</section>
<!-- ===================== 收尾 ===================== -->
<section>
<h2>当日产出验证</h2>
<div class="verify-box">
<h4>验收标准</h4>
<pre><code class="bash" data-trim>cmake --build build --target all_p01_ch04_s06
cmake --build build --target all_p01_ch04_s07
cmake --build build --target all_p01_ch04_s08</code></pre>
<p>全绿;产出「基类指针管理派生类对象、正确释放」的可运行小程序。</p>
</div>
<p class="footer-note"><small>明日预告:Day 3 —— 泛型与标准库(模板 / 类型转换 /
异常 / IO 流 / STL</small></p>
<p><a class="filelink" href="index.html">返回目录</a></p>
</section>
</div>
</div>
<script src="reveal/reveal.js"></script>
<script src="reveal/plugin/highlight/highlight.js"></script>
<script>
Reveal.initialize({
hash: true, controls: true, progress: true, center: true, slideNumber: true,
plugins: [ RevealHighlight ]
});
</script>
</body>
</html>
+259
View File
@@ -0,0 +1,259 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 3 · 泛型与标准库</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="reveal/reveal.css">
<link rel="stylesheet" href="reveal/theme.css">
<link rel="stylesheet" href="reveal/plugin/highlight/monokai.css">
<link rel="stylesheet" href="custom.css">
</head>
<body>
<div class="reveal">
<div class="slides">
<section>
<h1>Day 3</h1>
<h3>泛型与标准库</h3>
<p class="module-badge">Qt 方向实训 · 第 3 / 4 天 · 讲:练 ≈ 1:2.7</p>
<ul>
<li>模块 A(3 课时):模板 + 类型转换</li>
<li>模块 B(3 课时):异常 + IO 流 + STL</li>
</ul>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>模板 + 类型转换 <span class="tag tag-lecture"></span></h2>
<ul>
<li>函数模板 vs 类模板</li>
<li>模板的局限性</li>
<li>四种显式类型转换(static / dynamic / const_cast</li>
</ul>
</section>
<section>
<h3>类模板:类型不能自动推导 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
template&lt;class NameType, class AgeType&gt;
class Person{
public:
Person(NameType name, AgeType age){ mName = name; mAge = age; }
void showPerson(){ cout &lt;&lt; mName &lt;&lt; " " &lt;&lt; mAge &lt;&lt; endl; }
NameType mName; AgeType mAge;
};
// Person P1("德玛西亚", 18); // 非法——类模板不支持类型自动推导
Person&lt;string, int&gt; P1("德玛西亚", 18); // 必须显式写出模板实参
</code></pre>
<p><small>与函数模板最大的区别:函数模板能从实参推导类型,类模板不能——这是常见误用点。
出处:<a class="filelink" target="_blank"
href="../../../p01/ch05/class_template_basics.cpp">p01/ch05/class_template_basics.cpp</a></small></p>
</section>
<section>
<h3>dynamic_cast:运行期检查的向下转型 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Animal { public: virtual void ShowName() = 0; }; // 必须有虚函数(多态类型)
class Dog : public Animal { virtual void ShowName(){ /*...*/ } };
class Other { public: void PrintSomething(){} }; // 与 Animal 无继承关系
Dog* dog01 = new Dog;
Animal* animal02 = dynamic_cast&lt;Animal*&gt;(dog01); // 子类→父类,合法(其实用不着 dynamic_cast
// Dog* dog02 = dynamic_cast&lt;Dog*&gt;(animal01); // 父类→子类:运行期检查,失败返回 nullptr
// Animal* x = dynamic_cast&lt;Animal*&gt;(other); // 无继承关系:编译期报错
</code></pre>
<p><small>dynamic_cast 依赖 RTTI,只对多态类型(含虚函数)生效——这是与 static_cast
最本质的区别。出处:<a class="filelink" target="_blank"
href="../../../p01/ch06/dynamic_cast_demo.cpp">p01/ch06/dynamic_cast_demo.cpp</a></small></p>
</section>
</section>
<!-- ===================== 模块 A:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 练习 95min</p>
<h2>动手:模板与类型转换 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 写一个通用容器模板 <span class="tag tag-practice"></span></h4>
<p>仿照 <a class="filelink" target="_blank"
href="../../../p01/ch05/class_template_basics.cpp">class_template_basics.cpp</a>
写一个 <code>Pair&lt;T1, T2&gt;</code> 或简易 <code>Stack&lt;T&gt;</code>push/pop/top
栈空时的行为要明确定义)。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · dynamic_cast 安全向下转型 <span class="tag tag-practice"></span></h4>
<p>用 Day2 写的多态类层次(基类指针数组),对每个元素做
<code>dynamic_cast</code> 判断具体类型,转型失败(返回 nullptr)的分支要写出来,
不能假设转型一定成功。参照 <a class="filelink" target="_blank"
href="../../../p01/ch06/dynamic_cast_demo.cpp">dynamic_cast_demo.cpp</a></p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3 · 选做:类模板的派生关系 <span class="tag tag-practice"></span></h4>
<p>阅读 <a class="filelink" target="_blank"
href="../../../p01/ch05/class_template_derives_class.cpp">class_template_derives_class.cpp</a>
<a class="filelink" target="_blank"
href="../../../p01/ch05/class_template_derives_template.cpp">class_template_derives_template.cpp</a>
写一句话总结「普通类继承类模板」与「类模板继承类模板」在写法上的区别。</p>
</div>
</section>
</section>
<!-- ===================== 模块 B:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min(三块内容合并速览)</p>
<h2>异常 + IO 流 + STL <span class="tag tag-lecture"></span></h2>
<ul>
<li>异常:栈解旋、自定义异常类</li>
<li>文件 IO:文本 vs 二进制</li>
<li>STL:容器 + 迭代器 + 算法 + 函数对象</li>
</ul>
</section>
<section>
<h3>栈解旋(stack unwinding <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Person{
public:
Person(string name){ mName = name; cout &lt;&lt; mName &lt;&lt; "对象被创建!\n"; }
~Person(){ cout &lt;&lt; mName &lt;&lt; "对象被析构!\n"; }
string mName;
};
void TestFunction(){
Person p1("aaa"), p2("bbb"), p3("ccc");
throw 10; // 抛出异常时,p3/p2/p1 按逆序自动析构——这就是「栈解旋」
}
int main(){ try{ TestFunction(); } catch(...){ cout&lt;&lt;"异常被捕获!\n"; } }
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch07/stack_unwinding.cpp">p01/ch07/stack_unwinding.cpp</a></small></p>
</section>
<section>
<h3>自定义异常类:继承 std::exception <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class MyOutOfRange : public exception{
public:
MyOutOfRange(const string errorInfo){ m_Error = errorInfo; }
virtual const char* what() const noexcept { return m_Error.c_str(); }
string m_Error;
};
// 多个 catch 分支各自匹配不同的异常类型(都直接继承 exception,彼此是平级、不是父子)
// Person(151) 这里实际只会抛 MyOutOfRange,前两个 catch 分支不会命中——
// 写多个 catch 分支是为了同一段代码能同时应对「标准库异常」和「自定义异常」两种来源
try{ Person p(151); }
catch (out_of_range& e){ cout &lt;&lt; e.what(); } // 若改成 throw out_of_range(...) 会走这里
catch (MyOutOfRange e){ cout &lt;&lt; e.what(); } // 本例实际命中的分支
</code></pre>
<p><small>强化点:catch 分支的匹配顺序只在「类型存在继承关系」时才重要(子类 catch 必须写在
父类前面,否则父类分支会先吞掉子类异常);这里三个类型互不继承,顺序其实不影响本例结果。
出处:<a class="filelink" target="_blank"
href="../../../p01/ch07/custom_exception_class.cpp">p01/ch07/custom_exception_class.cpp</a></small></p>
</section>
<section>
<h3>二进制文件读写 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
ofstream osm("person.txt", ios::out | ios::binary);
osm.write((const char*)&p1, sizeof(Person)); // 直接写内存布局,不做任何格式转换
osm.close();
ifstream ism("person.txt", ios::in | ios::binary);
Person p3;
ism.read((char*)&p3, sizeof(Person)); // 按同样的 sizeof 读回——结构必须一致
</code></pre>
<p><small>二进制 IO 要求读写两端的结构体内存布局完全一致(含 padding),跨平台/跨编译器
不保证兼容。出处:<a class="filelink" target="_blank"
href="../../../p01/ch08/binary_file_io.cpp">p01/ch08/binary_file_io.cpp</a></small></p>
</section>
<section>
<h3>STL:容器 + 迭代器 + 算法 + 函数对象 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
// 谓词(函数对象)驱动算法
class GreaterThenFive{ public: bool operator()(int num){ return num &gt; 5; } };
vector&lt;int&gt;::iterator it = find_if(v.begin(), v.end(), GreaterThenFive());
class MyCompare{ public: bool operator()(int a,int b){ return a &gt; b; } };
sort(v.begin(), v.end(), MyCompare()); // 用函数对象改变算法策略——从小到大 → 从大到小
</code></pre>
<p><small>STL 的核心思想:容器负责存储,算法负责操作,迭代器是两者之间的胶水,
函数对象(谓词)负责定制算法行为。出处:
<a class="filelink" target="_blank" href="../../../p01/ch09/s03/vector_iterator.cpp">p01/ch09/s03/vector_iterator.cpp</a>
<a class="filelink" target="_blank" href="../../../p01/ch09/s04/predicate.cpp">p01/ch09/s04/predicate.cpp</a></small></p>
</section>
</section>
<!-- ===================== 模块 B:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 练习 95min(任选其二,第三个留参考)</p>
<h2>动手:异常 / STL / 文件 IO <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 给类加异常处理 <span class="tag tag-practice"></span></h4>
<p>给 Day1/Day2 写的类加一个自定义异常类(继承 <code>std::exception</code>),
非法参数(如负数年龄)抛出异常,调用方 <code>catch</code> 后打印信息、程序不崩溃。
参照 <a class="filelink" target="_blank"
href="../../../p01/ch07/custom_exception_class.cpp">custom_exception_class.cpp</a></p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · vector + 算法解决实际问题 <span class="tag tag-practice"></span></h4>
<p><code>vector</code> 存一组「学生成绩」,用 <code>sort</code> +
自定义谓词按成绩排序,再用 <code>find_if</code> 找出所有不及格的学生并打印。
参照 <a class="filelink" target="_blank"
href="../../../p01/ch09/s04/predicate.cpp">predicate.cpp</a></p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3 · 结构体数组的二进制读写 <span class="tag tag-practice"></span></h4>
<p>参照 <a class="filelink" target="_blank"
href="../../../p01/ch08/binary_file_io.cpp">binary_file_io.cpp</a>,把一个结构体数组
(如 5 个学生记录)写入二进制文件,再读出并逐条打印校验,确认与写入前一致。</p>
</div>
</section>
</section>
<!-- ===================== 收尾 ===================== -->
<section>
<h2>当日产出验证</h2>
<div class="verify-box">
<h4>验收标准</h4>
<pre><code class="bash" data-trim>cmake --build build --target all_p01_ch05
cmake --build build --target all_p01_ch07
cmake --build build --target all_p01_ch09_s04</code></pre>
<p>全绿;至少 1 个模板类 + 1 个用 STL 容器/算法解决实际小问题的程序。</p>
</div>
<p class="footer-note"><small>明日预告:Day 4 —— Qt 桌面全景(1 天快速建立骨架认知
+ 动手做一个可跑的小工具)</small></p>
<p><a class="filelink" href="index.html">返回目录</a></p>
</section>
</div>
</div>
<script src="reveal/reveal.js"></script>
<script src="reveal/plugin/highlight/highlight.js"></script>
<script>
Reveal.initialize({
hash: true, controls: true, progress: true, center: true, slideNumber: true,
plugins: [ RevealHighlight ]
});
</script>
</body>
</html>
+306
View File
@@ -0,0 +1,306 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 4 · Qt 桌面全景</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="reveal/reveal.css">
<link rel="stylesheet" href="reveal/theme.css">
<link rel="stylesheet" href="reveal/plugin/highlight/monokai.css">
<link rel="stylesheet" href="custom.css">
</head>
<body>
<div class="reveal">
<div class="slides">
<section>
<h1>Day 4</h1>
<h3>Qt 桌面全景</h3>
<p class="module-badge">Qt 方向实训 · 第 4 / 4 天 · 讲:练 ≈ 1:2.7</p>
<ul>
<li>模块 A(3 课时):项目结构 / 第一个程序 / 信号槽 / 主窗口</li>
<li>模块 B(3 课时):对话框 / 控件 / 事件 / 绘图 / 文件(速览 + 分组选做)</li>
</ul>
<p><small>定位声明:Day1–3 是「强化已知」,今天是「Qt 零基础入门」——讲授压缩为
骨架级讲解,练习是「跟着例子改」而不是从零写。9 个章节、29 个目标不追求全覆盖。</small></p>
<div class="verify-box" style="font-size:0.5em;">
<h4>开始前准备(一次性)</h4>
<pre><code class="bash" data-trim>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 -DCMAKE_BUILD_TYPE=Debug
cmake --build build</code></pre>
</div>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>项目结构 / 信号槽 / 主窗口 <span class="tag tag-lecture"></span></h2>
<ul>
<li>最小 Qt 程序结构</li>
<li>控件创建与显示</li>
<li>内置 / 自定义 / lambda 信号槽</li>
<li>QMainWindow 中心部件</li>
</ul>
</section>
<section>
<h3>最小 Qt 程序:QApplication + exec() <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Widget : public QWidget {
Q_OBJECT // 用到信号槽/属性系统的类都要有这个宏(触发 moc 代码生成)
public:
Widget(QWidget* parent = nullptr) : QWidget(parent) {}
};
int main(int argc, char* argv[]) {
QApplication app(argc, argv); // 整个程序唯一的 QApplication
Widget w;
w.show();
return app.exec(); // 进入事件循环,程序在这里“活着”
}
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch02/minimal_qt_app.cpp">p03/ch02/minimal_qt_app.cpp</a>(注意仓库版本额外注入了
<code>QTimer::singleShot</code> 200ms 自动退出,便于离屏自动化验证,真实项目不需要这一行)</small></p>
</section>
<section>
<h3>控件创建:两种写法 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
// 第一种:先构造再设属性
QPushButton* btn = new QPushButton;
btn->setParent(this);
btn->setText(QStringLiteral("德玛西亚"));
btn->move(100, 100);
// 第二种:构造时指定文本与父亲
QPushButton* btn2 = new QPushButton(QStringLiteral("孙悟空"), this);
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch03/button_creation.cpp">p03/ch03/button_creation.cpp</a></small></p>
</section>
<section>
<h3>信号槽:内置 / 自定义 / 重载消歧义 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Teacher : public QObject {
Q_OBJECT
signals:
void hungry(); // 无参信号
void hungry(QString food); // 带参信号(重载)
public slots:
void classIsOver(){ emit hungry(); emit hungry(QString("汉堡")); }
};
// 连接重载信号时必须用 QOverload 显式指定版本
connect(t, QOverload&lt;&gt;::of(&Teacher::hungry), s, QOverload&lt;&gt;::of(&Student::treat));
connect(t, QOverload&lt;QString&gt;::of(&Teacher::hungry), s, QOverload&lt;QString&gt;::of(&Student::treat));
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch04/custom_signal_slot.cpp">p03/ch04/custom_signal_slot.cpp</a>
lambda 槽 + 值捕获/引用捕获对比见
<a class="filelink" target="_blank" href="../../../p03/ch04/lambda_signal_slot.cpp">lambda_signal_slot.cpp</a></small></p>
</section>
<section>
<h3>QMainWindow:中心部件 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class DemoMainWindow : public QMainWindow {
Q_OBJECT
public:
DemoMainWindow(){
QTextEdit* edit = new QTextEdit(this);
setCentralWidget(edit); // QMainWindow 有固定布局:菜单栏/工具栏/中心部件/停靠窗口/状态栏
}
};
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch05/central_widget.cpp">p03/ch05/central_widget.cpp</a>;停靠窗口见
<a class="filelink" target="_blank" href="../../../p03/ch05/dock_widget.cpp">dock_widget.cpp</a></small></p>
</section>
</section>
<!-- ===================== 模块 A:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 练习 95min</p>
<h2>动手:第一个 Qt 程序 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 跑通最小程序(离屏验证) <span class="tag tag-practice"></span></h4>
<pre><code class="bash" data-trim>QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app
QT_QPA_PLATFORM=offscreen tools/run_qt.sh button_creation</code></pre>
<p>有 X 环境的同学可去掉 <code>QT_QPA_PLATFORM=offscreen</code> 直接看窗口弹出(200ms 后自动退出)。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · 自定义信号槽改文本 <span class="tag tag-practice"></span></h4>
<p>仿 <a class="filelink" target="_blank"
href="../../../p03/ch04/custom_signal_slot.cpp">custom_signal_slot.cpp</a>
给一个按钮连接自定义信号槽:点击后改变一个 <code>QLabel</code> 的文本内容。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3 · 窗口骨架 <span class="tag tag-practice"></span></h4>
<p><code>QMainWindow</code> + <a class="filelink" target="_blank"
href="../../../p03/ch05/dock_widget.cpp">dock_widget.cpp</a> 搭一个「中心区 + 侧边栏」
的窗口骨架。</p>
</div>
</section>
</section>
<!-- ===================== 模块 B:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min(五个方向各约 7min 速览)</p>
<h2>对话框 / 控件 / 事件 / 绘图 / 文件 <span class="tag tag-lecture"></span></h2>
<p><small>只求「这个方向能做什么」的骨架认知,不逐行讲解</small></p>
</section>
<section>
<h3>对话框:文件对话框 + QTextStream <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
void openFile() {
QString path = QFileDialog::getOpenFileName(this, QStringLiteral("打开文件"));
if (path.isEmpty()) return;
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { /* 提示失败 */ return; }
QTextStream in(&file);
QString text = in.readAll();
QMessageBox::information(this, QStringLiteral("内容"), text);
}
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch06/file_dialog.cpp">p03/ch06/file_dialog.cpp</a>;模态/非模态对话框见
<a class="filelink" target="_blank" href="../../../p03/ch06/modal_dialog.cpp">modal_dialog.cpp</a></small></p>
</section>
<section>
<h3>常用控件:QLabel 显示图片 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
QPixmap pixmap;
pixmap.load(":/Image/boat.jpg"); // 从 Qt 资源系统(.qrc)加载
QLabel* label = new QLabel(this);
label->setPixmap(pixmap);
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch08/label_pixmap.cpp">p03/ch08/label_pixmap.cpp</a></small></p>
</section>
<section>
<h3>事件机制:重写鼠标事件 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class EventLabel : public QLabel {
Q_OBJECT
public:
explicit EventLabel(QWidget* p=nullptr):QLabel(p){ setMouseTracking(true); }
protected:
void mouseMoveEvent(QMouseEvent* event) override {
setText(QString("Move: (%1, %2)").arg(event->x()).arg(event->y()));
}
};
</code></pre>
<p><small>重写 <code>xxxEvent</code> 是 Qt 事件处理的主线之一(另一条线是事件过滤器
<code>installEventFilter</code>)。出处:<a class="filelink" target="_blank"
href="../../../p03/ch09/mouse_event.cpp">p03/ch09/mouse_event.cpp</a></small></p>
</section>
<section>
<h3>绘图:QPainter <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
void paintEvent(QPaintEvent*) override {
QPainter painter(this);
painter.drawLine(QLine(10, 10, 200, 10));
painter.drawRect(QRect(10, 30, 100, 50));
painter.drawEllipse(QRect(150, 30, 100, 80));
}
</code></pre>
<p><small>所有绘制都发生在 <code>paintEvent</code> 里,由 Qt 在需要重绘时自动调用。
出处:<a class="filelink" target="_blank"
href="../../../p03/ch10/qpainter_basic.cpp">p03/ch10/qpainter_basic.cpp</a></small></p>
</section>
<section>
<h3>文件系统:QFile 读文本 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
QFile file("in.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "打开失败"; return -1; }
while (!file.atEnd()) { qDebug() << file.readLine(); }
QFileInfo info(file);
qDebug() << info.baseName() << info.suffix();
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch11/qfile_basic.cpp">p03/ch11/qfile_basic.cpp</a>;结构化读写见
<a class="filelink" target="_blank" href="../../../p03/ch11/qtextstream_io.cpp">qtextstream_io.cpp</a>
<a class="filelink" target="_blank" href="../../../p03/ch11/qdatastream_io.cpp">qdatastream_io.cpp</a></small></p>
</section>
</section>
<!-- ===================== 模块 B:练(分组三选一) ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 练习 95min(三选一,分组完成)</p>
<h2>动手:做一个可跑的小工具 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>方案 A · 图片查看器雏形 <span class="tag tag-practice"></span></h4>
<p>结合 <a class="filelink" target="_blank"
href="../../../p03/ch08/label_pixmap.cpp">label_pixmap.cpp</a> +
<a class="filelink" target="_blank" href="../../../p03/ch06/file_dialog.cpp">file_dialog.cpp</a>
用文件对话框选一张图片,加载到 <code>QLabel</code> 显示。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>方案 B · 简易绘图板 <span class="tag tag-practice"></span></h4>
<p>结合 <a class="filelink" target="_blank"
href="../../../p03/ch10/qpainter_basic.cpp">qpainter_basic.cpp</a> +
<a class="filelink" target="_blank" href="../../../p03/ch09/mouse_event.cpp">mouse_event.cpp</a>
监听鼠标拖动事件,记录轨迹点,在 <code>paintEvent</code> 里用 <code>QPainter</code> 画出轨迹。</p>
</div>
</section>
<section>
<div class="task-card">
<h4>方案 C · 文本文件小工具 <span class="tag tag-practice"></span></h4>
<p>结合 <a class="filelink" target="_blank"
href="../../../p03/ch11/qfile_basic.cpp">qfile_basic.cpp</a> +
<a class="filelink" target="_blank" href="../../../p03/ch11/qtextstream_io.cpp">qtextstream_io.cpp</a>
读一个文本文件,统计行数/字符数,把统计结果写回一个新文件。</p>
</div>
</section>
</section>
<!-- ===================== 收尾 ===================== -->
<section>
<h2>当日产出验证 · 实训总验收</h2>
<div class="verify-box">
<h4>现场演示</h4>
<pre><code class="bash" data-trim>QT_QPA_PLATFORM=offscreen tools/run_qt.sh &lt;target&gt;
# 有 X 环境:去掉 QT_QPA_PLATFORM=offscreen 直接看窗口</code></pre>
<p>每组演示自己选做的方案(A/B/C 三选一),教师验收「编译通过 + 交互符合预期」。</p>
</div>
<p class="footer-note"><small>4 天实训到此结束——大纲:
<a class="filelink" href="../OUTLINE.md">docs/teaching/OUTLINE.md</a></small></p>
<p><a class="filelink" href="index.html">返回目录</a></p>
</section>
</div>
</div>
<script src="reveal/reveal.js"></script>
<script src="reveal/plugin/highlight/highlight.js"></script>
<script>
Reveal.initialize({
hash: true, controls: true, progress: true, center: true, slideNumber: true,
plugins: [ RevealHighlight ]
});
</script>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Qt 方向实训 · 4 天教案幻灯片</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", sans-serif;
max-width: 760px; margin: 60px auto; color: #222; line-height: 1.6; }
h1 { font-size: 1.6em; }
ul { padding-left: 1.2em; }
li { margin: 14px 0; }
a.day { font-size: 1.1em; font-weight: 600; color: #1a4d8f; text-decoration: none; }
a.day:hover { text-decoration: underline; }
.desc { color: #555; font-size: 0.92em; }
code { background: #eef2f7; padding: 1px 6px; border-radius: 3px; }
</style>
</head>
<body>
<h1>川大 C++/LinuxC 课程「02. Qt 方向」—— 4 天实训幻灯片</h1>
<p class="desc">配套大纲:<code>docs/teaching/OUTLINE.md</code>
离线可用(reveal.js 已 vendor 进仓库);代码链接需通过本地 HTTP 服务打开才能正常跳转,
见仓库 <code>docs/teaching/OUTLINE.md</code> 「材料与播放方式」一节。</p>
<ul>
<li><a class="day" href="day1.html">Day 1 · C++ 对 C 的扩展 + 类和对象基础</a>
<div class="desc">p01/ch02、p01/ch03 强化精讲 → p01/ch04/s01s03 封装/构造析构</div></li>
<li><a class="day" href="day2.html">Day 2 · OOP 进阶</a>
<div class="desc">p01/ch04/s04s06 对象模型/友元/运算符重载 → p01/ch04/s07s08 继承/多态</div></li>
<li><a class="day" href="day3.html">Day 3 · 泛型与标准库</a>
<div class="desc">p01/ch05 模板 + p01/ch06 类型转换 → p01/ch07 异常 + p01/ch08 IO流 + p01/ch09 STL</div></li>
<li><a class="day" href="day4.html">Day 4 · Qt 桌面全景</a>
<div class="desc">p03/ch02ch05 项目/信号槽/主窗口 → p03/ch06、ch08ch11 对话框/控件/事件/绘图/文件</div></li>
</ul>
</body>
</html>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,71 @@
/*
Monokai style - ported by Luigi Maselli - http://grigio.org
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #272822;
color: #ddd;
}
.hljs-tag,
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-strong,
.hljs-name {
color: #f92672;
}
.hljs-code {
color: #66d9ef;
}
.hljs-class .hljs-title {
color: white;
}
.hljs-attribute,
.hljs-symbol,
.hljs-regexp,
.hljs-link {
color: #bf79db;
}
.hljs-string,
.hljs-bullet,
.hljs-subst,
.hljs-title,
.hljs-section,
.hljs-emphasis,
.hljs-type,
.hljs-built_in,
.hljs-builtin-name,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-addition,
.hljs-variable,
.hljs-template-tag,
.hljs-template-variable {
color: #a6e22e;
}
.hljs-comment,
.hljs-quote,
.hljs-deletion,
.hljs-meta {
color: #75715e;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-doctag,
.hljs-title,
.hljs-section,
.hljs-type,
.hljs-selector-id {
font-weight: bold;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+358
View File
@@ -0,0 +1,358 @@
/**
* White theme for reveal.js. This is the opposite of the 'black' theme.
*
* By Hakim El Hattab, http://hakim.se
*/
section.has-dark-background, section.has-dark-background h1, section.has-dark-background h2, section.has-dark-background h3, section.has-dark-background h4, section.has-dark-background h5, section.has-dark-background h6 {
color: #fff;
}
/*********************************************
* GLOBAL STYLES
*********************************************/
:root {
--r-background-color: #fff;
--r-main-font: Source Sans Pro, Helvetica, sans-serif;
--r-main-font-size: 42px;
--r-main-color: #222;
--r-block-margin: 20px;
--r-heading-margin: 0 0 20px 0;
--r-heading-font: Source Sans Pro, Helvetica, sans-serif;
--r-heading-color: #222;
--r-heading-line-height: 1.2;
--r-heading-letter-spacing: normal;
--r-heading-text-transform: uppercase;
--r-heading-text-shadow: none;
--r-heading-font-weight: 600;
--r-heading1-text-shadow: none;
--r-heading1-size: 2.5em;
--r-heading2-size: 1.6em;
--r-heading3-size: 1.3em;
--r-heading4-size: 1em;
--r-code-font: monospace;
--r-link-color: #2a76dd;
--r-link-color-dark: rgb(30.7720647773, 99.5566801619, 192.7779352227);
--r-link-color-hover: rgb(73.95, 138.55, 226.1);
--r-selection-background-color: rgb(95.25, 152.25, 229.5);
--r-selection-color: #fff;
--r-overlay-element-bg-color: 0, 0, 0;
--r-overlay-element-fg-color: 240, 240, 240;
}
.reveal-viewport {
background: #fff;
background-color: var(--r-background-color);
}
.reveal {
font-family: var(--r-main-font);
font-size: var(--r-main-font-size);
font-weight: normal;
color: var(--r-main-color);
}
.reveal ::selection {
color: var(--r-selection-color);
background: var(--r-selection-background-color);
text-shadow: none;
}
.reveal ::-moz-selection {
color: var(--r-selection-color);
background: var(--r-selection-background-color);
text-shadow: none;
}
.reveal .slides section,
.reveal .slides section > section {
line-height: 1.3;
font-weight: inherit;
}
/*********************************************
* HEADERS
*********************************************/
.reveal h1,
.reveal h2,
.reveal h3,
.reveal h4,
.reveal h5,
.reveal h6 {
margin: var(--r-heading-margin);
color: var(--r-heading-color);
font-family: var(--r-heading-font);
font-weight: var(--r-heading-font-weight);
line-height: var(--r-heading-line-height);
letter-spacing: var(--r-heading-letter-spacing);
text-transform: var(--r-heading-text-transform);
text-shadow: var(--r-heading-text-shadow);
word-wrap: break-word;
}
.reveal h1 {
font-size: var(--r-heading1-size);
}
.reveal h2 {
font-size: var(--r-heading2-size);
}
.reveal h3 {
font-size: var(--r-heading3-size);
}
.reveal h4 {
font-size: var(--r-heading4-size);
}
.reveal h1 {
text-shadow: var(--r-heading1-text-shadow);
}
/*********************************************
* OTHER
*********************************************/
.reveal p {
margin: var(--r-block-margin) 0;
line-height: 1.3;
}
/* Remove trailing margins after titles */
.reveal h1:last-child,
.reveal h2:last-child,
.reveal h3:last-child,
.reveal h4:last-child,
.reveal h5:last-child,
.reveal h6:last-child {
margin-bottom: 0;
}
/* Ensure certain elements are never larger than the slide itself */
.reveal img,
.reveal video,
.reveal iframe {
max-width: 95%;
max-height: 95%;
}
.reveal strong,
.reveal b {
font-weight: bold;
}
.reveal em {
font-style: italic;
}
.reveal ol,
.reveal dl,
.reveal ul {
display: inline-block;
text-align: left;
margin: 0 0 0 1em;
}
.reveal ol {
list-style-type: decimal;
}
.reveal ul {
list-style-type: disc;
}
.reveal ul ul {
list-style-type: square;
}
.reveal ul ul ul {
list-style-type: circle;
}
.reveal ul ul,
.reveal ul ol,
.reveal ol ol,
.reveal ol ul {
display: block;
margin-left: 40px;
}
.reveal dt {
font-weight: bold;
}
.reveal dd {
margin-left: 40px;
}
.reveal blockquote {
display: block;
position: relative;
width: 70%;
margin: var(--r-block-margin) auto;
padding: 5px;
font-style: italic;
background: rgba(255, 255, 255, 0.05);
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2);
}
.reveal blockquote p:first-child,
.reveal blockquote p:last-child {
display: inline-block;
}
.reveal q {
font-style: italic;
}
.reveal pre {
display: block;
position: relative;
width: 90%;
margin: var(--r-block-margin) auto;
text-align: left;
font-size: 0.55em;
font-family: var(--r-code-font);
line-height: 1.2em;
word-wrap: break-word;
box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.15);
}
.reveal code {
font-family: var(--r-code-font);
text-transform: none;
tab-size: 2;
}
.reveal pre code {
display: block;
padding: 5px;
overflow: auto;
max-height: 400px;
word-wrap: normal;
}
.reveal .code-wrapper {
white-space: normal;
}
.reveal .code-wrapper code {
white-space: pre;
}
.reveal table {
margin: auto;
border-collapse: collapse;
border-spacing: 0;
}
.reveal table th {
font-weight: bold;
}
.reveal table th,
.reveal table td {
text-align: left;
padding: 0.2em 0.5em 0.2em 0.5em;
border-bottom: 1px solid;
}
.reveal table th[align=center],
.reveal table td[align=center] {
text-align: center;
}
.reveal table th[align=right],
.reveal table td[align=right] {
text-align: right;
}
.reveal table tbody tr:last-child th,
.reveal table tbody tr:last-child td {
border-bottom: none;
}
.reveal sup {
vertical-align: super;
font-size: smaller;
}
.reveal sub {
vertical-align: sub;
font-size: smaller;
}
.reveal small {
display: inline-block;
font-size: 0.6em;
line-height: 1.2em;
vertical-align: top;
}
.reveal small * {
vertical-align: top;
}
.reveal img {
margin: var(--r-block-margin) 0;
}
/*********************************************
* LINKS
*********************************************/
.reveal a {
color: var(--r-link-color);
text-decoration: none;
transition: color 0.15s ease;
}
.reveal a:hover {
color: var(--r-link-color-hover);
text-shadow: none;
border: none;
}
.reveal .roll span:after {
color: #fff;
background: var(--r-link-color-dark);
}
/*********************************************
* Frame helper
*********************************************/
.reveal .r-frame {
border: 4px solid var(--r-main-color);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
}
.reveal a .r-frame {
transition: all 0.15s linear;
}
.reveal a:hover .r-frame {
border-color: var(--r-link-color);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.55);
}
/*********************************************
* NAVIGATION CONTROLS
*********************************************/
.reveal .controls {
color: var(--r-link-color);
}
/*********************************************
* PROGRESS BAR
*********************************************/
.reveal .progress {
background: rgba(0, 0, 0, 0.2);
color: var(--r-link-color);
}
/*********************************************
* PRINT BACKGROUND
*********************************************/
@media print {
.backgrounds {
background-color: var(--r-background-color);
}
}