Qt 深入解析信号与槽-面试必考

概念

在 Qt 编程中,信号与槽机制是实现对象间通信的核心工具。

信号(Signal):对象状态的变化或事件发生时发出的通知,本质上是一种特殊的成员函数声明,它不包含函数体,仅用于通知其他对象某一事件的发生。例如,当用户点击界面上的按钮时,按钮对象就会发出clicked信号,告知系统 “按钮被点击了” 这一事件。

槽(Slot):用于响应信号的普通成员函数。它与普通 C++ 函数类似,可以有参数,也能被重载,并且可以定义在类的public、protected或private部分。不同之处在于,槽函数能够与信号建立连接,一旦与之关联的信号被发射,槽函数便会自动被调用,执行相应的操作。

连接(Connect)::将信号和槽关联起来的关键步骤。通过QObject::connect()函数,我们能够指定信号的发送者、信号本身、接收者以及对应的槽函数,从而构建起信号与槽之间的通信桥梁,使得信号发射时能够准确触发相应的槽函数。

Qt信号槽机制的优缺点

优点:

  1. 解耦合:信号槽机制允许对象之间进行松耦合的通信。发送信号的对象不需要知道接收槽的对象,反之亦然。这使得代码更加模块化和可维护。

  2. 灵活性:一个信号可以连接到多个槽,一个槽也可以连接到多个信号。这种多对多的关系使得设计复杂的应用程序变得更加灵活。

  3. 类型安全:信号和槽在编译时进行类型检查,确保传递的参数类型匹配。这减少了运行时错误的可能性。

  4. 易于使用:信号槽机制的语法简单直观,易于理解和使用。开发者可以轻松地在对象之间建立复杂的通信关系。

  5. 支持异步通信:信号槽机制支持异步通信,可以在不同的线程中发送信号和处理槽函数。这有助于构建响应迅速、高效的应用程序。

缺点:

  1. 性能开销:信号槽机制涉及额外的函数调用和数据传递,可能会带来一定的性能开销。特别是在高频调用的情况下,这种开销可能变得显著。

  2. 调试难度:由于信号槽机制的松耦合特性,跟踪和调试信号槽之间的连接可能比较困难。尤其是在大型项目中,信号槽的连接关系可能变得复杂。

Qt信号槽机制实现原理

核心原理在于通过元对象系统(Meta-Object System 简称MOS)来实现对象之间的解耦合和类型安全的连接。

元对象系统的概念

元对象系统是Qt中实现对象间通信的核心,提供了运行时类型信息(RTTI)、属性管理、信号和槽的动态连接等功能。它通过Qt的元对象编译器(Meta-Object Compiler, MOC)实现,MOC会在编译时期读取C++代码中的特定宏(如Q_OBJECT),并生成附加的元数据代码。

  • 元对象系统是一个基于标准C++的扩展,为Qt提供了信号与槽机制、实时类型信息、动态属性系统。

  • 元对象系统的三个基本条件:类必须继承自QObject、类声明Q_OBJECT宏(默认私有有)、元对象编译器moc。

  • 元对象编译器Meta-Object Compiler(moc)预处理器,提将qt的程序转换为标准的c++程序,再由标准c++编译器进行编译。它为高层次的事件处理自动生成所需要的必要代码。Qt 程序在交由标准编译器编译之前,先要使用 moc 分析 C++ 源文件。如果moc发现在一个类头文件中包含了宏 Q_OBJECT,则会生成以moc_className.cpp(自定义类名)的.cpp文件。这个源文件中包含了 Q_OBJECT 宏的实现代码。新的文件同样将进入编译系统,与原文件一起参与编译。

  • QMetaObject类提供了访问元对象信息的接口。通过QMetaObject,可以动态地获取类的元数据,包括类名、属性、方法、信号和槽等

声明与实现

信号和槽的本质都是函数。

我们知道C++中的函数要有声明(declare),也要有实现(implement),

而信号只要声明,不需要写实现。这是因为moc会为我们自动生成。

另外触发信号时,不写emit关键字,直接调用信号函数,也是没有问题的。

这是因为emit是一个空的宏

#define emit

Q_OBJECT

public: \
    Q_OBJECT_CHECK \
    QT_WARNING_PUSH \
    Q_OBJECT_NO_OVERRIDE_WARNING \
    static const QMetaObject staticMetaObject; \
    virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
    QT_TR_FUNCTIONS \
private: \
    Q_OBJECT_NO_ATTRIBUTES_WARNING \
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    QT_WARNING_POP \
    struct QPrivateSignal {}; \
    QT_ANNOTATE_CLASS(qt_qobject, "")

声明了一个只读的静态成员变量staticMetaObject,以及3个public的成员函数

原理

元对象编译器使用 moc 分析 C++ 源文件。如果moc发现在一个类头文件中包含了宏 Q_OBJECT,则会生成以moc_className.cpp(自定义类名)的.cpp文件,生成的cpp文件中,就是变量staticMetaObject以及 public中的函数实现。

staticMetaObject是一个结构体,用来存储类的信号、槽等元信息,并把qt_static_metacall静态函数作为函数指针存储起来。

因为是静态成员,所以实例化多少个对象,它们的元对象信息都是一样的。

qt_static_metacall函数提供了两种“元调用的实现”:

如果是InvokeMetaMethod类型的调用,则直接 把参数中的QObject对象,转换成目标类然后调用其信号函数

如果是IndexOfMethod类型的调用,即获取元函数的索引号,则计算信号函数的偏移并返回。

信号的触发

信号的实现,直接调用了QMetaObject::activate函数。其中0代表信号这个函数的索引号。QMetaObject::activate函数的实现,在Qt源码的QObject.cpp文件中,略微复杂一些,且不同版本的Qt,实现差异都比较大,大致的实现为先找出与当前信号连接的所有对象-槽函数,再逐个处理。这里处理的方式,分为三种:

if((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                || (c->connectionType == Qt::QueuedConnection)) {
    // 队列处理
} else if (c->connectionType == Qt::BlockingQueuedConnection) {
    // 阻塞处理
    // 如果同线程,打印潜在死锁。
} else {
    //直接调用槽函数或回调函数
}

receiverInSameThread表示当前线程id和接收信号的对象的所在线程id是否相等。

如果信号-槽连接方式为QueuedConnection,不论是否在同一个线程,按队列处理。

如果信号-槽连接方式为Auto,且不在同一个线程,也按队列处理。

如果信号-槽连接方式为阻塞队列BlockingQueuedConnection,按阻塞处理。

(注意同一个线程就不要按阻塞队列调用了。因为同一个线程,同时只能做一件事,本身就是阻塞的,直接调用就好了,

如果走阻塞队列,则多了加锁的过程。如果槽中又发了同样的信号,就会出现死锁:加锁之后还未解锁,又来申请加锁。)

队列处理,就是把槽函数的调用,转化成了QMetaCallEvent事件,通过QCoreApplication::postEvent放进了事件循环。

等到下一次事件分发,相应的线程才会去调用槽函数。

槽和moc生成

slot函数我们自己实现了,moc不会做额外的处理,所以自动生成的moc_Jerry.cpp文件中,只有Q_OBJECT宏的展开。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值