元对象系统

最终目标:实现信号槽机制

信号槽机制,间接的调用函数

实现信号槽机制,就是间接调用,通过反射实现,反射在运行过程中,动态的调整对象的结构,所以在对象之外,会分配新的空间来保存结构信息,这就是元对象。元对象系统能够在运行时处理一些通常只能在编译时决定的事情,比如根据传入的参数修改对象的不同属性、在运行中给对象增加一个数据成员或者是成员函数等等。

在这里插入图片描述

自省和反射

​ 自省是指在运行时检查对象的属性和方法。在 Qt 中,可以通过 QObject::metaObject() 方法获取对象的元对象,然后利用元对象可以获取到对象的属性、方法和信号等信息。
​ 反射是更加高级的自省。它允许在运行时动态修改对象的属性值、新增或删除一个属性或者方法,甚至还可以间接地调用方法。在 Qt 中,可以使用 QMetaObject::invokeMethod() 方法来动态地调用对象的方法,也可以使用QObject::setProperty() 和 QObject::property() 方法来动态地设置和获取对象的属性值。这种能力让我们可以根据运行时的需要动态地调用对象的方法和操作属性,从而实现更加灵活和动态的编程。

示例

在之前我们定义一个类

class T {
	int a;
}
// 不能定义一个函数判断是否定义有某个数据成员
bool isMember(string attr) {
}
// 因为在程序运行的时候只给对象分配内存,没有对象的结构分配内存

Qt是如何支持反射的?

反射的作用:在运行的时候获取和调整对象的结构信息

元对象系统

如果一个类支持元对象系统要求:

  • 继承QObject ,并且第一个继承(这样可以确保有分配内存,并且在合适的位置上)
  • 添加一个宏 Q_OBJECT
  • 不要放在main.cpp当中

在这里插入图片描述

Q_OBJECT宏

引入一堆操作元对象信息的成员函数

在这里插入图片描述

Qt代码的生成流程

默认情况下,认为main.cpp 是标准的C++代码

Qt中的 C++文件会经过MOC(元对象编译器处理伪关键字)将C++方言(qt)转化为标准的C++代码

ui文件 也会经过 UIC 编译 转化为C++代码,最后交给g++ 进行编译

所以,规定不能放在main.cpp 中

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

发射可以在运行时获取对象的结构信息

这样就可以间接访问对象的属性和方法

间接访问比直接访问更灵活,效率差一些

间接访问先访问 元对象metaobject,再访问数据

直接访问 直接通过 . ->进行访问

反射常见的应用场景

  1. 服务端路由,根据客户端传入的参数,查找对应的方法,然后调用相应的回调函数
  2. 导表(比如策划会设计游戏人物的属性,然后有程序员生成对应的代码,但是人物的属性会经常修改,这样导表会方便策划对属性进行修改)

在这里插入图片描述

信号槽

对象间通信 采用函数调用的方式

对象间通信有一种比较好的方式,就是函数调用的方式

主调函数发送参数给被调函数,被调函数回传结果给主调方

因为对象同处一片地址空间,内存是共享的,这样如果A想要给B发送消息,A找到B,然后调用B的成员函数,这样是最方便的

如果,B做的事情是A设计的,这就是回调的一种机制

A设计一个回调函数,A给B通信,A先找到B,B添加这个回调函数,然后再调用这个回调函数,这种回调函数的机制效率是很高的,(适用于服务端,功能较少,对象的个数少,但是对性能的要求高)

在这里插入图片描述

但是如果一个动作要发送给多个对象的时候,在A的代码,要写大量接收方的信息(用户界面,一个按键多个功能,例如客户端)。

在这里插入图片描述

这就需要信号槽机制

牺牲了一点性能,提高了代码的美观程度和可读性

在这里插入图片描述

两个无关的结构,放在一起是耦合,对应回调,提升代码的美观程度就是解耦,就是信号槽机制

在这里插入图片描述

