引入
上篇讲到了信号与槽就是实现的观察者模式,那具体如何生成映射表就是moc做的事情。
一、moc简介
1. moc的定义
moc 全称是 Meta-Object Compiler,也就是“元对象编译器”,它主要用于处理C++源文件中的非标准C++代码。Qt 程序在交由标准编译器编译之前,先要使用 moc 分析 C++ 源文件。如果它发现在一个头文件中包含了宏 Q_OBJECT,则会生成另外一个 C++ 源文件。这个源文件中包含了 Q_OBJECT 宏的实现代码。这个新的文件名字将会是原文件名前面加上 moc_ 构成。这个新的文件同样将进入编译系统,最终被链接到二进制代码中去。因此我们可以知道,这个新的文件不是“替换”掉旧的文件,而是与原文件一起参与编译。另外,我们还可以看出一点,moc 的执行是在预处理器之前。因为预处理器执行之后,Q_OBJECT 宏就不存在了。
既然每个源文件都需要 moc 去处理,那么我们在什么时候调用了它呢?实际上,如果你使用 qmake 的话,这一步调用会在生成的 makefile 中展现出来。从本质上来说,qmake 不过是一个 makefile 生成器,因此,最终执行还是通过 make 完成的。
2. moc的作用
Moc的主要功能如下:
- 清理代码:Moc会删除源文件中与C++标准不符的代码,例如某些未使用的变量、类型或函数。
- 生成C++类和函数:Moc会将源文件中的预处理器指令和特殊C++语句转换为C++类和函数,这些类和函数可以在其他Qt模块中使用。
- 处理资源文件:Moc可以将源文件中的资源文件(如图像、字符串等)转换为C++类和函数,使得这些资源可以在其他Qt模块中访问。
- 提供扩展:Moc可以处理模板代码,从而使得代码可以根据特定的Qt模块或对象模型扩展。
- 清理和清理:Moc支持两次清理,第一次清理通常用于移除不需要的C++代码,第二次清理用于移除第一次清理后仍然存在的未使用的代码。
二、moc详解
2.1 moc的工作原理
前面我们说过,Qt 不是使用的“标准的” C++ 语言,而是对其进行了一定程度的“扩展”。这里我们从Qt新增加的关键字就可以看出来:signals、slots 或者 emit。所以有人会觉得 Qt 的程序编译速度慢,这主要是因为在 Qt 将源代码交给标准 C++ 编译器,如 gcc 之前,需要事先将这些扩展的语法去除掉。完成这一操作的就是 moc。
Qt库中的moc(Meta-Object Compiler)是一个为特定Qt库生成元对象代码(例如,QObject、QMetaObject、QMetaObject::invokeMethod等)的工具。moc的工作原理主要包括以下几个步骤:
- 扩展原始代码:moc首先读取源代码文件(通常是C++文件),并根据该文件中的元对象代码生成相应的元对象代码。moc将元对象代码添加到源代码文件中,以扩展原始代码。
- 解析元对象:moc接着解析源代码文件中的元对象代码。这些元对象代码包括:
a. Q_OBJECT宏:这是Qt库中最重要的元对象标识,表示该类需要实现元对象系统(如信号和槽机制)。moc为所有定义了Q_OBJECT宏的类生成相应的元对象代码。
b. 信号和槽:moc将读取和解析信号和槽的声明,以生成相应的元对象代码。
c. 其他元对象属性:moc解析并处理其他元对象属性,例如元对象导出表、元对象属性表等。 - 生成元对象代码:moc根据解析到的元对象代码生成相应的元对象代码。这些元对象代码包括:
- QMetaObject:生成的元对象代码包含了所有元对象的属性和方法。
- 元对象导出表:生成的元对象代码包含了类中所有元对象方法的符号,这些符号用于其他库或代码通过Q_OBJECT宏识别和连接这些方法。
- 元对象属性表:生成的元对象代码包含了类中所有元对象属性的符号,这些符号用于其他库或代码读取和设置这些属性。
- 输出元对象代码:moc将生成的元对象代码写回原始代码文件。此时,原始代码文件已经包含了额外的元对象代码,用于实现元对象系统。
2.2 moc生成的文件结构
对于每一个 QObject 类的派生类,qt 都会使用 moc 命令之生成附加的 moc_xxx.cpp 文件。在 moc_xxx.cpp 文件中,包含了 QObject 派生类的附加信息。
我们创建一个qt对象,生成出一个moc文件,然后具体去分析这个文件各个函数的意义:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
};
#endif // MAINWINDOW_H
/****************************************************************************
** Meta object code from reading C++ file 'mainwindow.h'
**
** Created by: The Qt Meta Object Compiler version 67 (Qt 5.12.10)
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/
#include "mainwindow.h"
#include <QtCore/qbytearray.h>
#include <QtCore/qmetatype.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'mainwindow.h' doesn't include <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 67
#error "This file was generated using the moc from 5.12.10. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif
QT_BEGIN_MOC_NAMESPACE
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
struct qt_meta_stringdata_MainWindow_t {
QByteArrayData data[1];
char stringdata0[11];
};
#define QT_MOC_LITERAL(idx, ofs, len) \
Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \
qptrdiff(offsetof(qt_meta_stringdata_MainWindow_t, stringdata0) + ofs \
- idx * sizeof(QByteArrayData)) \
)
// #: 这里是它的字符数据对应表
static const qt_meta_stringdata_MainWindow_t qt_meta_stringdata_MainWindow = {
{
QT_MOC_LITERAL(0, 0, 10) // "MainWindow"
},
"MainWindow"
};
#undef QT_MOC_LITERAL
static const uint qt_meta_data_MainWindow[] = {
// content:
8, // revision
0, // classname
0, 0, // classinfo
0, 0, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
0, // signalCount
0 // eod
};
//#:因为这里没有信号和槽函数,所以没有相应静态的元调用
void MainWindow::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
Q_UNUSED(_o);
Q_UNUSED(_id);
Q_UNUSED(_c);
Q_UNUSED(_a);
}
QT_INIT_METAOBJECT const QMetaObject MainWindow::staticMetaObject = { {
&QMainWindow::staticMetaObject,
qt_meta_stringdata_MainWindow.data,
qt_meta_data_MainWindow,
qt_static_metacall,
nullptr,
nullptr
} };
const QMetaObject *MainWindow::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}
void *MainWindow::qt_metacast(const char *_clname)
{
if (!_clname) return nullptr;
if (!strcmp(_clname, qt_meta_stringdata_MainWindow.stringdata0))
return static_cast<void*>(this);
return QMainWindow::qt_metacast(_clname);
}
int MainWindow::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QMainWindow::qt_metacall(_c, _id, _a);
return _id;
}
QT_WARNING_POP
QT_END_MOC_NAMESPACE
在生成的 moc_sender.cpp 中:
qt_meta_stringdata_Sender 是一个字面量表,也是可称之为符号表。
它的存在是为了能够以 idx 就能获取对应的字符串。比如这里我要找到MainWindow这个字符, idx = 0,对应的字串在则为 (const char *)(qt_meta_stringdata_Sender.stringdata0+0)
此后可以用该对应的QByteArrayData的data()方法取得Hello字符串
2.3 Q_OBJECT展开记录
可以看到,moc_test.cpp 里面为 Test 类增加了很多函数。然而,我们并没有实际写出这些函数,它是怎么加入类的呢?,他是通过Q_OBJECT宏定义出来的~
他带来了
👋• QT_WARNING_PUSH宏
QT_WARNING_PUSH宏是用来将编译器的警告设置推入堆栈的宏。它会暂时关闭编译器的警告,并将当前的警告设置保存在堆栈中。这样,在QT_WARNING_POP宏被调用之后,之前的警告设置会被恢复,从而保证代码的其他部分不受到QT_WARNING_PUSH宏所设置的警告影响。
• 👋Q_OBJECT_NO_OVERRIDE_WARNING宏
Q_OBJECT_NO_OVERRIDE_WARNING宏用于禁止编译器对未重写(override)父类中的虚函数发出的警告。在Qt的信号-槽机制中,通常需要在自定义的类中重写父类的虚函数来实现信号和槽的连接。但是,一些编译器可能会发出警告,提示这些虚函数未被重写。使用Q_OBJECT_NO_OVERRIDE_WARNING宏可以禁止这些警告,以避免在编译时产生大量的警告信息。
• 👋static const QMetaObject staticMetaObject;
• 👋virtual const QMetaObject *metaObject() const;
• 👋virtual void *qt_metacast(const char *);
• 👋virtual int qt_metacall(QMetaObject::Call, int, void **);
• 👋QT_TR_FUNCTIONS 宏
QT_TR_FUNCTIONS宏是用来定义一组用于国际化(i18n)的翻译函数的宏。这些函数包括tr()、QObject::tr()、QT_TRANSLATE_NOOP()等,用于在Qt应用程序中进行字符串的翻译和本地化。
这个宏的最关键的地方是是声明了一个只读的静态成员变量staticMetaObject,以及3个public的成员函数,通过moc工具就可以生成以下变量和函数的定义
三、实现信号与槽的实现(为啥是moc)
信号的触发
信号 就是普通的类成员函数,信号只要声明(declare),不需要实现(implement),实现由moc(元对象编译器)自动生成。
信号的触发,可以用emit,也可以直接调用函数。
信号的实现,是直接调用了QMetaObject::activate函数。其中0代表miao这个函数的索引号。
QMetaObject::activate函数的实现,在Qt源码的QObject.cpp文件中,略微复杂一些,且不同版本的Qt,实现差异都比较大,
这里总结一下大致的实现:
先找出与当前信号连接的所有对象-槽函数,再逐个处理。
这里处理的方式,分为三种:
if((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
|| (c->connectionType == Qt::QueuedConnection)) {
// 队列处理
} else if (c->connectionType == Qt::BlockingQueuedConnection) {
// 阻塞处理
// 如果同线程,打印潜在死锁。
} else {
//直接调用槽函数或回调函数
}
receiverInSameThread表示当前线程id和接收信号的对象的所在线程id是否相等。
- 如果信号-槽连接方式为QueuedConnection,不论是否在同一个线程,按队列处理。
- 如果信号-槽连接方式为Auto,且不在同一个线程,也按队列处理。
- 如果信号-槽连接方式为阻塞队列BlockingQueuedConnection,按阻塞处理。
(注意同一个线程就不要按阻塞队列调用了。因为同一个线程,同时只能做一件事,本身就是阻塞的,直接调用就好了,如果走阻塞队列,则多了加锁的过程。如果槽中又发了同样的信号,就会出现死锁:加锁之后还未解锁,又来申请加锁。)
队列处理,就是把槽函数的调用,转化成了QMetaCallEvent事件,通过QCoreApplication::postEvent放进了事件循环。
等到下一次事件分发,相应的线程才会去调用槽函数。
槽和moc生成
slot函数我们自己实现了,moc不会做额外的处理,所以自动生成的moc_Jerry.cpp文件中,只有Q_OBJECT宏的展开
他的具体调用方式就是在qt_static_metacall函数中调用具体的函数(不需要moc做额外处理)
使用moc的注意事项
一个没有定义 Q_OBJECT 宏的类与它最接近的父类是同一类型的。也就是说,如果 A 继承了 QObject 并且定义了 Q_OBJECT,B 继承了 A 但没有定义 Q_OBJECT,C 继承了 B,则 C 的 QMetaObject::className() 函数将返回 A,而不是本身的名字。因此,为了避免这一问题,所有继承了 QObject 的类都应该定义 Q_OBJECT 宏,不管你是不是使用信号槽。