目标读者:有一定 C++/Qt 基础,希望在实际项目中写出"更稳、更好维护"的 Qt 程序的工程师
开发环境示例:Qt 5.12+/Qt 5.15 + Qt Creator
目录
[一、问题背景:从"会用 connect"到"敢在项目里用"](#一、问题背景:从“会用 connect”到“敢在项目里用”)
[1. Q_OBJECT 与元对象系统:信号和槽的"身份证"](#1. Q_OBJECT 与元对象系统:信号和槽的“身份证”)
[2. 旧语法 vs 新语法:为什么不再推荐 SIGNAL()/SLOT()](#2. 旧语法 vs 新语法:为什么不再推荐 SIGNAL()/SLOT())
[3. 连接类型:Auto / Direct / Queued / BlockingQueued](#3. 连接类型:Auto / Direct / Queued / BlockingQueued)
[4. 信号、槽和事件循环:QueuedConnection 是怎么跑的?](#4. 信号、槽和事件循环:QueuedConnection 是怎么跑的?)
[5. lambda 槽:好用,但别忘了生命周期](#5. lambda 槽:好用,但别忘了生命周期)
[1. 工程结构](#1. 工程结构)
[2. 任务工作类:TaskWorker](#2. 任务工作类:TaskWorker)
[3. 主窗口:MainWindow](#3. 主窗口:MainWindow)
[1. 在子线程里直接操作 UI](#1. 在子线程里直接操作 UI)
[2. 信号参数是引用/指针,生命周期不受控](#2. 信号参数是引用/指针,生命周期不受控)
[3. 高频信号 + UI 刷新,导致界面严重延迟](#3. 高频信号 + UI 刷新,导致界面严重延迟)
[4. lambda 捕获 this 导致"幽灵回调"](#4. lambda 捕获 this 导致“幽灵回调”)
[5. BlockingQueuedConnection 用不好就是死锁制造机](#5. BlockingQueuedConnection 用不好就是死锁制造机)
一、问题背景:从"会用 connect"到"敢在项目里用"
接触 Qt 的第一天,大多数人都是从一个按钮开始的:
connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);
按钮点一下,槽函数被调用一下,窗口弹个对话框,一切看上去都很自然。于是我们开始在项目里到处写 connect,信号和槽越连越多,最后难免出现下面几类问题:
-
信号槽满天飞,谁调了谁完全看不清
一个中型桌面项目,窗口几十个、对话框十几个,每个窗口上十几个控件。随着需求迭代,"临时加一个信号"、"先在这里 connect 一下"的写法越来越多,半年之后就连自己都不记得当初是怎么连的了。
尤其是那种"从某个底层模块发信号,一路传到 UI 层"的设计,如果没有严格的命名约定和文档,排查 Bug 时常常要一路
Ctrl+点击跟着槽函数跳半天。 -
跨线程更新 UI 偶发崩溃
最典型的场景:
- 为了防止主线程卡顿,你把耗时操作放到了
QThread里; - 任务线程里处理数据,然后直接去改
QLabel的文本、给QTableWidget塞数据; - 在你本机测试没问题,上线到客户现场,一天之内崩溃两次,而且还是完全随机。
这类问题追踪起来非常痛苦:
- 有时是野指针;
- 有时是竞争条件;
- 有时是主线程对象已经析构了,子线程还在发信号。
- 为了防止主线程卡顿,你把耗时操作放到了
-
lambda 写得很爽,几个月后变成雷区
自从 Qt5 支持用 lambda 当槽函数,大部分人都爱上了这种写法,代码短了、文件也少了,看起来非常"现代 C++"。
但 lambda 有个很大的隐患:捕获变量的生命周期。
- 捕获
this,却没有考虑窗口何时关闭; - 捕获局部变量引用,却让信号延后触发;
- 在 lambda 里对外部对象"顺手一写"各种逻辑,却完全没有测试它在对象析构之后会发生什么。
- 捕获
-
高频信号导致 UI 卡顿,但 CPU 占用不算太高
还有一种常被忽略的问题:
- 你写了一个"实时日志监控"、"串口数据监视器"、"行情报价面板";
- 每次收到数据就发一条信号,UI 层在槽函数里刷新界面;
- 结果 UI 慢慢开始有明显延迟------明明数据刚到,界面却要过一两秒才刷新完。
非技术人员常常会说"你们这个程序真卡",但用分析器一看,CPU 使用率也不算夸张,只是事件队列里堆了一堆待处理的刷新操作。
这些问题的共同本质其实只有一个:我们只停留在"知道怎么写 connect",而没有真正理解"信号槽背后发生了什么"。
接下来这一期,我想结合一个完整的 Qt Creator 工程,把信号槽从语法、运行时调度、线程模型几个角度掰开讲清楚。文章会分五个部分:
- 问题背景(你正在看的这部分)
- 核心知识点:语法、元对象、连接类型、事件循环、lambda
- 代码实战:一个"多线程任务监控小工具"
- 实战中的坑与优化:我在项目中真实踩过的坑
- 小结:给出几条可以直接落地执行的实践守则
二、核心知识点:信号槽到底是怎么跑起来的?
1. Q_OBJECT 与元对象系统:信号和槽的"身份证"
所有涉及信号槽的类(包括你自己定义的类),只要是继承自 QObject,基本都会在类定义里加一个 Q_OBJECT 宏:
cpp
class TaskWorker : public QObject
{
Q_OBJECT
public:
explicit TaskWorker(QObject *parent = nullptr);
// ...
signals:
void progressUpdated(int value);
};
这一个宏背后,做了两件非常关键的事:
- 告诉 moc(元对象编译器):这个类需要生成元对象信息;
- 在编译阶段生成对应的
moc_xxx.cpp文件,里面包含了:- 类的元数据:类名、继承关系;
- 信号、槽函数的列表;
- 用于运行时调用的方法入口。
可以简单粗暴地理解为:
有 Q_OBJECT 的 QObject 子类,Qt 能在运行时"知道它有哪些信号和槽",并通过统一的调用入口来调度这些槽函数。
如果你忘了写 Q_OBJECT,通常会遇到这些症状:
- 信号发不出去(
connect成功返回,但运行时没有任何反应); QMetaObject::invokeMethod调不动对应的方法;- 运行时类型信息不完整。
所以在工程里,只要这个类需要:
- 发信号;
- 响应信号(做槽函数);
- 利用
Q_PROPERTY暴露属性; - 被放进 QML / 属性系统 / 动态调用中;
就老老实实写上 Q_OBJECT,这是成本很低的一件事。
2. 旧语法 vs 新语法:为什么不再推荐 SIGNAL()/SLOT()
Qt4 时代,我们基本都是这么写 connect 的:
cpp
connect(sender, SIGNAL(valueChanged(int)), receiver, SLOT(onValueChanged(int)));
这种写法的最大问题是:编译器看不到真正的函数签名 。
SIGNAL/SLOT 宏会把参数都转成字符串,检查全靠运行时,如果写错了:
cpp
// 槽函数其实是 onValueChanged(double)
// 这里写 onValueChanged(int) 也能编译过,但运行时就是连不上
connect(sender, SIGNAL(valueChanged(double)), receiver, SLOT(onValueChanged(int)));
你能得到的只有运行时调试输出里的一句警告,很容易被忽略。
到了 Qt5,新语法开始推荐使用函数指针形式:
cpp
// 现代写法
connect(sender, &Sender::valueChanged,
receiver, &Receiver::onValueChanged);
优点非常明显:
- 编译期检查:函数名、参数类型完全匹配才能编译通过;
- IDE 可跳转:在 Qt Creator 里按住 Ctrl 点击函数,可以直接看定义;
- 方便重构:改了函数名,IDE 可以自动跟着改 connect 的地方。
实践建议:
- 新项目一律使用新语法;
- 维护老项目时,如果需要大规模修改与信号槽有关的逻辑,尽量顺手把相关的
connect改成新语法。
3. 连接类型:Auto / Direct / Queued / BlockingQueued
信号发出以后,Qt 必须决定在哪个线程、什么时机调用槽函数。这就是连接类型要解决的问题。
常用的有四种:
-
Qt::AutoConnection(默认)- 发送者和接收者在同一线程 :相当于
DirectConnection,立即调用; - 发送者和接收者不在同一线程 :相当于
QueuedConnection,通过事件队列异步调用。
- 发送者和接收者在同一线程 :相当于
-
Qt::DirectConnection- 槽函数在发送信号的线程里立即被调用(就像普通成员函数调用一样);
- 如果发送者在子线程,接收者在主线程,槽就会在子线程里执行,这意味着:你可能在子线程里不小心操作 UI。
-
Qt::QueuedConnection- 信号触发时,不直接调槽,而是把"调用请求"排进接收者线程的事件队列;
- 槽函数会在接收者线程的事件循环中被执行;
- 用于跨线程通信时最安全的一种连接方式。
-
Qt::BlockingQueuedConnection- 和
QueuedConnection类似,但发送信号的线程会阻塞等待槽函数执行完; - 用在需要"同步调用另一个线程中的方法"时;
- 如果用不好,非常容易产生死锁(比如两个线程互相用 BlockingQueued 调对方的槽)。
- 和
在大多数桌面项目中,默认的 AutoConnection 就足够了,但在以下场景要明确指定类型:
- 子线程发信号更新 UI:明确写
Qt::QueuedConnection; - 需要同步拿到返回值:采用
BlockingQueuedConnection,但要非常慎重,确保不会出现循环等待。
4. 信号、槽和事件循环:QueuedConnection 是怎么跑的?
理解事件循环是理解信号槽的关键一步。很多人以为信号就是"直接调用槽函数",其实只有 DirectConnection 是这个行为。
对于 QueuedConnection:
- 发信号时,Qt 会创建一个内部的"调用事件"对象(可以认为是一种特殊的
QEvent); - 这个事件被投放到接收者所在线程的事件队列里;
- 接收者线程的事件循环从队列中取出事件,调用对应的槽函数。
所以跨线程信号槽的调用顺序,大致可以抽象成这样:
cpp
子线程 emit -> 生成调用事件 -> 放到主线程事件队列 -> 主线程事件循环取出 -> 调槽函数
这也是为什么你可以在子线程里安全地发信号,让主线程更新界面,而不用担心线程安全------前提是你用的是 QueuedConnection 或默认的 AutoConnection。
5. lambda 槽:好用,但别忘了生命周期
在 Qt5 里,你可以用 lambda 写出很简洁的槽函数:
cpp
connect(worker, &TaskWorker::progressUpdated, this,
[this](int value){
ui->progressBar->setValue(value);
});
这在 Demo 中看上去非常优雅,文件都变少了。但在长期运行的大项目中,如果不注意捕获列表 和对象生命周期,很容易出事。
几个需要小心的点:
-
捕获
this时,要弄清楚:- 这个
connect建立时,对象的生命周期范围; - 信号最晚可能在什么时候发出;
- 对象是否可能早于信号"结束"而被析构。
- 这个
-
如果信号来自子线程,而接收方在主线程,你最好显式写成
Qt::QueuedConnection,防止将来有其他人误改线程结构。 -
对长期存在的连接(比如全局对象、单例),不要在 lambda 里做太多复杂逻辑,尤其是涉及跨模块操作的逻辑。否则日后重构时,你很难一下子找出"有哪些地方在响应这个信号"。
比较稳妥的实践:
- 对象之间长期存在的信号槽关系,优先用"普通槽函数"而不是 lambda;
- 如果一定要用 lambda,尽量使用
QPointer之类的"弱引用":
cpp
QPointer<MainWindow> that = this;
connect(worker, &TaskWorker::progressUpdated, this,
[that](int value){
if (!that) return; // 窗口已经销毁了
that->updateProgress(value);
});
三、代码实战:用一个"任务监控工具"吃透信号槽
接下来我用一个可以在 Qt Creator 里直接构建运行的小工具,把上面讲的内容串起来。项目目标很简单:
- 模拟一个耗时任务,在子线程里跑;
- 实时把任务进度显示在主窗口的列表里;
- 支持在运行时切换连接模式(Direct / Queued / Lambda Direct / Lambda Queued);
- 通过肉眼就能观察到不同连接方式的差异。
1. 工程结构
假设工程名叫 TaskMonitor,目录结构大致如下:
cpp
TaskMonitor/
├── CMakeLists.txt
├── main.cpp
├── mainwindow.h / mainwindow.cpp
└── taskworker.h / taskworker.cpp
构建方式可以用 qmake,也可以用 CMake,这里以 CMake 为例,将重点放在逻辑上,CMakeLists 不再展开。
2. 任务工作类:TaskWorker
需求:
- 每隔一段时间更新一次进度;
- 模拟任务执行结果(成功或失败);
- 不直接操作 UI,只通过信号汇报状况。
cpp
// taskworker.h(节选)
class TaskWorker : public QObject
{
Q_OBJECT
public:
explicit TaskWorker(QObject *parent = nullptr);
void startTask(int taskId, int totalSteps);
void cancelTask();
bool isRunning() const { return m_isRunning; }
signals:
void progressUpdated(int taskId, int currentStep, int totalSteps);
void taskCompleted(int taskId);
void taskFailed(int taskId, const QString &error);
private slots:
void simulateStep();
private:
QTimer *m_timer { nullptr };
int m_taskId { -1 };
int m_currentStep { 0 };
int m_totalSteps { 0 };
bool m_isRunning { false };
bool m_shouldCancel{ false };
};
实现里,通过一个 QTimer 模拟任务执行:
cpp
// taskworker.cpp(核心逻辑节选)
TaskWorker::TaskWorker(QObject *parent)
: QObject(parent)
{
m_timer = new QTimer(this);
m_timer->setInterval(200); // 200ms 一步
connect(m_timer, &QTimer::timeout, this, &TaskWorker::simulateStep);
}
void TaskWorker::startTask(int taskId, int totalSteps)
{
if (m_isRunning)
return;
m_taskId = taskId;
m_totalSteps = totalSteps;
m_currentStep = 0;
m_isRunning = true;
m_shouldCancel = false;
emit progressUpdated(m_taskId, m_currentStep, m_totalSteps);
m_timer->start();
}
void TaskWorker::cancelTask()
{
m_shouldCancel = true;
}
void TaskWorker::simulateStep()
{
if (!m_isRunning || m_shouldCancel) {
m_timer->stop();
m_isRunning = false;
return;
}
++m_currentStep;
emit progressUpdated(m_taskId, m_currentStep, m_totalSteps);
if (m_currentStep >= m_totalSteps) {
m_timer->stop();
m_isRunning = false;
if (qrand() % 100 < 80) {
emit taskCompleted(m_taskId);
} else {
emit taskFailed(m_taskId, tr("模拟失败"));
}
}
}
这里刻意保持 TaskWorker 干净:不和 UI 发生任何联系,只用信号对外汇报状态。
3. 主窗口:MainWindow
主窗口负责:
- 创建并管理工作线程和
TaskWorker; - 提供一个简单的界面,用于添加任务、切换连接方式、查看进度;
- 在不同连接方式下,采用不同的槽函数来展示效果。
UI部分:
- 一个
QSpinBox选择任务总步数; - 一个
QComboBox选择连接方式; - 一个"开始任务"的按钮;
- 一个
QListWidget用于显示任务日志。
代码形式的槽函数结构如下(用来说明思路):
cpp
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void onAddTaskClicked();
void onConnectTypeChanged(int index);
void onTaskProgressDirect(int taskId, int cur, int total);
void onTaskProgressQueued(int taskId, int cur, int total);
// lambda 版本,用 connect 时直接写入
void onTaskCompleted(int taskId);
void onTaskFailed(int taskId, const QString &error);
private:
void setupConnections();
void startTask(int taskId, int totalSteps);
QThread *m_workerThread { nullptr };
TaskWorker *m_worker { nullptr };
enum class ConnectionType {
Direct,
Queued,
LambdaDirect,
LambdaQueued
} m_connType { ConnectionType::Direct };
int m_nextTaskId { 1 };
};
线程创建部分通常在构造函数中完成:
cpp
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
// 创建 Worker 和线程
m_workerThread = new QThread(this);
m_worker = new TaskWorker(); // 暂不设置 parent
m_worker->moveToThread(m_workerThread);
// 线程结束时清理 Worker
connect(m_workerThread, &QThread::finished,
m_worker, &QObject::deleteLater);
m_workerThread->start();
// 初始化 UI、连接按钮等...
setupConnections();
}
setupConnections() 根据当前选择的连接类型,去连接不同的槽:
cpp
void MainWindow::setupConnections()
{
// 先断开旧的
disconnect(m_worker, nullptr, this, nullptr);
switch (m_connType) {
case ConnectionType::Direct:
connect(m_worker, &TaskWorker::progressUpdated,
this, &MainWindow::onTaskProgressDirect);
break;
case ConnectionType::Queued:
connect(m_worker, &TaskWorker::progressUpdated,
this, &MainWindow::onTaskProgressQueued,
Qt::QueuedConnection);
break;
case ConnectionType::LambdaDirect:
connect(m_worker, &TaskWorker::progressUpdated,
this,
[this](int id, int cur, int total){
// 直接在 lambda 里更新 UI(Direct)
QString text = tr("[Lambda-Direct] 任务%1:%2/%3")
.arg(id).arg(cur).arg(total);
ui->listWidget->addItem(text);
});
break;
case ConnectionType::LambdaQueued:
connect(m_worker, &TaskWorker::progressUpdated,
this,
[this](int id, int cur, int total){
QString text = tr("[Lambda-Queued] 任务%1:%2/%3")
.arg(id).arg(cur).arg(total);
ui->listWidget->addItem(text);
},
Qt::QueuedConnection);
break;
}
// 任务完成/失败,这里无论哪种模式都一样
connect(m_worker, &TaskWorker::taskCompleted,
this, &MainWindow::onTaskCompleted);
connect(m_worker, &TaskWorker::taskFailed,
this, &MainWindow::onTaskFailed);
}
这样一来,当你在界面上切换连接类型(比如 QComboBox 改变 index)时,只需要:
cpp
void MainWindow::onConnectTypeChanged(int index)
{
switch (index) {
case 0: m_connType = ConnectionType::Direct; break;
case 1: m_connType = ConnectionType::Queued; break;
case 2: m_connType = ConnectionType::LambdaDirect; break;
case 3: m_connType = ConnectionType::LambdaQueued; break;
}
setupConnections();
}
启动任务时,用一个递增的任务 ID 来区分不同任务:
cpp
void MainWindow::startTask(int taskId, int totalSteps)
{
if (m_worker->isRunning()) {
m_worker->cancelTask();
}
// 通过信号把"开始任务"的请求发给 Worker 所在的线程也可以,
// 这里为了简化示例,直接调用(仅当 Worker 完全在子线程内工作时要再斟酌)
QMetaObject::invokeMethod(m_worker, "startTask",
Qt::QueuedConnection,
Q_ARG(int, taskId),
Q_ARG(int, totalSteps));
}
这样,通过不断切换连接模式,你可以肉眼看到列表里打印出的前缀:
[Direct][Queued][Lambda-Direct][Lambda-Queued]
配合在代码中刻意加入一些 QThread::msleep() 或者在 UI 槽里做"额外耗时"的逻辑,很容易观察出 Direct 模式下 UI 的卡顿情况。
四、实战中的坑与优化:几个典型坑
1. 在子线程里直接操作 UI
这是所有 Qt 教程都会反复强调的一条,但在真实项目中,依然有人会犯。
症状通常是:
- 偶发性崩溃;
- 崩溃栈里出现各种 QPaint、QWidget 内部函数;
- 只在某些客户机器、某种特定操作流程下才会发生。
根本原因 :
Qt 的 UI 控件不是线程安全的,官方明确只保证在主线程(GUI 线程)使用它们是安全的。你在子线程对 QLabel、QTableWidget 之类的控件进行读写,完全是"未定义行为"。
解决思路:任何子线程想影响 UI,都:
- 发信号;
- 在主线程里用槽函数更新界面;
- 跨线程时使用
QueuedConnection或默认为AutoConnection(自动选择 Queued)。
2. 信号参数是引用/指针,生命周期不受控
用引用当信号参数,例如:
cpp
signals:
void dataArrived(const QByteArray &data);
本意是想避免拷贝,但很多时候你根本不能保证:
- 信号发出时,
data的底层缓冲区是有效的; - 对方在槽函数里不会把这个引用存起来,后续使用。
在跨线程的场景下,这一问题更明显:
信号的触发和槽的执行不是一个时间点,中间可能隔了几毫秒甚至几秒。
更稳妥的做法:
- 优先用值传递;
- 大对象配合
std::move减少拷贝成本; - 必要时自定义"消息对象",内部再做引用计数。
3. 高频信号 + UI 刷新,导致界面严重延迟
串口通讯项目里常犯的错:
传感器每秒发送上万条数据,在收到数据的那一层就直接发信号,UI 每次收到信号就在一个 QTableWidget 里插入一行。
结果:
- 列表滚动极其卡顿;
- 稍微运行久一点,程序内存和 CPU 使用率飞涨。
最后的解决方法其实很朴素:节流 + 聚合。
- 底层数据到来时,只更新一个"缓存区",不立即刷新 UI;
- 每 100ms 启动一个定时器,从缓存区里取一批数据,合并后一次性刷新到界面上。
这跟前面提到的节流逻辑类似,本质就是:用一个相对较低频率的信号去驱动 UI,背后可以处理大量高频数据。
4. lambda 捕获 this 导致"幽灵回调"
一个非常诡异的问题:
- 某个窗口关闭之后,照理说里面的逻辑对象都应该被销毁;
- 但运行一段时间后,日志里还会打印出这个窗口相关的调试信息,仿佛它还在后台活着;
- 最后定位到:某个跨模块的单例对象里,保存着当初用 lambda 连接的槽,捕获了旧窗口的
this指针,导致窗口被"间接引用",无法销毁。
解决方式是把捕获改成弱引用,并且给模块之间的信号槽关系加了一层"中介",不再直接让窗口去和底层单例互相连线。
对 lambda 捕获 this 要非常谨慎------能用普通槽函数解决的,尽量不用 lambda。
5. BlockingQueuedConnection 用不好就是死锁制造机
有时我们会有这样的需求:
"我在主线程里发一个信号,希望在另一个线程里执行某个耗时操作,并且在这个槽函数执行完之前,本线程要阻塞等待结果"。
Qt 为此提供了 BlockingQueuedConnection,但如果你不了解事件循环和线程调度,很容易产生死锁。
最典型的死锁场景:
- 主线程发信号,使用
BlockingQueuedConnection,等待子线程槽函数执行完; - 子线程槽函数里又发了一个信号,试图通过
BlockingQueuedConnection去同步调用主线程的某个槽; - 双方就此互相等待,程序看上去"卡死",但 CPU 占用不高。
这种设计一般可以通过:
- 拆分为两个异步阶段;
- 或借助
QFuture/QEventLoop等机制重构; - 尽量避免真正意义上的"跨线程同步调用"。
在大多数桌面开发场景下,我个人建议是:尽量避免 BlockingQueuedConnection。如果非要用,一定要画明白调用链路,确认不会出现循环等待。
五、小结:几条可以立刻在项目里落地的实践守则
结合前面讲的原理和实战经验,最后给出一份在项目里非常实用的"信号槽使用清单",你可以直接贴到团队的编码规范里:
-
统一语法
- 新代码一律使用新语法:
connect(sender, &Sender::sig, receiver, &Receiver::slot); - 避免
SIGNAL()/SLOT()字符串写法,尤其是跨模块的连接。
- 新代码一律使用新语法:
-
线程相关约定
- 所有从子线程触发、涉及 UI 更新的连接,统一写成:
Qt::QueuedConnection; - 禁止在子线程里直接操作 QWidget / QQuickItem 等 UI 对象;
- 不在项目中滥用
BlockingQueuedConnection,如必须使用,需要 code review 特别关注。
- 所有从子线程触发、涉及 UI 更新的连接,统一写成:
-
lambda 使用约定
- 临时逻辑、小范围作用域用 lambda 没问题;
- 跨模块、长期存在的连接,优先写成普通槽函数;
- 捕获
this时务必确认对象生命周期;必要时用QPointer或std::weak_ptr。
-
信号设计约定
- 信号参数尽量使用值传递,不轻易传引用或裸指针;
- 高频信号要设计"节流/聚合"机制,不要一条数据一个信号;
- 信号名要体现"事件"语义,如
xxxChanged、taskCompleted,而不是随意起名字。
-
对象生命周期与连接管理
- 合理使用 QObject 的父子关系,让大部分对象生命周期"自动化";
- 对特殊场景的连接,用
QMetaObject::Connection保存句柄,适时调用disconnect; - 在复杂模块拆分时,优先用"中介对象/事件总线"来集中管理跨模块信号,而不是模块间互相直连。
如果你已经在项目中使用 Qt 一段时间,不妨结合一个具体模块,把里面所有的 connect 找出来,逐个对照这份清单做一次梳理。
往往只要改动少量关键连接,就能明显减少一些"随机崩溃"和"偶发卡顿"的问题。
这一期主要聚焦在信号槽的原理和用法上,用一个简单的多线程任务监控小工具把关键点串了一遍。下一期我们会继续往下深入,聊聊 Qt 对象的内存管理:QObject 的父子关系、deleteLater、智能指针和 QPointer 等等,基本上是"写稳定 Qt 程序"必备的另一半基石。