#(10):對象模型
標準 C++ 對象模型在運行時效率方面卓有成效,但是在某些特定問題域下的靜態特性就顯得捉襟見肘。GUI 界面需要同時具有運行時的效率以及更高級別的靈活性。為了解決這一問題,Qt “擴展”了標準 C++。所謂“擴展”,實際是在使用標準 C++ 編譯器編譯 Qt 源程序之前,Qt 先使用一個叫做 moc(Meta Object Compiler,元對象編譯器)的工具,先對 Qt 源代碼進行一次預處理(*注意,這個預處理與標準 C++ 的預處理有所不同。Qt 的 moc 預處理發生在標準 C++ 預處理器工作之前,并且 Qt 的 moc 預處理不是遞歸的。*),生成標準 C++ 源代碼,然后再使用標準 C++ 編譯器進行編譯。如果你曾經為信號函數這樣的語法感到奇怪(現在我們已經編譯過一些 Qt 程序,你應當注意到了,信號函數是不需要編寫實現代碼的,那怎么可以通過標準 C++ 的編譯呢?),這其實就是 moc 進行了處理之后的效果。
Qt 使用 moc,為標準 C++ 增加了一些特性:
* 信號槽機制,用于解決對象之間的通訊,這個我們已經了解過了,可以認為是 Qt 最明顯的特性之一;
* 可查詢,并且可設計的對象屬性;
* 強大的事件機制以及事件過濾器;
* 基于上下文的字符串翻譯機制(國際化),也就是 tr() 函數,我們簡單地介紹過;
* 復雜的定時器實現,用于在事件驅動的 GUI 中嵌入能夠精確控制的任務集成;
* 層次化的可查詢的對象樹,提供一種自然的方式管理對象關系。
* 智能指針(QPointer),在對象析構之后自動設為 0,防止野指針;
* 能夠跨越庫邊界的動態轉換機制。
通過繼承`QObject`類,我們可以很方便地獲得這些特性。當然,這些特性都是由 moc 幫助我們實現的。moc 其實實現的是一個叫做元對象系統(meta-object system)的機制。正如上面所說,這是一個標準 C++ 的擴展,使得標準 C++ 更適合于進行 GUI 編程。雖然利用模板可以達到類似的效果,但是 Qt 沒有選擇使用模板。按照 Qt 官方的說法,模板雖然是內置語言特性,但是其語法實在是復雜,并且由于 GUI 是動態的,利用靜態的模板機制有時候很難處理。而自己使用 moc 生成代碼更為靈活,雖然效率有些降低(一個信號槽的調用大約相當于四個模板函數調用),不過在現代計算機上,這點性能損耗實在是可以忽略。
在本節中,我們將主要介紹 Qt 的對象樹。還記得我們前面在`MainWindow`的例子中看到了 parent 指針嗎?現在我們就來解釋這個 parent 到底是干什么的。
`QObject`是以對象樹的形式組織起來的。當你創建一個`QObject`對象時,會看到`QObject`的構造函數接收一個`QObject`指針作為參數,這個參數就是 parent,也就是父對象指針。這相當于,在創建`QObject`對象時,可以提供一個其父對象,我們創建的這個`QObject`對象會自動添加到其父對象的`children()`列表。當父對象析構的時候,這個列表中的所有對象也會被析構。(**注意,這里的父對象并不是繼承意義上的父類!**)這種機制在 GUI 程序設計中相當有用。例如,一個按鈕有一個`QShortcut`(快捷鍵)對象作為其子對象。當我們刪除按鈕的時候,這個快捷鍵理應被刪除。這是合理的。
`QWidget`是能夠在屏幕上顯示的一切組件的父類。`QWidget`繼承自`QObject`,因此也繼承了這種對象樹關系。一個孩子自動地成為父組件的一個子組件。因此,它會顯示在父組件的坐標系統中,被父組件的邊界剪裁。例如,當用戶關閉一個對話框的時候,應用程序將其刪除,那么,我們希望屬于這個對話框的按鈕、圖標等應該一起被刪除。事實就是如此,因為這些都是對話框的子組件。
當然,我們也可以自己刪除子對象,它們會自動從其父對象列表中刪除。比如,當我們刪除了一個工具欄時,其所在的主窗口會自動將該工具欄從其子對象列表中刪除,并且自動調整屏幕顯示。
我們可以使用`QObject::dumpObjectTree()`和`QObject::dumpObjectInfo()`這兩個函數進行這方面的調試。
Qt 引入對象樹的概念,在一定程度上解決了內存問題。
當一個`QObject`對象在堆上創建的時候,Qt 會同時為其創建一個對象樹。不過,對象樹中對象的順序是沒有定義的。這意味著,銷毀這些對象的順序也是未定義的。Qt 保證的是,任何對象樹中的?`QObject`對象 delete 的時候,如果這個對象有 parent,則自動將其從 parent 的`children()`列表中刪除;如果有孩子,則自動 delete 每一個孩子。Qt 保證沒有`QObject`會被 delete 兩次,這是由析構順序決定的。
如果`QObject`在棧上創建,Qt 保持同樣的行為。正常情況下,這也不會發生什么問題。來看下下面的代碼片段:
~~~
{
QWidget window;
QPushButton quit("Quit", &window);
}
~~~
作為父組件的 window 和作為子組件的 quit 都是`QObject`的子類(事實上,它們都是`QWidget`的子類,而`QWidget`是`QObject`的子類)。這段代碼是正確的,quit 的析構函數不會被調用兩次,因為標準 C++ (ISO/IEC 14882:2003)要求,局部對象的析構順序應該按照其創建順序的相反過程。因此,這段代碼在超出作用域時,會先調用 quit 的析構函數,將其從父對象 window 的子對象列表中刪除,然后才會再調用 window 的析構函數。
但是,如果我們使用下面的代碼:
~~~
{
QPushButton quit("Quit");
QWidget window;
quit.setParent(&window);
}
~~~
情況又有所不同,析構順序就有了問題。我們看到,在上面的代碼中,作為父對象的 window 會首先被析構,因為它是最后一個創建的對象。在析構過程中,它會調用子對象列表中每一個對象的析構函數,也就是說, quit 此時就被析構了。然后,代碼繼續執行,在 window 析構之后,quit 也會被析構,因為 quit 也是一個局部變量,在超出作用域的時候當然也需要析構。但是,這時候已經是第二次調用 quit 的析構函數了,C++ 不允許調用兩次析構函數,因此,程序崩潰了。
由此我們看到,Qt 的對象樹機制雖然幫助我們在一定程度上解決了內存問題,但是也引入了一些值得注意的事情。這些細節在今后的開發過程中很可能時不時跳出來煩擾一下,所以,我們最好從開始就養成良好習慣,在 Qt 中,盡量在構造的時候就指定 parent 對象,并且大膽在堆上創建。
- (1)序
- (2)Qt 簡介
- (3)Hello, world!
- (4)信號槽
- (5)自定義信號槽
- (6)Qt 模塊簡介
- (7)MainWindow 簡介
- (8)添加動作
- (9)資源文件
- (10)對象模型
- (11)布局管理器
- (12)菜單欄、工具欄和狀態欄
- (13)對話框簡介
- (14)對話框數據傳遞
- (15)標準對話框 QMessageBox
- (16)深入 Qt5 信號槽新語法
- (17)文件對話框
- (18)事件
- (19)事件的接受與忽略
- (21)事件過濾器
- (22)事件總結
- (23)自定義事件
- (24)Qt 繪制系統簡介
- (25)畫刷和畫筆
- (26)反走樣
- (27)漸變
- (28)坐標系統
- (29)繪制設備
- (30)Graphics View Framework
- (31)貪吃蛇游戲(1)
- (32)貪吃蛇游戲(2)
- (33)貪吃蛇游戲(3)
- (34)貪吃蛇游戲(4)
- (35)文件
- (36)二進制文件讀寫
- (37)文本文件讀寫
- (38)存儲容器
- (39)遍歷容器
- (40)隱式數據共享
- (41)model/view 架構
- (42)QListWidget、QTreeWidget 和 QTableWidget
- (43)QStringListModel
- (44)QFileSystemModel
- (45)模型
- (46)視圖和委托
- (47)視圖選擇
- (48)QSortFilterProxyModel
- (49)自定義只讀模型
- (50)自定義可編輯模型
- (51)布爾表達式樹模型
- (52)使用拖放
- (53)自定義拖放數據
- (54)剪貼板
- (55)數據庫操作
- (56)使用模型操作數據庫
- (57)可視化顯示數據庫數據
- (58)編輯數據庫外鍵
- (59)使用流處理 XML
- (60)使用 DOM 處理 XML
- (61)使用 SAX 處理 XML
- (62)保存 XML
- (63)使用 QJson 處理 JSON
- (64)使用 QJsonDocument 處理 JSON
- (65)訪問網絡(1)
- (66)訪問網絡(2)
- (67)訪問網絡(3)
- (68)訪問網絡(4)
- (69)進程
- (70)進程間通信
- (71)線程簡介
- (72)線程和事件循環
- (73)Qt 線程相關類
- (74)線程和 QObject
- (75)線程總結
- (76)QML 和 QtQuick 2
- (77)QML 語法
- (78)QML 基本元素
- (79)QML 組件
- (80)定位器
- (81)元素布局
- (82)輸入元素
- (83)Qt Quick Controls
- (84)Repeater
- (85)動態視圖
- (86)視圖代理
- (87)模型-視圖高級技術
- (88)Canvas
- (89)Canvas(續)