Qt 信号与槽机制详解(二):连接方式详解与面试指南

📑 目录

  • 🔗 [1. 信号与槽的连接方式](#1. 信号与槽的连接方式)
    • 🔹 [1.1 一对一](#1.1 一对一)
    • 🔹 [1.2 一对多](#1.2 一对多)
    • 🔹 [1.3 多对一](#1.3 多对一)
  • 🧩 [5. 信号和槽的其他说明](#5. 信号和槽的其他说明)
    • ✂️ [5.1 信号与槽的断开](#5.1 信号与槽的断开)
    • 🕰️ [5.2 Qt4 版本信号与槽的连接](#5.2 Qt4 版本信号与槽的连接)
    • 🚀 [5.3 使用 Lambda 表达式定义槽函数](#5.3 使用 Lambda 表达式定义槽函数)
    • ⚖️ [5.4 信号与槽的优缺点](#5.4 信号与槽的优缺点)
  • 🎓 [6. 小总结](#6. 小总结)
  • 🎯 7. 经典面试题与解答(#7-经典面试题与解

🔗 1. 信号与槽的连接方式信号与槽的连接方式

📌 本章导读 :信号与槽的连接方式主要分为一对一、一对多、多对一三种。理解这些连接方式,是掌握 Qt 事件### 🔹 1.1 一对一## 1.1 一对一

🔗 概念 :一对一连接主要有两种形式,分别是:一个信号连接一个槽 和 一个信号连接一个信号。

(1)一个信号连接一个槽

💭 设计初衷:Qt 中信号和槽,connect 这个机制,设想是美好的?

1)解耦合

把触发用户操作的控件和处理对应用户的操作逻辑解耦合。

2)"多对多"效果

一个信号可以 connect 多个槽函数,一个槽函数可以被多个信号 connect。

📝 类比理解:设计数据库的表结构,就需要理清楚实体与实体之间的关系。Qt 引入信号和槽机制,最本质的目的(初心)是为了让信号和槽之间按照"多对多"的方式来进行关联。其他的 GUI 框架往往也不具备这样的特性。
⚠️ 实际经验:实际上,随着程序开发这件事,大家的经验越来越多。其实在 GUI 开发的过程中,"多对多"其实是一个伪需求。实际开发过程中很少会遇到,绝大多数场景下,一对一就够用了。

📋 示例:

1、在 widget.h 中声明信号和槽以及信号发射函数;

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

(2)一个信号连接另一个信号

🔄 说明:信号不仅可以连接槽函数,还可以连接另一个信号,实现信号的"接力传递"。

📋 示例:

在上述示例的基础上,在 widget.cpp 文件中添加如下代码:

!在这里插入图片描述(https://i-blog.csdnimg.cn/direct/162f4e9eeb314202a54ca### 🔹 1.2 一对多png)

1.2 一对多

  • 一个信号连接多个槽

(1)在 widget.h 头文件中声明一个信号和三个槽;

(2)在 widget.cpp 文件中实现槽函数以及连接信号和槽;

!在这里插入图片描述(https://i-blog.csdnimg.cn/direct/da5364a43ab74faeb619c### 🔹 1.3 多对一ng)

1.3 多对一

🎯 概念:多个信号连接一个槽函数。无论哪个信号被触发,都会调用同一个槽函数来处理。

📋 示例:

(1)在 widget.h 头文件中声明两个信号以及一个槽;

(2)在 widget.cpp 文件中实现槽函数以及连接信号和槽;

!在这里插入图片描述(https://i-blog.csdnimg.cn/direct/4f4829d166594614861c98e9## 🧩 5. 信号和槽的其他说明-

5. 信号和槽的其他说明

📌 本章导读:除了基本的连接方式,信号与槽还有一些重要的补充知识点,包括断开连接、Qt4 旧版语法以及 Lambda 表达式等高级用法。

5.1 信号与槽的断开

✂️ 概念 :使用 disconnect 即可完成断开。disconnect 的用法和 connect 基本一致。

📋 示例:

💡 经验之谈 :disconnect() 实际上开发中使用非常少。


5.2 Qt4 版本信号与槽的连接

🕰️ 背景 :Qt4 中的 connect 用法和 Qt5 相比是更复杂的。需要搭配 SIGNAL 和 SLOT 宏来完成。而且缺少必要的函数类型的检查,使代码更容易出错。

📋 示例:

(1)在 widget.h 头文件中声明信号和槽;

(2)在 widget.cpp 文件中实现槽函数以及连接信号与槽;

⚖️ Qt4 版本信号与槽连接的优缺点:

  • ✅ 优点:参数直观;
  • ❌ 缺点:参数类型不做检测;

示例:

5.3 使用 Lambda 表达式定义槽函数

🚀 概述 :Qt5 在 Qt4 的基础上提高了信号与槽的灵活性,允许使用任意函数作为槽函数。但如果想方便的编写槽函数,比如在编写函数时连函数名都不想定义,则可以通过 Lambda 表达式 来达到这个目的。
📖 背景知识:Lambda 表达式是 C++11 增加的特性。C++11 中的 Lambda 表达式用于定义并创建匿名的函数对象,以简化编程工作。

📐 Lambda 表达式的语法格式如下:

cpp 复制代码
[ capture ] ( params ) opt -> ret { 
    Function body; 
};

📋 参数说明:

部分 说明
capture 捕获列表
params 参数表
opt 函数选项
ret 返回值类型
Function body 函数体
1、局部变量引入方式 [ ]

📦 说明 :[ ] 标识一个 Lambda 表达式的开始。不可省略。

符号 说明
[ ] 局部变量捕获列表。Lambda 表达式不能访问外部函数体的任何局部变量
[a] 在函数体内部使用值传递的方式访问 a 变量
[&b] 在函数体内部使用引用传递的方式访问 b 变量
[=] 函数外的所有局部变量都通过值传递的方式使用,函数体内使用的是副本
[&] 以引用的方式使用 Lambda 表达式外部的所有变量
[=, &foo] foo 使用引用方式,其余是值传递的方式
[&, foo] foo 使用值传递方式,其余引用传递
[this] 在函数内部可以使用类的成员函数和成员变量,= 和 & 形式也都会默认引入

⚠️ 注意事项 :由于使用引用方式捕获对象会有局部变量释放了而 Lambda 函数还没有被调用的情况。如果执行 Lambda 函数,那么引用传递方式捕获进来的局部变量的值不可预知。所以绝大多数场合使用的形式为:[=] () { }
🛠️ 版本提示 :早期版本的 Qt,若要使⽤ Lambda 表达式,要在 .pro 文件中添加:CONFIG += C++11。因为 Lambda 表达式是 C++11 标准提出的。Qt5 以上的版本无需手动添加,在新建项目时会自动添加。

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

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

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

2、函数参数 ( )

📥 说明 :(params) 表示 Lambda 函数对象接收的参数,类似于函数定义中的小括号表示函数接收的参数类型和个数。参数可以通过按值(如:(int a, int b))和按引用(如:(int &a, int &b))两种方式进行传递。函数参数部分可以省略,省略后相当于无参的函数。

📋 示例:

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include<QDebug>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    auto func = [](int x,int y){return x * y;};

    int res = func(30,20);
    qDebug()<<"res = "<<res;//600

}
Widget::~Widget()
{
    delete ui;
}
3、选项 Opt

⚙️ 说明 :Opt 部分是可选项,最常用的是 mutable 声明,这部分可以省略。Lambda 表达式外部的局部变量通过值传递进来时,其默认是 const,所以不能修改这个局部变量的拷贝,加上 mutable 就可以修改。

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include<QDebug>
#include<QPushButton>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    QPushButton* btn1 = new QPushButton("btn1",this);
    QPushButton* btn2 = new QPushButton("btn2",this);

    //定义局部变量
    int n = 100;

    btn1->move(300,100);
    btn2->move(500,100);

    //添加mutable声明后,可以修改局部变量n的拷贝
    connect(btn1,&QPushButton::clicked,this,[n]()mutable{n = 20;qDebug()<<n;});//输出20

    //没有添加mutable声明,局部变量n的拷贝不能修改
    connect(btn2,&QPushButton::clicked,this,[n](){qDebug()<<n;});//输出10
}
Widget::~Widget()
{
    delete ui;
}
4、Lambda 表达式的返回值类型 ->

📤 说明:可以指定 Lambda 表达式返回值类型;如果不指定返回值类型,则编译器会根据代码实现为函数推导一个返回类型;如果没有返回值,则可忽略此部分。

📋 示例1:

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include<QDebug>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    auto func = [](int x,int y)->int{return x * y;};

    int res = func(30,20);
    qDebug()<<"res = "<<res;//600

}
Widget::~Widget()
{
    delete ui;
}

📋 示例2:

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include<QDebug>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    auto func = [](int x,int y){return x * y;};
    auto func1 = [](int x,int y)->int{return x * y;};

    int res = func(30,20);
    int res1 = func1(30,20);
    qDebug()<<"res = "<<res;//600
    qDebug()<<"res1 = "<<res;//600

}
Widget::~Widget()
{
    delete ui;
}
5、Lambda 表达式的函数体 { }

📄 说明 :Lambda 表达式的函数体部分与普通函数体一致。用 { } 标识函数的实现,不能省略,但函数体可以为空。

📋 示例:

cpp 复制代码
//Lambda表达式函数体为空
[](){};
6、槽函数使用 Lambda 表达式来实现

🎯 说明:后续如果我们对应的槽函数比较简单,而且是一次性使用的,就经常会写作这种 lambda 的形式。

📋 示例1:点击按钮关闭窗口

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include<QDebug>
#include<QPushButton>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    resize(500,400);
    QPushButton* btn = new QPushButton("关闭",this);
    connect(btn,&QPushButton::clicked,this,[=](){
        this->close();
    });
}
Widget::~Widget()
{
    delete ui;
}

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

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include<QDebug>
#include<QPushButton>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    resize(500,400);
    QPushButton* btn = new QPushButton("关闭",this);
    connect(btn,&QPushButton::clicked,[=](){
        this->close();
    });
}
Widget::~Widget()
{
    delete ui;
}

📌 理解:后续如果我们对应的槽函数比较简单,而且是一次性使用的,就经常会写作这种lambda的形式


5.4 信号与槽的优缺点

⚖️ 总结:信号与槽机制是 Qt 的核心特性,但它也有自身的优缺点。

✅ 优点:松散耦合

信号发送者不需要知道发出的信号被哪个对象的槽函数接收,槽函数也不需要知道哪些信号关联了自己,Qt 的信号槽机制保证了信号与槽函数的调用。支持信号槽机制的类或者父类必须继承于 QObject 类。

📝 补充理解:

  • 高耦合:一个模块出问题,另一些模块会受到影响,导致不能正常运行。
  • 低耦合:一个模块出问题,另一些模块不会受到影响,可以正常运行。
  • 低内聚:写代码的时候,实现某个功能点,围绕这个功能的相关代码被放到整个项目的各个地方。
  • 高内聚:写代码的时候,某个功能点的代码被集中放在一起了。

❌ 缺点:效率较低

与回调函数相比,信号和槽稍微慢一些,因为它们提供了更高的灵活性,尽管在实际应用程序中差别不大。通过信号调用的槽函数比直接调用的速度慢约 10 倍(这是定位信号的接收对象所需的开销;遍历所有关联;编组/解组传递的参数;多线程时,信号可能需要排队),这种调用速度对性能要求不是非常高的场景是可以忽略的,是可以满足绝大部分场景。

💡 深入理解:一个客户端程序中,最慢的环节往往是"人"。假设本身基于回调的方式是 10us,使用信号槽的方式是 100us。对于使用程序的人来说,是感知不到的。


6. 小总结

🎓 本章核心要点回顾:

  1. 连接方式:信号与槽支持一对一、一对多、多对一三种连接方式,满足不同场景下的解耦需求。
  2. 断开连接 :使用 disconnect 可以断开信号与槽的关联,但实际开发中使用较少。
  3. Qt4 vs Qt5 :Qt4 使用 SIGNAL 和 SLOT 宏,参数直观但缺少类型检查;Qt5 简化了语法,支持函数指针,更加安全灵活。
  4. Lambda 表达式:Qt5 支持使用 Lambda 表达式作为槽函数,特别适合简单、一次性使用的场景,代码更加简洁。
  5. 优缺点:信号与槽最大的优点是松散耦合,缺点是相比回调函数效率略低(约慢 10 倍),但对绝大多数应用场景可以忽略不计。

💡 自定义槽函数:自定义槽函数非常关键,开发中大部分情况都是需要自定义槽函数的。槽函数就是用户触发某个操作之后,要进行的业务逻辑。自定义信号比较少见,实际开发中很少需要自定义信号------信号就对应到用户的某个操作,在 GUI 中,用户能够进行某些操作是可以穷举的,Qt 中内置的信号基本能够覆盖掉上述所有可能的用户操作了。


7. 经典面试题与解答

🎯 以下整理了几道关于 Qt 信号与槽的经典面试题,帮助你在面试前快速复习。

面试题 1:请简述 Qt 中信号与槽的本质是什么?

✅ 参考答案:

信号的本质是事件。当用户对窗口或控件进行了某些操作(如按钮单击、双击、窗口刷新、鼠标移动、键盘输入等),导致窗口或控件产生了某个特定事件,这时 Qt 对应的窗口类会发出某个信号,以此对用户的操作做出反应。信号的呈现形式就是函数,某个事件产生了,Qt 框架就会调用某个对应的信号函数,通知使用者。

槽(Slot)就是对信号响应的函数。槽就是一个函数,与一般的 C++ 函数是一样的,可以定义在类的任何位置(public、protected 或 private),可以具有任何参数,可以被重载,也可以被直接调用(但是不能有默认参数)。槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。

信号和槽机制底层是通过函数间的相互调用实现的。每个信号都可以用函数来表示,称为信号函数;每个槽也可以用函数表示,称为槽函数。

面试题 2:信号函数和槽函数有哪些书写规范?

✅ 参考答案:

自定义信号函数书写规范:

  1. 自定义信号函数必须写到 signals 下;
  2. 返回值为 void,只需要声明,不需要实现;
  3. 可以有参数,也可以发生重载。

自定义槽函数书写规范:

  1. 早期的 Qt 版本要求槽函数必须写到 public slots 下,但是现在高级版本的 Qt 允许写到类的 public 作用域中或者全局下;
  2. 返回值为 void,需要声明,也需要实现;
  3. 可以有参数,可以发生重载。

面试题 3:信号与槽的参数匹配规则是什么?

✅ 参考答案:

信号函数的参数列表要和对应连接的槽函数参数列表一致。此时信号触发,调用到槽函数的时候,信号函数中的实参就能够被传递到槽函数的形参当中。

具体规则是:信号的参数个数可以多于槽函数的参数个数,但是槽的参数个数不能多于信号参数个数。类型和个数要满足要求(信号的参数个数要多于槽的参数个数)。但是实际开发中最好还是保持参数个数也能匹配一致。

面试题 4:Qt4 和 Qt5 中信号与槽的连接有什么区别?

✅ 参考答案:

Qt4 中的 connect 用法和 Qt5 相比更复杂,需要搭配 SIGNAL 和 SLOT 宏来完成,而且缺少必要的函数类型的检查,使代码更容易出错。优点是参数直观,缺点是参数类型不做检测。

Qt5 之后,对上述的写法进行了简化,不再需要写 SIGNAL 和 SLOT 宏了,给 connect 提供了重载版本,重载版本中,第二个和第四个参数成了泛型参数,允许传入任意类型的函数指针,编译期会进行类型检查,更加安全。

面试题 5:信号与槽机制的优缺点是什么?

✅ 参考答案:

优点:松散耦合。 信号发送者不需要知道发出的信号被哪个对象的槽函数接收,槽函数也不需要知道哪些信号关联了自己,Qt 的信号槽机制保证了信号与槽函数的调用。支持信号槽机制的类或者父类必须继承于 QObject 类。

缺点:效率较低。 与回调函数相比,信号和槽稍微慢一些,因为它们提供了更高的灵活性。通过信号调用的槽函数比直接调用的速度慢约 10 倍(这是定位信号的接收对象所需的开销;遍历所有关联;编组/解组传递的参数;多线程时,信号可能需要排队),这种调用速度对性能要求不是非常高的场景是可以忽略的,是可以满足绝大部分场景。

相关推荐
霸道流氓气质1 小时前
LangGraph式图编排引擎Java实现:状态机模型、Checkpoint恢复与生产级落地实战
java·开发语言·数据库
2601_962218611 小时前
C++ AVL树概念与实现详解
开发语言·c++
炘爚1 小时前
C++(STL)
开发语言·c++
波力海苔夹心脆6751 小时前
C# 海康威视摄像头二次开发入门:HCNetSDK 登录、实时预览、云台控制、录像与抓图(WinForms 实战)
开发语言·windows·经验分享·tcp/ip·c#
数据狐(Datafox)1 小时前
淘宝商品详情API实战:多语言代购商城自动同步数据完整方案
开发语言·前端·数据库·爬虫·json
芯次元玩家2 小时前
技术岗转物联网解决方案架构师,行业洞察、商业模式该从哪开始学?
java·开发语言
2601_962218612 小时前
C++ string 类原理、踩坑与对象语义详解
开发语言·c++
隔窗听雨眠2 小时前
FunProxy用Rust构建跨平台全链路测试抓包代理工具
开发语言·后端·rust
y = xⁿ2 小时前
关于Agent智能体开发
开发语言·前端·javascript