這篇文章首先釋出於我的主頁 http://www.devbean.info,以後也會直接釋出在那裡。現在有 Flex 4 的一篇和 《從 C++ 到 Objective-C》系列,感謝大家支援!

強型別語言在建立物件時總會顯式或隱式地包含物件的型別資訊。也就是說,強型別語言在分配物件記憶體空間時,總會關聯上物件的型別。相比之下,弱型別 語言則不會這樣做。在分配了記憶體空間之後,有兩種方法釋放空間:手工釋放,或者是使用垃圾收集器。C++ 要求開發者手工釋放記憶體空間。這樣做的好處是,開發者對記憶體有完全的控制能力,知道什麼時候釋放比較合適。Java 則使用垃圾收集器。它在後臺會有一個執行緒根據一定的演算法不停地檢視哪些物件已經不被使用,可以被回收。這樣做則可以將開發者從底層實現中解放出來,只需關 注於業務邏輯。

本文關注於 Qt 的記憶體管理,這裡會使用 Qt 的機制,來實現一個簡單的垃圾回收器。

C++ 記憶體管理機制

C++ 要求開發者自己管理記憶體。有三種策略:

  1. 讓建立的物件自己 delete 自己的子物件(這裡所說的子物件,是指物件的屬性,而不是子類,以下類似);
  2. 讓最後一個物件處理 delete;
  3. 不管記憶體。

最後一種通常成為“記憶體洩漏”,被認為是一種 bug。所以,我們現在就是要選出前面兩種哪一種更合適一些。有時候,delete 建立的物件要比 delete 它的所有子物件簡單得多;有時候,找出最後一個物件也是相當困難的。

Qt 記憶體管理機制

Qt 在內部能夠維護物件的層次結構。對於可視元素,這種層次結構就是子元件與父元件的關係;對於非可視元素,則是一個物件與另一個物件的從屬關係。在 Qt 中,刪除父物件會將其子物件一起刪除。這有助於減少 90% 的記憶體問題,形成一種類似垃圾回收的機制。

QPointer

QPointer 是一個模板類。它很類似一個普通的指標,不同之處在於,QPointer 可以監視動態分配空間的物件,並且在物件被 delete 的時候及時更新。

  1. // QPointer 表現類似普通指標 
  2. QDate *mydate = new QDate(QDate::currentDate()); 
  3. QPointer mypointer = mydata; 
  4. mydate->year();    // -> 2005 
  5. mypointer->year(); // -> 2005 
  6.   
  7. // 當物件 delete 之後,QPointer 會有不同的表現 
  8. delete mydate; 
  9.   
  10. if(mydate == NULL) 
  11.     printf("clean pointer"); 
  12. else 
  13.     printf("dangling pointer"); 
  14. // 輸出 dangling pointer 
  15.   
  16. if(mypointer.isNull()) 
  17.     printf("clean pointer"); 
  18. else 
  19.     printf("dangling pointer"); 
  20. // 輸出 clean pointer 

注意上面的程式碼。一個原始指標 delete 之後,其值不會被設定為 NULL,因此會成為野指標。但是,QPionter 沒有這個問題。

QObjectCleanupHandler

Qt 物件清理器是實現自動垃圾回收的很重要的一部分。它可以註冊很多子物件,並在自己刪除的時候自動刪除所有子物件。同時,它也可以識別出是否有子物件被刪 除,從而將其從它的子物件列表中刪除。這個類可以用於不在同一層次中的類的清理操作,例如,當按鈕按下時需要關閉很多視窗,由於視窗的 parent 屬性不可能設定為別的視窗的 button,此時使用這個類就會相當方便。

  1. // 建立例項 
  2. QObjectCleanupHandler *cleaner = new QObjectCleanupHandler; 
  3. // 建立視窗 
  4. QPushButton *w = new QPushButton("Remove Me"); 
  5. w->show(); 
  6. // 註冊第一個按鈕 
  7. cleaner->add(w); 
  8. // 如果第一個按鈕點選之後,刪除自身 
  9. connect(w, SIGNAL(clicked()), w, SLOT(deleteLater())); 
  10. // 建立第二個按鈕,注意,這個按鈕沒有任何動作 
  11. w = new QPushButton("Nothing"); 
  12. cleaner->add(w); 
  13. w->show(); 
  14. // 建立第三個按鈕,刪除所有 
  15. w = new QPushButton("Remove All"); 
  16. cleaner->add(w); 
  17. connect(w, SIGNAL(clicked()), cleaner, SLOT(deleteLater())); 
  18. w->show(); 

