Qt總結(jié)之十一:內(nèi)存泄漏

原文地址: https://blog.csdn.net/Aidam_Bo/article/details/85698862

一鼠哥、簡介

Qt內(nèi)存管理機制:Qt 在內(nèi)部能夠維護對象的層次結(jié)構(gòu)熟菲。對于可視元素,這種層次結(jié)構(gòu)就是子組件與父組件的關(guān)系朴恳;對于非可視元素抄罕,則是一個對象與另一個對象的從屬關(guān)系。在 Qt 中于颖,在 Qt 中呆贿,刪除父對象會將其子對象一起刪除。

C++中delete 和 new 必須配對使用(一 一對應(yīng)):delete少了森渐,則內(nèi)存泄露做入,多了麻煩更大。Qt中使用了new卻很少delete同衣,因為QObject的類及其繼承的類竟块,設(shè)置了parent(也可在構(gòu)造時使用setParent函數(shù)或parent的addChild)故parent被delete時,這個parent的相關(guān)所有child都會自動delete耐齐,不用用戶手動處理浪秘。但parent是不區(qū)分它的child是new出來的還是在棧上分配的。這體現(xiàn)delete的強大埠况,可以釋放掉任何的對象耸携,而delete棧上對象就會導(dǎo)致內(nèi)存出錯,這需要了解Qt的半自動的內(nèi)存管理辕翰。另一個問題:child不知道它自己是否被delete掉了夺衍,故可能會出現(xiàn)野指針。那就要了解Qt的智能指針QPointer金蜀。

二刷后、關(guān)聯(lián)圖

(1)Linux內(nèi)存圖,主要了解堆棧上分配內(nèi)存的不同方式渊抄。

(2)在Qt中尝胆,最基礎(chǔ)和核心的類是:QObject,QObject內(nèi)部有一個list护桦,會保存children含衔,還有一個指針保存parent,當自己析構(gòu)時二庵,會自己從parent列表中刪除并且析構(gòu)所有的children贪染。

三、詳解

1催享、Qt的半自動化的內(nèi)存管理
(1)QObject及其派生類的對象杭隙,如果其parent非0,那么其parent析構(gòu)時會析構(gòu)該對象因妙。

(2)QWidget及其派生類的對象痰憎,可以設(shè)置 Qt::WA_DeleteOnClose 標志位(當close時會析構(gòu)該對象)票髓。

(3)QAbstractAnimation派生類的對象,可以設(shè)置 QAbstractAnimation::DeleteWhenStopped铣耘。

(4)QRunnable::setAutoDelete()洽沟、MediaSource::setAutoDelete()。

(5)父子關(guān)系:父對象蜗细、子對象裆操、父子關(guān)系。這是Qt中所特有的炉媒,與類的繼承關(guān)系無關(guān)踪区,傳遞參數(shù)是與parent有關(guān)(基類、派生類橱野,或父類朽缴、子類,這是對于派生體系來說的水援,與parent無關(guān))密强。

2、內(nèi)存問題例子
\color{red}{例子一}

#include <QApplication>
#include <QLabel>
 
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QLabel *label = new QLabel("Hello Qt!");
    label->show();
    return a.exec();
}

分析:(1)label 既沒有指定parent蜗元,也沒有對其調(diào)用delete或渤,所以會造成內(nèi)存泄漏。書中的這種小例子也會出現(xiàn)指針內(nèi)存的問題奕扣。
改進方式:(1)分配對象到棧上而不是堆上

#include <QApplication>
#include <QLabel>
 
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QLabel label("Hello Qt!");
    label.show();
    return a.exec();
}

(2)設(shè)置標志位薪鹦,close()后會delete label。

label->setAttribute(Qt::WA_DeleteOnClose);

(3)new后手動delete

#include <QApplication>
#include <QLabel>
 
int main(int argc, char *argv[])
{
    int ret = 0;
    QApplication a(argc, argv);
    QLabel *label = new QLabel("Hello Qt!");
    label->show();
    ret = a.exec();
    delete label;
    return ret;
}

\color{red}{例子二}

#include <QApplication>
#include <QLabel>
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QLabel label("Hello Qt!");
    label.show();
    label.setAttribute(Qt::WA_DeleteOnClose);
    return app.exec();
}

運行:


