009-Qt 的元对象系统

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// main.cpp
#include "monster.h"
#include <QDebug>
#include <QPushButton>
#include <QMetaObject>
#include <QMetaProperty>
#include <qmetaobject.h>
#include <qobjectdefs.h>

void myshow(QObject *obj) {
auto meta = obj->metaObject();
// 所有 obj 支持的属性成员展示
qDebug() << "count: " << meta->propertyCount();
for (int i = 0; i < meta->propertyCount(); ++i) {
QMetaProperty property = meta->property(i);
qDebug() << property.name() << " " << property.typeName();
}
}

void lost(QObject *obj, const char *propertyName) {
auto meta = obj->metaObject();
int index = meta->indexOfProperty(propertyName);

// 确保索引有效(不等于 -1 说明找到了)
if (index != -1) {
QMetaProperty property = meta->property(index);

// 读一下
int cur = property.read(obj).toInt();
// 修改一下
--cur;
// 写入修改
property.write(obj, cur);

// 可选:加个日志方便调试
// qDebug() << "属性" << propertyName << "已修改为:" << cur;
} else {
// 如果没找到,打印一条警告信息,防止程序死得不明不白
qWarning() << "警告:在对象中未找到属性 -" << propertyName;
}
}

// 元对象模拟
void test() {
Monster mon1;
myshow(&mon1);
mon1.show_info();
lost(&mon1, "health");
mon1.show_info();
lost(&mon1, "mana");
mon1.show_info();
}

int main() {
test();

return 0;
}

monster.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// monster.h
#ifndef MONSTER_H
#define MONSTER_H

#include <QObject>
#include <qobject.h>
#include <qtmetamacros.h>

class Monster : public QObject {
Q_OBJECT
Q_PROPERTY(int health MEMBER m_health)
Q_PROPERTY(int mana MEMBER m_mana)
public:
explicit Monster (QObject *parent = nullptr);
void show_info() const;
private:
int m_health;
int m_mana;

};

#endif

monster.cpp

1
2
3
4
5
6
7
8
9
10
#include "monster.h"
#include <iostream>

Monster::Monster (QObject *parent): QObject(parent), m_health(100), m_mana(10) {

}

void Monster::show_info() const {
std::cout << "hp: " << m_health << ", mp:" << m_mana << std::endl;
}

架构原理

先不看图

首先讲一下没见过的宏:

1
Q_PROPERTY(int health MEMBER m_health)

意思是,Monster 类告诉 Qt :

  • 名字:我有一个属性叫 health
  • 类型:它的类型是 int
  • 绑定:当你(Qt 系统)想要读写这个属性时,请直接操作我类里的成员变量 m_health(这就是 MEMBER 关键字的作用)。

MOC 干了什么:

当你点击编译时,Qt 的 MOC(元对象编译器) 会扫描你的头文件:

  1. 生成元数据:MOC 会生成一个隐藏的 C++ 文件(通常叫 moc_monster.cpp)。
  2. 登记表:在这个隐藏文件里,MOC 维护了一张巨大的表,记录了 Monster 类里有一个叫 "health" 的字符串,对应的是 int 类型。
  3. 反射机制:它实现了“反射”(Reflection)。这意味着你可以在运行时通过字符串来操作变量。

然后我们再来对应着代码看图:

代码是怎么印证架构图的呢?

  1. 蓝区(不需要解析的普通成员): 对应 Monster 类里的私有变量 m_healthm_mana。它们是 C++ 原生的,藏在黑盒里。

  2. 红区(元对象空间 结构信息): 对应 Q_PROPERTY(int health MEMBER m_health)。这相当于在 Qt 的“户口本”上做了登记,暴露了一个对外的字符串名字 "health"

  3. 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 函数,照样能扣血!这就是面向字符串编程(反射)带来的极大灵活性。