Qt 不是使用“標準的”C++語言編寫,而是對其進行了一定程度的擴展。我們可以從Qt增加的關鍵字看出來:signals、slots或emit。但是使用gcc編譯時,編譯器并不認識這些非標準c++的關鍵字,那么就需要Qt自己將擴展的關鍵字處理成標準的C++代碼。Qt在編譯之前會分析源文件,當發現包含了 Q_OBJECT 宏,則會生成另外一個標準的C++源文件,這個源文件中包含了 Q_OBJECT 宏的實現代碼,這個源文件名字是將原文件名前面加上 moc_ 構成,這個新的文件同樣將進入編譯系統,終被鏈接到二進制代碼中去,此時,Qt將自己增加的擴展轉換成了標準的C++文件,moc 全稱是 Meta-Object Compiler,也就是“元對象編譯器”。這就是moc文件的由來。
下面我們來分析一下Moc文件:
一 示例代碼如下:
#include
class CTestMoc : public QObject
{
Q_OBJECT
public:
CTestMoc(){}
~CTestMoc(){}
signals:
void Test1();
void Test2(int iTemp);
private slots:
void OnTest1();
void OnTest2(int iTemp);
};
二 Q_OBJECT宏
#define Q_OBJECT \
public: \
Q_OBJECT_CHECK \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
QT_TR_FUNCTIONS \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
private: \
Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
struct QPrivateSignal {};
此宏在QObjectdefs.h頭文件中定義
1 Q_OBJECT_CHECK 定義如下:
#define Q_OBJECT_CHECK \
template
{ int i = qYouForgotTheQ_OBJECT_Macro(this, &_q_argument); i = i + 1; }
宏展開終會調用qYouForgotTheQ_OBJECT_Macro這個內聯函數。這個函數始終返回0,但是很不明白,為什么之后還要添加一句 i=i?,刨根之后,發現Q_OBJECT_CHECK宏并沒有做什么工作。
inline int qYouForgotTheQ_OBJECT_Macro(T, T) { return 0; }
2 static const QMetaObject staticMetaObject 靜態的元對象,這個對象在moc文件里會構建,在那里就能看到整個信號&槽的全貌。
3 virtual const QMetaObject *metaObject() const; 返回一個元對象。
4 virtual void *qt_metacast(const char *); 元對象中的字符數據轉換。
5 virtual int qt_metacall(QMetaObject::Call, int, void **); 元對象調用入口,注意此函數是public的,槽函數調用也是由這個函數開始。
6 static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); 由qt_metacall函數調用,槽函數調用真正處理函數。Q_DECL_HIDDEN_STATIC_METACALL這個宏看到后和linux系統有關,其它系統這個宏是一個空的宏。
三 Moc文件分析
先說結論在這里。
1 Qt的信號&槽之間的調用不是通過指針方式調用的而是通過索引方式來調用的.
2 信號也是一個函數。
Moc文件有幾個重要數據結構,把這幾個結構之間關系講清楚大家就清楚Qt的信號槽機制是如何工作的了。
第一個結構是 qt_meta_stringdata_CTestMoc_t 定義如下:
struct qt_meta_stringdata_CTestMoc_t {
QByteArrayData data[7];
char stringdata[45];
};
data字段是一個由byte數組組成的數組,數組大小根據信號&槽個數有關,這個數組在調用QObject的connect函數時用來匹配信號名或槽名。
stringdata 存放的是字符資源,存放全部的信號名、槽名、類名。
static const qt_meta_stringdata_CTestMoc_t qt_meta_stringdata_CTestMoc = {
{
QT_MOC_LITERAL(0, 0, 8),
QT_MOC_LITERAL(1, 9, 5),
QT_MOC_LITERAL(2, 15, 0),
QT_MOC_LITERAL(3, 16, 5),
QT_MOC_LITERAL(4, 22, 5),
QT_MOC_LITERAL(5, 28, 7),
QT_MOC_LITERAL(6, 36, 7)
},
"CTestMoc\0Test1\0\0Test2\0iTemp\0OnTest1\0"
"OnTest2\0"
};
qt_meta_stringdata_CTestMoc 這個就是一個 qt_meta_stringdata_CTestMoc_t結構體的實例。
QT_MOC_LITERAL(0, 0, 8), 這個宏生成一個byte數組,第一參數是索引,大家可以看到索引是由 0 - 6 共7個組成,對應的是data字段的長度7,第二個參數是在stringdata字段中的開始位置,第三個參數是長度。
例如 QT_MOC_LITERAL(0, 0, 8) 索引是0, 開始位置是0, 長度是8,對應的字符是"CTestMoc",后面的以此類推。
第二個結構是 static const uint qt_meta_data_CTestMoc[]
這個結構體描述的是信號&槽在調用時的索引、參數、返回值等信息。
static const uint qt_meta_data_CTestMoc[] = {
// content:
7, // revision
0, // classname
0, 0, // classinfo
4, 14, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
2, // signalCount
// signals: name, argc, parameters, tag, flags
1, 0, 34, 2, 0x06,
3, 1, 35, 2, 0x06,
// slots: name, argc, parameters, tag, flags
5, 0, 38, 2, 0x08,
6, 1, 39, 2, 0x08,
// signals: parameters
QMetaType::Void,
QMetaType::Void, QMetaType::Int, 4,
// slots: parameters
QMetaType::Void,
QMetaType::Void, QMetaType::Int, 4,
0 // eod
};
這個數組的前14個uint 描述的是元對象的私有信息,定義在qmetaobject_p.h文件的QMetaObjectPrivate結構體當中,QMetaObjectPrivate結構體我們不做深入分析,但是,在這個結構體中4, 14, // methods這個信息描述的是信號&槽的個數和在表中的偏移量,即14個uint之后是信息&槽的信息
qt_meta_data_CTestMoc這個表中我們可以看到每描述一個信號或槽需要5個uint
例如,從表的第14個uint開始描述的信號信息
// signals: name, argc, parameters, tag, flags
1, 0, 34, 2, 0x06,
3, 1, 35, 2, 0x06,
name:對應的是qt_meta_stringdata_CTestMoc 索引,例如1 對應的是Test1
argc:參數個數
parameters : 參數的在qt_meta_data_CTestMoc這個表中的索引位置。
例如 // signals: parameters
QMetaType::Void,
QMetaType::Void, QMetaType::Int, 4,
void 是信號的返回值,QMetaType::Int是參數類型, 4 是參數名,在qt_meta_stringdata_CTestMoc中的索引值。
tag:這個字段的數值對應的是qt_meta_stringdata_CTestMoc 索引,在這個moc文件里對應的是一個空字符串,具體怎么用,在源代碼里沒看懂。
flags:是一個特征值,是在 enum MethodFlags 枚舉中定義。
enum MethodFlags {
AccessPrivate = 0x00,
AccessProtected = 0x01,
AccessPublic = 0x02,
AccessMask = 0x03, //mask
MethodMethod = 0x00,
MethodSignal = 0x04,
MethodSlot = 0x08,
MethodConstructor = 0x0c,
MethodTypeMask = 0x0c,
MethodCompatibility = 0x10,
MethodCloned = 0x20,
MethodScriptable = 0x40,
MethodRevisioned = 0x80
};
大家可以看到,槽對應的是MethodSlot 0x08, 信號對應的是MethodSignal 和AccessPublic 相或。
第三部分 QObject 中靜態函數 qt_static_metacall 實現
void CTestMoc::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
CTestMoc *_t = static_cast
switch (_id) {
case 0: _t->Test1(); break;
case 1: _t->Test2((*reinterpret_cast< int(*)>(_a[1]))); break;
case 2: _t->OnTest1(); break;
case 3: _t->OnTest2((*reinterpret_cast< int(*)>(_a[1]))); break;
default: ;
}
..........
}
現在看這個就比較直觀了,qt_metacall方法通過索引調用其它內部方法。Qt動態機制不采用指針,而由索引實現。實際調用方法的工作由編譯器實現。這使得信號和槽的機制執行效率比較高。
參數由一個指向指針數組的指針進行傳遞,并在調用方法時進行適當的轉換。當然,使用指針是將不同類型的參數放在一個數組的唯一辦法。參數索引從1開始,因為0號代表函數返回值。
第四部分 QObject 中靜態staticMetaObject的賦值
const QMetaObject CTestMoc::staticMetaObject = {
{ &QObject::staticMetaObject, qt_meta_stringdata_CTestMoc.data,
qt_meta_data_CTestMoc, qt_static_metacall, 0, 0}
};
ok,通過這個靜態變量就保存了moc文件的信號&槽的調用索引信息。在信號&槽綁定的時候就是通過這些信息一步一步建立的綁定關系。
第四部分 信號就是函數。
大家可以Moc文件中看到信號的實現。