Qt5 进阶【1】信号与槽机制深度剖析——从语法到运行时调度

目标读者:有一定 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,信号和槽越连越多,最后难免出现下面几类问题:

  1. 信号槽满天飞,谁调了谁完全看不清

    一个中型桌面项目,窗口几十个、对话框十几个,每个窗口上十几个控件。随着需求迭代,"临时加一个信号"、"先在这里 connect 一下"的写法越来越多,半年之后就连自己都不记得当初是怎么连的了。

    尤其是那种"从某个底层模块发信号,一路传到 UI 层"的设计,如果没有严格的命名约定和文档,排查 Bug 时常常要一路 Ctrl+点击 跟着槽函数跳半天。

  2. 跨线程更新 UI 偶发崩溃

    最典型的场景:

    • 为了防止主线程卡顿,你把耗时操作放到了 QThread 里;
    • 任务线程里处理数据,然后直接去改 QLabel 的文本、给 QTableWidget 塞数据;
    • 在你本机测试没问题,上线到客户现场,一天之内崩溃两次,而且还是完全随机。

    这类问题追踪起来非常痛苦:

    • 有时是野指针;
    • 有时是竞争条件;
    • 有时是主线程对象已经析构了,子线程还在发信号。
  3. lambda 写得很爽,几个月后变成雷区

    自从 Qt5 支持用 lambda 当槽函数,大部分人都爱上了这种写法,代码短了、文件也少了,看起来非常"现代 C++"。

    但 lambda 有个很大的隐患:捕获变量的生命周期

    • 捕获 this,却没有考虑窗口何时关闭;
    • 捕获局部变量引用,却让信号延后触发;
    • 在 lambda 里对外部对象"顺手一写"各种逻辑,却完全没有测试它在对象析构之后会发生什么。
  4. 高频信号导致 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);
};

这一个宏背后,做了两件非常关键的事:

  1. 告诉 moc(元对象编译器):这个类需要生成元对象信息
  2. 在编译阶段生成对应的 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 必须决定在哪个线程、什么时机调用槽函数。这就是连接类型要解决的问题。

常用的有四种:

  1. Qt::AutoConnection(默认)

    • 发送者和接收者在同一线程 :相当于 DirectConnection,立即调用;
    • 发送者和接收者不在同一线程 :相当于 QueuedConnection,通过事件队列异步调用。
  2. Qt::DirectConnection

    • 槽函数在发送信号的线程里立即被调用(就像普通成员函数调用一样);
    • 如果发送者在子线程,接收者在主线程,槽就会在子线程里执行,这意味着:你可能在子线程里不小心操作 UI
  3. Qt::QueuedConnection

    • 信号触发时,不直接调槽,而是把"调用请求"排进接收者线程的事件队列;
    • 槽函数会在接收者线程的事件循环中被执行;
    • 用于跨线程通信时最安全的一种连接方式。
  4. Qt::BlockingQueuedConnection

    • QueuedConnection 类似,但发送信号的线程会阻塞等待槽函数执行完;
    • 用在需要"同步调用另一个线程中的方法"时;
    • 如果用不好,非常容易产生死锁(比如两个线程互相用 BlockingQueued 调对方的槽)。

在大多数桌面项目中,默认的 AutoConnection 就足够了,但在以下场景要明确指定类型:

  • 子线程发信号更新 UI:明确写 Qt::QueuedConnection
  • 需要同步拿到返回值:采用 BlockingQueuedConnection,但要非常慎重,确保不会出现循环等待。

4. 信号、槽和事件循环:QueuedConnection 是怎么跑的?

理解事件循环是理解信号槽的关键一步。很多人以为信号就是"直接调用槽函数",其实只有 DirectConnection 是这个行为。

对于 QueuedConnection

  1. 发信号时,Qt 会创建一个内部的"调用事件"对象(可以认为是一种特殊的 QEvent);
  2. 这个事件被投放到接收者所在线程的事件队列里;
  3. 接收者线程的事件循环从队列中取出事件,调用对应的槽函数。

所以跨线程信号槽的调用顺序,大致可以抽象成这样:

cpp 复制代码
子线程 emit -> 生成调用事件 -> 放到主线程事件队列 -> 主线程事件循环取出 -> 调槽函数

