Qt總結_對象模型_組件parent的理解

標準 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 到底是幹什麼的。


 

QObjects 是以對象樹的形式組織起來的。當你創建一個 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 對象,並且大膽在堆上創建。

 

參考:https://www.qter.org/forum.php?mod=viewthread&tid=628

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章