QObject 生命周期与线程亲和性
1. Parent 是所有权,不是分类关系
parent 析构时删除 children;child 提前析构会从 parent 列表移除。它不追踪任意引用、不处理无 parent 对象和一般引用环,因此不是 GC。
栈上 child 若 parent 比它晚析构会有风险,通常让栈 QObject 无 parent,或保证构造/析构顺序正确。不要让 parent 树和独立 smart pointer 同时认为自己负责 delete。
2. QPointer
QPointer<T> 是 guarded pointer:QObject 销毁时自动变 null。它不拥有对象,不让对象存活,也不使“检查后使用”跨线程原子安全。
QPointer<Panel> panel = ...;
if (panel) panel->refresh();这适合 GUI 单线程事件间观察;跨线程仍应将调用排队到对象线程,并通过上下文/状态机管理生命周期。
3. deleteLater
deleteLater() 发布 deferred delete 事件,实际删除依赖对象线程的事件循环和 Qt 对线程结束的处理语义。它不是“任何时候跨线程 delete 都自动安全”的口令。
request deleteLater
-> event queued to affinity thread
-> current nested/event handling unwinds
-> event loop processes deferred delete
-> QObject destructor事件循环已停止或对象线程关闭顺序错误时,清理可能延迟/遗漏。worker-object 常连接 QThread::finished 到 worker deleteLater,但仍需核对 quit、事件投递和外部所有者顺序。
4. 线程亲和性
QObject 创建时属于当前线程。moveToThread 改变对象及其 children 的 affinity,要求对象无 parent。QThread 对象本身通常活在创建它的线程,不等于 run() 执行线程。
queued event/slot 在 affinity 线程事件循环处理。若该线程没有运行事件循环,queued 调用不会按预期执行。
5. Worker-object 关闭
owner requests cancel
-> queued stop / atomic cancellation state
-> worker exits operation, emits finished
-> thread.quit
-> worker deferred delete
-> thread finishes
-> owner wait / deletes thread object阻塞库调用必须有独立取消/超时,单纯 queued stop() 可能因 worker 正忙且事件循环不处理而永远不到达。
6. 非 QObject 资源
parent tree 只删除 QObject。文件、GPU buffer、VTK pipeline、线程句柄和标准容器仍应用 RAII/明确 owner。QObject 析构在哪个线程发生会影响这些成员的析构要求。
7. 面试速答
QObject parent 与 shared_ptr 可混用吗? 可以在非常明确的非重复所有权设计中出现,但不要让二者独立 delete 同一对象;通常择一所有者模型。
QPointer 是否线程安全弱引用? 不是通用同步方案,它是 QObject 销毁感知观察指针。
moveToThread 后成员函数都会自动在新线程执行吗? 不会,普通直接调用仍在调用线程;queued signal/event 才按 affinity 调度。
8. 自测答案
对象有 parent 能否 moveToThread?不能直接移动,需先重新设计/移除 parent。deleteLater 后能否继续解引用?不能把对象视作稳定;调用已请求销毁,应停止发布新工作并用 QPointer/context 防旧回调。
QObject 树的析构顺序
Window(parent)
├── Toolbar(child)
└── Panel(child)
└── Button(grandchild)
delete Window -> children recursively deleted子对象也可早于 parent 手工删除,QObject 会从父 children 列表移除。不要同时让独立 unique_ptr 与 parent 都认为自己负责删除同一对象,除非自定义 deleter/释放协议明确避免双删。
栈对象 parent 顺序陷阱
若先构造 child 栈对象、后构造 parent,再把 child 设 parent,离开作用域时 parent 先析构并 delete 栈 child,随后 child 还会自动析构,造成错误。栈 QObject 应依靠作用域逆序,不把较早构造的栈对象交给较晚构造 parent 删除。
deleteLater 的事件语义
call deleteLater
-> post DeferredDelete event to object's thread
-> control returns
-> event loop reaches safe point
-> QObject destructor runs调用后对象并非立刻死亡,后续直接访问仍可能逻辑错误。若目标线程事件循环已停止,DeferredDelete 可能无法按预期处理;关闭流程要在 quit 前安排销毁或显式控制。
QPointer 与 weak_ptr 的差异
QPointer 是 QObject 的非拥有受保护指针,对象析构时自动变 null;它不保活对象,也不能阻止“检查非空后另一线程删除”的竞态。QObject 通常要求亲和线程操作,跨线程应通过 queued 消息而不是拿 QPointer 当并发共享指针。
Thread affinity 决定事件在哪里处理
QObject 默认属于创建线程,thread() 可查询。queued signal、posted event 和 timer 在亲和线程事件循环处理:
sender any thread -> post event -> receiver affinity thread loop -> slot/event直接从其他线程调用普通成员函数不会自动排队;只有 signal/invokeMethod 等明确 queued 机制才切换执行上下文。
moveToThread 的前置条件
对象有 parent 时不能单独移动;移动 parent 会让 children 作为树一起保持一致。对象的 active timer、socket notifier 和正在处理事件使迁移更复杂,应在未启动/静止状态配置 affinity。
推荐结构:
GUI creates QThread object (lives GUI thread)
GUI creates Worker(no parent)
Worker moveToThread(thread)
thread.started -> Worker::start (queued)
Worker::finished -> thread.quit
thread.finished -> Worker::deleteLater
GUI waits thread on shutdownQThread 子类的常见误解
QThread 实例的 slots 默认在对象亲和线程(常为 GUI)运行,只有 run() 函数体在新线程。把 worker 状态作为 QThread 子类成员后从 GUI slot 与 run 同时访问,仍需同步。若只是工作对象,worker-object 模式更清楚。
事件循环与嵌套循环
模态对话框、同步等待可能启动嵌套 event loop,使原本以为“函数返回前不会发生”的 deleteLater、signal 和输入重入。业务状态修改应能承受事件重入,避免用 processEvents() 修复卡顿。
生命周期关闭图
stop new commands
-> queued requestStop to worker
-> worker cancels bounded operation, emits finished
-> thread quit
-> wait/join
-> delete worker/thread/controller不要在线程仍运行时销毁 QThread,也不要在 GUI 析构后让 worker 信号回调已死 UI。
连续追问与自测
问:QObject parent 与 C++ 继承有关系吗? 答:没有,它是运行时对象树所有权关系。
问:moveToThread 后成员调用自动去新线程吗? 答:不会,普通直接调用仍在调用线程执行;queued 事件/信号才由亲和线程处理。
- QPointer 是否拥有对象? 答:不拥有,只在 QObject 销毁时清空。
- deleteLater 何时删除? 答:对象亲和线程事件循环处理 DeferredDelete 时。
- 为什么 shutdown 要 wait QThread? 答:确保 worker 不再访问即将销毁的状态与回调目标。
工程深化:把本篇知识落到项目里
这一节不是为了凑篇幅,而是把《QObject 生命周期与线程亲和性》从“知道概念”推进到“能在项目里稳定使用”。 阅读时可以把每个知识点都追问成四件事:它保护什么边界,失败时有什么现象,怎样最小复现,怎样写进团队流程。
concept -> boundary -> failure signal -> minimal proof -> project rule工程切片 1:最小可复现样例
**场景。**围绕 parent/child 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。
**边界。**先判断这里讨论的是 parent/child、deleteLater 还是 线程亲和性 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> parent/child -> deleteLater -> 线程亲和性 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 2:接口边界
**场景。**围绕 deleteLater 建一个很小的案例,不要一开始就放进完整业务系统。写清调用方能依赖什么、不能依赖什么,并把隐式假设转换成命名函数、配置项或测试。
**边界。**先判断这里讨论的是 deleteLater、线程亲和性 还是 事件循环 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> deleteLater -> 线程亲和性 -> 事件循环 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 3:失败注入
**场景。**围绕 线程亲和性 建一个很小的案例,不要一开始就放进完整业务系统。故意制造一个常见错误,让日志、断言或测试先失败,再用修复后的证据说明规则生效。
**边界。**先判断这里讨论的是 线程亲和性、事件循环 还是 悬空指针 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 线程亲和性 -> 事件循环 -> 悬空指针 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 4:跨平台差异
**场景。**围绕 事件循环 建一个很小的案例,不要一开始就放进完整业务系统。至少比较 Windows、Linux 或不同编译器下的一个差异,记录差异属于标准、实现还是平台约定。
**边界。**先判断这里讨论的是 事件循环、悬空指针 还是 生命周期日志 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 事件循环 -> 悬空指针 -> 生命周期日志 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 5:性能观察
**场景。**围绕 悬空指针 建一个很小的案例,不要一开始就放进完整业务系统。用小规模和压力规模各跑一次,区分算法成本、同步成本、I/O 成本和工具链配置成本。
**边界。**先判断这里讨论的是 悬空指针、生命周期日志 还是 parent/child 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 悬空指针 -> 生命周期日志 -> parent/child -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 6:生命周期
**场景。**围绕 生命周期日志 建一个很小的案例,不要一开始就放进完整业务系统。标出资源创建、转移、共享、停止和销毁的顺序,特别关注错误返回和提前退出路径。
**边界。**先判断这里讨论的是 生命周期日志、parent/child 还是 deleteLater 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 生命周期日志 -> parent/child -> deleteLater -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 7:诊断证据
**场景。**围绕 parent/child 建一个很小的案例,不要一开始就放进完整业务系统。保留命令、日志、符号、栈、测试输出或截图,让结论可以被另一个环境重新验证。
**边界。**先判断这里讨论的是 parent/child、deleteLater 还是 线程亲和性 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> parent/child -> deleteLater -> 线程亲和性 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 8:维护策略
**场景。**围绕 deleteLater 建一个很小的案例,不要一开始就放进完整业务系统。把一次性经验沉淀到 README、CI、脚本、示例工程或检查表里,避免只存在个人记忆中。
**边界。**先判断这里讨论的是 deleteLater、线程亲和性 还是 事件循环 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> deleteLater -> 线程亲和性 -> 事件循环 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 9:版本演进
**场景。**围绕 线程亲和性 建一个很小的案例,不要一开始就放进完整业务系统。为未来变更预留兼容策略:版本号、特性位、弃用窗口、迁移脚本和回滚入口。
**边界。**先判断这里讨论的是 线程亲和性、事件循环 还是 悬空指针 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 线程亲和性 -> 事件循环 -> 悬空指针 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 10:最小消费者
**场景。**围绕 事件循环 建一个很小的案例,不要一开始就放进完整业务系统。从使用者角度写一个小程序或小工程,只依赖公开接口,验证安装包、头文件和运行时行为。
**边界。**先判断这里讨论的是 事件循环、悬空指针 还是 生命周期日志 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 事件循环 -> 悬空指针 -> 生命周期日志 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 11:边界复盘
**场景。**围绕 悬空指针 建一个很小的案例,不要一开始就放进完整业务系统。用一张表说明问题发生在源码、编译、链接、加载、运行、关闭或发布中的哪一层。
**边界。**先判断这里讨论的是 悬空指针、生命周期日志 还是 parent/child 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 悬空指针 -> 生命周期日志 -> parent/child -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 12:反例训练
**场景。**围绕 生命周期日志 建一个很小的案例,不要一开始就放进完整业务系统。写出一个看似能工作但不可维护的方案,再说明它在规模、团队协作或发布时会怎样失败。
**边界。**先判断这里讨论的是 生命周期日志、parent/child 还是 deleteLater 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 生命周期日志 -> parent/child -> deleteLater -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 13:最小可复现样例
**场景。**围绕 parent/child 建一个很小的案例,不要一开始就放进完整业务系统。把问题压缩到一个可以提交给同事的目录,保留源码、构建命令、版本输出和预期现象。
**边界。**先判断这里讨论的是 parent/child、deleteLater 还是 线程亲和性 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> parent/child -> deleteLater -> 线程亲和性 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 14:接口边界
**场景。**围绕 deleteLater 建一个很小的案例,不要一开始就放进完整业务系统。写清调用方能依赖什么、不能依赖什么,并把隐式假设转换成命名函数、配置项或测试。
**边界。**先判断这里讨论的是 deleteLater、线程亲和性 还是 事件循环 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> deleteLater -> 线程亲和性 -> 事件循环 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 15:失败注入
**场景。**围绕 线程亲和性 建一个很小的案例,不要一开始就放进完整业务系统。故意制造一个常见错误,让日志、断言或测试先失败,再用修复后的证据说明规则生效。
**边界。**先判断这里讨论的是 线程亲和性、事件循环 还是 悬空指针 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 线程亲和性 -> 事件循环 -> 悬空指针 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 16:跨平台差异
**场景。**围绕 事件循环 建一个很小的案例,不要一开始就放进完整业务系统。至少比较 Windows、Linux 或不同编译器下的一个差异,记录差异属于标准、实现还是平台约定。
**边界。**先判断这里讨论的是 事件循环、悬空指针 还是 生命周期日志 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 事件循环 -> 悬空指针 -> 生命周期日志 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 17:性能观察
**场景。**围绕 悬空指针 建一个很小的案例,不要一开始就放进完整业务系统。用小规模和压力规模各跑一次,区分算法成本、同步成本、I/O 成本和工具链配置成本。
**边界。**先判断这里讨论的是 悬空指针、生命周期日志 还是 parent/child 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 悬空指针 -> 生命周期日志 -> parent/child -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 18:生命周期
**场景。**围绕 生命周期日志 建一个很小的案例,不要一开始就放进完整业务系统。标出资源创建、转移、共享、停止和销毁的顺序,特别关注错误返回和提前退出路径。
**边界。**先判断这里讨论的是 生命周期日志、parent/child 还是 deleteLater 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 生命周期日志 -> parent/child -> deleteLater -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 19:诊断证据
**场景。**围绕 parent/child 建一个很小的案例,不要一开始就放进完整业务系统。保留命令、日志、符号、栈、测试输出或截图,让结论可以被另一个环境重新验证。
**边界。**先判断这里讨论的是 parent/child、deleteLater 还是 线程亲和性 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> parent/child -> deleteLater -> 线程亲和性 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 20:维护策略
**场景。**围绕 deleteLater 建一个很小的案例,不要一开始就放进完整业务系统。把一次性经验沉淀到 README、CI、脚本、示例工程或检查表里,避免只存在个人记忆中。
**边界。**先判断这里讨论的是 deleteLater、线程亲和性 还是 事件循环 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> deleteLater -> 线程亲和性 -> 事件循环 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 21:版本演进
**场景。**围绕 线程亲和性 建一个很小的案例,不要一开始就放进完整业务系统。为未来变更预留兼容策略:版本号、特性位、弃用窗口、迁移脚本和回滚入口。
**边界。**先判断这里讨论的是 线程亲和性、事件循环 还是 悬空指针 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 线程亲和性 -> 事件循环 -> 悬空指针 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 22:最小消费者
**场景。**围绕 事件循环 建一个很小的案例,不要一开始就放进完整业务系统。从使用者角度写一个小程序或小工程,只依赖公开接口,验证安装包、头文件和运行时行为。
**边界。**先判断这里讨论的是 事件循环、悬空指针 还是 生命周期日志 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 事件循环 -> 悬空指针 -> 生命周期日志 -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
工程切片 23:边界复盘
**场景。**围绕 悬空指针 建一个很小的案例,不要一开始就放进完整业务系统。用一张表说明问题发生在源码、编译、链接、加载、运行、关闭或发布中的哪一层。
**边界。**先判断这里讨论的是 悬空指针、生命周期日志 还是 parent/child 的责任;如果三者混在一起,先把输入、输出和所有权拆开。
input -> 悬空指针 -> 生命周期日志 -> parent/child -> observable result**验证。**记录一条能重复运行的命令或测试,并写明成功输出和失败输出。只说“本机能跑”不够,至少要记录工具版本、构建类型和关键参数。
**常见错误。**把偶然通过当成规则、把私有实现当成公开契约、或者让错误路径绕开清理逻辑,都会让本篇主题在真实项目里变得不可维护。
**复盘问题。**如果明天换一个编译器、Qt 版本、插件版本、输入规模或发布目录,这个结论是否仍成立?不成立时,应该由文档、测试还是 CI 给出提示?
本篇收束
掌握《QObject 生命周期与线程亲和性》的标志,不是记住所有名词,而是能把 parent/child、deleteLater、线程亲和性、事件循环、悬空指针、生命周期日志 放进一条可验证的工程链路。 先用最小样例建立判断,再用测试和脚本固定判断,最后把失败证据留给未来的自己和团队。