創業團隊軟體技術負責人
我們進入 qt/src 資料夾。你可能對這裡的目錄名時曾相識,因為幾乎這裡的所有資料夾名都對應著 Qt 的模組的名字:gui,network,multimedia等等。我們從最核心的 QtCore 開始。這個模組對應的是corelib資料夾。
首先我們要去尋找 QObject 這個類。之所以選擇 QObject,一是因為它是 Qt 的核心類,另外一個很重要的原因是,QObject類是一個典型的Qt類,我們可以通過這個類學習到Qt的設計思路。
回憶一下我們編寫 Qt 程式碼的時候,使用的語句是:
當我們進行 #include 語句時,前處理器尋找的是qt/include/QtCore這個目錄。我們在這裡面找到了 QObject 檔案,而這裡面只有一個語句:
然後我們在同一個目錄下又找到了這個 qobject.h,這裡面也只有一句:
而這個路徑就是我們前面找到的那個 qobject 所在的位置!
因此我們回到在 corelib 裡面,可以看到 kernel 資料夾。看到名字就應該知道,這就是Qt corelib 的核心。在這裡面,我們可以找到有四個檔案以 qobject 打頭:
qobject.h:QObject 的類定義,這個就是 QObject 檔案引用的檔案,也就是我們使用的實際標頭檔案;
qobject.cpp:QObject的實現程式碼;
qobjectdefs.h:這個檔案中定義了很多用到的巨集,並且定義了QMetaObject類,而這個類是實現signal- slot的基礎;
qobject_p.h:對 QObject 的輔助資料類;
實際上我們還會看到另外兩個檔案:qobjectcleanuphandler.h 和 qobjectcleanuphandler.cpp。不過如果開啟這兩個檔案就會發現,這裡面定義的是一個QObjectCleanupHandler 類,而這個類是繼承了 QObject 的,因此這只是一個普通的工具類,不在我們目前的討論之列。因此我們可以認為,QOjbect 類是由4個檔案共同實現的:qobject.h,qobject.cpp,qobjectdefs.h和qobject_p.h。
如果你閱讀了 Qt 的原始碼,你會看到一堆奇奇怪怪的巨集,例如 Q_D,Q_Q。我們的Qt原始碼之旅就從理解這些巨集說起。
下面先看一個C++的例子。
這是一個很普通的 C++ 類 Person,他有兩個屬性 name 和 age。我們試想一下,這個類要怎麼去使用呢?如果你不想給我原始碼,那麼至少也要給我一個 dll 或者其他類似的東西,並且你要把這個標頭檔案給我,這樣我才能把它 include 到我的程式碼中使用。我只能使用你暴露給我的 public 的介面。按理說,private 的東西我是不應該知道的,但是現在我知道了!為什麼呢?因為我會去讀這個標頭檔案,我知道了,原來在 Person 中,age 就是一個 int,name 就是一個 string。這是你不希望看到的,因為既然你把它宣告成了
private 的,就是不想讓我知道這些東西。那麼怎麼辦呢?嗯,我有一個解決方案。來看下面的程式碼:
//person.h
- 1
//persondata.cpp
- 1
怎麼樣?在 person.h 中看不到我是怎麼儲存的資料了吧?嗯嗯,也許你很聰明:我還可以在 persondata.cpp 中找到那些宣告啊!當然,這是C++語法規定的,我們已經左右不了——但是,我為什麼非要把 cpp 檔案一併給你呢?因為你使用我的類庫的話完全不需要使用 cpp 檔案啊。
這就是一種資訊隱藏的方法。看上去很麻煩,原本很簡單的對 name 和 age 的訪問都不得不通過一個指標去訪問它,何必呢?其實這樣做是有好處的:
1、減少標頭檔案的依賴。像這樣我們把資料成員都寫在 cpp 檔案中,當我們需要修改資料成員的時候就只需要修改 cpp 檔案。雖然都是修改,但這和修改 .h 檔案是不一樣的!原因在於,如果 .h 檔案發生改變,編譯器會重新編譯所有 include 了這個 .h 檔案的檔案。如果你這個類相當底層,那就會花費很長時間。
2、增加類的資訊封裝。這意味著你根本看不到具體資料型別,必須使用 getter 和 setter 去訪問。我們知道 C++ 有一個 typedef 語句,我定義一個數據型別 ABC,如果你看不到具體檔案,你會知道這個 ABC 是 string 還是 int 麼?
這就是 C++ 的一種設計方法,被稱為 Private Class,大約就是私有類吧!更確切地說應該是私有資料類。據說,這也是 Qt 2.x 的實現方式。但是如果你去看你的 Qt SDK 程式碼,你是看不到這樣的語句的,取而代之的則是一些我們開頭所說的 Q_D 這些巨集。或許你已經隱隱約約地猜到了,這些巨集就是實現這個的:Private Data。
下面在上一篇的基礎上,我們進入Qt的原始碼,看看Qt4.x是如何實現 Private Classes 的。
正如前面我們說的,或許你會看到很多類似 Q_D 或者 Q_Q 這類的巨集。那麼,我們來試著看一下這樣的程式碼
按照傳統 C++ 的類,如果我們要實現這樣的 getter 和 setter,我們應該使用一個私有變數 _i,然後操作這個變數。按照上一篇說的 Private Class 的做法,我們就要建一個 MyClassPrivateData 這樣的類,然後使用指標對所有的資料操作進行委託。
再來看一個比較 Qt 的例子:
在來看一下 Qt 的實現:
這個例子很簡單,一個使用傳統方法實現,另一個採用了 Qt4.x 的方法。Qt4.x 的方法被稱為 D-Pointer,因為它會使用一個名為 d 的指標,正如上面寫的那個 d_ptr。使用傳統方法,我們需要在 private 裡面寫上所有的私有變數,通常這會讓整個檔案變得很長,更為重要的是,使用者並不需要這些資訊。而使用 D-Pointer 的方法,我們的介面變得很漂亮:再也沒有那一串長長的私有變量了。你不再需要將你的私有變數一起釋出出去,它們就在你的 d 指標裡面。如果你要修改資料型別這些資訊,你也不需要去修改標頭檔案,只需改變私有資料類即可。
需要注意的一點是,與單純的 C++ 類不同,如果你的私有類需要定義 signals 和 slots,就應該把這個定義放在標頭檔案中,而不是像上一篇所說的放在 cpp 檔案中。這是因為 qmake 只會檢測 .h 檔案中的 Q_OBJECT 巨集(這一點大家務必注意)。當然,你不應該把這樣的 private class 放在你的類的同一個標頭檔案中,因為這樣做的話就沒有意義了。常見做法是,定義一個 private 的標頭檔案,例如使用 myclass_p.h 的命名方式(這也是 Qt 的命名方式)。並且記住,不要把 private
標頭檔案放到你釋出的 include 下面!因為這不是你釋出的一部分,它們是私有的。然後,在你的 myclass 標頭檔案中,使用
這種前向宣告而不是直接:
這種方式。這也是為了避免將私有的標頭檔案釋出出去,並且前向宣告可以縮短編譯時間。
在這個類的 private 部分,我們使用了一個 MyClassPrivate 的 const 指標 d_ptr。如果你需要讓這個類的子類也能夠使用這個指標,就應該把這個 d_ptr 放在 protected 部分,正如上面的程式碼那樣。並且,我們還加上了 const 關鍵字,來確保它只能被初始化一次。
下面,我們遇到了一個神奇的巨集:Q_DECLARE_PRIVATE。這是幹什麼用的?那麼,我們先來看一下這個巨集的展開:
如果你看不大懂,那麼就用我們的 Q_DECLARE_PRIVATE(MyClass) 看看展開之後是什麼吧:
它實際上建立了兩個 inline 的 d_func() 函式,返回值分別是我們的 d_ptr 指標和 const 指標。另外,它還把 MyClassPrivate 類宣告為 MyClass 的 friend。這樣的話,我們就可以在 MyClass 這個類裡面使用 Q_D(MyClass) 以及 Q_D(const MyClass)。還記得我們最先看到的那段程式碼嗎?現在我們來看看這個 Q_D 倒是是何方神聖!
下面還是自己展開一下這個巨集,就成了
簡單來說,Qt 為我們把從 d_func() 獲取 MyClassPrivate 指標的程式碼給封裝起來了,這樣我們就可以比較面向物件的使用 getter 函式獲取這個指標了。
現在我們已經比較清楚的知道 Qt 是如何使用 D-Pointer 實現我們前面所說的資訊隱藏的了。但是,還有一個問題:如果我們把大部分程式碼集中到 MyClassPrivate 裡面,很可能需要讓 MyClassPrivate 的實現訪問到 MyClass 的一些東西。現在我們讓主類通過 D-Pointer 訪問 MyClassPrivate 的資料,但是怎麼反過來讓 MyClassPrivate 訪問主類的資料呢?Qt 也提供了相應的解決方案,那就是 Q_Q 巨集,例如:
在 private 類 MyObjectPrivate 中,通過建構函式將主類 MyObject 的指標傳給 q_ptr。然後我們使用類似主類中使用的 Q_DECLARE_PRIVATE 的巨集一樣的另外的巨集 Q_DECLARE_PUBLIC。這個巨集所做的就是讓你能夠通過 Q_Q(Class) 巨集使用主類指標。與 D-Pointer 不同,這時候你需要使用的是 Q_Pointer。這兩個是完全相對的,這裡也就不再贅述。
現在我們已經能夠使用比較 Qt 的方式來使用 Private Classes 實現資訊隱藏了。這不僅僅是 Qt 的實現,當然,你也可以不用 Q_D 和 Q_Q,而是使用自己的方式,這些都無關緊要。最主要的是,我們瞭解了一種 C++ 類的設計思路,這是 Qt 的原始碼教給我們的。
前面我們已經看到了怎樣使用標準的 C++ 程式碼以及 Qt 提供的 API 來達到資訊隱藏這一目標。下面我們來看一下 Qt 是如何實現的。
還是以 QObject 的原始碼作為例子。先開啟 qobject.h,找到 QObjectData 這個類的宣告。具體程式碼如下所示:
然後在下面就可以找到 QObject 的宣告:
注意,這裡我們只是列出了我們所需要的程式碼,並且我的 Qt 版本是 2010.03。這部分程式碼可能會隨著不同的 Qt 版本所有不同。
首先先了解一下 Qt 的設計思路。既然每個類都應該把自己的資料放在一個 private 類中,那麼,為什麼不把這個操作放在幾乎所有類的父類 QObject 中呢?所以,Qt 實際上是用了這樣一個思路,實現了我們前面介紹的資料隱藏機制。
首先回憶一下,我們前面說的 D-Pointer 需要有一個 private 或者 protected 的指向自己資料類的指標。在 QObject 中,
就扮演了這麼一個角色。或許,你可以把它理解成
這不就和我們前面說的 D-Pointer 技術差不多了?QScopedPointer 是 Qt 提供的一個輔助類,這個類儲存有一個指標,它的行為類似於一種智慧指標:它能夠保證在這個作用域結束後,裡面的所有指標都能夠被自動 delete 掉。也就是說,它其實就是一個比普通指標更多功能的指標。而這個指標被宣告成 protected 的,也就是隻有它本身以及其子類才能夠訪問到它。這就提供了讓子類不必須宣告這個 D-Pointer 的可能。
那麼,前面我們說,QObjectData 這種資料類不應該放在公開的標頭檔案中,可 Qt 怎麼把它放進來了呢?這樣做的用途是,QObject 的子類的資料類都可能繼承自這個 QObjectData。這個類有一個純虛的解構函式。沒有實現程式碼,保證了這個類不能被初始化;虛的解構函式,保證了其子類都能夠被正確的析構。
回到我們前面說明的 Q_DECLARE_PRIVATE 這個巨集:
我們把程式碼中的 Q_DECLARE_PRIVATE(QObject) 展開看看是什麼東西
清楚是清楚,只是這個 QObjectPrivate 是哪裡來的?既然是 Private,那麼它肯定不會在公開的標頭檔案中。於是我們立刻想到到 qobject.cpp 或者是 qobject_p.h 中尋找。終於,我們在 qobject_p.h 中找到了這個類的宣告:
這個類是繼承 QObjectData 的!想想也是,因為我們說過,QObjectData 是不能被例項化的,如果要使用,必須建立它的一個子類。顯然,QObjectPrivate 就扮演了這麼一個角色了。不僅如此,我們還在這裡看到了熟悉的 Q_DECLARE_PUBLIC 巨集。好在我們已經知道它的含義了。
在 qobject.cpp 中,我們看一下 QObject 的建構函式:
第一個建構函式就是我們經常見到的那個。它使用自己建立的 QObjectPrivate 指標對 d_ptr 初始化。第二個建構函式使用傳入的 QObjectPrivate 物件,但它是 protected 的,也就是說,你不能在外部類中使用這個建構函式。那麼這個建構函式有什麼用呢?我們來看一下 QWidget 的程式碼:
QWidget 是 QObject 的子類,然後看它的建構函式
它呼叫了那個QObject 的 protected 建構函式,並且傳入一個 QWidgetPrivate !這個 QWidgetPrivate 顯然繼承了 QObjectPrivate。於是我們已經明白,為什麼 QWidget 中找不到 d_ptr 了,因為所有的 d_ptr 都已經在父類 QObject 中定義好了!嘗試展開一下 Q_DECLARE_PRIVATE 巨集,你就能夠發現,它實際上把父類的 QObjectPrivate 指標偷偷地轉換成了 QWidgetPrivate 的指標。這個就是前面說的 Qt
的設計思路。
前面我們說過,Qt 不是使用的“標準的” C++ 語言,而是對其進行了一定程度的“擴充套件”。這裡我們從Qt新增加的關鍵字就可以看出來:signals、slots 或者 emit。所以有人會覺得 Qt 的程式編譯速度慢,這主要是因為在 Qt 將原始碼交給標準 C++ 編譯器,如 gcc 之前,需要事先將這些擴充套件的語法去除掉。完成這一操作的就是 moc。
moc 全稱是 Meta-Object Compiler,也就是“元物件編譯器”。Qt 程式在交由標準編譯器編譯之前,先要使用 moc 分析 C++ 原始檔。如果它發現在一個頭檔案中包含了巨集 Q_OBJECT,則會生成另外一個 C++ 原始檔。這個原始檔中包含了 Q_OBJECT 巨集的實現程式碼。這個新的檔名字將會是原檔名前面加上 moc_ 構成。這個新的檔案同樣將進入編譯系統,最終被連結到二進位制程式碼中去。因此我們可以知道,這個新的檔案不是“替換”掉舊的檔案,而是與原檔案一起參與編譯。另外,我們還可以看出一點,moc 的執行是在前處理器之前。因為前處理器執行之後,Q_OBJECT 巨集就不存在了。
既然每個原始檔都需要 moc 去處理,那麼我們在什麼時候呼叫了它呢?實際上,如果你使用 qmake 的話,這一步呼叫會在生成的 makefile 中展現出來。從本質上來說,qmake 不過是一個 makefile 生成器,因此,最終執行還是通過 make 完成的。
為了檢視 moc 生成的檔案,我們使用一個很簡單的 cpp 來測試:
//test.cpp
- 1
這是一個空白的類,什麼都沒有實現。在經過編譯之後,我們會在輸出資料夾中找到 moc_test.cpp:
//moc_test.cpp:
- 1
可以看到,moc_test.cpp 裡面為 Test 類增加了很多函式。然而,我們並沒有實際寫出這些函式,它是怎麼加入類的呢?別忘了,我們還有 Q_OBJECT 這個巨集呢!在 qobjectdefs.h 裡面,找到 Q_OBJECT 巨集的定義:
這下了解了:正是對 Q_OBJECT 巨集的展開,使我們的 Test 類擁有了這些多出來的屬性和函式。注意,QT_TR_FUNCTIONS 這個巨集也是在這裡定義的。也就是說,如果你要使用 tr() 國際化,就必須使用 Q_OBJECT 巨集,否則是沒有 tr() 函式的。這期間最重要的就是 virtual const QMetaObject *metaObject() const; 函式。這個函式返回 QMetaObject 元物件類的例項,通過它,你就獲得了 Qt 類的反射的能力:獲取本物件的型別之類,而這一切,都不需要 C++ 編譯器的 RTTI 支援。Qt 也提供了一個類似 C++ 的 dynamic_cast() 的函式 qobject_case(),而這一函式的實現也不需要 RTTI。另外,一個沒有定義 Q_OBJECT 巨集的類與它最接近的父類是同一型別的。也就是說,如果 A 繼承了 QObject 並且定義了 Q_OBJECT,B 繼承了 A 但沒有定義 Q_OBJECT,C 繼承了 B,則 C 的 QMetaObject::className() 函式將返回 A,而不是本身的名字。因此,為了避免這一問題,所有繼承了 QObject 的類都應該定義 Q_OBJECT 巨集,不管你是不是使用訊號槽。