我們進入 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 宏,不管你是不是使用信號槽。
轉載自:http://blog.sina.com.cn/s/blog_6e80f1390100qoc0.html
http://blog.csdn.net/u011125673/article/details/51354281