Note
本篇讲解 Qt 最重要的对象间通信机制——信号与槽。你将理解它为什么优于传统回调函数,学会 connect 的多种写法(字符串宏、函数指针、lambda),并亲手自定义信号和槽、掌握带参数的连接与断开连接。这是 Qt 面试和实际开发中出现频率最高的知识点,适合读完上一篇、能让窗口跑起来的读者。
为什么需要信号与槽
图形界面本质上就是”事件驱动”:用户点了按钮,程序要响应;下载完成了,界面要刷新。任何 GUI 框架都要解决同一个问题——对象 A 状态变化时,如何通知对象 B。
传统 C++ 的答案是回调函数:把 B 的函数指针交给 A,A 在状态变化时调用它。回调能用,但有明显缺陷:
- 类型不安全:裸函数指针没有类型信息,签名不匹配编译期发现不了,运行时才崩。
- 强耦合:A 必须知道 B 的函数签名,甚至持有 B 的指针;B 析构后 A 里的指针就成了悬空指针。
- 只能一对一:一个回调位置挂一个函数,想多对象响应要自己实现列表。
Qt 的信号槽对回调做了系统性升级:
- 信号是对象发出的事件通知(如按钮的
clicked()),发出者不关心谁来接收。 - 槽是接收并响应信号的普通成员函数(如窗口的关闭逻辑)。
- connect 把两者关联起来,关联时校验签名,接收者销毁时自动断开,且支持一对多、多对一。
发出者与接收者彻底解耦——按钮不知道窗口的存在,窗口也不知道按钮的实现,双方只认”信号”和”槽”这两个接口。这就是 Qt 用元对象系统的运行时信息换来的能力。整条链路可以概括为下图:
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);匹配规则允许”信号参数比槽多”(槽只取前几个),不允许反过来——槽需要的参数信号必须给得出来。类型不同但可隐式转换(如 int → double)也可以连,类型检查在编译期由函数指针写法完成。
// 信号 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。
自测:三个问题检验掌握程度
- 相比传统回调函数,信号与槽解决了哪三个痛点?
- 自定义信号需要写函数实现吗?用什么关键字发射信号?
- “信号参数比槽参数多”和”槽参数比信号参数多”,哪种情况合法?
(答案都在上文)