Note

本篇讲解 Qt 最重要的对象间通信机制——信号与槽。你将理解它为什么优于传统回调函数,学会 connect 的多种写法(字符串宏、函数指针、lambda),并亲手自定义信号和槽、掌握带参数的连接与断开连接。这是 Qt 面试和实际开发中出现频率最高的知识点,适合读完上一篇、能让窗口跑起来的读者。

为什么需要信号与槽

图形界面本质上就是”事件驱动”:用户点了按钮,程序要响应;下载完成了,界面要刷新。任何 GUI 框架都要解决同一个问题——对象 A 状态变化时,如何通知对象 B

传统 C++ 的答案是回调函数:把 B 的函数指针交给 A,A 在状态变化时调用它。回调能用,但有明显缺陷:

  • 类型不安全:裸函数指针没有类型信息,签名不匹配编译期发现不了,运行时才崩。
  • 强耦合:A 必须知道 B 的函数签名,甚至持有 B 的指针;B 析构后 A 里的指针就成了悬空指针。
  • 只能一对一:一个回调位置挂一个函数,想多对象响应要自己实现列表。

Qt 的信号槽对回调做了系统性升级:

  • 信号是对象发出的事件通知(如按钮的 clicked()),发出者不关心谁来接收。
  • 是接收并响应信号的普通成员函数(如窗口的关闭逻辑)。
  • connect 把两者关联起来,关联时校验签名,接收者销毁时自动断开,且支持一对多、多对一。

发出者与接收者彻底解耦——按钮不知道窗口的存在,窗口也不知道按钮的实现,双方只认”信号”和”槽”这两个接口。这就是 Qt 用元对象系统的运行时信息换来的能力。整条链路可以概括为下图:

信号与槽的连接机制:发射者的信号经 connect 关联到接收者的槽

connect 的几种写法

connect 语法随 Qt 版本演进,Qt5 引入函数指针写法,Qt6 沿用至今。同一个”点击按钮退出程序”的需求有三种表达:

写法一:SIGNAL/SLOT 宏(Qt4 传统写法)

// 把信号和槽包装成字符串,运行时按名字查找匹配
connect(button, SIGNAL(clicked()),
        this,   SLOT(closeWindow()));

缺点是编译期完全不检查:信号名打错、参数不匹配,编译照样通过,运行时才在输出面板打印 No such signal 警告。老代码里常见,新代码不建议用。

写法二:函数指针(Qt5 起的标准写法)

// 编译期就能检查信号与槽的参数是否匹配
connect(button, &QPushButton::clicked,
        this,   &MainWindow::closeWindow);
 
// 接收者是 this 时,可以省略第三个参数
connect(button, &QPushButton::clicked,
        this,   &MainWindow::closeWindow);
// ↑ 完整形式;下面是省略形式
connect(button, &QPushButton::clicked,
        [](){ /* ... */ });

新项目一律推荐这种写法:写错类名或函数名编译直接报错,参数匹配也能提前验证,还能配合 IDE 自动补全。

写法三:lambda 表达式

// 短小的响应逻辑不必专门定义槽函数,直接内联
connect(button, &QPushButton::clicked, this, [=]() {
    statusBar()->showMessage("按钮被点击了");
});

lambda 极其适合”点一下弹个提示”这类一次性逻辑。注意第三个参数传 this 作为上下文对象——当 this(窗口)销毁时连接自动断开,避免悬空捕获。

Warning

lambda 捕获了外部局部变量又没有传上下文对象时,若对象先于 lambda 被销毁,后续触发信号会崩溃。带捕获的 lambda 务必把 this 或接收者指针作为 connect 的第三个参数,让 Qt 帮你管理生命周期。

Warning

同一对信号槽重复调用 connect 会建立多条连接:点一次按钮,槽函数被执行两次。若怀疑重复连接,可先调用 disconnect 再 connect,或检查连接代码是否在会被多次执行的函数里(如构造函数外的刷新函数)。

自定义信号与槽