分析:程序崩潰惯豆,因為label被close時池磁,delete &label;但label對象是在棧上分配的內(nèi)存空間,delete棧上的地址會出錯楷兽。
有些朋友理解為label被delete兩次而錯誤地熄,可以測試QLabel label("Hello Qt!"); label.show();delete &label;第一次delete就會出錯。
$\color{red}{例子三}

#include <QApplication>
#include <QLabel>
int main(int argc, char* argv[])
{
   QApplication app(argc, argv);
   QLabel label("Hello Qt!");
   QWidget w;
   label.setParent(&w);
   w.show();
   return app.exec();
}

分析:Object內(nèi)部有一個list芯杀,會保存children端考,還有一個指針保存parent,當自己析構(gòu)時揭厚,會自己從parent列表中刪除并且析構(gòu)所有的children却特。
w比label先被析構(gòu),當w被析構(gòu)時筛圆,會刪除chilren列表中的對象label裂明,但label是分配到棧上的,因delete棧上的對象而出錯太援。

改進方式:(1)調(diào)整一下順序闽晦,確保label先于其parent被析構(gòu)轰绵,label析構(gòu)時將自己從父對象的列表中移除自己,w析構(gòu)時尼荆,children列表中就不會有分配在stack中的對象了。

#include <QApplication>
#include <QLabel>
int main(int argc, char* argv[])
{
   QApplication app(argc, argv);
   QWidget w;
   QLabel label("Hello Qt!");
   label.setParent(&w);
   w.show();
   return app.exec();
}

(2)將label分配到堆上

QLabel *label = new QLabel("Hello Qt!");
label->setParent(&w)
 
或者QLabel *label = new QLabel("Hello Qt!",this);

\color{red}{例子四:野指針}

#include <QApplication>
#include <QLabel>
int main(int argc, char* argv[])
{
   QApplication app(argc, argv);
   QWidget *w = new QWidget;
   QLabel *label = new QLabel("Hello Qt!");
   label->setParent(w);
   w->show();
   delete w;
   label->setText("go");     //野指針
   return app.exec();
}

(上述程序不顯示Label唧垦,僅作測試)
分析:程序異常結(jié)束捅儒,delete w時會delete label,label成為野指針振亮,調(diào)用label->setText("go");出錯巧还。

改進方式:QPointer智能指針

#include <QApplication>
#include <QLabel>
#include <QPointer>
int main(int argc, char* argv[])
{
   QApplication app(argc, argv);
   QWidget *w = new QWidget;
   QLabel *label = new QLabel("Hello Qt!");
   label->setParent(w);
   QPointer<QLabel> p = label;
   w->show();
   delete w;
   if (!p.isNull()) {
     label->setText("go");
   }
   return app.exec();
}

\color{red}{例子五:deleteLater}
當一個QObject正在接受事件隊列時如果中途被你銷毀掉了,就是出現(xiàn)問題了坊秸,所以QT中建大家不要直接Delete掉一個QObject麸祷,如果一定要這樣做,要使用QObject的deleteLater()函數(shù)褒搔,它會讓所有事件都發(fā)送完一切處理好后馬上清除這片內(nèi)存阶牍,而且就算調(diào)用多次的deletelater也不會有問題。

發(fā)送一個刪除事件到事件系統(tǒng):

void QObject::deleteLater()
{
    QCoreApplication::postEvent(this, new QEvent(QEvent::DeferredDelete));
}

3星瘾、智能指針

如果沒有智能指針走孽,程序員必須保證new對象能在正確的時機delete,四處編寫異常捕獲代碼以釋放資源琳状,而智能指針則可以在退出作用域時(不管是正常流程離開或是因異常離開)總調(diào)用delete來析構(gòu)在堆上動態(tài)分配的對象磕瓷。

Qt家族的智能指針:



(1)QPointer

  QPointer是一個模板類。它很類似一個普通的指針念逞,不同之處在于困食,QPointer 可以監(jiān)視動態(tài)分配空間的對象,并且在對象被 delete 的時候及時更新翎承。

 QPointer的現(xiàn)實原理:在QPointer保存了一個QObject的指針硕盹,并把這個指針的指針(雙指針)交給全局變量管理,而QObject 在銷毀時(析構(gòu)函數(shù)审洞,QWidget是通過自己的析構(gòu)函數(shù)的莱睁,而不是依賴QObject的)會調(diào)用QObjectPrivate::clearGuards 函數(shù)來把全局 GuardHash 的那個雙指針置為*零,因為是雙指針的問題芒澜,所以QPointer中指針當然也為零了仰剿。用isNull 判斷就為空了。