在上面的程式碼中,建立了三個僅有一個按鈕的視窗。第一個按鈕點選後,會刪除掉自己(通過 deleteLater() 槽),此時,cleaner 會自動將其從自己的列表中清除。第三個按鈕點選後會刪除 cleaner,這樣做會同時刪除掉所有未關閉的視窗。

Qt 垃圾收集

隨著物件變得越來越複雜,很多地方都要使用這個物件的時候,什麼時候作 delete 操作很難決定。好在 Qt 對所有繼承自 QObject 的類都有很好的垃圾收集機制。垃圾收集有很多種實現方法,最簡單的是引用計數,還有一種是儲存所有物件。下面我們將詳細講解這兩種實現方法。

引用計數

應用計數是最簡單的垃圾回收實現:每建立一個物件,計數器加 1,每刪除一個則減 1。

  1. class CountedObject 
  2. public
  3.     CountedObject() 
  4.     { 
  5.         ctr=0; 
  6.     } 
  7.   
  8.     void attach() 
  9.     { 
  10.         ctr++; 
  11.     } 
  12.   
  13.     void detach() 
  14.     { 
  15.         ctr--; 
  16.         if(ctr <= 0) 
  17.             delete this
  18.     } 
  19. private
  20.     int ctr; 
  21. }; 

 

每一個子物件在建立之後都應該呼叫 attach() 函式,使計數器加 1,刪除的時候則應該呼叫 detach() 更新計數器。不過,這個類很原始,沒有使用 Qt 方便的機制。下面我們給出一個 Qt 版本的實現:

  1. class CountedObject : public QObject 
  2.     Q_OBJECT 
  3. public
  4.     CountedObject() 
  5.     { 
  6.         ctr=0; 
  7.     } 
  8.   
  9.     void attach(QObject *obj) 
  10.     { 
  11.         ctr++; 
  12.         connect(obj, SIGNAL(destroyed(QObject*)), SLOT(detach())); 
  13.     } 
  14.   
  15. public slots: 
  16.     void detach() 
  17.     { 
  18.         ctr--; 
  19.         if(ctr <= 0) 
  20.             delete this
  21.     } 
  22.   
  23. private
  24.     int ctr; 
  25. }; 

我們利用 Qt 的訊號槽機制,在物件銷燬的時候自動減少計數器的值。但是,我們的實現並不能防止物件建立的時候呼叫了兩次 attach()。

記錄所有者

更合適的實現是,不僅僅記住有幾個物件持有引用,而且要記住是哪些物件。例如:

  1. class CountedObject : public QObject 
  2. public
  3.     CountedObject() 
  4.     { 
  5.     } 
  6.   
  7.     void attach(QObject *obj) 
  8.     { 
  9.         // 檢查所有者 
  10.         if(obj == 0) 
  11.             return
  12.         // 檢查是否已經新增過 
  13.         if(owners.contains(obj)) 
  14.             return
  15.         // 註冊 
  16.         owners.append(obj); 
  17.         connect(obj, SIGNAL(destroyed(QObject*)), SLOT(detach(QObject*))); 
  18.     } 
  19.   
  20. public slots: 
  21.     void detach(QObject *obj) 
  22.     { 
  23.         // 刪除 
  24.         owners.removeAll(obj); 
  25.         // 如果最後一個物件也被 delete,刪除自身 
  26.         if(owners.size() == 0) 
  27.             delete this
  28.     } 
  29.   
  30. private
  31.     QList owners; 
  32. }; 

現在我們的實現已經可以做到防止一個物件多次呼叫 attach() 和 detach() 了。然而,還有一個問題是,我們不能保證物件一定會呼叫 attach() 函式進行註冊。畢竟,這不是 C++ 內建機制。有一個解決方案是,重定義 new 運算子(這一實現同樣很複雜,不過可以避免出現有物件不呼叫 attach() 註冊的情況)。


本文來自 DevBean`s World:http://www.devbean.info
轉載時請標明文章原始出處:
http://www.devbean.info/2011/03/qt_memory_management/