使用信号槽

前提:支持元对象系统,间接调用

步骤:

​ 1 设计信号

​ 2 设计槽函数

​ 3 关联信号和槽函数

​ 4 在一个合适的地方调用发送信号

设计一个信号

信号只写声明,不写定义,MOC会自动生成定义

在这里插入图片描述

设计一个槽函数

在这里插入图片描述

关联

SIGNAL 和 SLOT 会将方法函数包装成一个字符串

在这里插入图片描述

发送信号

在这里插入图片描述

DirectConnection 直接连接,信号会直接调用槽函数,等槽函数执行完之后,信号的流程才会走完,是一种同步机制

QueueConnection 信号发送到队列中,排队,就相当于非阻塞模式,如果两个线程,线程1发送信号,线程2来做槽函数

BlockingQueuedConnectior 阻塞模式,线程1发信号卡在那里,等待线程2执行完成

在这里插入图片描述

connect的另一种重载形式

第二个参数是指向成员函数的指针

在C语言的版本中

void func() {}
// func 和 &func 都是函数的指针

//但是在C++中,出现模板之后会传类型,就不知道是指针还是类型
class A {
 void method() {}
}
// A::method 是函数
// & A::method 才是函数指针

在这里插入图片描述

// 之前的字符串版本也可以使用函数指针这个重载的版本
QObject::connect(&a, &A::a_signal, &b, &B::b_slot);

信号槽是同步还是异步的呢?

经过验证,信号槽是同步的,很像在A中调用了B。‘

在这里插入图片描述

信号可以携带参数

信号的参数与槽的参数一致 好

信号的参数比槽多 可以但是不好

信号的参数比槽的参数少 不行 如果是字符串形式的重载,报运行时错误,指针形式的重载,报编译时错误

在这里插入图片描述

信号槽函数重载

因为信号槽函数是成员函数,所以可能会发生信号重载

编译会报错

在这里插入图片描述

解决办法:

1 使用字符串形式的connect重载形式

2 重载决议(手动确认重载决议)

在这里插入图片描述

在这里插入图片描述

重复关联会咋样?

重复关联不会去重,在今后调用的时候,每关联一次,后面就会多调用一次槽函数

一个信号可以关联多个槽函数

信号发送顺序按关联顺序调用

在这里插入图片描述
在这里插入图片描述

信号槽的自由关联

信号和槽都是成员函数,信号是函数体为空的函数

槽函数在执行过程中,可以发送信号

信号可以是槽函数,但是槽函数不可以是信号

connect的第三种重载形式

第一种和第二种的connect中,槽函数都是成员函数

在这里插入图片描述

​ 槽函数也可以直接是一个函数对象,lamda表达式本质是重载了()的一个类,捕获列表[]本质上是这个类的数据成员。我们要改变label,所以我们可以捕获,然后进行修改。

​ 不想继承接收方,只需要把槽函数写成函数对象,只需要找到概念上的接收方,然后改掉就可以

在这里插入图片描述

使用函数对象的优势: 不需要继承接收方

劣势:不适合写复杂的代码

sender

在这里插入图片描述

发现我们拿到的是基类的指针,但是我们要去做事情的话,拿到派生类的指针是最好的,因为数据成员更多一些,所以我们需要将基类指针转化为派生类指针,也就是向下转换。

向下转化

向上转化 派生类转化为基类,是去掉一些东西,是比较简单的,

向下转化是有风险的,可能会失败,不能使用static_cast 他只管转化,不会判断失败

可以使用 dynamic_cast 但是Qt在设计的时候没有这个,但是 qt 中有 qobject_cast()

在这里插入图片描述

在这里插入图片描述

信号槽总结

优势:

  • 松耦合 分离了发送方与接收方
  • 类型安全
  • 关联自由
  • 生态好 有很多预设的信号和槽

劣势:

  • 性能差

准备时间 非虚函数 1 虚函数 5 槽函数 10

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