moc 干的事只有一件:给每个类生成一张「名字 → 函数」的表。 C++ 编译器不生成这张表,所以只能另写一个工具生成。
前四节把信号槽的用法讲完了,这一节往下一层,看它靠什么实现。 看懂 moc,你就明白为什么 Qt 的报错那么难读、为什么编译多一步。 D38 用四十行 Python 复现了 moc 的产物,对照着看最清楚。
一、它生成三样东西
moc 扫一遍头文件,生成一个 .moc 文件,里面是三样东西:
| 生成物 | 干什么 | 谁用 |
|---|---|---|
| staticMetaObject | 类的元信息入口 | 所有反射操作 |
| 信号名数组 | 按名字找信号 | 字符串式 connect |
| qt_static_metacall | 按索引转发调用 | 内部派发 |
第二行是核心:connect(obj, "clicked()", …) 里那个字符串,要靠这张表才能变成函数调用。
没有 moc,字符串就永远只是个字符串。
二、为什么 C++ 非要这一步
因为 C++ 没有反射。 这是最根本的原因。
| 语言 | 有反射吗 | 需要 moc 吗 |
|---|---|---|
| C++ | 没有 | 需要 |
| Python | 有(getattr) |
不需要 |
| Java | 有 | 不需要 |
D38 里那个 meta_signals() 就是 Python 版的 moc:遍历类的属性,挑出 Signal 类型的收进字典。
Python 能这么干,是因为对象在运行时有完整的属性表;C++ 编完之后,类型信息基本没了。
三、moc 的两个代价
这两个代价现在还在,用 Qt 就得接受:
| 代价 | 表现 | 缓解 |
|---|---|---|
| 多一步编译 | 构建变慢 | 增量编译,改哪个编哪个 |
| 报错难读 | 错在 .moc 里,不是你写的行 | 看第一个错,别看后面一堆 |
第二行让很多人崩溃:一个拼写错误能引发几十行 moc 报错。
诀窍是只看第一个错误,后面都是连锁反应。
而且第一行的错误位置往往指向 .moc 文件——那说明真正的错在头文件的信号声明里。
四、Qt6 的改进
Qt6 加了「编译期连接」,moc 的戏份变小了。
| 写法 | 检查时机 | 要 moc 吗 |
|---|---|---|
connect(a, SIGNAL(x()), b, SLOT(y())) |
运行期 | 要,靠字符串 |
connect(a, &A::x, b, &B::y) |
编译期 | 只要元信息 |
第二行的类型不匹配会直接编译失败,不用等到运行才发现「连不上」。 这是从「运行期找错」到「编译期找错」的迁移,和现代 C++ 的整体趋势一致。 新代码一律用第二行,除非要连的信号是动态生成的。
五、动手 Demo:用四十行 Python 复现 moc 的产物
定义一个有信号的类,扫出它的信号表,再按名字连一个槽并触发。 这就是 moc 生成的那张表在干的事——只不过 Python 不需要额外工具。
class QObject(object):
@classmethod
def meta_signals(cls):
out = {}
for klass in cls.__mro__:
for k, v in vars(klass).items():
if isinstance(v, Signal):
out.setdefault(k, v)
return out
class Button(QObject):
clicked = Signal()
pressed = Signal()
released = Signal()
print(list(Button.meta_signals())) # 三个信号名
实跑输出:
--------------------------------------------------------------------
moc 到底生成了什么,用四十行 Python 看明白
--------------------------------------------------------------------
**Qt 的 `signals:` 那段声明不是 C++,是给 moc 看的标记。**
**moc 扫一遍头文件,生成一个 `.moc` 文件,里面是字符串表加函数指针。**
moc 生成的 用来干什么 为什么编译器做不到
----------------------------------------------------------------
staticMetaObject 类的元信息入口 签名要运行时查
信号名数组 按名字找信号 字符串在任何地方连
qt_static_metacall 按索引转发调用 要绕过编译期绑定
**为什么非要这一步**:C++ 是静态语言,
`connect(obj, "clicked()", ...)` 里的字符串在编译期没人检查,
要支持「按名字连」,就必须有一张「名字 → 函数」的表。
**这张表编译器不生成,只能另写一个工具生成。**
**Python 不需要 moc,因为它本来就有这张表**(反射)。下面这个类就等价于 moc 的产物:
扫出来的信号表:**clicked、pressed、released**
每个信号名对应一个 Signal 对象,**名字就是属性名**——这就是「按名字连」的底座。
按一次按钮,三个信号依次发出,clicked 的两个槽都跑了:槽A → 槽B
**moc 的代价要说清**:它多一次编译步骤,写错 `signals:` 里的签名时报错信息很难读。
**这就是 Qt6 后来加了「编译期连接」**(`&Class::signal` 取地址)的原因——那套写法不需要字符串,
moc 只用来生成元信息。
扫出来三个信号名:clicked、pressed、released。这就是 moc 生成的那张表的 Python 版。
vars() 拿的是类自己的属性字典,__mro__ 往上走一遍就能收到继承的。
这正是 C++ 里 moc 要手写 <Class>::staticMetaObject 的原因——C++ 没这个字典。
按一次按钮,三个信号依次发出,clicked 的两个槽都跑了。 这说明「一个信号连多个槽」在底层就是「一个列表里两个函数」。
moc 的产物其实就是这些:一个字符串数组、一个函数指针数组、一个按索引跳转的 switch。 没有魔法,只有代码生成。
30 秒实验
- 给
Button加一个子类,在里面加一个新信号, 看meta_signals()能不能收到——这就是「moc 要处理继承」。 - 把
meta_signals()改成返回「信号名 → 参数个数」,想想 moc 怎么知道参数类型。 - 查一下
Q_OBJECT宏展开后是什么,它为什么要放在类声明的第一行。
常见误区
- 以为 moc 是编译器的一部分。 它是独立的可执行文件,由构建系统在编译前调用。 所以「忘了 moc」的表现是「链接找不到符号」。
- 以为 moc 报错要看全部。 只看第一个就行,后面几十行都是连锁反应,而且位置指向 .moc 文件。
- 以为 Qt 不需要 moc 也能用信号槽。 老式字符串连接必须要, 新式取地址连接也要(元信息还是 moc 生成的)。
自测三问(点开对答案)
1. moc 生成的三个主要产物是什么? —— staticMetaObject(元信息入口)、信号名数组、qt_static_metacall(按索引派发)。 2. 为什么 Python 不需要 moc? —— Python 有反射,运行时能拿到类的完整属性字典。 C++ 编译后类型信息基本丢失,只能靠工具提前生成一张表。 3. Qt6 的「编译期连接」好在哪? —— 类型不匹配直接编译失败,不用等到运行才发现 connect 返回 false。下一节开始做案例:一个按钮一个标签,把标准信号槽用一遍。
- ☐ (本节完,下一节 03.04.06 案例:标准信号槽)