基于Qt5.14.2和mingw的Qt源码学习(三) — 元对象系统简介及moc工具是如何保存类属性和方法的

  • 一、什么是元对象系统
    • 1、元对象系统目的
    • 2、实现元对象系统的关键
    • 3、元对象系统的其他一些特性
  • 二、Q_OBJECT
    • 1、QT_WARNING_PUSH QT_WARNING_POP
      • (1)_Pragma #pragma
      • (2)作用
    • 2、Q_OBJECT_NO_OVERRIDE_WARNING
    • 3、QT_TR_FUNCTIONS
    • 4、QT_ANNOTATE_CLASS(qt_qobject, "")
  • 三、QMetaObject 和 moc
    • 1、QMetaObject 中的 struct SuperData
      • (1)nullptr std::nullptr_t
      • (2)constexpr
      • (3)link函数
    • 2、moc 工具
      • (1)moc源代码位置
      • (2)运行过程
      • (3)调用方法
    • 3、qt_meta_stringdata_QObject_t、QT_MOC_LITERAL、qt_meta_stringdata_QObject
      • (1)qt_meta_stringdata_QObject_t::stringdata及其初始化
      • (2)QT_MOC_LITERAL
        • a. offsetof 即 __builtin_offsetof(type, member-designator)
        • b. qptrdiff
        • c. qptrdiff(offsetof(qt_meta_stringdata_CLS_ReflectionTest_t, stringdata0) + ofs - idx * sizeof(QByteArrayData))
      • (3)qt_meta_stringdata_QObject_t::data
      • (4)小结
    • 4、staticMetaObject
      • (1)__attribute__((init_priority()))
      • (2) link函数的作用和超对象
      • (3)qt_meta_data_CLS_ReflectionTest
        • a. content
        • b. ClassInfo
        • c. signals slots methods : name, argc, parameters, tag, flags
        • d. parameters
        • e. properties : name, type, flags
        • f. properties: notify_signal_id
        • g. enums: name, alias, flags, count, data
        • h. enum data: key, value
        • i. constructors: name, argc, parameters, tag, flags

Qt中的特性信号槽,其实现就依赖于Qt的元对象系统。那么Qt的元对象系统是指什么呢?Qt的元对象系统依赖于什么实现的,又怎样使用呢?

一、什么是元对象系统

首先看下Qt官方的对元对象系统的描述The Meta-Object System

1、元对象系统目的

对象间通信
运行时类型信息
动态属性

2、实现元对象系统的关键

继承自QObject
声明Q_OBJECT宏
moc工具自动生成必要代码

3、元对象系统的其他一些特性

QObject::metaObject()
QMetaObject::className()
QObject::inherits()
QObject::tr() and QObject::trUtf8()
QObject::setProperty() and QObject::property()
QMetaObject::newInstance()
qobject_cast

二、Q_OBJECT

// qobjectdefs.h
/* qmake ignore Q_OBJECT */
#define Q_OBJECT \
public: \QT_WARNING_PUSH \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 \
private: \Q_OBJECT_NO_ATTRIBUTES_WARNING \Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \QT_WARNING_POP \struct QPrivateSignal {}; \QT_ANNOTATE_CLASS(qt_qobject, "")

这里定义的变量和方法我们下面会说道,都是用于获取类属性及方法调用的,现在主要来看看这里的宏的作用。

1、QT_WARNING_PUSH QT_WARNING_POP

//   qcompilerdetection.h
#define QT_DO_PRAGMA(text)                      _Pragma(#text)
#  define QT_WARNING_PUSH                       QT_DO_PRAGMA(clang diagnostic push)
#  define QT_WARNING_POP                        QT_DO_PRAGMA(clang diagnostic pop)

这两个宏对应的都是编译器提供的。

(1)_Pragma #pragma

这两个的作用其实类似,都是用在具体编译器实现特定编译选项的宏。#pragma 是C90的定义。其缺点每次声明编译选项时都要重新写一遍,而无法用一个宏定义去简化。因为#在宏定义中的作用咱们也说过。因此C99中就提供了 _Pragma 关键字简化我们的使用。

(2)作用

参考clang diagnostic的使用。其作用就是用于忽略代码中间产生的特定种类的警告。

2、Q_OBJECT_NO_OVERRIDE_WARNING

#  define Q_OBJECT_NO_OVERRIDE_WARNING      QT_WARNING_DISABLE_CLANG("-Winconsistent-missing-override")

这个警告的作用是子类继承父类并覆盖虚函数实现时如果没有使用override关键字会发出警告。

3、QT_TR_FUNCTIONS

#  define QT_TR_FUNCTIONS \static inline QString tr(const char *s, const char *c = nullptr, int n = -1) \{ return staticMetaObject.tr(s, c, n); } \QT_DEPRECATED static inline QString trUtf8(const char *s, const char *c = nullptr, int n = -1) \{ return staticMetaObject.tr(s, c, n); }

这部分主要的作用是对于本地化的支持,即使用QTranslator时可以支持多语言。

4、QT_ANNOTATE_CLASS(qt_qobject, “”)

// The following macros can be defined by tools that understand Qt
// to have the information from the macro.
#ifndef QT_ANNOTATE_CLASS
# define QT_ANNOTATE_CLASS(type, ...)
#endif

展开的话发现其实什么都没有。但是注意上面的注释,用于工具去解读信息。那么我们看下源码中对这个宏的调用。全局搜了下只有在qdoc工具中找到了,以后学习qdoc咱们再来看看。

