1. 什么是對象樹?
對象樹是一種基于父子關系的對象管理機制。在 Qt 中,所有繼承自 QObject
的類都可以參與到對象樹中。
當一個對象被設置為另一個對象的父對象時,子對象會被添加到父對象的內部列表中,形成一種樹狀結構。
Qt 提供了一個調試工具方法 dumpObjectTree()
,可以幫助開發者打印對象樹的結構。
QObject* parent = new QObject();QObject* child = new QObject();child->setParent(parent);parent->dumpObjectTree();
1.1 對象樹結構
在 Qt 中,對象樹的每個節點是一個 QObject 對象,而 “邊” 代表父子關系。具體來說:
- 根節點:沒有父對象的對象(即 parent == nullptr)。
- 子節點:有父對象的對象,它們會被添加到父對象的
子對象列表
中。 - 葉子節點:沒有子對象的對象。
grandparent 是根節點,parent 是 grandparent 的子節點,child 是 parent 的子節點,形成了一個三層的樹形結構。
1.2 為什么選擇 new 而不是棧上分配?
如果對象是在棧上創建的(如 局部變量),當作用域結束時,該對象會自動銷毀。可能導致以下問題:
- 如果子對象比父對象先銷毀,父對象的 子對象列表 可能會指向無效內存。直觀表現可能為:子對象區域顯示為空白狀態。
通過 new 創建對象,可以確保對象的生命周期由對象樹管理,而不是由作用域控制。 這樣可以避免上述問題。
2. 對象樹的工作原理
在 Qt 中,繼承自 QObject 的對象能夠參與到對象樹的前提是 該類必須正確地聲明
Q_OBJECT
宏。
對象樹通過以下兩個關鍵成員變量實現:
QObject* parent
:指向父對象的指針;QList<QObject*> children
:存儲子對象的列表。
每個 QObject 對象可以有一個父對象 parent,和有多個子對象 children;子對象會被自動添加到父對象的 children 列表中。
2.1 對象創建時
當創建一個新的 QObject 對象并指定父對象時,會發生:
- 新對象的 parent 指針被設置為指定的父對象;
- 新對象被自動添加到父對象的 children 列表中。
2.2 對象銷毀時
當一個對象被銷毀時,以下操作會發生:
- 該對象的所有子對象會被遞歸銷毀;
- 該對象從其父對象的 children 列表中移除。
? Qt 的實現確保了在子對象的析構函數中,它會調用父對象的相關方法(如 QObjectPrivate::removeChild()
),將自己從父對象的 children 列表中移除;顯式移除子對象,是為了避免父對象的 children 列表中殘留無效指針,從而確保對象樹的完整性。
class MyObject : public QObject
{Q_OBJECT
public:QString name; // 對象名稱explicit MyObject(QString name, QObject *parent) : name(name), QObject(parent) {}~MyObject(){qDebug() << name << " entering destructor.";if (parent()) {qDebug() << name << " is being removed from parent's children list.";}}
};void CreateObjects()
{MyObject* grandparent = new MyObject(QString("grandparent"));MyObject* parent = new MyObject(QString("parent"), grandparent);MyObject* child = new MyObject(QString("child"), parent);delete grandparent;
}
實際銷毀順序是:child -> parent -> grandparent —— 根據打印信息,child 開始析構時 parent 仍然存在。
3. 對象樹的其它作用
在 Qt 中,對象樹的核心作用是 簡化對象的生命周期管理。通過對象樹,開發者無需過多擔心內存泄露或懸空指針等問題。
“簡化對象的生命周期管理” 在 “工作原理” 部分已經解釋過,除此之外,對象樹的作用還包括:
3.1 層次化組織對象
? 在 GUI 應用程序中,對象之間的關系通常是層次化的。例如,一個窗口可能包含多個控件,而每個控件又可能包含子控件。對象樹提供了一種自然的方式來組織這些對象之間的關系。
int main(int argc, char *argv[])
{QApplication a(argc, argv);Widget w;w.show();return a.exec();
}
-
Widget
是一個繼承自 QWidget 的類,而 QWidget 又繼承自 QObject。因此 Widget 對象可以參與到 Qt 的對象樹機制中。 -
代碼中
w
是一個在棧上分配的對象。如果用戶手動關閉窗口(通過點擊窗口的關閉按鈕 或 調用
w.close()
),窗口會被隱藏(調用了hide()
方法),但 w 本身不會被銷毀,因此它的子對象也不會被銷毀 —— 棧上分配的對象由 C++ 的作用域管理,只有當作用域結束時(即 main() 函數返回時),w 才會被銷毀。 -
設置
Qt::WA_DeleteOnClose
。Qt::WA_DeleteOnClose 是一個窗口屬性,用于指示窗口在關閉時自動刪除自身。如果設置了該屬性,當用戶手動關閉窗口時,w 會被銷毀,并觸發對象樹的遞歸銷毀。
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);// 設置關閉時自動銷毀this->setAttribute(Qt::WA_DeleteOnClose); }
3.2 其它作用
-
支持信號與槽機制:
Qt 的信號與槽機制依賴于對象樹來確保對象的生命周期一致性。
如果一個對象被銷毀,Qt 會自動斷開與其相關的信號和槽連接;
這一機制由 QObject 的析構函數觸發,確保信號不會發送到已銷毀的對象。
-
簡化資源管理:
通過將資源封裝成 QObject 的子類,并將其添加到對象樹中,可以確保資源在父對象銷毀時被正確釋放。
class FileWrapper : public QObject {Q_OBJECT public:FileWrapper(const QString& path, QObject* parent = nullptr): QObject(parent), file(path){}~FileWrapper(){file.close();} private:QFile file; };QObject* parent = new QObject(); FileWrapper* file = new FileWrapper("test.txt", parent);
-
提供遍歷和查找功能:通過 QObject::children() 方法,可以獲取某個對象的所有子類對象。
-
避免重復釋放:對象樹通過集中管理對象的銷毀過程,避免了重復釋放的問題。
4. 注意事項
-
避免循環引用:如果兩個對象互相設置對方為父對象,會導致循環引用,從而引發內存泄露。
解決方案: 確保父子關系是單向的。
-
避免跨線程操作:如果父子對象位于不同的線程中,銷毀順序可能會受到線程調度的影響.
QThread thread; QObject* obj = new QObject(); obj->moveToThread(&thread); QObject* child = new QObject(obj);
如果子對象在父對象銷毀后仍然被訪問(如 調用 child->parent() ),會導致未定義行為。
解決方案:
確保父子對象始終位于同一個線程;
如果需要跨線程操作,可以使用信號與槽機制進行線程間通信,而不是直接操作對象樹。
-
避免手動干預銷毀:如果手動調用 delete 銷毀一個對象,而該對象同時參與對象樹,可能會導致重復釋放的問題。
QObject* parent = new QObject(); QObject* child = new QObject(parent);delete parent; // 自動銷毀 child delete child; // 再次銷毀 child,重復釋放
解決方案:
讓對象樹完全管理對象的生命周期,避免手動調用 delete ;
如果必須手動管理對象,確保不將其添加到對象樹中。