Qt 控件自带的信号再多,业务逻辑总要自己定义。自定义遵循固定三步:

// ---------- student.h ----------
#ifndef STUDENT_H
#define STUDENT_H
 
#include <QObject>
#include <QString>
 
class Student : public QObject
{
    Q_OBJECT                     // 启用元对象特性,信号槽的开关
public:
    explicit Student(QObject *parent = nullptr);
 
signals:                         // 信号区:只声明,不实现
    void scoreChanged(int newScore);           // 自定义信号,可带参数
 
public slots:                    // 槽区:像普通函数一样声明和实现
    void onScoreChanged(int score) {
        // 带参数的槽:信号传来的参数依次传入
        qDebug() << "收到新分数:" << score;
    }
};
 
#endif
// ---------- main.cpp ----------
#include <QApplication>
#include "student.h"
 
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
 
    Student s;
    // 自定义信号连接自定义槽
    QObject::connect(&s, &Student::scoreChanged,
                     &s, &Student::onScoreChanged);
 
    // 用 emit 关键字发射信号,参数随信号传出
    emit s.scoreChanged(95);     // 输出:收到新分数: 95
 
    return 0;                    // 本例无界面,不必进入事件循环
}

三条关键规则:信号只需在 signals: 区声明,函数体由 moc 自动生成,永远不要自己写实现;发射信号用 emit 前缀(只是增强可读性的宏,不写也能发);槽本质是普通成员函数,除了能连信号,也可以像普通函数一样直接调用。

Success

槽函数声明在 public slots 还是 private slots 不影响连接,只影响访问权限,按封装需要选择即可。若槽是简单的打印、赋值逻辑,直接用 lambda 更省事;跨对象、被多处复用的响应逻辑才值得定义成正式槽函数。

带参数的信号槽

信号参数是信号向槽传递信息的通道。规则很直观:信号的第 1 个参数传给槽的第 1 个参数,以此类推。

// QSlider 拖动时发出:void valueChanged(int value)
// QProgressBar 提供:  void setValue(int value)
 
// 滑块一动,进度条同步 —— 参数类型一致,自动接上
connect(slider, &QSlider::valueChanged,
        bar,    &QProgressBar::setValue);

匹配规则允许”信号参数比槽多”(槽只取前几个),不允许反过来——槽需要的参数信号必须给得出来。类型不同但可隐式转换(如 intdouble)也可以连,类型检查在编译期由函数指针写法完成。

// 信号 4 个参数,槽只要 1 个 —— 合法,槽取第一个参数
connect(process, &Media::progressChanged,
        bar,     &QProgressBar::setValue);

断开连接

connect 返回一个 QMetaObject::Connection 对象,代表这条连接关系,断开有两种方式:

// 方式一:自动断开(最常见)
// 接收者对象析构时,Qt 自动断开所有与它相关的连接,无需手动干预
{
    Student s;
    QObject::connect(&s, &Student::scoreChanged, &s, &Student::onScoreChanged);
}   // s 销毁,连接自动失效
 
// 方式二:手动断开
auto conn = connect(button, &QPushButton::clicked,
                    this,   &MainWindow::refresh);
disconnect(conn);            // 断开这一条
// 或按对象断开:
disconnect(button, &QPushButton::clicked, this, &MainWindow::refresh);

手动断开的典型场景:动态切换响应逻辑(比如编辑模式与预览模式接不同的槽)、或防止重复 connect 时先断旧连接。

Success

多线程场景下 connect 的第五个参数 Qt::ConnectionType 有讲究:同线程默认直连(立即执行槽),跨线程默认队列连接(槽被投递到接收者线程的事件队列执行)。Qt::AutoConnection 自动判断线程归属,日常保持默认即可,涉及多线程通信时再深入学习。

本篇小结

信号槽是被类型安全和生命周期管理加持过的回调机制;新代码统一用函数指针写法 connect,短逻辑用 lambda;自定义信号只声明不实现,emit 发射;参数沿位置传递,信号参数可以多于槽参数;接收者析构自动断开,特殊场景手动 disconnect。