QT父子與QT物件delete

pamxy發表於2013-04-21

轉自:http://wenku.baidu.com/view/048b8f85bceb19e8b8f6ba64.html

很多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,下面我們舉例說明。

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

1        intmain()

2        {

3        QObject*objParent = new QObject(NULL);

4        QObject*objChild = new QObject(objParent);

5        QObject*objChild2 = new QObject(objParent);

6        deleteobjParent;

7        }

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

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

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

8        intmain()

9        {

10     QObject*objParent = new QObject(NULL);

11     QObject*objChild = new QObject(objParent);

12     QObject*objChild2 = new QObject(objParent);

13     deleteobjChild;

14     deleteobjParent;

15     }

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

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

我們在看一看這一段程式碼:

16     intmain()

17     {

18     QWidgetwindow;

19     QPushButtonquit("Exit", &window);

20     }

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

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

21     intmain()

22     {

23     QPushButtonquit("Exit");

24     QWidgetwindow;

25     quit.setParent(&window);

26     }

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

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

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

27     typedefQList<QObject*> QObjectList;

28     classQObjectData

29     {

30     public:

31     QObject*parent;

32     QObjectListchildren;

33     //忽略其它成員定義

34     };

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




相關文章