在上一篇文章中,我们已经系统学习了Qt信号与槽的基本概念,并完成了从认识信号与槽,到自定义信号槽,再到带参数信号槽的完整学习过程。不过,当项目稍微复杂一些之后,只掌握最基础的连接方式就不太够用了。
一个信号能不能连接多个槽?多个信号又能不能连接同一个槽?信号之间能不能互相连接?已经建立的连接又该如何断开?除此之外,现在Qt开发中也经常使用Lambda表达式来处理简单的槽逻辑,那么 Lambda 到底应该怎么写,又为什么适合用在信号与槽中?
这些问题,正是本文要继续解决的。
本文将在上一篇的基础上进一步深入信号与槽的进阶用法,从多种连接关系开始,逐步介绍信号槽的断开、不同 Qt 版本的连接方式以及 Lambda 表达式的使用。同时,也会讨论信号与槽的优缺点,帮助你不仅知道"怎么用",还能够理解什么时候这样用更合适。
掌握这些内容之后,面对实际Qt项目中的复杂对象交互,就不会只停留在最基础的connect()使用层面了。
目录
[1.1 一对一连接------一个信号对应一个槽](#1.1 一对一连接——一个信号对应一个槽)
[1.1.1 一个信号连接一个槽](#1.1.1 一个信号连接一个槽)
[1.1.2 一个信号连接另一个信号](#1.1.2 一个信号连接另一个信号)
[1.2 一对多连接------一个信号连接多个槽](#1.2 一对多连接——一个信号连接多个槽)
[1.3 多对一连接------多个信号连接一个槽](#1.3 多对一连接——多个信号连接一个槽)
[1.4 连接方式总结](#1.4 连接方式总结)
[2.1 如何断开信号与槽的连接?](#2.1 如何断开信号与槽的连接?)
[2.2 Qt4版本信号与槽的连接](#2.2 Qt4版本信号与槽的连接)
[2.3 使用Lambda表达式实现槽函数](#2.3 使用Lambda表达式实现槽函数)
[2.3.1 Lambda表达式的捕获列表\[\]](#2.3.1 Lambda表达式的捕获列表[])
[2.3.2 Lambda表达式的参数列表()](#2.3.2 Lambda表达式的参数列表())
[2.3.3 Lambda表达式的mutable选项](#2.3.3 Lambda表达式的mutable选项)
[2.3.4 Lambda表达式的返回值类型 ->](#2.3.4 Lambda表达式的返回值类型 ->)
[2.3.5 Lambda表达式的函数体 {}](#2.3.5 Lambda表达式的函数体 {})
[2.3.6 使用Lambda表达式实现槽函数](#2.3.6 使用Lambda表达式实现槽函数)
[2.4 信号与槽的优缺点](#2.4 信号与槽的优缺点)
[2.4.1 优点------松散耦合](#2.4.1 优点——松散耦合)
[2.4.2 缺点------存在一定的调用开销](#2.4.2 缺点——存在一定的调用开销)
[2.5 信号与槽补充知识](#2.5 信号与槽补充知识)
[2.5.1 两个容易忽略的信号与槽细节](#2.5.1 两个容易忽略的信号与槽细节)
[3.1 信号与槽核心知识回顾](#3.1 信号与槽核心知识回顾)
一、信号与槽的连接方式
1.1 一对一连接------一个信号对应一个槽
一对一是信号槽中最基础的连接形态,又可以细分成两种:一个信号连接一个槽,以及一个信号连接另一个信号。
1.1.1 一个信号连接一个槽

这是最常见、也最直观的用法。当发送者的某个信号触发时,接收者的对应槽函数会被自动调用。比如,把按钮的clicked()信号连接到窗口的close()槽上,就能实现"点击按钮,窗口关闭"的效果。
示例:在widget.h中声明信号、槽以及信号发射函数

在widget.cpp中实现槽函数、信号发射函数以及连接信号和槽

1.1.2 一个信号连接另一个信号

继续沿用上面的示例,我们在widget.cpp中再补一段代码,看看信号和信号之间是怎么完成"接力"的:

1.2 一对多连接------一个信号连接多个槽
信号和槽之间,从来不是非得一对一。一个信号完全可以同时连上好几个槽,信号一响,这些槽就会被依次拉起,谁都不会漏。

下面我们在widget.h里声明一个信号和三个槽,看看这种"一呼多应"的场面是怎么搭起来的。
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget();
signals:
// 只声明这一个信号,后面会让它同时驱动三个槽
void mySignal(const QString& text);
public slots:
// 三个槽,功能各不相同
void handleSlotOne(const QString& text);
void handleSlotTwo(const QString& text);
void handleSlotThree(const QString& text);
};
#endif // WIDGET_H
在widget.cpp里,我们只需把同一个信号重复 connect 到三个不同的槽上:
cpp
#include "widget.h"
#include <QPushButton>
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
// 用一个按钮来触发信号
QPushButton *button = new QPushButton("发射信号", this);
button->move(100, 80);
// 同一个信号连接三个不同的槽
connect(this, &Widget::mySignal, this, &Widget::handleSlotOne);
connect(this, &Widget::mySignal, this, &Widget::handleSlotTwo);
connect(this, &Widget::mySignal, this, &Widget::handleSlotThree);
// 按钮点击触发 mySignal 信号
connect(button, &QPushButton::clicked, this, [this]() {
emit mySignal("一呼三应");
});
}
Widget::~Widget()
{
}
void Widget::handleSlotOne(const QString& text)
{
qDebug() << "槽 1 收到:" << text;
}
void Widget::handleSlotTwo(const QString& text)
{
qDebug() << "槽 2 收到:" << text;
}
void Widget::handleSlotThree(const QString& text)
{
qDebug() << "槽 3 收到:" << text;
}
1.3 多对一连接------多个信号连接一个槽
前面聊了一对多,现在反过来,多个信号,可以共同指向同一个槽。这也是信号槽机制里非常实用的一招:不同的事件来源,最终汇入同一套处理逻辑。

先在widget.h里声明两个信号和一个槽:
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget();
signals:
// 两个来源不同的信号
void signalFromButton();
void signalFromTimer();
public slots:
// 一个公共的槽函数,两个信号都会汇聚到这里
void commonSlot();
};
#endif // WIDGET_H
再在widget.cpp里,把两个信号都接到这个槽上:
cpp
#include "widget.h"
#include <QPushButton>
#include <QTimer>
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
QPushButton *button = new QPushButton("点击触发信号一", this);
button->move(100, 80);
QTimer *timer = new QTimer(this);
timer->setInterval(2000); // 每 2 秒触发一次
// 两个不同的信号,连到同一个槽
connect(button, &QPushButton::clicked, this, &Widget::commonSlot);
connect(timer, &QTimer::timeout, this, &Widget::commonSlot);
// 启动定时器,让第二个信号也开始工作
timer->start();
}
Widget::~Widget()
{
}
// 公共槽函数:无论哪个信号来了,都走这里
void Widget::commonSlot()
{
qDebug() << "公共槽函数被触发!";
}
这里有两个完全不同的信号来源:
-
一个是按钮的clicked信号,用户点击时触发。
-
一个是定时器的timeout信号,每2秒自动触发一次。
它们都通过connect指向了同一个槽函数commonSlot。也就是说,不管是用户点了按钮,还是定时器到点了,最终都会跑进commonSlot这同一个地方。
这种"多对一"的设计,最大的好处是复用。如果两件事的处理逻辑是一样的,就没必要写两份槽函数,直接把两个信号接过来共用一份代码。改逻辑的时候,也只改这一处,维护成本降了不少。
1.4 连接方式总结


二、信号与槽的进阶用法
2.1 如何断开信号与槽的连接?
连接用connect,断开自然就用disconnect。它的用法跟connect几乎一模一样,只是作用反过来,把已经建立的信号槽关系一刀两断。一旦断开,信号再发射,槽函数也不会被叫起来了。下面看一个简单的例子,感受一下"断开前"和"断开后"的区别:
cpp
#include "widget.h"
#include <QPushButton>
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
QPushButton *button = new QPushButton("点击", this);
button->move(100, 80);
// 连接:按钮点击 → 窗口关闭
connect(button, &QPushButton::clicked, this, &Widget::close);
// 断开:把刚才建立的连接拆掉
disconnect(button, &QPushButton::clicked, this, &Widget::close);
}
这个例子里,我们先connect把"点击按钮"和"关闭窗口"连起来,紧接着又用disconnect把这条线剪断。结果就是:按钮怎么点,窗口都无动于衷,因为信号和槽之间的联系已经不存在了。disconnect的参数跟connect一样,也是四个核心项:
cpp
disconnect(sender, signal, receiver, method);
- sender:信号发送者。
- signal:要断开的信号。
- receiver:信号接收者。
- method:要断开的槽函数。
所以,disconnect不是随便乱断的,它必须精准指出"谁和谁之间的哪条连接"要拆掉。断错了,别的连接不受影响;断对了,那对信号槽就彻底失联。
实际开发里,什么时候会用到disconnect?常见场景有这么几种:
- **临时禁用某个响应:**比如某个操作执行期间,先断开一个按钮的信号,防止用户乱点;操作完成后再connect回来。
- **动态切换行为:**同一个按钮,在不同阶段要触发不同的槽,那就先断开旧的,再连接新的。
- **释放对象前解除绑定:**有的对象生命周期结束了,但信号还跟别的槽连着,提前断开可以避免悬挂指针的隐患。
2.2 Qt4版本信号与槽的连接
Qt4时期的connect用法,要比Qt5复杂不少。那时候还不能直接传函数指针,必须配合SIGNAL和SLOT宏,把信号和槽转成字符串再交给connect处理。这种写法少了编译期的类型检查,函数名拼错、参数写错,编译都不会报错,问题全都要等到运行时才暴露,调试起来相当折磨人。
下面是Qt4风格的典型示例。在widget.h中声明信号和槽:
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
~Widget();
signals:
void mySignal(const QString& text);
public slots:
void mySlot(const QString& text);
};
#endif // WIDGET_H
在widget.cpp中实现槽函数并连接信号与槽
cpp
#include "widget.h"
#include <QPushButton>
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
// 创建一个按钮,父对象为当前窗口
QPushButton *button = new QPushButton("点击", this);
button->move(100, 80);
// Qt4 风格连接:必须用 SIGNAL 和 SLOT 宏包一层
connect(button, SIGNAL(clicked()),
this, SLOT(mySlot(const QString&)));
}
Widget::~Widget()
{
}
// 槽函数实现:收到信号后,把窗口标题改为信号携带的文本
void Widget::mySlot(const QString& text)
{
this->setWindowTitle(text);
qDebug() << "槽函数收到:" << text;
}
这里有两个需要留心的地方:
SIGNAL和SLOT宏里只写函数签名,不写参数名,参数类型写在括号里即可。比如SLOT(mySlot(const QString&)),而不是SLOT(mySlot(const QString& text))。
类型完全靠字符串比对。一旦信号或槽的名字、参数类型拼错,编译期根本发现不了,运行时才会出现"连接失败"这类难以排查的问题。这就是为什么现在更推荐用 Qt5 的现代写法,把类型检查交给编译器。
Qt4 版本信号与槽连接的优缺点
-
优点:参数直观。 信号和槽的参数在宏里写得明明白白,一眼就能看出该传什么类型,调用关系比较直白。
-
缺点:参数类型不做编译期检测。 因为信号和槽是靠字符串匹配的,拼错一个字母、写错一个参数类型,编译器都毫无察觉,问题会一直潜伏到运行时才爆发。排查起来很费劲,维护成本也高。

2.3 使用Lambda表达式实现槽函数
Qt5在Qt4的基础上,把信号与槽的灵活性又拔高了一截,槽函数不再非得是类里的成员函数,任意可调用对象都能直接上岗。
但如果你更进一步,想在写槽函数的时候连函数名都懒得起,那 Lambda 表达式就是为你准备的。它允许你在connect的时候直接把处理逻辑现场写出来,声明、实现、连接一气呵成,省得再跑到类里去加一个专门的槽。
Lambda表达式是C++11引入的一个利器,专门用来定义并创建匿名的函数对象。它最大的好处,就是让那些"只在一个地方用一下"的小逻辑,不用再一本正经地起名字、找位置,就地写就地用。它的语法格式长这样:
cpp
[ capture ] ( params ) opt -> ret {
Function body;
};
别被这一串符号吓到,拆开来看,其实就五块:
| 部分 | 说明 |
| capture | 捕获列表:把函数体外面用到的变量抓进来。 |
| params | 参数表:跟普通函数一样,要什么参数写什么。 |
| opt | 函数选项:比如mutable之类的修饰。 |
| ret | 返回值类型:不写的话,编译器自己推导。 |
| Function body | 函数体:真正干活的代码。 |
|---|
2.3.1 Lambda表达式的捕获列表\[\]
Lambda表达式最让人又爱又恨的地方,八成就是开头那个方括号,捕获列表。它就像是Lambd对外部世界开的一道门:你想让函数体用上外面的变量?可以,但得先在这张清单上登记好。如果清单是空的,那Lambda就彻底"与世隔绝",外面的局部变量一个都碰不到。捕获方式有很多种,我们一个一个来看:
-
[]:空捕获列表。两耳不闻窗外事,函数体里只能用自己的参数,外面的变量全都看不见摸不着。 -
[a]:按值捕获变量a。相当于复印了一份,你在Lambda里面怎么改,外面的a纹丝不动。 -
[&b]:按引用捕获变量b。相当于借来了b的地址,里面改它,外面的b也会跟着变。 -
[=]:把外部所有局部变量都按值捕获。用起来省心,所有变量都是副本,但变量多了拷贝开销也会上来。 -
[&]:把所有局部变量都按引用捕获。上手很猛,但风险也高,变量一旦提前释放,你就抓空了。 -
[=, &foo]:默认按值捕获,唯独foo搞特殊,按引用捕获。 -
[&, foo]:默认按引用捕获,唯独foo按值捕获。 -
[this]:把当前对象的this指针抓进来,这样函数体里就能直接使用类的成员变量和成员函数。注意,[=]和[&]实际上也会默认把this带进来。
有一点必须重点提醒:引用捕获是个危险动作。 如果Lambda函数还没被执行,外部的局部变量却先一步销毁了,那这个引用就变成了悬空引用,里面的值不可预知,稍不留神就是内存错误。所以,除非你非常清楚变量和Lambda的生命周期谁长谁短,否则绝大多数场合,老老实实用 = 按值捕获才是保命之选。
最后提一嘴版本问题。Lambda表达式是C++11才有的特性,早期Qt版本如果要用它,得在.pro文件里手动加上:

示例1:Lambda表达式的使用。

示例2:以 方式传递,外部的所有变量在Lambda表达式中都可以使用。

示例3:以a方式传递,在Lambda表达式中只能使用传递进来的a。

2.3.2 Lambda表达式的参数列表()
讲完捕获列表,再看它后面的那对小括号。在Lambda表达式里,紧跟捕获列表之后的小括号,就是参数列表。它和普通函数的小括号作用完全一样:告诉编译器这个Lambda要接收什么类型、几个参数。
参数传递方式也不新鲜,就两种,按值和按引用。(int a, int b)是按值传,(int &a, int &b)是按引用传。
如果这个Lambda压根不需要参数,直接省略小括号就行,省掉之后它就是一个无参函数。
有个细节值得留意:如果Lambda同时还要加mutable之类的修饰,或者需要显式标注返回值类型,那即使参数为空,小括号也得老老实实写上。不然编译器可能把后面的修饰当成参数列表的一部分,语法就乱了。所以省括号可以,但要分场合。

2.3.3 Lambda表达式的mutable选项
Opt部分是个可选项,其中最常用的就是mutable声明。它完全可以根据需要省略。
这里有个很容易踩坑的细节:Lambda按值捕获进来的外部局部变量,默认是const的。换句话说,它给你的是一个"只读副本",你可以拿着看,但不能动手改。如果非要在Lambda内部修改这个副本,就得在参数列表后面加上mutable关键字,相当于给这个副本解了锁。
所以,mutable就是那把打开"只读副本"的钥匙。加上它,你就能在函数体里修改按值捕获的变量;不加,就老老实实当个读客。不过要记住,改的始终是副本,外面的原变量依然纹丝不动。

2.3.4 Lambda表达式的返回值类型 ->
->后面跟着的,就是Lambda的返回值类型。这个语法叫"尾随返回类型",类型写在参数列表后面,而不是前面。
但实际写代码的时候,我们很少手动去写它。原因很简单:如果你不指定,编译器会根据函数体里的return语句自动推导出一个类型。你只管写逻辑,类型的事交给编译器。
如果Lambda压根没有返回值,函数体里没有return,或者只是调用别的函数、打印点东西,那这部分直接省略就行,编译器会把它当成void处理。
所以这个->的存在感其实很低,但有两个场景它必须出场:
-
函数体里有多个
return,且返回类型不一致:编译器推导不出来,必须显式指定。 -
Lambda写得很复杂,你想让返回类型一目了然:显式标注一下,读代码的人就不用猜了。


2.3.5 Lambda表达式的函数体 {}
Lambda的函数体,跟普通函数没什么两样,用一对花括号把实现包起来。这对花括号不能省略,哪怕里面空空如也,也得老老实实写上。你想在信号一响时顺手干点什么,就全写在这对括号里。

2.3.6 使用Lambda表达式实现槽函数
示例1:点击按钮关闭窗口。

示例2:当"connect"函数第三个参数为"this"时,第四个参数使用Lambda表达式时,可以省略掉"this"------

2.4 信号与槽的优缺点
2.4.1 优点------松散耦合
信号与槽最迷人的地方,就是它把"谁发消息"和"谁处理消息"彻底解耦了。信号发送者根本不需要知道自己的信号会被哪个对象的哪个槽接住;槽函数也不用关心有哪些信号连到了自己头上。两者各自独立,只通过connect这一条细线牵着。
这种松耦合带来的好处是实打实的:控件之间、模块之间的依赖被降到最低,代码可以更自由地组装和拆分。当然,要享受这套机制有个前提,支持信号槽的类,自己或者它的父类,必须继承自QObject。这是门票,没它上不了车。
2.4.2 缺点------存在一定的调用开销
跟裸回调相比,信号和槽确实要慢一些。这份慢,是它为"高度灵活性"付的代价。通过信号调用槽函数,比直接函数调用慢上大约10倍。这10倍的耗时都花在哪了?定位信号的接收对象、遍历所有关联、编组和解组传递的参数,如果跨线程,信号还可能要排队等调度。
但说句公道话,这点开销对绝大多数应用来说,根本不值一提。一个客户端程序里,最慢的环节从来不是信号槽,而是人。
假设基于回调的方式跑一次要10微秒,换成信号槽要100微秒。对于坐在屏幕前点按钮的人来说,这90微秒的差距,人类肉眼根本感知不到。用户点一下鼠标,光手指按下去的时间,都已经甩这十几条街了。
所以,除非你在写高频交易系统、实时音视频处理这种对延迟苛刻到微秒级的场景,否则信号槽的性能完全够用。用那点微不足道的速度,换来代码的清晰和解耦,这笔买卖,稳赚不赔。
2.5 信号与槽补充知识
2.5.1 两个容易忽略的信号与槽细节






三、信号与槽总结
3.1 信号与槽核心知识回顾




如果这篇文章对你有帮助,欢迎点赞、收藏、关注三连支持。你的每一个正反馈,都是我继续肝下一篇的最大动力。我们下篇见。