Inside Qt Series (十二):Qt對象之間的父子關係

來源:互聯網
上載者:User

很多C/C++初學者常犯的一個錯誤就是,使用malloc、new分配了一塊記憶體卻忘記釋放,導致記憶體流失。Qt的物件模型提供了一種Qt對象之間的父子關係,當很多個對象都按一定次序建立起來這種父子關係的時候,就組織成了一顆樹。當delete一個父物件的時候,Qt的物件模型機制保證了會自動的把它的所有子物件,以及孫對象,等等,全部delete,從而保證不會有記憶體流失的情況發生。

任何事情都有正反兩面作用,這種機制看上去挺好,但是卻會對很多Qt的初學者造成困擾,我經常給別人回答的問題是:1,new了一個Qt對象之後,在什麼情況下應該delete它?2,Qt的解構函式是不是有bug?3,為什麼正常delete一個Qt對象卻會產生segment fault?等等諸如此類的問題,這篇文章就是針對這個問題的詳細解釋。

在每一個Qt對象中,都有一個鏈表,這個鏈表儲存有它所有子物件的指標。當建立一個新的Qt對象的時候,如果把另外一個Qt對象指定為這個對象的父物件,那麼父物件就會在它的子物件鏈表中加入這個子物件的指標。另外,對於任意一個Qt對象而言,在其生命週期的任何時候,都還可以通過setParent函數重新設定它的父物件。當一個父物件在被delete的時候,它會自動的把它所有的子物件全部delete。當一個子物件在delete的時候,會把它自己從它的父物件的子物件鏈表中刪除。

QWidget是所有在螢幕上顯示出來的介面對象的基類,它擴充了Qt對象的父子關係。一個Widget對象也就自然的成為其父Widget對象的子Widget,並且顯示在它的父Widget的座標系統中。例如,一個對話方塊(dialog)上的按鈕(button)應該是這個對話方塊的子Widget。

關於Qt對象的new和delete,下面我們舉例說明。

例如,下面這一段代碼是正確的:

int main()
{
  QObject* objParent = new QObject(NULL);
  QObject* objChild = new QObject(objParent);
  QObject* objChild2 = new QObject(objParent);
  delete objParent;
}

我們用一張圖來描述這三個對象之間的關係:

在上述程式碼片段中,objParent是objChild的父物件,在objParent對象中有一個子物件鏈表,這個鏈表中儲存它所有子物件的指標,在這裡,就是儲存了objChild和objChild2的指標。在代碼的結束部分,就只有delete了一個對象objParent,在objParent對象的解構函式會遍曆它的子物件鏈表,並且把它所有的子物件(objChild和objChild2)一一刪除。所以上面這段代碼是安全的,不會造成記憶體流失。

如果我們把上面這段代碼改成這樣,也是正確的:

int main()
{
  QObject* objParent = new QObject(NULL);
  QObject* objChild = new QObject(objParent);
  QObject* objChild2 = new QObject(objParent);
  delete objChild;
  delete objParent;
}

在這段代碼中,我們就只看一下和上一段代碼不一樣的地方,就是在delete objParent對象之前,先delete objChild對象。在delete objChild對象的時候,objChild對象會自動的把自己從objParent對象的子物件鏈表中刪除,也就是說,在objChild對象被delete完成之後,objParent對象就只有一個子物件(objChild2)了。然後在delete objParent對象的時候,會自動把objChild2對象也delete。所以,這段代碼也是安全的。

Qt的這種設計對某些調試工具來說卻是不友好的,比如valgrind。比如上面這段代碼,valgrind工具在分析代碼的時候,就會認為objChild2對象沒有被正確的delete,從而會報告說,這段代碼存在記憶體流失。哈哈,我們知道,這個報告是不對的。

我們在看一看這一段代碼:

int main()
{
  QWidget window;
  QPushButton quit("Exit", &window);
}

在這段代碼中,我們建立了兩個widget對象,第一個是window,第二個是quit,他們都是Qt對象,因為QPushButton是從QWidget派生出來的,而QWidget是從QObject派生出來的。這兩個對象之間的關係是,window對象是quit對象的父物件,由於他們都會被分配在棧(stack)上面,那麼quit對象是不是會被析構兩次呢?我們知道,在一個函數體內部聲明的變數,在這個函數退出的時候就會被析構,那麼在這段代碼中,window和quit兩個對象在函數退出的時候解構函式都會被調用。那麼,假設,如果是window的解構函式先被調用的話,它就會去delete quit對象;然後quit的解構函式再次被調用,程式就出錯了。事實情況不是這樣的,C++標準規定,本機物件的解構函式的調用順序與他們的構造順序相反。那麼在這段代碼中,這就是quit對象的解構函式一定會比window對象的解構函式先被調用,所以,在window對象析構的時候,quit對象已經不存在了,不會被析構兩次。

如果我們把代碼改成這個樣子,就會出錯了,對照前面的解釋,請你自己來分析一下吧。

int main()
{
  QPushButton quit("Exit");
  QWidget window;
  quit.setParent(&window);
}

但是我們自己在寫程式的時候,也必須重點注意一項,千萬不要delete子物件兩次,就像前面這段代碼那樣,程式肯定就crash了。

最後,讓我們來結合Qt source code,來看看這parent/child關係是如何?的。

在本專欄文章的第一部分“對象資料存放區”,我們說到過,所有Qt對象的私人資料成員的基類是QObjectData類,這個類的定義如下

typedef QList<QObject*> QObjectList;
class QObjectData
{
public:
  QObject *parent;
  QObjectList children;
  // 忽略其它成員定義
};

我們可以看到,在這裡定義了指向parent的指標,和儲存子物件的列表。其實,把一個對象設定成另一個對象的父物件,無非就是在操作這兩個資料。把子物件中的這個parent變數設定為指向其父物件;而在父物件的children列表中加入子物件的指標。當然,我這裡說的非常簡單,在實際的代碼中複雜的多,包含有很多條件判斷,有興趣的朋友可以自己去讀一下Qt的原始碼。

======================================================================
聲明:
《Inside Qt Series》專欄文章是(http://www.qkevin.com)原創技術文章。
本系列專欄文章可隨意轉載,但必須保留本段聲明和每一篇文章的原始地址。
作者保留著作權,未經作者同意,不得用於任何商業用途

《Inside Qt Series》專欄文章總索引: http://www.qkevin.com/qt
本文原始地址:http://www.qkevin.com/archives/93

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.