4. 信号与槽机制:信号与槽原理、connect函数详解、自定义信号与槽、Lambda表达式

4.1 信号与槽的核心原理

信号与槽,是Qt最核心的机制。说白了,它就是一种对象间的通信方式。

我刚开始接触Qt时,觉得这东西挺玄乎。后来想明白了——信号就是一个对象发出的通知,槽就是另一个对象对这个通知的响应。你发出信号,我执行槽函数,就这么简单。

举个例子:你点了个按钮,按钮发出clicked信号,窗口收到信号后执行close槽,窗口就关了。整个过程不需要你写任何循环检测代码。

为什么会这么方便?因为Qt在底层帮我们做了事件循环和回调注册。我个人习惯把信号理解为“广播”,把槽理解为“接收器”。一个信号可以连接多个槽,多个信号也可以连接同一个槽。

核心特点:

  • 信号与槽是类型安全的——编译期就会检查参数类型是否匹配
  • 信号与槽是松耦合的——发送者不知道也不关心谁在接收
  • 信号与槽可以跨线程——通过队列连接实现线程安全

下面我用一张图来展示信号与槽的工作流程:

发送者对象 (如 QPushButton) emit 信号() connect 触发 接收者对象 (如 QWidget) 槽函数() 流程:用户操作 → 发送者emit信号 → 自动调用接收者的槽函数

4.2 connect函数详解

connect是信号与槽的“接线员”。它的完整签名长这样:

QMetaObject::Connection QObject::connect(
    const QObject *sender,       // 发送者
    const char *signal,          // 信号
    const QObject *receiver,     // 接收者
    const char *method,          // 槽
    Qt::ConnectionType type = Qt::AutoConnection  // 连接类型
);

嗯,这里要注意——信号和槽的宏必须用SIGNAL()和SLOT()包裹。这是老式写法,但很多遗留代码还在用。

我在项目中遇到过一个问题:信号参数和槽参数不匹配,编译通过了,运行时却收不到信号。后来排查了半天,发现是参数类型写错了。所以我现在更推荐用新式写法:

// 新式写法(Qt5+推荐)
connect(sender, &QPushButton::clicked, receiver, &QWidget::close);

// 带参数的新式写法
connect(spinBox, &QSpinBox::valueChanged, 
        slider, &QSlider::setValue);

新式写法好在哪?编译期就能检查类型,参数不对直接报错,不会留到运行时才出问题。

小技巧:新式写法中,如果信号有重载,需要用函数指针显式指定。比如:

connect(comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
        [=](int index){ /* ... */ });

4.3 连接类型

connect的最后一个参数是连接类型,我列个表给你看:

连接类型 说明 使用场景
AutoConnection 默认值。同线程用Direct,跨线程用Queued 大多数情况
DirectConnection 信号发出时立即调用槽,在发送者线程执行 同线程、需要同步响应
QueuedConnection 信号发出后,槽在接收者的事件循环中执行 跨线程通信
BlockingQueuedConnection 类似Queued,但发送者会阻塞等待槽执行完 跨线程且需要等待结果
UniqueConnection 可与上述类型组合,防止重复连接 避免多次connect同一个信号槽对

我曾经踩过一个坑:在子线程中emit信号,槽在主线程,忘了用QueuedConnection。结果槽直接在子线程执行了,访问UI控件时程序崩溃。后来我养成了习惯——跨线程通信一律显式指定QueuedConnection。

4.4 自定义信号与槽

光用Qt自带的信号槽不够?那就自己写。

自定义信号很简单,记住三个要点:

  • 信号必须在类声明中用 signals: 关键字标记
  • 信号函数只需要声明,不需要实现——Qt的moc会帮你生成
  • 信号返回类型必须是void

自定义槽更灵活:

  • 可以用 public slots:、protected slots:、private slots: 控制访问权限
  • 槽可以是普通成员函数、虚函数、Lambda表达式
  • 槽的返回类型可以是任意类型,但信号连接时只取void

