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 程序"必备的另一半基石。

相关推荐
余衫马2 小时前
Qt for Python:PySide6 入门指南(下篇)
c++·python·qt
w***76552 小时前
PHP vs Go:动态与静态语言的巅峰对决
开发语言·golang·php
HellowAmy2 小时前
我的C++规范 - 请转移到文件
开发语言·c++·代码规范
大闲在人2 小时前
25. 连续盘点系统(Q-R 策略):总成本优化与基于缺货成本的再订货点设定
开发语言·数据分析·供应链管理·智能制造·工业工程
skywalk81632 小时前
介绍一下QuantConnect Lean(python 15k star)
开发语言·python·量化
不凡而大米、2 小时前
报错:传入的请求具有过多的参数。该服务器支持最多2100个参数
java·开发语言·mybatis
打工的小王2 小时前
单例模式的实现
java·开发语言·单例模式
strive-debug2 小时前
cpp篇~~类和对象
开发语言·c++
是宇写的啊2 小时前
单例模式-阻塞队列
java·开发语言·单例模式