📹 第三章 FFmpeg二次开发采集并预览本地摄像头
📄 一句话概括

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 秒实验

  1. 给 Button 加一个子类,在里面加一个新信号, 看 meta_signals() 能不能收到——这就是「moc 要处理继承」。
  2. 把 meta_signals() 改成返回「信号名 → 参数个数」,想想 moc 怎么知道参数类型。
  3. 查一下 Q_OBJECT 宏展开后是什么,它为什么要放在类声明的第一行。

常见误区

  1. 以为 moc 是编译器的一部分。 它是独立的可执行文件,由构建系统在编译前调用。 所以「忘了 moc」的表现是「链接找不到符号」。
  2. 以为 moc 报错要看全部。 只看第一个就行,后面几十行都是连锁反应,而且位置指向 .moc 文件。
  3. 以为 Qt 不需要 moc 也能用信号槽。 老式字符串连接必须要, 新式取地址连接也要(元信息还是 moc 生成的)。
自测三问(点开对答案) 1. moc 生成的三个主要产物是什么? —— staticMetaObject(元信息入口)、信号名数组、qt_static_metacall(按索引派发)。 2. 为什么 Python 不需要 moc? —— Python 有反射,运行时能拿到类的完整属性字典。 C++ 编译后类型信息基本丢失,只能靠工具提前生成一张表。 3. Qt6 的「编译期连接」好在哪? —— 类型不匹配直接编译失败,不用等到运行才发现 connect 返回 false。

下一节开始做案例:一个按钮一个标签,把标准信号槽用一遍。

相关笔记