Qt信号与槽机制的基石-MOC详解

引入

上篇讲到了信号与槽就是实现的观察者模式,那具体如何生成映射表就是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的主要功能如下:

  1. 清理代码:Moc会删除源文件中与C++标准不符的代码,例如某些未使用的变量、类型或函数。
  2. 生成C++类和函数:Moc会将源文件中的预处理器指令和特殊C++语句转换为C++类和函数,这些类和函数可以在其他Qt模块中使用。
  3. 处理资源文件:Moc可以将源文件中的资源文件(如图像、字符串等)转换为C++类和函数,使得这些资源可以在其他Qt模块中访问。
  4. 提供扩展:Moc可以处理模板代码,从而使得代码可以根据特定的Qt模块或对象模型扩展。
  5. 清理和清理: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的工作原理主要包括以下几个步骤:

  1. 扩展原始代码:moc首先读取源代码文件(通常是C++文件),并根据该文件中的元对象代码生成相应的元对象代码。moc将元对象代码添加到源代码文件中,以扩展原始代码。
  2. 解析元对象 :moc接着解析源代码文件中的元对象代码。这些元对象代码包括:
    a. Q_OBJECT宏:这是Qt库中最重要的元对象标识,表示该类需要实现元对象系统(如信号和槽机制)。moc为所有定义了Q_OBJECT宏的类生成相应的元对象代码。
    b. 信号和槽:moc将读取和解析信号和槽的声明,以生成相应的元对象代码。
    c. 其他元对象属性:moc解析并处理其他元对象属性,例如元对象导出表、元对象属性表等。
  3. 生成元对象代码 :moc根据解析到的元对象代码生成相应的元对象代码。这些元对象代码包括:
    • QMetaObject:生成的元对象代码包含了所有元对象的属性和方法。
    • 元对象导出表:生成的元对象代码包含了类中所有元对象方法的符号,这些符号用于其他库或代码通过Q_OBJECT宏识别和连接这些方法。
    • 元对象属性表:生成的元对象代码包含了类中所有元对象属性的符号,这些符号用于其他库或代码读取和设置这些属性。
  4. 输出元对象代码:moc将生成的元对象代码写回原始代码文件。此时,原始代码文件已经包含了额外的元对象代码,用于实现元对象系统。

2.2 moc生成的文件结构

对于每一个 QObject 类的派生类,qt 都会使用 moc 命令之生成附加的 moc_xxx.cpp 文件。在 moc_xxx.cpp 文件中,包含了 QObject 派生类的附加信息。

我们创建一个qt对象,生成出一个moc文件,然后具体去分析这个文件各个函数的意义:

c 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
};
#endif // MAINWINDOW_H
c 复制代码
/****************************************************************************
** 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,实现差异都比较大,

这里总结一下大致的实现:

先找出与当前信号连接的所有对象-槽函数,再逐个处理。

这里处理的方式,分为三种:

c 复制代码
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 宏,不管你是不是使用信号槽。

相关推荐
李少兄15 分钟前
Unirest:优雅的Java HTTP客户端库
java·开发语言·http
此木|西贝21 分钟前
【设计模式】原型模式
java·设计模式·原型模式
CoderIsArt34 分钟前
QT中已知4个坐标位置求倾斜平面与倾斜角度
qt·平面
可乐加.糖38 分钟前
一篇关于Netty相关的梳理总结
java·后端·网络协议·netty·信息与通信
s91236010140 分钟前
rust 同时处理多个异步任务
java·数据库·rust
9号达人41 分钟前
java9新特性详解与实践
java·后端·面试
cg50171 小时前
Spring Boot 的配置文件
java·linux·spring boot
啊喜拔牙1 小时前
1. hadoop 集群的常用命令
java·大数据·开发语言·python·scala
anlogic1 小时前
Java基础 4.3
java·开发语言
非ban必选2 小时前
spring-ai-alibaba第七章阿里dashscope集成RedisChatMemory实现对话记忆
java·后端·spring