// clangcodeparser.cpp
static const char *defaultArgs_[] = {"-std=c++14",
#ifndef Q_OS_WIN"-fPIC",
#else"-fms-compatibility-version=19",
#endif"-DQ_QDOC","-DQ_CLANG_QDOC","-DQT_DISABLE_DEPRECATED_BEFORE=0","-DQT_ANNOTATE_CLASS(type,...)=static_assert(sizeof(#__VA_ARGS__),#type);","-DQT_ANNOTATE_CLASS2(type,a1,a2)=static_assert(sizeof(#a1,#a2),#type);","-DQT_ANNOTATE_FUNCTION(a)=__attribute__((annotate(#a)))","-DQT_ANNOTATE_ACCESS_SPECIFIER(a)=__attribute__((annotate(#a)))","-Wno-constant-logical-operand","-Wno-macro-redefined","-Wno-nullability-completeness","-ferror-limit=0","-I" CLANG_RESOURCE_DIR
};

三、QMetaObject 和 moc

QMetaObject可以说是元对象的基础。不过大部分时候这部分的实现都是由编译器调用 moc 工具自动生成的。我们就从这个类开始看元对象系统的构成。还是自下而上的进行分析,中间可能为了了某些功能的作用或实现可能要分析moc源码。

1、QMetaObject 中的 struct SuperData

这个结构体定义的实际是元对象的超类对象(什么是超类对象下面再说)。声明如下:

// qobjectdefs.h
struct SuperData {const QMetaObject *direct;SuperData() = default;constexpr SuperData(std::nullptr_t) : direct(nullptr) {}constexpr SuperData(const QMetaObject *mo) : direct(mo) {}constexpr const QMetaObject *operator->() const { return operator const QMetaObject *(); }#ifdef QT_NO_DATA_RELOCATIONusing Getter = const QMetaObject *(*)();Getter indirect = nullptr;constexpr SuperData(Getter g) : direct(nullptr), indirect(g) {}constexpr operator const QMetaObject *() const{ return indirect ? indirect() : direct; }template <const QMetaObject &MO> static constexpr SuperData link(){ return SuperData(QMetaObject::staticMetaObject<MO>); }
#elseconstexpr operator const QMetaObject *() const{ return direct; }template <const QMetaObject &MO> static constexpr SuperData link(){ return SuperData(QMetaObject::staticMetaObject<MO>()); }
#endif};

这里direct就是实际保存的超类对象指针。

(1)nullptr std::nullptr_t

C++11支持的两个特性。
nullptr 关键字用于声明空指针,主要是为了解决C++重载时对于C中的 NULL 无法判断类型的问题,详情可以参考nullptr详解。
std::nullptr_t 是空指针类型,定义如下。

// c++config.h
#if __cplusplus >= 201103Ltypedef decltype(nullptr) nullptr_t;
#endif

这种类型主要用于重载函数时,若参数为指针,可以用此类型指定传参为空指针所调用的函数。详情可以参考std::nullptr_t。

(2)constexpr

constexpr也是C++11支持的特性。其主要作用是用于 优化编译,告诉编译器编译的时候即可得出该表达式的值。
这个关键字和 const 的区别在于 const 只是用于声明具名常量的,在编译时并不会把具名常量直接用所对应的值替代。

(3)link函数

这个函数是一个 static 函数,用于直接通过类名调用。模板类为 QMetaObject 类引用。这里调用的 QMetaObject::staticMetaObject 也是 QMetaObject 类的函数,声明如下:

template <const QMetaObject &MO> static constexpr const QMetaObject *staticMetaObject()
{return &MO;
}

其实就是返回引用的地址,具体的使用下面再说。

2、moc 工具

这里并不主要讨论moc是怎么编译文件的,感兴趣的可以参考Qt元对象系统(Meta-Object)(四)、Moc源代码分析(其实这篇文章我也没有深入看过,不过如果有需要我自己也会看下源码)。其实这过程感觉就类似ini文件的读写,都是字符串操作。我们就简单说下moc源代码的位置以及其实现过程中调用的函数。

(1)moc源代码位置

Qt安装目录下Src\qtbase\src\tools\moc
主函数 main.cpp->main->runMoc

(2)运行过程

解析命令行参数
预处理 调用 Preprocessor::preprocessed 位于preprocessor.cpp中
解析文件 调用 Moc::parse — 位于moc.cpp文件中
生成moc文件 调用 Moc::generate -> Generator::generateCode — 位于generator.cpp文件中

这里有一个关键点就是预处理过程中的 符号解析。解析的目的就是把特殊符号对应的行数,起止字符位置,以及token类型解读出来。包括 #include class 这些都是被解析的类型。解析后方便后续生成过程的处理。这里解析的时候用到了两个数组,keywordskeyword_trans 用的是一种表驱动法对数据进行处理。

(3)调用方法

moc 源文件名 -o 目标文件名

3、qt_meta_stringdata_QObject_t、QT_MOC_LITERAL、qt_meta_stringdata_QObject

这部分用于初始化元对象。我们使用 moc 工具对源码中的 qobject.h 进行编译得到 moc_qobject.cpp,我们从这个文件进行学习。

struct qt_meta_stringdata_QObject_t {QByteArrayData data[8];char stringdata0[87];
};
#define QT_MOC_LITERAL(idx, ofs, len) \Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \qptrdiff(offsetof(qt_meta_stringdata_QObject_t, stringdata0) + ofs \- idx * sizeof(QByteArrayData)) \)
static const qt_meta_stringdata_QObject_t qt_meta_stringdata_QObject = {{QT_MOC_LITERAL(0, 0, 7), // "QObject"
QT_MOC_LITERAL(1, 8, 9), // "destroyed"
QT_MOC_LITERAL(2, 18, 0), // ""
QT_MOC_LITERAL(3, 19, 17), // "objectNameChanged"
QT_MOC_LITERAL(4, 37, 10), // "objectName"
QT_MOC_LITERAL(5, 48, 11), // "deleteLater"
QT_MOC_LITERAL(6, 60, 19), // "_q_reregisterTimers"
QT_MOC_LITERAL(7, 80, 6) // "parent"},"QObject\0destroyed\0\0objectNameChanged\0""objectName\0deleteLater\0_q_reregisterTimers\0""parent"
};

qt_meta_stringdata_CLS_ReflectionTest_t 是根据类名生成的结构体,qt_meta_stringdata_CLS_ReflectionTest 是用这个结构体定义的一个变量。QT_MOC_LITERAL 是用来初始化结构体的一个宏定义。
这三个概念的理解得放在一起,首先我们知道了这个结构体的定义。先不管这个宏的意义,我们先从 qt_meta_stringdata_QObject 的初始化开始。

(1)qt_meta_stringdata_QObject_t::stringdata及其初始化

首先我们要弄明白这个字段表示什么意思。从下面初始化的内容中我们可以看出来至少里面是包括了一些方法名称,同时各字段以"\0"分隔。那么具体包含那些特殊的方法?或者是否还包含成员变量呢?我们需要简单看下 moc 工具的源代码。

// moc.cpp
// void Moc::generate(FILE *out)
for (i = 0; i < classList.size(); ++i) {Generator generator(&classList[i], metaTypes, knownQObjectClasses, knownGadgets, out);generator.generateCode();
}

这里我们可以看出对每个类都会创建这样一段由类名定义的类型及变量。
那么每个类中的stringdata字段由哪些数据构成呢?

// generator.cpp
// Generator::generateCode()
strreg(cdef->qualified);
registerClassInfoStrings();
registerFunctionStrings(cdef->signalList);
registerFunctionStrings(cdef->slotList);
registerFunctionStrings(cdef->methodList);
registerFunctionStrings(cdef->constructorList);
registerByteArrayVector(cdef->nonClassSignalList);
registerPropertyStrings();
registerEnumStrings();

这里一共注册了9种字符串内容:
类名 — (由 class 声明)
类的信息 — (宏定义 CLASSINFO 自行指定的,如作者等,详见Qt手册对该宏的介绍)
信号 — (由 signals 声明)
槽函数 — (由 slots 声明)
注册的方法 — (宏定义 Q_INVOKABLE 修饰的方法)
构造函数
含有 NOTIFY 访问器的注册的属性
其余注册的属性 — (由 Q_PROPERTY 声明)
枚举类型 —(由 Q_ENUM 声明)

其实仔细看源码的话,每种其实还有些其他限制条件,我们这里不一点点看了。下面在自己的程序中把这些都尝试一下。

// CLS_ReflectionTest.h
#ifndef CLS_REFLECTIONTEST_H
#define CLS_REFLECTIONTEST_H#include <QObject>class CLS_ReflectionTest : public QObject
{Q_OBJECTQ_CLASSINFO("Author", "coding-zwh")Q_PROPERTY(int num READ GetNum WRITE SetNum)Q_PROPERTY(int color READ GetColor WRITE SetColor NOTIFY sigColorChanged)
public:enum EnumTest{e_Test_1st,e_Test_2nd};Q_ENUM(EnumTest);Q_INVOKABLE CLS_ReflectionTest(QObject *p = nullptr, QString qstr = "");Q_INVOKABLE void Test();signals:void sigTest(int _iPara);public slots:void slotTest(int _iPara);
};#endif // CLS_REFLECTIONTEST_H
// moc_CLS_ReflectionTest.cpp
static const qt_meta_stringdata_CLS_ReflectionTest_t qt_meta_stringdata_CLS_ReflectionTest = {{QT_MOC_LITERAL(0, 0, 18), // "CLS_ReflectionTest" - classname
QT_MOC_LITERAL(1, 19, 6), // "Author" - classinfo
QT_MOC_LITERAL(2, 26, 10), // "coding-zwh" - classinfo
QT_MOC_LITERAL(3, 37, 7), // "sigTest" - signal
QT_MOC_LITERAL(4, 45, 0), // "" - signal
QT_MOC_LITERAL(5, 46, 6), // "_iPara" - signal
QT_MOC_LITERAL(6, 53, 8), // "slotTest" - slot
QT_MOC_LITERAL(7, 62, 4), // "Test" - invokable function
QT_MOC_LITERAL(8, 67, 1), // "p" - constructor
QT_MOC_LITERAL(9, 69, 4), // "qstr" - constructor
QT_MOC_LITERAL(10, 74, 15), // "sigColorChanged" - notify property
QT_MOC_LITERAL(11, 90, 3), // "num" - property
QT_MOC_LITERAL(12, 94, 5) // "color" - protperty
QT_MOC_LITERAL(13, 100, 8), // "EnumTest" - enum
QT_MOC_LITERAL(14, 109, 10), // "e_Test_1st" - enum
QT_MOC_LITERAL(15, 120, 10) // "e_Test_2nd" - enum},"CLS_ReflectionTest\0Author\0coding-zwh\0""sigTest\0\0_iPara\0slotTest\0Test\0p\0qstr\0""sigColorChanged\0num\0color\0EnumTest\0""e_Test_1st\0e_Test_2nd"
};

那么现在我们就知道了,我们例子中 stringdata 后面接了个数字0,长度是87。这个87是字符串数据的总长度。这个0根据总长度的大小计算,每个数组长度的上限是65536。如果超过了这个上限就会出现后缀1、2、3……。关于上面这九个元素的作用我们下面再具体分析。

(2)QT_MOC_LITERAL

a. offsetof 即 __builtin_offsetof(type, member-designator)

用于计算结构体成员相对于结构体首地址的偏移量。原理可以参考offsetof(s,m)解析。

b. qptrdiff

用于指针类型的声明,保证所有的指针在qt所支持的平台上所占位数都是一样的。实则就是计算了 void* 的指针长度,然后使用模板函数对其长度进行专门化处理。

c. qptrdiff(offsetof(qt_meta_stringdata_CLS_ReflectionTest_t, stringdata0) + ofs - idx * sizeof(QByteArrayData))

我们结合 qt_meta_stringdata_QObject 初始化中 QT_MOC_LITERAL 的调用看下上面的表达式在做什么。根据后面的注释,QByteArrayData 中每个元素的初始化都是三个参数:元素的index,元素相对于首地址的偏移量, 当前元素长度。同时我们发现 qt_meta_stringdata_QObject_t::QByteArrayData data 元素的个数和字符串常量的个数正好是相等的。那么上面这个表达式的作用就是:
对于第 index 个字符串常量。首先计算字符串常量数组首地址相对于结构体的偏移量,再加上该字符串常量首地址相对于字符串常量数组首地址的偏移量,再减去 index * QByteArrayData 的大小,那么得出来的数据大小实际上就是第index个字符串常量首地址与其对应的 QByteArrayData 数组元素首地址的偏移量。再将其转化为 qptrdiff 类型的数据。
那么 QT_MOC_LITERAL 这个宏的计算就很简单了,下面说说计算的结果具体意义是什么。

(3)qt_meta_stringdata_QObject_t::data

这里我们实际初始化的类型是 QArrayData,其被初始化的数据定义如下:

// qarraydata.h
struct Q_CORE_EXPORT QArrayData
{QtPrivate::RefCount ref;int size;uint alloc : 31;uint capacityReserved : 1;qptrdiff offset; // in bytes from beginning of header...
}

结合我们上面的分析,再把宏 Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET 展开。我们发现实际初始化的是 sizeoffset
sizelen 很好理解,字符串常量的长度
offsetqptrdiff(offsetof(qt_meta_stringdata_CLS_ReflectionTest_t, stringdata0) + ofs - idx * sizeof(QByteArrayData)) 这个一开始我没看懂。但是如果我们看下__QArrayData::data__ 函数的实现:

// qarraydata.h
void *data()
{Q_ASSERT(size == 0|| offset < 0 || size_t(offset) >= sizeof(QArrayData));return reinterpret_cast<char *>(this) + offset;
}const void *data() const
{Q_ASSERT(size == 0|| offset < 0 || size_t(offset) >= sizeof(QArrayData));return reinterpret_cast<const char *>(this) + offset;
}

我们可以看出来把 this 的地址加上偏移量得到的正是对应字符串常量的地址。因此应该是为了在调用 data 时能直接取到对应字符串常量的值。

(4)小结

那么现在分析了这么多我们就能看出来这部分最开始列出的结构,变量以及宏定义的作用,其实就是把用到的字符串常量存在结构体中。为了方便访问,用 QByteArrayData 类型的保存了每个字符串常量的偏移量和长度。
这里有一个小问题可以分析下,为什么不用 QStringList,而用 QByteArray 呢?
我个人觉得原因有两个:
节约空间。这里的空间包含两部分。字符串部分其实二者基本没什么差别。但是QString类使用了q\d指针的封装,所以多了一层内部引用的空间。
减少不必要的字符转化。QString的构造函数在不同的系统中由于系统默认编码类型不同,可能实现方式也不同。

4、staticMetaObject

Q_OBJECT宏中定义了一个 static 变量叫 staticMetaObject。那么由于是静态变量,我们可以知道其实这个 __staticMetaObject__是属于整个类的,也就是Qt的元对象系统这里的元对象是针对每个类生成一个元对象,而不是针对每一个对象。该变量的初始化代码如下:

// moc_CLS_ReflectionTest.cpp
QT_INIT_METAOBJECT const QMetaObject CLS_ReflectionTest::staticMetaObject = { {QMetaObject::SuperData::link<QObject::staticMetaObject>(),qt_meta_stringdata_CLS_ReflectionTest.data,qt_meta_data_CLS_ReflectionTest,qt_static_metacall,nullptr,nullptr
} };

那么现在我们看看这个初始化的表达式初始化的哪些数据呢?

// qobjectdefs.h
struct { // private dataSuperData superdata;const QByteArrayData *stringdata;const uint *data;typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);StaticMetacallFunction static_metacall;const SuperData *relatedMetaObjects;void *extradata; //reserved for future use} d

这里把它命名为d,应该是想和d指针的使用保持一致。事实上它的作用和d指针也是一样的,即保存私有数据。
那么我们先把这些代码搞懂(其实想把它搞懂,moc文件的一半就弄明白了)。

(1)attribute((init_priority()))

define QT_INIT_METAOBJECT __attribute__((init_priority(101)))

这是用于gcc的关键字,用于控制变量初始化顺序。首先我们要知道,在标准C++中,在同一名称空间中,变量的初始化顺序是按照代码顺序进行的,在不同名称空间的变量初始化顺序是不确定的。而在gcc中,我们可以使用上面的表达式控制变量初始化的顺序。有两点需要注意:
a. 在括号中填入不同的数字表示变量初始化的不同顺序,数字的绝对大小并不影响其顺序,只有相对大小影响;
b. 数字取值范围介于101和65535之间(含101和65535)。

(2) link函数的作用和超对象

上面我们讲了调用这个函数是初始化元对象的超类对象。结合上面的一系列流程,我们能想到使用引用作为模板类的关键就是节约空间。因为这样每个元对象只需要保存其超对象指针便可访问对应的静态对象。否则每个类都要存储被它继承的类以及再向上追溯的所有类的元对象,着实浪费空间。
当然我这里显示的 link 函数传的模板为 QObject::staticMetaObject 是因为我写的类继承自QObject。如果把继承关系改了,那么这里相应的模板也会更改。moc_qobject.cpp 中的初始化方式如下:

QT_INIT_METAOBJECT const QMetaObject QObject::staticMetaObject = { {nullptr,qt_meta_stringdata_QObject.data,qt_meta_data_QObject,qt_static_metacall,nullptr,nullptr
} };

超对象 — 一个类的元对象的 superclass 是基类的元对象,不是父对象的元对象。

(3)qt_meta_data_CLS_ReflectionTest

// moc_CLS_ReflectionTest
static const uint qt_meta_data_CLS_ReflectionTest[] = {// content:8,       // revision0,       // classname1,   14, // classinfo3,   16, // methods2,   47, // properties1,   55, // enums/sets3,   64, // constructors0,       // flags1,       // signalCount// classinfo: key, value1,    2,// signals: name, argc, parameters, tag, flags3,    1,   31,    4, 0x06 /* Public */,// slots: name, argc, parameters, tag, flags6,    1,   34,    4, 0x0a /* Public */,// methods: name, argc, parameters, tag, flags7,    0,   37,    4, 0x02 /* Public */,// signals: parametersQMetaType::Void, QMetaType::Int,    5,// slots: parametersQMetaType::Void, QMetaType::Int,    5,// methods: parametersQMetaType::Void,// constructors: parameters0x80000000 | 4, QMetaType::QObjectStar, QMetaType::QString,    8,    9,0x80000000 | 4, QMetaType::QObjectStar,    8,0x80000000 | 4,// properties: name, type, flags11, QMetaType::Int, 0x00095003,12, QMetaType::Int, 0x00495003,// properties: notify_signal_id0,1879048202,// enums: name, alias, flags, count, data13,   13, 0x0,    2,   60,// enum data: key, value14, uint(CLS_ReflectionTest::e_Test_1st),15, uint(CLS_ReflectionTest::e_Test_2nd),// constructors: name, argc, parameters, tag, flags0,    2,   38,    4, 0x0e /* Public */,0,    1,   43,    4, 0x2e /* Public | MethodCloned */,0,    0,   46,    4, 0x2e /* Public | MethodCloned */,0        // eod
};

这个变量是一个 int 数组,内部实际储存了很多东西。我们跟着注释学习。

a. content

// content:8,       // revision0,       // classname1,   14, // classinfo3,   16, // methods2,   47, // properties1,   55, // enums/sets3,   64, // constructors0,       // flags1,       // signalCount

这部分的长度是由枚举 MetaObjectPrivateFieldCount 定义的,对比这个变量所对应的初始化变量 data 的用法:

// qmetaobject_p.h
static inline const QMetaObjectPrivate *get(const QMetaObject *metaobject){ return reinterpret_cast<const QMetaObjectPrivate*>(metaobject->d.data); }

我们知道它实际存储的是元对象的私有类 QMetaObjectPrivate 对象。通过对这个数组进行强制类型转换可以得到该对象指针(二者基地址相同)。

struct QMetaObjectPrivate
{// revision 7 is Qt 5.0 everything lower is not supported// revision 8 is Qt 5.12: It adds the enum name to QMetaEnumenum { OutputRevision = 8 }; // Used by moc, qmetaobjectbuilder and qdbusint revision; // OutputRevision int className; // index of classname in a fileint classInfoCount, classInfoData; // classInfoCount - num of macro Q_CLASSINFO used in classint methodCount, methodData;// methodCount - num of macro Q_INVOKABLE add num of signals and slotsint propertyCount, propertyData;// propertyCount - num of macro Q_PROPERTY except NOTIFYint enumeratorCount, enumeratorData;// enumeratorCount - num of macro Q_ENUMint constructorCount, constructorData;// propertyCount - num of constructor functionint flags;// Ideally, all the classes could have that flag. But this broke classes generated// by qdbusxml2cpp which generate code that require that we call qt_metacall for propertiesint signalCount;// num of signals...
}

上面对一部分变量进行了注释,其余带data的数据都是储存了相对 qt_meta_data_CLS_ReflectionTest 基地址的偏移量。举个例子,QMetaObjectPrivate 类对象的大小为 14 * sizeof(int),那么classinfo的起始地址就是 classInfoData + 14 * sizeof(int)。可以参考以下代码进行了解:

// generator.cpp
int index = MetaObjectPrivateFieldCount;fprintf(out, "static const uint qt_meta_data_%s[] = {\n", qualifiedClassNameIdentifier.constData());fprintf(out, "\n // content:\n");fprintf(out, "    %4d,       // revision\n", int(QMetaObjectPrivate::OutputRevision));fprintf(out, "    %4d,       // classname\n", stridx(cdef->qualified));fprintf(out, "    %4d, %4d, // classinfo\n", cdef->classInfoList.count(), cdef->classInfoList.count() ? index : 0);index += cdef->classInfoList.count() * 2;// variable index refers to class member with data

b. ClassInfo

// classinfo: key, value1,    2,

这里的值为1,2,看起来就像是index,那么具体咱们可以结合下面的代码看看是不是index。

// qmetaobject.cpp
QMetaClassInfo QMetaObject::classInfo(int index) const
{int i = index;i -= classInfoOffset();if (i < 0 && d.superdata)return d.superdata->classInfo(index);QMetaClassInfo result;if (i >= 0 && i < priv(d.data)->classInfoCount) {result.mobj = this;result.handle = priv(d.data)->classInfoData + 2*i;}return result;
}

这里的 priv 定义如下:

// qmetaobject.cpp
static inline const QMetaObjectPrivate *priv(const uint* data)
{ return reinterpret_cast<const QMetaObjectPrivate*>(data); }

结合我们刚才看的 classInfoData,我们知道这里达到的的 result.handle 实际上就是第 i 个 ClassInfo 的信息。
那么我们看下这里的 handle 是怎么被调用的。

// qmetaobject.cpp
const char *QMetaClassInfo::name() const
{if (!mobj)return 0;return rawStringData(mobj, mobj->d.data[handle]);
}const char* QMetaClassInfo::value() const
{if (!mobj)return 0;return rawStringData(mobj, mobj->d.data[handle + 1]);
}
// qmetaobject.cpp
static inline const char *rawStringData(const QMetaObject *mo, int index)
{return stringData(mo, index).data();
}
// qmetaobject.cpp
static inline const QByteArray stringData(const QMetaObject *mo, int index)
{Q_ASSERT(priv(mo->d.data)->revision >= 7);const QByteArrayDataPtr data = { const_cast<QByteArrayData*>(&mo->d.stringdata[index]) };Q_ASSERT(data.ptr->ref.isStatic());Q_ASSERT(data.ptr->alloc == 0);Q_ASSERT(data.ptr->capacityReserved == 0);Q_ASSERT(data.ptr->size >= 0);return data;
}

结合这几段代码代码,我们知道这里的1,2表示的就是 ClassInfoqt_meta_stringdata_QObject_t::data 即我们前面讨论过的字符串常量结构体中的下标。也就是通过这两个数字来访问对应的 ClassInfo 的键值对。

c. signals slots methods : name, argc, parameters, tag, flags

// signals: name, argc, parameters, tag, flags3,    1,   31,    4, 0x06 /* Public */,// slots: name, argc, parameters, tag, flags6,    1,   34,    4, 0x0a /* Public */,// methods: name, argc, parameters, tag, flags7,    0,   37,    4, 0x02 /* Public */,

第一个数字同样是字符串常量对应的index。第二个数字是参数个数。后面的我们同样结合源码来看。

// qmetaobject.cpp
int QMetaMethodPrivate::typesDataIndex() const
{Q_ASSERT(priv(mobj->d.data)->revision >= 7);return mobj->d.data[handle + 2];
}const char *QMetaMethodPrivate::rawReturnTypeName() const
{Q_ASSERT(priv(mobj->d.data)->revision >= 7);uint typeInfo = mobj->d.data[typesDataIndex()];if (typeInfo & IsUnresolvedType)return rawStringData(mobj, typeInfo & TypeNameIndexMask);elsereturn QMetaType::typeName(typeInfo);
}

这里可以看出第三个数字实际是 qt_meta_data_CLS_ReflectionTest 的下标,表示的是返回值的类型在此数组中的下标。

// qmetaobject.cpp
QByteArray QMetaMethodPrivate::tag() const
{Q_ASSERT(priv(mobj->d.data)->revision >= 7);return stringData(mobj, mobj->d.data[handle + 3]);
}

这个同样是在字符串常量中的index。表示的是自定义标签。如果所有的信号、槽、方法都没有自定义标签,而且三者加起来至少存在一个,则会在字符串常量中插入一个空字符串。相关使用可以参考Qt手册中 QMetaMethod::tag

// qmetaobject.cpp
int QMetaMethod::attributes() const
{if (!mobj)return false;return ((mobj->d.data[handle + 4])>>4);
}int QMetaMethod::revision() const
{if (!mobj)return 0;if ((QMetaMethod::Access)(mobj->d.data[handle + 4] & MethodRevisioned)) {int offset = priv(mobj->d.data)->methodData+ priv(mobj->d.data)->methodCount * 5+ QMetaMethodPrivate::get(this)->ownMethodIndex();return mobj->d.data[offset];}return 0;
}QMetaMethod::Access QMetaMethod::access() const
{if (!mobj)return Private;return (QMetaMethod::Access)(mobj->d.data[handle + 4] & AccessMask);
}QMetaMethod::MethodType QMetaMethod::methodType() const
{if (!mobj)return QMetaMethod::Method;return (QMetaMethod::MethodType)((mobj->d.data[handle + 4] & MethodTypeMask)>>2);
}

从几个方法我们可以看出来,最后一个参数实际上是按位保存属性的。共8位。从高位到低位看:
第7位是表示此方法是否有版本号。版本号由宏定义 Q_REVISION 定义在方法前面。那么根据上面的 revision 函数,我们可以看出来方法对应的版本同样存在 当前变量中,紧跟在我们分析这部分之后。
第5、6位表示的是方法的性质。由 QMetaMethod 中的枚举类型 Attributes 定义。

// qmetaobject.h
enum Attributes { Compatibility = 0x1, Cloned = 0x2, Scriptable = 0x4 };

这里的 CompatibilityScriptable 我看了半天没看出来什么意思。结合源码的话 Compatibility 可能是判断信号和槽是否能连接,Scriptable 是一种脚本方法,用在 QDBusMetaObject 中。这两个属性分别由宏 QT_MOC_COMPATQ_SCRIPTABLE 修饰。(这部分是猜测)
这里的 Cloned 并不是由关键字标明的。函数每增加一个默认参数,就会增加一个带 Cloned 标识的函数。那么这里的用处应该和下次要讨论的反射有关。
第3、4位表示的是方法的种类。由 QMetaMethod 中的枚举类型 MethodType 定义。

// qmetaobject.h
enum MethodType { Method, Signal, Slot, Constructor };

第1、2位表示的是方法的访问权限。由 QMetaMethod 中的枚举类型 Access 定义。

// qmetaobject.h
enum Access { Private, Protected, Public };

d. parameters

 // signals: parametersQMetaType::Void, QMetaType::Int,    5,// slots: parametersQMetaType::Void, QMetaType::Int,    5,// methods: parametersQMetaType::Void,// constructors: parameters0x80000000 | 4, QMetaType::QObjectStar, QMetaType::QString,    8,    9,0x80000000 | 4, QMetaType::QObjectStar,    8,0x80000000 | 4,

这里第一列我们刚才讨论过,其实就是方法部分第三个参数返回值类型所对应的偏移量。第二个参数结合源码分析:

// qmetaobject.cpp
int QMetaMethodPrivate::parametersDataIndex() const
{Q_ASSERT(priv(mobj->d.data)->revision >= 7);return typesDataIndex() + 1;
}uint QMetaMethodPrivate::parameterTypeInfo(int index) const
{Q_ASSERT(priv(mobj->d.data)->revision >= 7);return mobj->d.data[parametersDataIndex() + index];
}

它表示的应该是入参的类型。
我并没有找到调用 typesDataIndex() + 2 的地方。因此这里我们只能看下源码写入的部分。

// generator.cpp
void Generator::generateFunctionParameters(const QVector<FunctionDef>& list, const char *functype)
{...// Parameter namesfor (int j = 0; j < argsCount; ++j) {const ArgumentDef &arg = f.arguments.at(j);fprintf(out, " %4d,", stridx(arg.name));}...
}

从字面意思可以看出来这是个Index。那么对比我们moc文件里保存的两个数组,我们知道这个index实际存的是 qt_meta_stringdata_CLS_ReflectionTest 中的字符串常量index,对应的是形参名称。那么我们也就知道了不同函数的相同形参名称在字符串常量数组中只保存了一份。

e. properties : name, type, flags

 // properties: name, type, flags11, QMetaType::Int, 0x00095003,12, QMetaType::Int, 0x00495003,

第一个参数,很明显是字符串常量数组中的index。第二个参数是类型。第三个参数是不同属性关键字的位与。

// qmetaobject_p.h
enum PropertyFlags  {Invalid = 0x00000000,Readable = 0x00000001,Writable = 0x00000002,Resettable = 0x00000004,EnumOrFlag = 0x00000008,StdCppSet = 0x00000100,
//     Override = 0x00000200,Constant = 0x00000400,Final = 0x00000800,Designable = 0x00001000,ResolveDesignable = 0x00002000,Scriptable = 0x00004000,ResolveScriptable = 0x00008000,Stored = 0x00010000,ResolveStored = 0x00020000,Editable = 0x00040000,ResolveEditable = 0x00080000,User = 0x00100000,ResolveUser = 0x00200000,Notify = 0x00400000,Revisioned = 0x00800000
};

f. properties: notify_signal_id

// properties: notify_signal_id0,1879048202,

根据源代码看下作用

// moc.cpp
// void Moc::checkProperties(ClassDef *cdef)
if(!p.notify.isEmpty()) {int notifyId = -1;for (int j = 0; j < cdef->signalList.count(); ++j) {const FunctionDef &f = cdef->signalList.at(j);if(f.name != p.notify) {continue;} else {notifyId = j /* Signal indexes start from 0 */;break;}}p.notifyId = notifyId;if (notifyId == -1) {int index = cdef->nonClassSignalList.indexOf(p.notify);if (index == -1) {cdef->nonClassSignalList << p.notify;p.notifyId = -1 - cdef->nonClassSignalList.count();} else {p.notifyId = -2 - index;}}
}

这里的index指的是被声明为notify类型的方法名在signal列表中的index。如果没有对应的signal,会去notify方法的list中去寻找是否有同名方法。如果没有,则为-1减去该list的长度;如果有,则为-2-index。
注意这里的index不可能为-1。

// generator.cpp
// void Generator::generateProperties()
if(cdef->notifyableProperties) {fprintf(out, "\n // properties: notify_signal_id\n");for (int i = 0; i < cdef->propertyList.count(); ++i) {const PropertyDef &p = cdef->propertyList.at(i);if (p.notifyId == -1) {fprintf(out, "    %4d,\n",0);} else if (p.notifyId > -1) {fprintf(out, "    %4d,\n",p.notifyId);} else {const int indexInStrings = strings.indexOf(p.notify);fprintf(out, "    %4d,\n",indexInStrings | IsUnresolvedSignal);}}}

这里有三个分支。第一个分支对应的是没有声明NOTIFY方法的属性。剩下两个分支对应咱们上面讨论的情况。因此我们moc文件里打印出来的两个数字:0代表第一个属性,1879048202代表声明了NOTIFY却没有对应信号声明。

g. enums: name, alias, flags, count, data

// enums: name, alias, flags, count, data13,   13, 0x0,    2,   60,

第一个参数还是index。下面继续看源码。

// generator.cpp
// void Generator::generateCode()
QVector<EnumDef> enumList;
for (int i = 0; i < cdef->enumList.count(); ++i) {EnumDef def = cdef->enumList.at(i);if (cdef->enumDeclarations.contains(def.name)) {enumList += def;}def.enumName = def.name;QByteArray alias = cdef->flagAliases.value(def.name);if (cdef->enumDeclarations.contains(alias)) {def.name = alias;enumList += def;}
}
cdef->enumList = enumList;

这里是唯一一处对enumName赋值的地方。

// generator.cpp
// void Generator::generateEnums(int index)
fprintf(out, "\n // enums: name, alias, flags, count, data\n");index += 5 * cdef->enumList.count();int i;for (i = 0; i < cdef->enumList.count(); ++i) {const EnumDef &e = cdef->enumList.at(i);int flags = 0;if (cdef->enumDeclarations.value(e.name))flags |= EnumIsFlag;if (e.isEnumClass)flags |= EnumIsScoped;fprintf(out, "    %4d, %4d, 0x%.1x, %4d, %4d,\n",stridx(e.name),e.enumName.isNull() ? stridx(e.name) : stridx(e.enumName),flags,e.values.count(),index);index += e.values.count() * 2;}

这里我们看出来第二个参数实际和第一个参数应该是相同的。alias — 别名
第三个参数表示的是枚举的两个属性:flag 表示该枚举类型是由宏Q_FLAG声明的,其作用是表明该枚举类型内的定义可以用按位与的方式进行组合。scope 表明该枚举类型被 class 或者struct 修饰。
第四个参数表示枚举中数值的个数。
第五个参数是从外部传过来的index。咱们上面也分析过类似的index,实则就是在当前数组中的下标。用来访问咱们下面提到的枚举类型键值对。

h. enum data: key, value

// enum data: key, value14, uint(CLS_ReflectionTest::EnumTest::e_Test_1st),15, uint(CLS_ReflectionTest::EnumTest::e_Test_2nd),

这部分很简单就是存储了字符串常量数组中枚举常量的名称及其对应值。

i. constructors: name, argc, parameters, tag, flags

// constructors: name, argc, parameters, tag, flags0,    2,   38,    4, 0x0e /* Public */,0,    1,   43,    4, 0x2e /* Public | MethodCloned */,0,    0,   46,    4, 0x2e /* Public | MethodCloned */,0        // eod

这部分和前面d部分是一样的,只是存储在了数组的不同位置。最后打了一个结束表示(end of data)。

基于Qt5.14.2和mingw的Qt源码学习(三) — 元对象系统简介及moc工具是如何保存类属性和方法的相关推荐

  1. 基于JAVA校内图书馆智能管理系统计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVA校内图书馆智能管理系统计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVA校内图书馆智能管理系统计算机毕业设计源码+数据库+lw文档+系统+部署 本源码技术栈: 项目架构:B/S ...

  2. 基于JAVA响应式交友网站计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVA响应式交友网站计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVA响应式交友网站计算机毕业设计源码+数据库+lw文档+系统+部署 本源码技术栈: 项目架构:B/S架构 开发语言: ...

  3. 基于JAVA网上家教信息管理系统计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVA网上家教信息管理系统计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVA网上家教信息管理系统计算机毕业设计源码+数据库+lw文档+系统+部署 本源码技术栈: 项目架构:B/S架构 ...

  4. 基于JAVA图书馆座位预约管理系统计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVA图书馆座位预约管理系统计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVA图书馆座位预约管理系统计算机毕业设计源码+数据库+lw文档+系统+部署 本源码技术栈: 项目架构:B/S ...

  5. 基于JAVA线上动漫周边商城计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVA线上动漫周边商城计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVA线上动漫周边商城计算机毕业设计源码+数据库+lw文档+系统+部署 本源码技术栈: 项目架构:B/S架构 开发语 ...

  6. 基于JAVA服装连锁店后台管理系统计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVA服装连锁店后台管理系统计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVA服装连锁店后台管理系统计算机毕业设计源码+数据库+lw文档+系统+部署 本源码技术栈: 项目架构:B/S ...

  7. 基于JAVA皮皮狗宠物用品商城计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVA皮皮狗宠物用品商城计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVA皮皮狗宠物用品商城计算机毕业设计源码+数据库+lw文档+系统+部署 项目架构:B/S架构 开发语言:Java ...

  8. 基于JAVA高考报考指南网站计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVA高考报考指南网站计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVA高考报考指南网站计算机毕业设计源码+数据库+lw文档+系统+部署 本源码技术栈: 项目架构:B/S架构 开发语 ...

  9. 基于JAVA国产精品动漫网站计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVA国产精品动漫网站计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVA国产精品动漫网站计算机毕业设计源码+数据库+lw文档+系统+部署 本源码技术栈: 项目架构:B/S架构 开发语 ...

最新文章

  1. Java值类型与引用类型的不同
  2. php大数组循环嵌套的性能优化
  3. 201521123092《java程序设计》第九周学习总结
  4. 很现实、很暴力的面试法则 —— 来自招聘官的自述
  5. python 初始化一个4维向量_看图学NumPy:掌握n维数组基础知识点,看这一篇就够了...
  6. dockerHub国内镜像设置
  7. oracle大型数据库系统在AIX/unix上的实战详解 讨论76 Oracle备份问题
  8. 2014 ACM/ICPC Asia Regional Beijing Site
  9. 【小工具】根据定义的白名单字段进行Bean的拷贝
  10. IAT-Hook 劫持进程Api调用
  11. Docker for Mac配置阿里加速器
  12. 各省产业结构-高级化指数(二产与三产比值)合理化指数
  13. 联想G480安装WinXp
  14. 新托业模拟考试感言—了解一下参加过托业考试前辈们的经验01
  15. rabbitMq实现公平分发策略
  16. DOS命令的英文全称
  17. 抖音小程序入口和玩法,快速收割短视频红利!
  18. 戴尔(Dell)笔记本电脑开机后插上耳机没反应怎么办
  19. Java jcmd内存远大于top_Java堆外内存排查小结
  20. 什么叫数据于程序的物理独立性?什么叫数据与程序的逻辑独立性?为什么数据库系统具有数据于程序的独立性?

热门文章

  1. 利用MatLab对数据进行插值计算(分段插值和三次样条插值)
  2. 高斯白噪声及Matlab常用实现方法
  3. html5复选框控制按钮状态,HTML input checkbox复选按钮简介说明
  4. 安卓期末大作业——Android数独游戏
  5. centos7安装dos2unix
  6. 圆角矩形 shader
  7. 【读书笔记】《曾国藩的正面与侧面(三)》
  8. 关于ROS(Robot OS 机器人操作系统)
  9. 原子操作(Atomic)
  10. 什么叫原子操作?使用这个东西有什么目的?