1. 底層數(shù)據(jù)結(jié)構(gòu)——建立連接時建立的什么。
讀者可先大致瀏覽一下qobject_p.cpp
中添加連接的實(shí)現(xiàn),回頭再細(xì)看:
void QObjectPrivate::addConnection(int signal, Connection *c)
{
/*
* 中心數(shù)據(jù)結(jié)構(gòu)是二維廣義表
* 不妨看作一個二維數(shù)組:
* A B C D E...
* 0
* 1
* 2
* 3
* 4
* ...
* 縱向序號0123..為signal序號,每一個signal對應(yīng)一個connectionList(無s)單鏈表。
* 所有conectionList 存放在類的 connectionLists這個向量中。
* 橫向序號為接收者reciver,相應(yīng)每一列為該reciver對應(yīng)接收signal的connection節(jié)點(diǎn)(有的話才分配,順序不一定按0123)
* 每列組成相應(yīng)縱向單鏈表senders(含義為該reciver的senders)。
* 每個對象自己保存著自己的senders單鏈表
* 該二維表為全局?jǐn)?shù)據(jù)結(jié)構(gòu),包含以signal索引的行鏈表connectionLists和每個對象自己保存的相應(yīng)senders鏈表
*
*/
Q_ASSERT(c->sender == q_ptr);
if (!connectionLists)
connectionLists = new QObjectConnectionListVector();
if (signal >= connectionLists->count())
connectionLists->resize(signal + 1);
ConnectionList &connectionList = (*connectionLists)[signal];
if (connectionList.last) {
connectionList.last->nextConnectionList = c;
} else {
connectionList.first = c;
}
connectionList.last = c;
cleanConnectionLists();
/*
* 插入節(jié)點(diǎn)的技巧:
* prev為二級指針,指向本對象的senders列表頭的地址addr(addr中保存的是表頭節(jié)點(diǎn)的地址)
* senders表本身為一個單鏈表。
* 但鏈表中每一個節(jié)點(diǎn)的prev都指向表頭地址addr,即訪問某一個節(jié)點(diǎn),也可以繼續(xù)訪問整個senders表。
* 更新senders表時,直接將c插到原表頭,原表頭變?yōu)閏的next。addr內(nèi)容變?yōu)閏的地址,但addr本身地址不變。
*/
c->prev = &(QObjectPrivate::get(c->receiver)->senders);
c->next = *c->prev;
*c->prev = c;
if (c->next)
c->next->prev = &c->next;
if (signal < 0) {
connectedSignals[0] = connectedSignals[1] = ~0;
} else if (signal < (int)sizeof(connectedSignals) * 8) {
connectedSignals[signal >> 5] |= (1 << (signal & 0x1f));
}
}
先看看傳入?yún)?shù):
傳入?yún)?shù)signal
就是所謂的信號,是一個整數(shù)。
Connection 類的定義,在qobject_p.h
中。
它是在QObjectPrivate類中定義的一個結(jié)構(gòu)體:
struct Connection
{
QObject *sender;
QObject *receiver;
union {
StaticMetaCallFunction callFunction;
QtPrivate::QSlotObjectBase *slotObj;
};
// The next pointer for the singly-linked ConnectionList
Connection *nextConnectionList;
//senders linked list
Connection *next;
Connection **prev;
QAtomicPointer<const int> argumentTypes;
QAtomicInt ref_;
ushort method_offset;
ushort method_relative;
uint signal_index : 27; // In signal range (see QObjectPrivate::signalIndex())
ushort connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
ushort isSlotObject : 1;
ushort ownArgumentTypes : 1;
Connection() : nextConnectionList(nullptr), ref_(2), ownArgumentTypes(true) {
//ref_ is 2 for the use in the internal lists, and for the use in QMetaObject::Connection
}
~Connection();
int method() const { Q_ASSERT(!isSlotObject); return method_offset + method_relative; }
void ref() { ref_.ref(); }
void deref() {
if (!ref_.deref()) {
Q_ASSERT(!receiver);
delete this;
}
}
};
可以看到,與鏈表相關(guān)的有三個指針,其中prev
是個二級指針。為什么這么用,在addConnection
函數(shù)我寫的注釋里可以看到一些解釋。其他的結(jié)構(gòu)體參數(shù)在這里不作深究,有些也可以見名知意。
采用二維表的目的有二:
- 能根據(jù)發(fā)出的信號找到所有接受者
- 能將某一個接受者接收的所有信號統(tǒng)一管理
采用鏈表的目的,自然就是方便動態(tài)管理了。
既然知道了建立的是什么數(shù)據(jù)結(jié)構(gòu),那么下一步就是理清是怎么建立的。
另外,我們都知道Qt中基本上所有的類都繼承于QObject這個類,這個類包含些什么?為什么連接的數(shù)據(jù)結(jié)構(gòu)是存在QObjectPrivate這個類中?QObjectPrivate和QObject是什么關(guān)系?為什么要這樣定義?這是我們最后要討論的問題。
2. 從信號和槽的定義到整數(shù)的轉(zhuǎn)換
(1) 神奇的關(guān)鍵字
再看信號和槽的基本定義和使用方式。
一個簡單例子:
定義一個 Counter類
class Counter : public QObject
{
Q_OBJECT
int m_value;
public:
int value() const { return m_value; }
public slots: //使用public slots聲明槽函數(shù)
void setValue(int value);
signals: //使用signal聲明信號,也是函數(shù)
void valueChanged(int newValue);
};
信號的發(fā)射方式。在某處發(fā)射信號的寫法:
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
//使用emit關(guān)鍵字發(fā)射信號,附帶參數(shù)。
emit valueChanged(value);
}
}
有了信號和槽,還需要將它們建立連接關(guān)系:
Counter a, b;
QObject::connect(&a, SIGNAL(valueChanged(int)),
&b, SLOT(setValue(int)));
或
//可省略QObject::默認(rèn)的是同一個函數(shù)
// 信號和對應(yīng)的槽,參數(shù)要一致
connect(&a, &a->valueChanged,&b, b->setValue);
首先來看這幾個Qt中特有的關(guān)鍵字的含義:slots,signals,emit
在no-keywords.h
中:
#define signals Q_SIGNALS
#define slots Q_SLOTS
#define emit Q_EMIT
具體地:
# define Q_SLOTS QT_ANNOTATE_ACCESS_SPECIFIER(qt_slot)
# define Q_SIGNALS public QT_ANNOTATE_ACCESS_SPECIFIER(qt_signal) //主要有個public
//進(jìn)一步可見:
# define QT_ANNOTATE_ACCESS_SPECIFIER(x)
#define Q_EMIT //(空)
簡單來說,emit就只是個寫程序用到的邏輯關(guān)鍵字,起提示作用。
而signals除了public關(guān)鍵字聲明為公有,其他部分就只在預(yù)處理過程中起作用。
slots就是只有預(yù)處理作用。
由此可見,定義的信號和槽函數(shù), 最終變成了整數(shù),而定義的特殊性,僅在于多一個預(yù)處理特征(包括#define xx
本身)。那么可以推測,是使用某種預(yù)處理機(jī)制,進(jìn)行了轉(zhuǎn)化。那么這是什么機(jī)制?如何運(yùn)作的?
(2) Qt 的MOC(the Meta Object Compiler)預(yù)處理器
文檔翻譯時間:
Qt信號/槽和屬性系統(tǒng)基于在運(yùn)行時內(nèi)省對象的能力。內(nèi)省意味著能夠列出對象的方法和屬性,并具有關(guān)于它們的各種信息,例如它們的參數(shù)類型。
C ++本身不提供內(nèi)省支持,因此Qt附帶了一個提供它的工具。該工具是MOC。它是一個代碼生成器(而不是像某些人所說的預(yù)處理器)。它解析頭文件并生成一個額外的C ++文件,該文件與程序的其余部分一起編譯。生成的C ++文件包含內(nèi)省所需的所有信息。
引用說那不是預(yù)處理,但是我仍然認(rèn)為,既然需要用到了宏定義,先對代碼進(jìn)行一遍處理(生成相關(guān)代碼也是一種處理),仍然可以看作是預(yù)處理,只不過是全部預(yù)處理的一部分。
再來看每個基于QObject類都需要聲明的一個宏Q_OBJECT:
#define Q_OBJECT \
public: \
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 /* translations helper */ \
private: \
Q_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);
Q_OBJECT定義了一堆函數(shù)和一個靜態(tài)QMetaObject這些函數(shù)在MOC生成的文件中實(shí)現(xiàn)。
簡單說,就是MOC根據(jù)代碼的關(guān)鍵字,自動提取出信號和槽,并進(jìn)行處理,生成了相應(yīng)的cpp文件,相關(guān)要使用的函數(shù)即由Q_OBJECT定義,也生成在相應(yīng)cpp文件中,隨整個工程一同進(jìn)行編譯鏈接。
對于老版本的信號和槽:
Q_CORE_EXPORT const char *qFlagLocation(const char *method);
#ifndef QT_NO_DEBUG
# define QLOCATION "\0" __FILE__ ":" QTOSTRING(__LINE__)
# define SLOT(a) qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
#else
# define SLOT(a) "1"#a
# define SIGNAL(a) "2"#a
#endif
這些宏只是使用預(yù)處理器將參數(shù)轉(zhuǎn)換為字符串,并在前面添加代碼。在debug模式下,如果信號連接不起作用,我們還會使用文件位置注釋字符串以顯示警告消息。這是以兼容的方式在Qt 4.5中添加的。為了知道哪些字符串具有行信息,我們使用qFlagLocation,它將在具有兩個條目的表中注冊字符串地址。
也即直接將函數(shù)名翻譯成字符串,在前面加1或2字符,所謂的信號和槽,也就是相應(yīng)的字符串。另外只需要處理參數(shù)傳遞問題。
新版用法中,傳遞的也是函數(shù)名,但從方式上看并沒有轉(zhuǎn)換成字符串,而是直接傳遞函數(shù)指針。總之,就是兩種區(qū)分名稱的方法而以。函數(shù)指針方式能方便編輯器和編譯器檢查語法錯誤,使用更安全。