看个完整例子:

// worker.h
class Worker : public QObject {
    Q_OBJECT
public:
    explicit Worker(QObject *parent = nullptr);

signals:
    // 自定义信号——只需声明
    void progressChanged(int percent);
    void workFinished(const QString &result);

public slots:
    // 自定义槽
    void startWork() {
        for (int i = 0; i <= 100; i += 10) {
            emit progressChanged(i);  // 发射信号
            QThread::msleep(200);
        }
        emit workFinished("Done!");
    }
};

注意:自定义信号类必须继承自QObject,并且在类开头加上Q_OBJECT宏。否则moc不会处理这个类,信号槽就没法用。

我在项目中经常用自定义信号来做异步任务的状态反馈。比如下载文件时,用progressChanged信号更新进度条,用downloadFinished信号通知结果。这样UI线程和后台线程各司其职,代码清晰多了。

4.5 Lambda表达式

Lambda是C++11引入的匿名函数。在Qt中,它让信号槽连接变得极其灵活。

基本语法:

[捕获列表](参数列表) -> 返回类型 { 函数体 }

在connect中使用Lambda:

connect(button, &QPushButton::clicked, [=]() {
    qDebug() << "按钮被点击了";
    // 这里可以访问外部变量
    label->setText("Hello");
});

捕获列表的几种方式:

  • [=] —— 按值捕获所有外部变量
  • [&] —— 按引用捕获所有外部变量
  • [this] —— 捕获当前类的this指针
  • [=, &x] —— 混合捕获,x按引用,其他按值

我的建议:在Qt的connect中,尽量用 [=] 或 [this]。用 [&] 要小心——如果Lambda在异步执行时,引用的局部变量可能已经销毁了。我曾经因为这个原因排查了一下午的野指针问题。

Lambda配合信号槽,最爽的地方在于——不需要专门写一个槽函数。比如临时处理一个按钮点击,写个Lambda就完事了,不用在头文件里声明槽,不用在cpp里实现。代码量直接减半。

看个实际例子:

// 传统写法——需要声明槽
connect(btn, SIGNAL(clicked()), this, SLOT(onBtnClicked()));

// Lambda写法——一行搞定
connect(btn, &QPushButton::clicked, [=]() {
    static int count = 0;
    count++;
    label->setText(QString("点击次数: %1").arg(count));
});

你想想看,如果只是简单更新个标签,专门写个槽函数是不是有点小题大做?Lambda就是为这种场景准备的。

4.6 断开连接与生命周期

信号槽连接了,怎么断开?

// 断开特定连接
disconnect(sender, &Sender::signal, receiver, &Receiver::slot);

// 断开某个对象的所有连接
disconnect(sender, nullptr, nullptr, nullptr);

// 断开所有与接收者相关的连接
disconnect(sender, nullptr, receiver, nullptr);

不过大多数时候你不需要手动断开。Qt有个自动管理机制:当发送者或接收者被销毁时,相关的连接会自动断开。这就是QObject父子对象管理的优势。

但有一点要注意——Lambda表达式如果捕获了指针,要确保指针的生命周期比连接长。否则Lambda执行时,指针可能已经悬空了。

避坑指南:我曾经在Lambda中捕获了一个局部QWidget指针,然后这个widget被提前删除了。结果下次点击按钮时,程序直接崩溃。后来我改用QPointer来安全地持有对象:

QPointer<QLabel> safeLabel = label;
connect(btn, &QPushButton::clicked, [=]() {
    if (safeLabel) safeLabel->setText("安全更新");
});

4.7 总结

信号与槽是Qt的灵魂。理解它,你就能写出松耦合、易维护的代码。

记住几个关键点:

  • 信号只管发,槽只管收,两者互不依赖
  • 新式connect更安全,推荐优先使用
  • 自定义信号槽时,别忘了Q_OBJECT宏
  • Lambda让代码更简洁,但要注意生命周期

这些内容我平时开发中天天用。你多写几个例子,很快就能上手。