type
status
date
slug
summary
tags
category
icon
password
Qt 核心机制源码分析之元对象系统
在 Qt Core 的官方文档中,介绍了几大核心机制:
- The Meta-Object System 元对象系统
- The Property System 属性系统
- Object Model 对象模型
- Object Trees & Ownership 对象树和所有权
- Signals & Slots 信号与槽机制
本文基于 Qt 6.8 分析 Qt 核心机制的实现,部分细节不进行细究。
元对象系统
元对象系统是后面几个核心机制的基础。所谓的元对象系统,我个人理解是在编译期将类的各种元信息(例如类名、方法名等)编码到程序中,在运行时便可以获取到这些元信息。
元对象系统主要基于:
- QObject 类,所有需要使用元对象系统的类都需要继承自 QObject 类,像 Qt 中大部分类都是基于 QObject 的。
- Q_OBJECT 宏,可以通过 moc 编译器展开并生成各类元信息,这也是本文重点分析部分。
- Moc 编译器。
接下来从一个例子出发:
定义一个头文件 test.h
在这个例子中,我使用到了 Qt 的属性系统、信号槽、枚举(之前使用moc生成过文件,了解到主要存储这些元信息)。因为分析的目的是想弄明白到底存有哪些元信息?元信息是如何存储的?元信息是如何给予信号槽机制支持的?
接下来使用 moc 编译器生成 moc 文件:
moc_test.cpp 如下:
我们从头到尾分析一编:
1、 元字符串数据 qt_meta_stringdata_CLASSTestENDCLASS
这是一个存储类的元信息的数据结构,它在编译期确定,并编码进基类
QObject
中(稍后可以看到)。元字符串数据通过
qt_meta_stringdata_CLASSTestENDCLASS
结构存储所有标识符的字符串字面量,包括:
- 类名、信号与槽名称
- 参数名称(如信号signal_3
的参数i
和j
)
- 属性名testProperty
- 枚举项(如TestEnum_0
和TestFlag_1
)存储优化:
字符串以
\0
分隔连续存储于stringdata0
数组,通过offsetsAndSizes
记录各字符串的偏移量和长度,实现高效内存访问。我们看一下它构造时候传递的参数:
其生成的对象结构如下:
stringdata0
为元信息存储的地方,以字符串的方式进行存储。大致如下:

我们可以看到的是,这个数据结构高效地存储了:
1、类名 (class) : “Test”
2、信号(signal)名 : “signal_1”, ““,”signal_2”, “i”, “signal_3”, “j” , “testPropertyChanged”
3、槽(slot)名 : “slot_1”, “slot_2”, “slot_3”
4、信号和槽的参数名称: “”, “i”, “j”
5、属性(property)名 : “testProperty”
6、枚举(Enum)和标志(Flags,也是一种枚举)名 : “TestEnum”, “TestEnum_0”, “TestEnum_1”, “TestFlags”, “TestFlag”, “TestFlag_1”, “TestFlag_2”` , “TestFlag_4”。
offsetsAndSizes
存储了这些信号槽、枚举标志等一些名称在 stringdata0
中的位置。我们可以通过索引获取到各个元信息。2、元数据数组 qt_meta_data_CLASSTestENDCLASS
qt_meta_data_CLASSTestENDCLASS
以紧凑的整型数组编码结构化元信息在前面,我们介绍了元字符串数据,但是元字符串数据仅仅是存储“名称”,但是作为”现代”的反射系统,当然不止这些信息,不然 C++ 原生的 RTTI 足矣。
因此这里的元数据数组存储的是另一些额外的元信息,从生成代码的注释我们可以看出,这个数组还存储了参数的类型、属性的类型等信息。但是一个整形数组是怎么存储如此多的信息的呢?接下来一一分析:
这里我们结合注释看。
类信息内容 content
对应的数据结构为:
信号元信息 signals: name, argc, parameters, tag, flags, initial metatype offsets
对应的数据结构为:
槽元信息 slots: name, argc, parameters, tag, flags, initial metatype offsets
同上面的信号一样,其实本质上都是 methods
信号和槽的参数元信息 signals: parameters 和 // slots: parameters
前面也介绍过了,就是表示信号和槽的参数类型,以及参数名字在元字符串数据中的偏移索引。
属性元信息 properties: name, type, flags, notifyId, revision
这个 properties 如果不看源码真的很难猜,而且看源码也只能看其调用或解析的方式,只能说有点吃力,其实这只是Qt自己定的规则,其实没必要了解的太多,就不详细分析了。
枚举元信息 enums: name, alias, flags, count, data
枚举数据键值对元信息 enum data: key, value
enum data 就是上面说的枚举的元信息
小结
元字符串数据 和 元数据数组 相互交织为反射机制提供了足够的元信息包括:信号和槽名、信号和槽参数及参数名、属性名、属性值、枚举名和值名等等。
3、静态元对象 staticMetaObject
最终的元信息是存储在
staticMetaObject
中的,这也是 Q_OBJECT
宏展开的对象。作为元对象系统的入口,整合了:
- 继承链:通过
SuperData::link
关联父类QObject
的元对象
- 类型系统:
qt_incomplete_metaTypeArray
提供类型擦除支持,确保模板类型安全
- 跨平台支持:
QMetaTypeInterface
抽象了类型操作,实现元信息的平台无关性
这里用了一个列表初始化的方法,
QMetaObject
中并没有声明自定义的构造器,而下面的 Data 是 public 属性的。我们可以看到,所有数据项都是一一对应的。
其中,有一个特别重要的点是
qt_incomplete_metaTypeArray
,这是一个 QMetaTypeInterface
类型的集合。我们可以从名字中看出,这是一个类似与 Java 的 Interface 的东西,我认为这个类型是为了跨平台而存在的,统一元类型的各种操作。
这个数组提供了属性、枚举、类本身、方法(信号和槽)及其参数的类型信息。 它列出了完整的类型,并使用
QtPrivate::TypeAndForceComplete
,这可能有助于在模板实例化期间管理完整和不完整类型。4、静态元调用 qt_static_metacall
这可以说是整个元对象系统中最为重要的东西了。无论是信号的发射、槽的调用、属性的操作都是通过这个函数实现的。
我们先看一下这个函数的签名(signature)
这是个类内的静态方法,也就是说他是属于类的,而不是属于对象的。对象的信息通过
QObject* _o
来访问。而
QMetaObject::Call
表示的是元调用的方式,我们从moc生成的代码上看,他一共生成了一下方式:1、InvokeMetaMethod 通过信号和槽的元信息索引调用对应的信号和槽
我们可以看到,
_id
就是信号和槽名字的索引,而参数 _a
明显就是调用时候的实参。2、IndexOfMethod 根据传入的函数指针获取其在元信息中的索引
我们可以看到,这里的
_a
是一个指针数组。其中- _a[0] 为返回的索引的指针
- _a[1] 为查找的信号或槽的函数指针
3、ReadProperty 、WriteProperty、ResetProperty、BindableProperty
是属性的读写接口等操作的元调用
5、动态元类型转换和元调用 qt_metacast
, qt_metacall
qt_metacast 动态类型转换:支持运行时类名检查,实现安全的动态类型转换
可以看出,该类接受一个字符串,该字符串表示要进行转换的目标类的名称,比较当前我们自定义的
Test
类与目标类的类名元信息是否相同。如果相同则直接转换(返回this)指针。
否则则退化到
QObject
类这个共同的基类。qt_metacall 动态元调用:负责动态调用信号槽、属性访问、类型注册等功能。
6、信号发射的实现
信号本质是编译器生成的胶水代码,其核心逻辑为:
QMetaObject::activate
的调用关于
QMetaObject::activate
,后续在信号和槽的篇章中介绍。总结
元对象系统通过编译期代码生成(Moc)和运行时元信息查询,在C++静态类型系统上实现了动态反射能力。这种”代码生成+静态数据”的设计,在性能与灵活性之间取得了巧妙平衡,是Qt框架的核心基石。