1. 程式人生 > >Qt中記憶體洩露和半自動記憶體管理

Qt中記憶體洩露和半自動記憶體管理

Qt中幫程式設計師做了一些記憶體回收的事情,但正因為這些反而讓對此不熟悉的人會屢屢犯錯。

收錄一篇不錯的文章:

在C++中學習過程中,我們都知道:

  • delete 和 new 必須 配對使用(一 一對應):delete少了,則記憶體洩露,多了麻煩更大。

Qt作為C++的庫,顯然是不會違背C++的前述原則的。可是:

  • 在Qt中,我們很多時候都瘋狂地用new,卻很少用delete,缺少的 delete 去哪兒了?!

注:本文暫不涉及智慧指標(smart pointer)相關的東西,你可以考慮 Qt 智慧指標學習 一文

Qt半自動的記憶體管理

在Qt中,以下情況下你new出的物件你可以不用

親自去delete (但你應該清楚delete在何處被Qt呼叫的,怎麼被呼叫的):

  • QObject及其派生類的物件,如果其parent非0,那麼其parent析構時會析構該物件(本文內容圍繞這一點展開)

除此之外,有些類的物件可以接收設定一些特別的標記,比如:

  • QWidget及其派生類的物件,可以設定 Qt::WA_DeleteOnClose 標誌位(當close時會析構該物件)
  • QAbstractAnimation派生類的物件,可以設定 QAbstractAnimation::DeleteWhenStopped
  • QRunnable::setAutoDelete()
  • MediaSource::setAutoDelete()
  • ...

注意:這些用法會有些陷阱,請注意看本文最後的3個小例子。

在Qt中,最基礎和核心的類是:QObject 。它的魔力很大,本文只關注兩點:

  • 父子關係
  • deleteLater

父子關係

在Qt中,每個 QObject 內部都有一個list,用來儲存所有的 children,還有一個指標,儲存自己的parent。當它自己析構時,它會將自己從parent的列表中刪除,並且析構掉所有的children。

  • 注意:在 Qt 中,我們經常會遇到
    • 基類、派生類,或父類、子類。這是對於派生體系來說的,和在C++相關書中看到的完全一樣,與這的parent無關
    • 父物件、子物件、父子關係。
      這是Qt中所特有的,也就是這兒的parent所引入的,與類的繼承關係無關

建立與解除

Q_INVOKABLE QObject::QObject ( QObject * parent = 0 )
  • 建立一個QObject物件時,如果指定了父物件,它就會將自己新增到父物件的 children 列表中
QObject::~QObject () [virtual]
  • 當一個QObject物件析構時,它會將自己從父物件的 children 列表中移除(parent非0的話)
void QObject::setParent ( QObject * parent )
  • 通過該函式,將自己從原父物件的children中刪除,新增到新parent的children列表中

注:這三個函式都是通過一個內部私有函式來實現的,這就是

QObjectPrivate::setParent_helper(QObject *o)

獲取父、子物件

每個QObject只有一個父物件:

QObject * QObject::parent () const

子物件可以有多個

const QObjectList & QObject::children () const

所以可以根據條件來查詢嘍:

T QObject::findChild ( const QString & name = QString() ) const
QList<T> QObject::findChildren ( const QString & name = QString() ) const

deleteLater

deleteLater 包含兩層意思了

  • delete
  • later

呵呵,似乎這是廢話哈。

刪除自己

在去年春節前的時候吧,有人對

obj-> deleteLater()

會像下面一樣呼叫delete:

delete obj;

感到不解。然後我寫了這樣一個C++例子:

class A
{
  public:
  A(){}
  void deleteMe()
  {
      delete this;
  }
};

int main()
{
  A * a = new A;
  a->deleteMe();
  return 0;
} 

應該不需要解釋吧

later

Qt 是事件驅動的,所以傳送一個刪除事件到事件系統就可以啦:

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

事件迴圈稍後看到該事件就會將其派發會這個widget:

bool QObject::event(QEvent *e)
{
    switch (e->type()) {
...
    case QEvent::DeferredDelete:
         ...

一些例子

無關痛癢?

很簡短、很熟悉的一個例子是不?但是 如果你發現物件的解構函式始終不被成功呼叫,會有什麼感覺?

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

這是 C++ GUI Programming with Qt 4 一書的第一個例子。我們注意到這兒的 label 既沒有指定parent,也沒有對其呼叫delete。

所以,這兒會造成記憶體洩露。

書中解釋說,對於這種小例子,這點記憶體洩露不算什麼。不清楚官方這個例子的意圖是什麼,或許是一開始就讓大家用指標吧。

三種改進方式

  • 分配物件到stack而不是heap中
QLabel label("Hello Qt!");
label.show();
  • 設定標誌位,這樣,當我們點選關閉按鈕時,close()函式將會呼叫deleteLater
label->setAttribute(Qt::WA_DeleteOnClose);
  • 動手呼叫delete(不就是少了一個麼,我們補上還不行麼)
int ret = app.exec();
delete label;
return ret;

單獨列一個吧

強化一下對前一個例子的瞭解

#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物件卻不是通過new分配到heap中的。

為了使得使用者減少自己顯式使用delete,Qt將delete隱藏的比較深。這樣一來,不使用new為物件分配空間時,反倒需要多多小心了。

隱蔽很深?

看個小例子:這個程式退出時會直接崩潰。

#include <QtGui>
int main(int argc, char* argv[])
{
   QApplication app(argc, argv);
   QLabel label(tr"Hello Qt!");
   QWidget w;
   label.setParent(&w);
   w.show();
   return app.exec();
}
  • 問題出在哪兒呢?因為退出時,w 比 label 先被析構,當 w 被析構時,會刪除chilren列表中的物件,也就是這兒的 label。但 label 卻不是通過new分配在heap中,而是在stack中,可想而知,delete 一個再stack中的物件會怎麼樣了。相當於
QLabel label();
delete &label;
  • 兩種改進辦法:
    • 一是,將label分配到heap中
   QLabel *label = new QLabel("Hello Qt!");
   label.setParent(&w)
  • 再一種就是,確保label先於其parent被析構(調整一下順序),這樣,label析構時將自己從父物件的列表中移除自己,w析構時,children列表中就不會有分配在stack中的物件了。
   QWidget w;
   QLabel label(tr"Hello Qt!");

Qt 物件的父子關係的引入,簡化了我們對記憶體的管理,但是,由於它會在你不太注意的地方呼叫 delete,所以,使用時還是要當心。