C++ 原生本身只有类、继承、成员、虚函数 ;QObject 对象模型,是 Qt 在标准 C++ 语法之上额外实现的一套扩展机制 ,依托QObject基类 + moc元对象编译器,给 C++ 类增加原生 C++ 做不到的能力:信号槽、运行时类型识别、动态属性、元信息、对象树内存管理。
你那句话:Qt 类本质就是 C++ 类,只是 Qt 扩展了 QObject 对象模型 含义:
THints、THintsMenu、THintsDialog这些类,底层仍然是标准 C++ 的 class,能使用 C++ 所有语法(构造函数、继承、引用、指针、初始化列表);只要继承 QObject,就接入 Qt 这套额外的对象模型,获得原生 C++ 没有的特性;不继承 QObject 的普通 C++ 类,就没有这套能力。
一、标准原生 C++ 类,缺了什么?
原生 C++:
- 编译完成后,类的信息全部被编译器砍掉。运行时程序不知道:类叫什么名字、有哪些成员函数、有哪些属性;
- 没有内置 "回调绑定机制",想做事件回调,只能手动写函数指针;
- 没有动态属性:一个类定义完,成员就固定,运行时不能临时新增属性;
- 内存管理需要自己手动
new/delete,没有自动父子托管; - 多继承有虚继承、菱形继承的麻烦。
原生 C++ 的信息只在编译期存在;编译完二进制里面几乎不保留类的描述信息。
二、QObject 对象模型由哪几部分组成
1. 基类:QObject
所有想使用这套模型的类,必须直接 / 间接继承 QObject,就像你代码里:
THintsMenu : public QObject, public QDesignerTaskMenuExtensionTHintsDialog : public QDialog,而QDialog继承QWidget,QWidget继承QObject
继承 QObject 是入场券。
2. 元对象宏 Q_OBJECT(核心!)
cpp
class THintsMenu : public QObject
{
Q_OBJECT // 关键宏
...
};
只要写Q_OBJECT,编译前 Qt 的moc(Meta Object Compiler,元对象编译器) 会扫描你的头文件,自动生成额外 C++ 代码(moc_thintsmenu.cpp)。
moc 不是 C++ 标准编译器,是 Qt 单独的预处理器。 moc 会提取这个类的信息:类名、信号、槽、属性,生成一个元对象类 QMetaObject。
3. QMetaObject:元对象(保存类的描述信息)
moc 生成的QMetaObject,相当于这个类的 "运行时说明书",里面存:
- 类名字符串
- 所有信号列表、槽函数列表
- 属性列表(property)
- 方法信息
👉 原生 C++ 运行时没有这份说明书;QObject 模型把这份说明书打包进程序,运行时可以读取。
三、QObject 对象模型提供的五大核心能力(结合你刚才看的源码举例)
① 信号与槽(Signal & Slot)------ 最常用
原生 C++ 要用函数指针手动绑定回调,非常麻烦; Qt 信号槽由元对象系统实现:connect可以把一个对象的信号,连接到另一个对象的槽函数。
cpp
connect(edit_state_action_, &QAction::triggered, this, &THintsMenu::edit_state);
原理: 运行时,通过QMetaObject找到信号、槽的信息;信号触发时,元对象系统自动找到所有连接的槽函数调用。
信号槽支持跨线程,这是普通 C++ 函数指针很难安全实现的。
② 运行时类型识别 qobject_cast(你代码里出现过!)
cpp
THints *hints = qobject_cast<THints *>(object);
- 原生 C++ 有
dynamic_cast,但依赖 RTTI,有开销、限制多; qobject_cast是 Qt 元对象系统实现的类型转换,不需要 C++ 的 RTTI 开关 ,只对继承 QObject 的类生效。 运行时读取QMetaObject的类名信息,判断对象是不是目标类型,失败返回 nullptr。 你工厂类THintsMenuFactory::createExtension就是靠这个判断选中的控件是不是 THints。
③ 动态属性系统 setProperty /property(你 on_finished 里面用到!)
cpp
form_window->cursor()->setProperty("tips_type_", thints_.property("tips_type_"));
原生 C++:类定义写死成员变量,运行不能临时增加变量 。 QObject 对象模型: 可以在运行时,给 QObject 派生对象动态附加键值对属性,不需要在 class 里预先定义成员。 Qt 设计师保存控件属性到 ui 文件,底层就是这套动态 property。
④ 对象树(Object Tree)内存自动管理(Qt 内存管理核心)
QObject 派生类构造可以传入parent父对象指针:
cpp
THintsMenu::THintsMenu(THints *control, QObject *parent)
: QObject(parent)
规则:
- 子对象加入父对象的对象树;
- 父对象析构的时候,会自动 delete 所有子 QObject 对象;
- 不用你一个个手动 delete 子控件,避免内存泄漏。
注意:普通 C++ 对象没有这个父子托管;只有 QObject 子类才有对象树。 你代码里
new QAction(tr("设置THints..."), this),this作为父对象,THintsMenu 销毁时自动释放这个 QAction。
区分:
ui指针是普通堆对象,不属于 QObject 对象树,所以你必须在析构写delete ui;- QAction、THintsMenu、THintsDialog 属于 QObject,走对象树。
⑤ 元属性、反射,支持 Qt Designer 属性面板
Qt 设计师可以在右侧属性面板直接修改控件参数,原理就是QMetaObject 反射: Qt 设计器运行时读取控件的元对象信息,自动识别属性,展示到属性面板;修改后用 setProperty 写回去。 你写的 THints 自定义控件能在 Qt 设计师拖拽、编辑属性,完全依赖这套对象模型。
四、关键限制(QObject 模型的代价)
- 必须继承 QObject,并且写 Q_OBJECT 宏,moc 才会处理;
- 多重继承时,QObject 必须放在第一个继承位置;
- 不能模板类直接使用 Q_OBJECT;
- moc 是预编译步骤,忘记 qmake/CMake 开启 moc 就编译报错。
五、一句话对比
- 普通 C++ 类:编译完,类信息丢弃,只有代码逻辑;内存、回调、类型判断全部自己写。
- 继承 QObject 的 Qt 类 :依然是 C++ 类,但是 moc 额外生成元对象代码,在运行时携带类的描述信息 ,以此实现信号槽、动态属性、对象树、qobject_cast,这套整体就叫 QObject 对象模型。