教学示例补 5 个:3 个坑点复现 + QTableWidget + Day5 学员管理器参考实现

- pitfall_stack_widget / pitfall_lambda_capture / pitfall_double_connect:
  默认跑正确写法(离屏验证退出码 0),--crash / --dangle 开关课堂现场触发
  真实崩溃/悬空访问
- table_widget:QTableWidget 单元格事件最小示例(新安排 Day4「表格事件」)
- student_manager:Day5 综合案例参考实现,离屏自测覆盖 JSON round-trip
  与 QThread 后台导入闭环
- 均接入 examples/CMakeLists.txt 的 add_teaching_example,手工维护区不影响生成器
This commit is contained in:
张宗平
2026-07-05 12:49:20 +08:00
parent 6640baffae
commit 6d1895d696
11 changed files with 798 additions and 1 deletions
@@ -0,0 +1,40 @@
# 坑点复现:lambda 槽的生命周期与 connect 第五参数 —— 教学补充示例
> 对应教案 [`OUTLINE_4DAY.md`](../../OUTLINE_4DAY.md) Day2 下午 ★坑卡
> 「lambda 槽的生命周期与第五参数」。手工维护,非生成产物。
## 概念
`connect` 的第五参数(context object)决定这条连接**跟谁的生命周期挂钩**:
- `connect(sender, signal, lambda)`——连接只跟 sender 挂钩,**接收方对象销毁后
lambda 仍会被调用**,捕获的指针就是悬空指针;
- `connect(sender, signal, receiver, lambda)`——receiver 析构时 Qt 自动断开
连接,lambda 不可能再访问已销毁的对象。
规则:**lambda 里捕获了某个 QObject,就把它作为第五参数传进去**。
## 运行结果解读
默认运行打印两次 emit 的对比:delete receiver 之后,只有「无 context」的连接
还在触发。`--dangle` 让这条 lambda 真的去访问已销毁对象——你可能看到程序崩溃,
也可能看到它**打印出看似正常的旧值**(刚释放的内存还没被复用)。后者更危险:
UB 在开发机上「看起来正常」,到了演示/交付现场才炸。
## 验证与演示方式
```bash
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_pitfall_lambda_capture # 确定性对比输出,退出码 0
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_pitfall_lambda_capture --dangle # 课堂演示:真实悬空访问(UB
```
## 练习方向(供任务卡引用)
- 把连接 A 的 lambda 改成按引用捕获一个局部 `QString`,在函数返回后触发信号,
分析这是哪一种悬空(对象悬空 vs 捕获变量悬空);
-`QObject::destroyed` 信号打印 receiver 的销毁时机,确认连接 B 断开发生在何时。
## 参考
- 本仓库对照:[`p03/ch04/lambda_signal_slot.cpp`](../../../../p03/ch04/lambda_signal_slot.cpp)lambda 槽基本写法)
- Qt 官方文档:*Differences between String-Based and Functor-Based Connections*(选读·需联网)
@@ -0,0 +1,70 @@
// ============================================================================
// qt_pitfall_lambda_capture —— 坑点最小复现:lambda 槽的生命周期与第五参数
// 对应教案:OUTLINE_4DAY.md Day2 下午 ★坑卡「lambda 槽的生命周期与第五参数」
// 配套文档:docs/teaching/examples/pitfall_lambda_capture/README.md
//
// 默认运行:对比两条连接——不带 context 的 lambda 在接收者销毁后**仍会被调用**,
// 带 contextconnect 第五参数)的连接随接收者销毁自动断开。退出码 0。
// 加 --dangle:不带 context 的 lambda 里真的去访问已销毁的接收者(悬空指针,
// UB,课堂演示专用——可能打印垃圾值、也可能直接崩,两种结果都是教学素材)。
// ============================================================================
#include <QCoreApplication>
#include <QDebug>
#include <QObject>
#include <cstring>
class Sender : public QObject {
Q_OBJECT
public:
using QObject::QObject;
signals:
void ping();
};
class Receiver : public QObject {
Q_OBJECT
public:
explicit Receiver(QObject* parent = nullptr)
: QObject(parent), m_name(QStringLiteral("接收者甲")) {}
QString name() const { return m_name; }
private:
QString m_name;
};
int main(int argc, char* argv[]) {
QCoreApplication app(argc, argv);
const bool dangle = (argc > 1 && std::strcmp(argv[1], "--dangle") == 0);
Sender sender;
auto* receiver = new Receiver;
// 连接 A(错误示范):lambda 捕获了 receiver 指针,但没有传第五参数。
// 这条连接的生存期只跟 sender 挂钩——receiver 死了它也不会断开。
QObject::connect(&sender, &Sender::ping, [receiver, dangle]() {
qInfo() << "[无 context] lambda 被调用";
if (dangle) {
// 悬空访问:第二次触发时 receiver 已被 delete —— UB
qInfo() << " 访问 receiver->name() =" << receiver->name();
}
});
// 连接 B(正确写法):第五参数传 receiver 作为 context object
// receiver 析构时 Qt 自动断开这条连接,lambda 不可能再被调用。
QObject::connect(&sender, &Sender::ping, receiver, [receiver]() {
qInfo() << "[带 context] lambda 被调用,receiver->name() =" << receiver->name();
});
qInfo() << "---- 第一次 emitreceiver 活着,两条连接都触发)----";
emit sender.ping();
delete receiver;
qInfo() << "---- delete receiver 之后,第二次 emit ----";
emit sender.ping(); // 只有连接 A 还会触发——这就是悬空风险所在
qInfo() << "结论:lambda 槽捕获了对象指针,就必须把该对象作为 connect 的"
"第五参数(context),让连接的生命周期跟着对象走。";
return 0;
}
#include "qt_pitfall_lambda_capture.moc"