009-Qt 的元对象系统

009-Qt 的元对象系统
abinng😶🌫️auther: abinng date: 2026-03-20 22:53
createDate:2026-03-20 22:53
本篇主要了解即可,初学时不深入了解也行
引入
Qt 的元对象使用特性
- 类中写 Q_OBJECT 宏
- 继承 QObject
- 不能放到 cpp 文件中
前两个的效果就可以理解成,普通的类结构外扩了一部分,就叫做元对象,就是一个空间,包含结构信息
而我们的 Qt Core 要解析这部分外扩的结构,需要一个API接口(Qt 已经实现好的),所以其中的结构也要按照 Qt 要求的结构实现
原来的那一部分就不需要解析,这是我们自己定义的成员
那为什么不能放到 cpp 文件中呢?
类的定义肯定是在 .h 文件中,对应的函数实现在 .cpp 中,API 接口就可以调用函数实现,来提取这部分外扩的结构信息。这部分函数实现也是 Qt 生成的
之前了解过 UIC 和 RCC
UIC:将 UI 文件转换成 .h 文件,会被打包到可执行文件中 RCC:将资源文件打包成 cpp 文件,会被打包到可执行文件中
而现在这个元对象系统可以称为 MOC (Meta-Object Compiler),把当前目录下所有包含外扩部分的头文件,将函数实现生成到一个 cpp 文件中,最后也会被打包到可执行文件中
其实该过程就是 001-Qt 的编译原理 中那幅图
MOC 主要是为了保证运行时,可以动态调整对象的属性值。相当于一个反射的思想。
例如:设计一个游戏的机制,刚开始设计时,可能只考虑到了掉血,但后面逐渐扩充游戏玩法,有的攻击可能会掉血,有的攻击可能会掉魔法值,或者其他值,此时要重新设计框架,就会很复杂。只需要看攻击的武器需要减什么类型的数值,对应受击英雄有没有对应的属性,来执行对应的操作或者其他操作就好。这么一来就解耦了
通过一个字符串来查找是否有响应的属性,来执行相关动作
由此引出信号和槽,其本身设计很慢,但很方便,信号发出者和信号处理者之间解耦,由第三方来决定谁对应谁。
由于外扩了部分结构信息,还增加了智能选择处理函数的过程,时间和空间都增加了
与之对应的是回调函数的情况,触发了某某条件,就直接调用对应的函数,直接绑定了,相当于得提前知道对应关系,比较不灵活
代码案例
main.cpp
1 | // main.cpp |
monster.h
1 | // monster.h |
monster.cpp
1 |
|
架构原理
先不看图
首先讲一下没见过的宏:
1 | Q_PROPERTY(int health MEMBER m_health) |
意思是,Monster 类告诉 Qt :
- 名字:我有一个属性叫
health。 - 类型:它的类型是
int。 - 绑定:当你(Qt
系统)想要读写这个属性时,请直接操作我类里的成员变量
m_health(这就是MEMBER关键字的作用)。
MOC 干了什么:
当你点击编译时,Qt 的 MOC(元对象编译器) 会扫描你的头文件:
- 生成元数据:MOC 会生成一个隐藏的 C++ 文件(通常叫
moc_monster.cpp)。 - 登记表:在这个隐藏文件里,MOC
维护了一张巨大的表,记录了
Monster类里有一个叫"health"的字符串,对应的是int类型。 - 反射机制:它实现了“反射”(Reflection)。这意味着你可以在运行时通过字符串来操作变量。
然后我们再来对应着代码看图:
代码是怎么印证架构图的呢?
蓝区(不需要解析的普通成员): 对应
Monster类里的私有变量m_health和m_mana。它们是 C++ 原生的,藏在黑盒里。红区(元对象空间 结构信息): 对应
Q_PROPERTY(int health MEMBER m_health)。这相当于在 Qt 的“户口本”上做了登记,暴露了一个对外的字符串名字"health"。API 接口与反射执行(核心模拟):
我们看
lost函数,它完全不知道传入的对象是Monster还是其他什么怪物,它只认QObject指针。obj->metaObject():这就是获取图中的 “API 接口”,拿到了这个对象的元信息名册。meta->indexOfProperty(propertyName):这就是图注里写的 “通过字符串来查找是否有属性”。传入"health",它就去红区里找有没有这个名字。property.read()/property.write():一旦找到,不需要手动调用mon1.m_health,直接通过 Qt Core 提供的 API 接口动态修改了底层数据。
这段代码的意义(为什么说它解耦了?): 注意看
lost(QObject *obj, const char *propertyName)
这个函数。假设以后增加了一个 Hero 类,只要它也有
"health" 这个
Q_PROPERTY,你不需要改动任何逻辑,直接把
Hero 传给 lost
函数,照样能扣血!这就是面向字符串编程(反射)带来的极大灵活性。









