C++/Qt 內存管理機制

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

C++ 內存管理機制
C++ 要求開發者自己管理內存。有三種策略:
讓創建的對象自己 delete 自己的子對象(這裏所說的子對象,是指對象的屬性,而不是子類,以下類似);
讓最後一個對象處理 delete;
不管內存。
最後一種通常成爲“內存泄漏”,被認爲是一種 bug。所以,我們現在就是要選出前面兩種哪一種更合適一些。有時候,delete 創建的對象要比 delete 它的所有子對象簡單得多;有時候,找出最後一個對象也是相當困難的。
Qt 內存管理機制
Qt 在內部能夠維護對象的層次結構。對於可視元素,這種層次結構就是子組件與父組件的關係;對於非可視元素,則是一個對象與另一個對象的從屬關係。在 Qt 中,刪除父對象會將其子對象一起刪除。這有助於減少 90% 的內存問題,形成一種類似垃圾回收的機制。
QPointer
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 沒有這個問題。
QObjectCleanupHandler
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,這樣做會同時刪除掉所有未關閉的窗口。
Qt 垃圾收集
隨着對象變得越來越複雜,很多地方都要使用這個對象的時候,什麼時候作 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

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