3、信号与槽机制(上):信号与槽概念、connect函数详解、自定义信号与槽、Lambda表达式在槽函数中的应用

3.1 信号与槽:Qt 最核心的“通讯兵”

信号与槽,说白了就是 Qt 里对象之间“喊话”和“接话”的机制。

我刚开始学 Qt 时,觉得这东西有点绕。后来做项目多了,才明白它有多重要。没有它,Qt 就只是个普通的 C++ 图形库。

想象一下这个场景:你点了一个按钮,窗口要关闭。按钮是“喊话”的,窗口是“接话”的。按钮发出“我被点击了”的信号,窗口收到后执行“关闭自己”的槽函数。

这就是信号与槽的核心逻辑:一个对象发出信号,另一个对象用槽函数来响应。

关键点:信号与槽是类型安全的。编译器会帮你检查信号和槽的参数是否匹配。这一点比回调函数靠谱多了。

我画了一张图,帮你理清信号与槽的通信流程:

发送者 QPushButton 发出信号 接收者 QWidget 执行槽函数 connect() 连接 信号 → 槽 信号与槽通信流程 1. 用户点击按钮 → 2. 按钮发出 clicked() 信号 3. connect() 将信号传递给接收者 → 4. 接收者执行槽函数

3.2 connect 函数:把信号和槽“焊”在一起

信号和槽不会自动配对。你得用 connect() 把它们连起来。就像电线,不接上就没法通电。

connect 函数有几种写法,我一个个说。

3.2.1 经典写法(Qt4 风格)

connect(sender, SIGNAL(clicked()), receiver, SLOT(close()));

这种写法用宏 SIGNAL() 和 SLOT() 把函数名转成字符串。好处是简单,坏处是编译器不检查参数类型。写错了,运行时才报错。

注意:Qt4 风格在编译期不做类型检查。我见过有人把参数个数写错,程序跑起来才崩溃。调试起来很头疼。

3.2.2 新式写法(Qt5 风格,推荐)

connect(sender, &QPushButton::clicked, receiver, &QWidget::close);

这种写法用函数指针。编译器会检查信号和槽的参数是否匹配。不匹配?编译直接报错。省心多了。

我个人习惯用 Qt5 风格。为什么?因为安全。编译期就能发现问题,不用等到运行时。

3.2.3 带参数的 connect

信号和槽可以传参数。比如 QSpinBox 的值改变时,会把新值传出来:

connect(spinBox, &QSpinBox::valueChanged,
        slider,   &QSlider::setValue);

这里 valueChanged(int) 信号带一个 int 参数,setValue(int) 槽也带一个 int 参数。类型匹配,完美对接。

小技巧:信号参数可以比槽参数多。多的参数会被忽略。但反过来不行——槽的参数不能比信号多。

3.3 自定义信号与槽:自己造轮子

Qt 自带的信号和槽够用吗?大部分时候够。但有些场景你得自己定义。

比如你写了一个温度传感器类,温度超过阈值时要通知主界面。这时候就需要自定义信号。

3.3.1 自定义信号

信号用 signals: 关键字声明,不需要实现。你只管声明,Qt 的 moc(元对象编译器)会帮你生成实现代码。

class TemperatureSensor : public QObject
{
    Q_OBJECT

public:
    TemperatureSensor(QObject *parent = nullptr);

signals:
    void temperatureExceeded(double currentTemp, double threshold);
};

注意几点:

  • 信号函数返回类型必须是 void
  • 信号只需要声明,不需要写实现
  • 信号可以用 emit 关键字来发射

3.3.2 自定义槽

槽就是普通的成员函数,只是多了个 slots: 关键字。它可以被信号触发,也可以像普通函数一样直接调用。

class MainWindow : public QWidget
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);

public slots:
    void onTemperatureExceeded(double currentTemp, double threshold);
};

然后在 .cpp 文件里实现这个槽函数:

void MainWindow::onTemperatureExceeded(double currentTemp, double threshold)
{
    qWarning() << "温度超标!当前温度:" << currentTemp
               << ",阈值:" << threshold;
    // 这里可以弹窗报警、记录日志等
}

3.3.3 连接自定义信号与槽

TemperatureSensor *sensor = new TemperatureSensor(this);
MainWindow *window = new MainWindow;

connect(sensor, &TemperatureSensor::temperatureExceeded,
        window, &MainWindow::onTemperatureExceeded);

当传感器检测到温度超标时,调用 emit temperatureExceeded(85.5, 80.0);,主窗口的槽函数就会自动执行。

经验之谈:我在做工业控制项目时,用自定义信号和槽处理了十几个传感器的事件。每个传感器一个信号,主界面一个槽。代码结构非常清晰。如果不用信号与槽,用回调函数或者事件循环,代码会乱成一团。

3.4 Lambda 表达式:让槽函数更灵活

C++11 引入了 Lambda 表达式。Qt5 开始,槽函数可以直接用 Lambda 来写。

为什么要用 Lambda?因为有时候槽函数逻辑很简单,专门写一个成员函数太麻烦。Lambda 可以让你“就地”写逻辑。

3.4.1 基本用法

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

这里 [this] 是捕获列表,表示 Lambda 内部可以访问当前对象的成员。() 是参数列表,{} 是函数体。

3.4.2 捕获外部变量

Lambda 可以捕获外部变量。比如:

int count = 0;
connect(button, &QPushButton::clicked, [&count]() {
    count++;
    qDebug() << "点击次数:" << count;
});

[&count] 表示按引用捕获 count 变量。每次点击按钮,count 都会增加。

注意:按引用捕获要小心。如果 Lambda 在对象销毁后还被调用,引用就悬空了。程序会崩溃。我建议用按值捕获 [=] 或者显式捕获 [this, count]。

3.4.3 Lambda 槽的典型场景

我经常用 Lambda 来处理一些临时逻辑。比如:

// 点击按钮后,只执行一次操作
QTimer::singleShot(0, [this]() {
    // 在事件循环启动后立即执行
    this->initialize();
});

// 带参数的 Lambda
connect(spinBox, &QSpinBox::valueChanged, [this](int value) {
    ui->label->setText(QString("当前值:%1").arg(value));
});

建议:Lambda 适合短小的逻辑。如果槽函数超过 10 行,我建议还是写成独立的成员函数。代码可读性更好。

3.5 避坑指南

做项目这么多年,我在信号与槽上踩过不少坑。分享几个典型的:

  • 信号与槽参数不匹配:我曾经把 QString 和 const char* 混用,编译通过了,运行时数据全是乱码。后来改用 Qt5 风格,编译器直接报错,省了很多事。
  • 多线程中信号与槽的连接方式:默认是 Qt::AutoConnection。如果信号和槽在不同线程,会自动变成队列连接。但如果你手动指定了 Qt::DirectConnection,槽函数会在信号线程中执行,可能会引发线程安全问题。
  • Lambda 中捕获了 this 指针,但对象被销毁了:这是最常见的崩溃原因。解决方案是用 QPointer 或者 QWeakPointer 来安全地访问对象。

嗯,信号与槽的内容先讲到这里。这部分是 Qt 的基石,理解透了,后面的内容就好学了。