// QPointer 表現(xiàn)類似普通指針 
    QDate *mydate = new QDate(QDate::currentDate()); 
    QPointer mypointer = mydata; 
    mydate->year();    // -> 2005 
    mypointer->year(); // -> 2005 
      
    // 當對象 delete 之后痴晦,QPointer 會有不同的表現(xiàn) 
    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

(2)std::auto_ptr

// QPointer 表現(xiàn)類似普通指針 
    QDate *mydate = new QDate(QDate::currentDate()); 
    QPointer mypointer = mydata; 
    mydate->year();    // -> 2005 
    mypointer->year(); // -> 2005 
      
    // 當對象 delete 之后南吮,QPointer 會有不同的表現(xiàn) 
    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 pointe
auto_ptr被銷毀時會自動刪除它指向的對象。
std::auto_ptr<QLabel> label(new QLabel("Hello Dbzhang800!"));

(3)其他的類參考相應(yīng)文檔誊酌。

4部凑、自動垃圾回收機制

(1)QObjectCleanupHandler
Qt 對象清理器是實現(xiàn)自動垃圾回收的很重要的一部分露乏。QObjectCleanupHandler可以注冊很多子對象,并在自己刪除的時候自動刪除所有子對象涂邀。同時瘟仿,它也可以識別出是否有子對象被刪 除,從而將其從它的子對象列表中刪除比勉。這個類可以用于不在同一層次中的類的清理操作劳较,例如,當按鈕按下時需要關(guān)閉很多窗口浩聋,由于窗口的 parent 屬性不可能設(shè)置為別的窗口的 button观蜗,此時使用這個類就會相當方便。

#include <QApplication>
#include <QObjectCleanupHandler>
#include <QPushButton>
 
int main(int argc, char* argv[])
{
   QApplication app(argc, argv);
   // 創(chuàng)建實例
   QObjectCleanupHandler *cleaner = new QObjectCleanupHandler;
   // 創(chuàng)建窗口
   QPushButton *w = new QPushButton("Remove Me");
   w->show();
   // 注冊第一個按鈕
   cleaner->add(w);
   // 如果第一個按鈕點擊之后衣洁,刪除自身
   QObject::connect(w, SIGNAL(clicked()), w, SLOT(deleteLater()));
   // 創(chuàng)建第二個按鈕墓捻,注意,這個按鈕沒有任何動作
   w = new QPushButton("Nothing");
   cleaner->add(w);
   w->show();
   // 創(chuàng)建第三個按鈕坊夫,刪除所有
   w = new QPushButton("Remove All");
   cleaner->add(w);
   QObject::connect(w, SIGNAL(clicked()), cleaner, SLOT(deleteLater()));
   w->show();
   return app.exec();
}

在上面的代碼中砖第,創(chuàng)建了三個僅有一個按鈕的窗口。第一個按鈕點擊后环凿,會刪除掉自己(通過 deleteLater() 槽)厂画,此時,cleaner 會自動將其從自己的列表中清除拷邢。第三個按鈕點擊后會刪除 cleaner袱院,這樣做會同時刪除掉所有未關(guān)閉的窗口。

(2)引用計數(shù)
應(yīng)用計數(shù)是最簡單的垃圾回收實現(xiàn):每創(chuàng)建一個對象瞭稼,計數(shù)器加 1忽洛,每刪除一個則減 1。

class CountedObject : public QObject 
{ 
    Q_OBJECT 
public: 
    CountedObject() 
    { 
        ctr=0; 
    } 
  
    void attach(QObject *obj) 
    { 
        ctr++; 
        connect(obj, SIGNAL(destroyed(QObject*)), this, SLOT(detach())); 
    } 
  
public slots: 
    void detach() 
    { 
        ctr--; 
        if(ctr <= 0) 
            delete this; 
    } 
  
private: 
    int ctr; 
};

利用Qt的信號槽機制环肘,在對象銷毀的時候自動減少計數(shù)器的值欲虚。但是,我們的實現(xiàn)并不能防止對象創(chuàng)建的時候調(diào)用了兩次attach()悔雹。
(3)記錄所有者

 更合適的實現(xiàn)是复哆,不僅僅記住有幾個對象持有引用,而且要記住是哪些對象腌零。例如:
