知其然,知其所以然
今天面试的时候问到了Qt信号槽底层原理的实现,然后发现自己不懂,很尴尬,现在需要过来恶补一下相关的知识,同时也给自己一个教训,以后学习新内容不能只知道怎么用,还要尽可能掌握底层原理的实现。
信号——槽机制复习
我们首先看一下官方文档是如何使用信号与槽的,我们定义一个计数器,其头文件如下:
1 | class Counter : public QObject |
在cpp文件中,我们定义setValue
函数如下:
1 | void Counter::setValue(int value) |
我们可以这样使用我们的计数器:
1 | Counter a, b; |
从Qt诞生之初,我们就延续着这样的使用方式,但是其具体实现经历了几次变革,我们来看一下现在的Qt是如何实现信号与槽机制的。
MOC——元对象编译
Qt的信号与槽基于运行期对对象的解析,这个解析的意思是能够在运行期列出对象的方法和属性,同时获得这些方法及属性的全部信息,例如变量的类型等等。C++采用RTTI(Runtime Type Information)来获取运行期信息。而Qt自己提供了一套工具实现内省功能,即MOC,一个代码生成器(不是预处理器),MOC会解析头文件,生成一个额外的C++文件,在Qt中我们看到的是文件名中包含moc的文件,该文件包含了需要进行运行期内省(introspection)的代码。
内省(Introspection)是面向对象语言和环境的一个强大特性,内省是对象揭示自己作为一个运行时对象的详细信息的一种能力。这些详细信息包括对象在继承树上的位置,对象是否遵循特定的协议,以及是否可以响应特定的消息
一些宏定义
我们在使用Qt的过程中,经常会用到一些非C++本身的关键字,例如 signals, slots, Q_OBJECT, emit, SIGNAL, SLOT
,这些是Qt对C++的一个扩展,实际上是一些简单的宏定义,在qobjectdefs.h文件中给出
1 |
我们可以看到,实际上public和slots根本什么也没有给出,更多的是对开发人员的一种提示,同理还有emit,即使你不写emit,直接写信号函数,也会触发相应的connect,说明信号本身就是一种函数。然后是著名的Q_Object宏,定义如下:
1 |
|
我们可以看到,该宏定义了一系列的function和一个静态的QMetaObject
,这些函数在哪里实现呢?就在我们的MOC文件中。
对于SLOT和SIGNAL,其定义如下:
1 | Q_CORE_EXPORT const char *qFlagLocation(const char *method); |
这两个宏定义就是简单地把函数名转换为字符串,然后添加到代码中。
MOC生成了哪些代码
接下来我们分析MOC具体生成的代码有什么作用。
The QMetaObject
1 | //static const QMetaObject staticMetaObject; |
定义在qobjectdefs.h的元对象如下:
1 | struct QMetaObject |
此处的间接d
表示这些对象应当为私有的,但实际并不是私有的,是为了能够静态初始化以及让其作为POD类
POD类类型就是指class、struct、union,且不具有用户定义的构造函数、析构函数、拷贝算子、赋值算子;不具有继承关系,因此没有基类;不具有虚函数,所以就没有虚表;非静态数据成员没有私有或保护属性的、没有引用类型的、没有非POD类类型的(即嵌套类都必须是POD)、没有指针到成员类型的(因为这个类型内含了this指针)。
QMetaObject *superdata
和父类的元对象一起初始化(此处为QObject::staticMetaObject,因为Counter继承自QObject),stringdata
and data
稍后解释。
Introspection Tables——内省表
我们首先分析一下元对象集成的数据
1 | static const uint qt_meta_data_Counter[] = { |
前13个整数如果包含两列,第一列为计数,第二列表示方法描述在该array中的index,在本例中,我们有2个方法,同时方法的描述从14位开始。方法的描述由5个整数组成,具体如下:
- name:一个string table中的索引
- argc:参数个数
- parameters:参数位置索引
- tag和flags暂略
对于每个函数,moc还会保存每个parameter的返回类型,以及他们对于name的类型和索引位置。
参考文献
- 1.信号与槽底层内容讲解 ↩