教学示例补 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:
@@ -0,0 +1,45 @@
|
||||
# 坑点复现:栈对象与父子所有权打架(双重释放)—— 教学补充示例
|
||||
|
||||
> 对应教案 [`OUTLINE_4DAY.md`](../../OUTLINE_4DAY.md) Day2 上午 ★坑卡
|
||||
> 「栈对象与父子所有权打架」。手工维护,非 `tools/gen_part3.py` 生成产物。
|
||||
|
||||
## 概念
|
||||
|
||||
Qt 的父子树是一套**所有权**机制:parent 析构时会 `delete` 它的所有子对象。
|
||||
这隐含一个硬性前提——**交给父子树管的对象必须建在堆上**。一个对象只能有一个
|
||||
所有者:给了 parent,就不能再由栈作用域、智能指针或手动 `delete` 管理。
|
||||
|
||||
## 错在哪(`--crash` 演示的代码)
|
||||
|
||||
```cpp
|
||||
QPushButton btn("我建在栈上"); // 先声明 → 后析构
|
||||
QWidget window; // 后声明 → 先析构
|
||||
btn.setParent(&window);
|
||||
// 离开作用域:window 先析构 → 父子树 delete &btn(栈地址!)→
|
||||
// glibc 报 "free(): invalid pointer" 后 abort
|
||||
```
|
||||
|
||||
注意声明顺序反过来(parent 先声明)时这段代码**恰好不崩**——`btn` 先析构时会把
|
||||
自己从 parent 的子对象列表里摘除。「有时不崩」正是这类所有权错误难查的原因:
|
||||
不要依赖析构顺序的运气,规则只有一条——**给了 parent 就必须在堆上**。
|
||||
|
||||
## 验证与演示方式
|
||||
|
||||
```bash
|
||||
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_pitfall_stack_widget # 正确写法,退出码 0
|
||||
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_pitfall_stack_widget --crash # 课堂演示:现场崩给学员看
|
||||
```
|
||||
|
||||
`--crash` 必然非正常退出(core dump),**不要**放进自动化验证。
|
||||
|
||||
## 练习方向(供任务卡引用)
|
||||
|
||||
- 把错误版本的两行声明顺序对调,观察「恰好不崩」,再解释为什么它仍然是错的;
|
||||
- 同一误区的第三次出现:非模态对话框建在栈上一闪而过,见
|
||||
[`p03/ch06/modeless_dialog_stack.cpp`](../../../../p03/ch06/modeless_dialog_stack.cpp)
|
||||
与堆版/`WA_DeleteOnClose` 版的三方对照。
|
||||
|
||||
## 参考
|
||||
|
||||
- 本仓库对照:[`p03/ch03/button_creation.cpp`](../../../../p03/ch03/button_creation.cpp)(正确的 parent 用法)
|
||||
- 前置概念:Day1 ★坑卡「所有权心智模型:谁负责 delete」
|
||||
@@ -0,0 +1,62 @@
|
||||
// ============================================================================
|
||||
// qt_pitfall_stack_widget —— 坑点最小复现:栈对象与父子所有权打架(双重释放)
|
||||
// 对应教案:OUTLINE_4DAY.md Day2 上午 ★坑卡「栈对象与父子所有权打架」
|
||||
// 配套文档:docs/teaching/examples/pitfall_stack_widget/README.md
|
||||
//
|
||||
// 默认运行:正确写法(子控件建在堆上、交给 parent 管),离屏验证退出码 0。
|
||||
// 加 --crash:错误写法(带 parent 的控件建在栈上),亲眼看双重释放崩溃——
|
||||
// 课堂演示专用,退出码必然非 0,不要用于自动化验证。
|
||||
// ============================================================================
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
#include <cstring>
|
||||
|
||||
// 错误写法:child 声明在 parent 之前 → 作用域结束时 parent(后声明)先析构,
|
||||
// 父子树机制会 delete 它的所有子对象——包括这个根本不在堆上的 btn(UB,典型
|
||||
// 表现是 glibc 报 "free(): invalid pointer" 后 abort);随后 btn 自己的栈析构
|
||||
// 又会跑一遍。一个对象只能有一个所有者:交给了 Qt 父子树,就不能再由栈管理。
|
||||
static void wrongVersion() {
|
||||
QPushButton btn(QStringLiteral("我建在栈上")); // 先声明 → 后析构
|
||||
QWidget window; // 后声明 → 先析构
|
||||
btn.setParent(&window);
|
||||
// 离开作用域:window 析构 → delete &btn(栈地址!)→ 崩溃
|
||||
}
|
||||
|
||||
// 正确写法:子控件一律 new 在堆上、把 parent 传进去(或加进布局,布局会代为
|
||||
// 设置 parent),此后它的生命周期完全由父子树负责,不写任何 delete。
|
||||
static QWidget* correctVersion() {
|
||||
auto* window = new QWidget;
|
||||
auto* layout = new QVBoxLayout(window);
|
||||
layout->addWidget(new QLabel(QStringLiteral("子控件都在堆上,由父子树负责释放"), window));
|
||||
layout->addWidget(new QPushButton(QStringLiteral("确定"), window));
|
||||
return window; // window 析构时自动 delete 上面所有子对象
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
|
||||
if (argc > 1 && std::strcmp(argv[1], "--crash") == 0) {
|
||||
qInfo() << "演示错误写法:栈上控件交给父子树管……";
|
||||
wrongVersion(); // 走不到下一行就崩
|
||||
qInfo() << "(如果你看到这行,说明该平台的 UB 恰好没炸——它仍然是错的)";
|
||||
return 0;
|
||||
}
|
||||
|
||||
QWidget* window = correctVersion();
|
||||
window->setWindowTitle(QStringLiteral("正确写法:堆 + parent"));
|
||||
window->resize(320, 120);
|
||||
window->show();
|
||||
|
||||
if (qgetenv("QT_QPA_PLATFORM") == "offscreen") {
|
||||
QTimer::singleShot(200, &app, &QApplication::quit);
|
||||
}
|
||||
int rc = app.exec();
|
||||
delete window; // 顶层窗口没有 parent,是唯一需要我们自己负责的对象
|
||||
return rc;
|
||||
}
|
||||
Reference in New Issue
Block a user