class CountedObject : public QObject 
 { 
    public: 
       CountedObject() {} 
      
       void attach(QObject *obj) { 
         // 檢查所有者 
         if(obj == 0) 
           return; 
         // 檢查是否已經(jīng)添加過 
         if(owners.contains(obj)) 
           return; 
         // 注冊 
         owners.append(obj); 
         connect(obj, SIGNAL(destroyed(QObject*)), this, SLOT(detach(QObject*))); 
       }  
    public slots: 
       void detach(QObject *obj) { 
         // 刪除 
         owners.removeAll(obj); 
         // 如果最后一個對象也被 delete梯找,刪除自身 
         if(owners.size() == 0) 
            delete this; 
        }   
    private: 
        QList owners; 
};

現(xiàn)在我們的實現(xiàn)已經(jīng)可以做到防止一個對象多次調(diào)用 attach() 和 detach() 了。然而益涧,還有一個問題是锈锤,我們不能保證對象一定會調(diào)用 attach() 函數(shù)進行注冊。畢竟,這不是 C++ 內(nèi)置機制久免。有一個解決方案是浅辙,重定義 new 運算符(這一實現(xiàn)同樣很復(fù)雜,不過可以避免出現(xiàn)有對象不調(diào)用 attach() 注冊的情況)阎姥。

四记舆、總結(jié)

Qt 簡化了我們對內(nèi)存的管理,但是呼巴,由于它會在不太注意的地方調(diào)用 delete氨淌,所以,使用時還是要當心伊磺。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市删咱,隨后出現(xiàn)的幾起案子屑埋,更是在濱河造成了極大的恐慌,老刑警劉巖痰滋,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摘能,死亡現(xiàn)場離奇詭異,居然都是意外死亡敲街,警方通過查閱死者的電腦和手機团搞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來多艇,“玉大人逻恐,你說我怎么就攤上這事【颍” “怎么了复隆?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長姆涩。 經(jīng)常有香客問我挽拂,道長,這世上最難降的妖魔是什么骨饿? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任亏栈,我火速辦了婚禮,結(jié)果婚禮上宏赘,老公的妹妹穿的比我還像新娘绒北。我一直安慰自己,他們只是感情好察署,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布镇饮。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪储藐。 梳的紋絲不亂的頭發(fā)上俱济,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機與錄音钙勃,去河邊找鬼蛛碌。 笑死,一個胖子當著我的面吹牛辖源,可吹牛的內(nèi)容都是我干的蔚携。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼克饶,長吁一口氣:“原來是場噩夢啊……” “哼酝蜒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起矾湃,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤亡脑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后邀跃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霉咨,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年拍屑,在試婚紗的時候發(fā)現(xiàn)自己被綠了途戒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡僵驰,死狀恐怖喷斋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蒜茴,我是刑警寧澤继准,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站矮男,受9級特大地震影響移必,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜毡鉴,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一崔泵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧猪瞬,春花似錦憎瘸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春锅风,著一層夾襖步出監(jiān)牢的瞬間酥诽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工皱埠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肮帐,地道東北人。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓边器,卻偏偏與公主長得像训枢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子忘巧,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容

  • 原文地址: https://blog.csdn.net/dbzhang800/article/details/63...
    Caiaolun閱讀 775評論 0 1
  • 問題:為什么用deleteLater deletelater的原理是 QObject::deleteLater()...
    行走的代碼閱讀 508評論 0 0
  • 為什么在頭文件中有的是使用前置聲明恒界,而有的是包含頭文件? 如下代碼: 前置聲明(forward declarati...
    Joe_HUST閱讀 1,268評論 0 6
  • 《Qt 學(xué)習(xí)之路 2》原文地址 Qt跨平臺策略 GUI 模擬:任何平臺都提供了圖形繪制函數(shù)砚嘴,例如畫點十酣、畫線、畫面等...
    CharlesZhangCh閱讀 2,009評論 0 5
  • 1.new 和 delete 所有繼承自QOBJECT類的類枣宫,如果在new的時候指定了父親,那么它的清理時在父親被...
    _紀琛閱讀 2,099評論 0 2