這篇文章首先發布於我的主頁 http://www.devbean.info,以後也會直接發布在那裡。現在有 Flex 4 的一篇和 《從 C++ 到 Objective-C》系列,感謝大家支持!
強類型語言在創建對象時總會顯式或隱式地包含對象的類型信息。也就是說,強類型語言在分配對象內存空間時,總會關聯上對象的類型。相比之下,弱類型 語言則不會這樣做。在分配了內存空間之後,有兩種方法釋放空間:手工釋放,或者是使用垃圾收集器。C++ 要求開發者手工釋放內存空間。這樣做的好處是,開發者對內存有完全的控制能力,知道什麼時候釋放比較合適。Java 則使用垃圾收集器。它在後台會有一個線程根據一定的算法不停地查看哪些對象已經不被使用,可以被回收。這樣做則可以將開發者從底層實現中解放出來,只需關 注於業務邏輯。
本文關注於 Qt 的內存管理,這裡會使用 Qt 的機制,來實現一個簡單的垃圾回收器。
C++ 要求開發者自己管理內存。有三種策略:
最後一種通常成為“內存洩漏”,被認為是一種 bug。所以,我們現在就是要選出前面兩種哪一種更合適一些。有時候,delete 創建的對象要比 delete 它的所有子對象簡單得多;有時候,找出最後一個對象也是相當困難的。
Qt 在內部能夠維護對象的層次結構。對於可視元素,這種層次結構就是子組件與父組件的關系;對於非可視元素,則是一個對象與另一個對象的從屬關系。在 Qt 中,刪除父對象會將其子對象一起刪除。這有助於減少 90% 的內存問題,形成一種類似垃圾回收的機制。
QPointer 是一個模板類。它很類似一個普通的指針,不同之處在於,QPointer 可以監視動態分配空間的對象,並且在對象被 delete 的時候及時更新。
- // QPointer 表現類似普通指針
- QDate *mydate = new QDate(QDate::currentDate());
- QPointer mypointer = mydata;
- mydate->year(); // -> 2005
- mypointer->year(); // -> 2005
- // 當對象 delete 之後,QPointer 會有不同的表現
- delete mydate;
- if(mydate == NULL)
- printf("clean pointer");
- else
- printf("dangling pointer");
- // 輸出 dangling pointer
- if(mypointer.isNull())
- printf("clean pointer");
- else
- printf("dangling pointer");
- // 輸出 clean pointer
注意上面的代碼。一個原始指針 delete 之後,其值不會被設置為 NULL,因此會成為野指針。但是,QPionter 沒有這個問題。
Qt 對象清理器是實現自動垃圾回收的很重要的一部分。它可以注冊很多子對象,並在自己刪除的時候自動刪除所有子對象。同時,它也可以識別出是否有子對象被刪 除,從而將其從它的子對象列表中刪除。這個類可以用於不在同一層次中的類的清理操作,例如,當按鈕按下時需要關閉很多窗口,由於窗口的 parent 屬性不可能設置為別的窗口的 button,此時使用這個類就會相當方便。
- // 創建實例
- QObjectCleanupHandler *cleaner = new QObjectCleanupHandler;
- // 創建窗口
- QPushButton *w = new QPushButton("Remove Me");
- w->show();
- // 注冊第一個按鈕
- cleaner->add(w);
- // 如果第一個按鈕點擊之後,刪除自身
- connect(w, SIGNAL(clicked()), w, SLOT(deleteLater()));
- // 創建第二個按鈕,注意,這個按鈕沒有任何動作
- w = new QPushButton("Nothing");
- cleaner->add(w);
- w->show();
- // 創建第三個按鈕,刪除所有
- w = new QPushButton("Remove All");
- cleaner->add(w);
- connect(w, SIGNAL(clicked()), cleaner, SLOT(deleteLater()));
- w->show();
在上面的代碼中,創建了三個僅有一個按鈕的窗口。第一個按鈕點擊後,會刪除掉自己通過 deleteLater() 槽),此時,cleaner 會自動將其從自己的列表中清除。第三個按鈕點擊後會刪除 cleaner,這樣做會同時刪除掉所有未關閉的窗口。
隨著對象變得越來越復雜,很多地方都要使用這個對象的時候,什麼時候作 delete 操作很難決定。好在 Qt 對所有繼承自 QObject 的類都有很好的垃圾收集機制。垃圾收集有很多種實現方法,最簡單的是引用計數,還有一種是保存所有對象。下面我們將詳細講解這兩種實現方法。
引用計數
應用計數是最簡單的垃圾回收實現:每創建一個對象,計數器加 1,每刪除一個則減 1。
- class CountedObject
- {
- public:
- CountedObject()
- {
- ctr=0;
- }
- void attach()
- {
- ctr++;
- }
- void detach()
- {
- ctr--;
- if(ctr <= 0)
- delete this;
- }
- private:
- int ctr;
- };
每一個子對象在創建之後都應該調用 attach() 函數,使計數器加 1,刪除的時候則應該調用 detach() 更新計數器。不過,這個類很原始,沒有使用 Qt 方便的機制。下面我們給出一個 Qt 版本的實現:
- class CountedObject : public QObject
- {
- Q_OBJECT
- public:
- CountedObject()
- {
- ctr=0;
- }
- void attach(QObject *obj)
- {
- ctr++;
- connect(obj, SIGNAL(destroyed(QObject*)), SLOT(detach()));
- }
- public slots:
- void detach()
- {
- ctr--;
- if(ctr <= 0)
- delete this;
- }
- private:
- int ctr;
- };
我們利用 Qt 的信號槽機制,在對象銷毀的時候自動減少計數器的值。但是,我們的實現並不能防止對象創建的時候調用了兩次 attach()。
記錄所有者
更合適的實現是,不僅僅記住有幾個對象持有引用,而且要記住是哪些對象。例如:
- class CountedObject : public QObject
- {
- public:
- CountedObject()
- {
- }
- void attach(QObject *obj)
- {
- // 檢查所有者
- if(obj == 0)
- return;
- // 檢查是否已經添加過
- if(owners.contains(obj))
- return;
- // 注冊
- owners.append(obj);
- connect(obj, SIGNAL(destroyed(QObject*)), SLOT(detach(QObject*)));
- }
- public slots:
- void detach(QObject *obj)
- {
- // 刪除
- owners.removeAll(obj);
- // 如果最後一個對象也被 delete,刪除自身
- if(owners.size() == 0)
- delete this;
- }
- private:
- QList owners;
- };
現在我們的實現已經可以做到防止一個對象多次調用 attach() 和 detach() 了。然而,還有一個問題是,我們不能保證對象一定會調用 attach() 函數進行注冊。畢竟,這不是 C++ 內置機制。有一個解決方案是,重定義 new 運算符這一實現同樣很復雜,不過可以避免出現有對象不調用 attach() 注冊的情況)。
本文來自 DevBean's World:http://www.devbean.info。
轉載時請標明文章原始出處:http://www.devbean.info/2011/03/qt_memory_management/。
本文出自 “豆子空間” 博客,請務必保留此出處http://devbean.blog.51cto.com/448512/526734