这也是为什么你可以在子线程里安全地发信号,让主线程更新界面,而不用担心线程安全------前提是你用的是 QueuedConnection 或默认的 AutoConnection


5. lambda 槽:好用,但别忘了生命周期

在 Qt5 里,你可以用 lambda 写出很简洁的槽函数:

cpp 复制代码
connect(worker, &TaskWorker::progressUpdated, this,
        [this](int value){
            ui->progressBar->setValue(value);
        });

这在 Demo 中看上去非常优雅,文件都变少了。但在长期运行的大项目中,如果不注意捕获列表对象生命周期,很容易出事。

几个需要小心的点:

  1. 捕获 this 时,要弄清楚:

    • 这个 connect 建立时,对象的生命周期范围;
    • 信号最晚可能在什么时候发出;
    • 对象是否可能早于信号"结束"而被析构。
  2. 如果信号来自子线程,而接收方在主线程,你最好显式写成 Qt::QueuedConnection,防止将来有其他人误改线程结构。

  3. 对长期存在的连接(比如全局对象、单例),不要在 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 线程)使用它们是安全的。你在子线程对 QLabelQTableWidget 之类的控件进行读写,完全是"未定义行为"。

解决思路:任何子线程想影响 UI,都:

  1. 发信号;
  2. 在主线程里用槽函数更新界面;
  3. 跨线程时使用 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。如果非要用,一定要画明白调用链路,确认不会出现循环等待。


五、小结:几条可以立刻在项目里落地的实践守则

结合前面讲的原理和实战经验,最后给出一份在项目里非常实用的"信号槽使用清单",你可以直接贴到团队的编码规范里:

  1. 统一语法

    • 新代码一律使用新语法:
      connect(sender, &Sender::sig, receiver, &Receiver::slot);
    • 避免 SIGNAL() / SLOT() 字符串写法,尤其是跨模块的连接。
  2. 线程相关约定

    • 所有从子线程触发、涉及 UI 更新的连接,统一写成:
      Qt::QueuedConnection
    • 禁止在子线程里直接操作 QWidget / QQuickItem 等 UI 对象;
    • 不在项目中滥用 BlockingQueuedConnection,如必须使用,需要 code review 特别关注。
  3. lambda 使用约定

    • 临时逻辑、小范围作用域用 lambda 没问题;
    • 跨模块、长期存在的连接,优先写成普通槽函数;
    • 捕获 this 时务必确认对象生命周期;必要时用 QPointerstd::weak_ptr
  4. 信号设计约定

    • 信号参数尽量使用值传递,不轻易传引用或裸指针;
    • 高频信号要设计"节流/聚合"机制,不要一条数据一个信号;
    • 信号名要体现"事件"语义,如 xxxChangedtaskCompleted,而不是随意起名字。
  5. 对象生命周期与连接管理

    • 合理使用 QObject 的父子关系,让大部分对象生命周期"自动化";
    • 对特殊场景的连接,用 QMetaObject::Connection 保存句柄,适时调用 disconnect
    • 在复杂模块拆分时,优先用"中介对象/事件总线"来集中管理跨模块信号,而不是模块间互相直连。

如果你已经在项目中使用 Qt 一段时间,不妨结合一个具体模块,把里面所有的 connect 找出来,逐个对照这份清单做一次梳理。

往往只要改动少量关键连接,就能明显减少一些"随机崩溃"和"偶发卡顿"的问题。


这一期主要聚焦在信号槽的原理和用法上,用一个简单的多线程任务监控小工具把关键点串了一遍。下一期我们会继续往下深入,聊聊 Qt 对象的内存管理:QObject 的父子关系、deleteLater、智能指针和 QPointer 等等,基本上是"写稳定 Qt 程序"必备的另一半基石。

相关推荐
Quz2 天前
QML Hello World 入门示例
qt
xcyxiner5 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner6 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner6 天前
DicomViewer (添加模型类)3
qt
xcyxiner7 天前
DicomViewer (目录调整) 2
qt
xcyxiner7 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR0069 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术9 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript
码云数智-园园9 天前
C++20 Modules 模块详解
java·开发语言·spring
swordbob9 天前
NIO的channel中什么是 fd(File Descriptor,文件描述符)
java·开发语言·nio