2. Qt信号槽机制

1.1 信号槽的本质:观察者模式的Qt实现

信号槽是Qt最核心的机制,没有之一。说白了,它就是一种对象间通信的方式。你想想看,在传统C++里,对象A想通知对象B,通常得让A持有B的指针,然后直接调用B的方法。这样耦合度太高了,改起来很痛苦。

Qt的信号槽解决了这个问题。它让发送者(信号)和接收者(槽函数)完全解耦。发送者不知道谁会响应,接收者也不知道谁在发送。我刚开始用Qt时,觉得这玩意儿不就是回调函数吗?后来深入源码才发现,它比回调函数强大得多。

核心要点:信号槽是类型安全的,编译期就会检查信号和槽的参数是否匹配。这一点比传统的回调函数靠谱多了。

1.2 connect函数详解——你写对了吗?

connect函数是信号槽的桥梁。它的标准形式是:

connect(sender, &SenderClass::signalName,
        receiver, &ReceiverClass::slotName,
        connectionType);

这里有几个坑,我一个个说。

第一个坑:参数类型必须匹配。 信号和槽的参数类型要一致,或者槽的参数可以比信号少(多余的参数会被忽略)。但反过来不行。

第二个坑:重载信号的处理。 如果一个信号有多个重载版本,比如:

class MyWidget : public QWidget {
    Q_OBJECT
signals:
    void clicked();
    void clicked(bool checked);
};

这时候connect会报歧义。我习惯用函数指针显式指定:

connect(button, static_cast<void (QPushButton::*)(bool)>(&QPushButton::clicked),
        this, &MyWidget::onClicked);

嗯,代码是丑了点,但安全。

第三个坑:Lambda表达式作为槽。 这是C++11之后我最喜欢的方式:

connect(button, &QPushButton::clicked, this, [this]() {
    qDebug() << "按钮被点击了";
});

注意这里的this上下文。如果Lambda里捕获了this,而this指向的对象被销毁了,程序会崩溃。我建议用QPointer或者QWeakPointer来保护。

1.3 同步与异步——信号槽的执行模式

connect的第五个参数connectionType决定了信号槽是同步还是异步执行。默认是Qt::AutoConnection。

连接类型 行为 适用场景
Qt::DirectConnection 信号发出时立即调用槽函数,在发送者线程执行 同一线程内的对象通信
Qt::QueuedConnection 信号发出后,槽函数在接收者的事件循环中排队执行 跨线程通信
Qt::AutoConnection 默认值。同一线程用Direct,不同线程用Queued 大多数情况
Qt::BlockingQueuedConnection 类似Queued,但发送者线程会阻塞等待槽执行完毕 需要同步结果的跨线程调用

我曾经踩过的坑: 用BlockingQueuedConnection时,如果发送者和接收者在同一个线程,会死锁。因为发送者阻塞等待自己执行槽函数,但自己又被阻塞了。嗯,这就像左手抓右手,永远抓不到。

为什么会这样?因为Qt的事件循环是单线程的。发送者阻塞了,事件循环就停了,槽函数永远排不上队。所以BlockingQueuedConnection只能用于不同线程。

1.4 自定义信号与槽——从零开始

自定义信号和槽其实很简单。记住三个步骤:

  1. 类必须继承自QObject,并在头文件里加上Q_OBJECT宏
  2. 信号用signals关键字声明,只需要声明,不需要实现
  3. 槽用public slots声明,需要实现函数体

来看一个完整的例子:

// sensor.h
class Sensor : public QObject {
    Q_OBJECT
public:
    explicit Sensor(QObject *parent = nullptr);
    
signals:
    void temperatureChanged(double value);
    void alarmTriggered(const QString &message);
    
public slots:
    void startMonitoring();
    void stopMonitoring();
    
private:
    bool m_running;
};

// sensor.cpp
void Sensor::startMonitoring() {
    m_running = true;
    while (m_running) {
        double temp = readTemperature();
        emit temperatureChanged(temp);
        if (temp > 100.0) {
            emit alarmTriggered("温度过高!");
        }
        QThread::msleep(1000);
    }
}

注意看,信号用emit关键字发射。这个emit其实是个宏,会被预处理器替换成空。它纯粹是为了代码可读性。我个人习惯是一定要写emit,这样一眼就能看出哪里在发射信号。

小技巧: 自定义信号可以带参数,但参数类型必须是Qt的元类型(Meta Type)。如果是自定义结构体,需要用Q_DECLARE_METATYPE注册一下。

1.5 信号槽的底层原理——MOC的秘密

信号槽不是C++标准的一部分,它是Qt通过元对象编译器(MOC)实现的。MOC会扫描头文件中的Q_OBJECT宏,然后生成一个额外的C++源文件。

这个生成的文件里包含了:

  • 信号的实现代码(其实就是调用QMetaObject::activate)
  • 元对象信息(类名、信号列表、槽列表等)
  • 类型信息(用于运行时类型识别)

我画了一张图,帮你理解整个流程:

信号槽执行流程 发送者对象 信号 (emit) MOC生成的代码 连接类型判断 DirectConnection QueuedConnection 接收者对象 事件循环排队 Qt对象 信号 MOC代码 连接类型 执行路径

从这张图可以看出,信号槽的执行路径取决于连接类型。Direct是同步调用,Queued是异步排队。MOC生成的代码负责在运行时查找连接关系,然后根据连接类型决定执行方式。

1.6 避坑指南——我踩过的那些坑

坑1:信号槽连接不生效

我曾经花了一整天调试一个信号槽,怎么都连不上。最后发现是Q_OBJECT宏没写。记住,任何使用信号槽的类,必须在类定义里加上Q_OBJECT。而且加了之后要重新qmake,否则MOC不会重新生成代码。

坑2:跨线程信号槽丢失

如果你在子线程里发射信号,接收者在主线程,一定要确保接收者的事件循环在运行。否则信号会丢失。我习惯在接收者的构造函数里调用moveToThread,或者用QMetaObject::invokeMethod来确保安全。

坑3:信号槽参数类型不匹配

信号和槽的参数类型必须完全一致,包括const引用。比如信号是const QString &,槽也必须是const QString &,不能是QString。否则编译期不会报错,但运行时连接会失败。嗯,这个坑我踩了两次才记住。

1.7 总结

信号槽是Qt的灵魂。理解它的原理,你就能写出更健壮、更易维护的Qt程序。记住几个关键点:

  • 信号槽是类型安全的观察者模式
  • connect的第五个参数控制同步/异步
  • 自定义信号和槽需要Q_OBJECT宏
  • MOC在背后做了大量工作
  • 跨线程通信要小心事件循环