一、QT对象树的概念
先来看一下 QObject 的构造函数:
通过帮助文档我们可以看到,QObject 的构造函数中会传入一个 Parent 父对象指针,children() 函数返回 QObjectList。即每一个 QObject 对象有且仅有一个父对象,但可以有很多个子对象。按照这种形式排列就会形成一个对象树的结构,最上层是父对象,下面是子对象,在再下面是孙子对象,以此类推。
那么Qt为什么要这么设计呢?或者说这样设计的好处是什么呢?很简单,就是为了方便内存管理。我们在创建 QObject 对象时,提供一个父对象,那么我们创建的这个 QObject 对象会自动添加到其父对象的 children() 列表。当父对象析构的时候,这个子对象列表中的所有对象都会被析构,当析构子对象的时候,会自动从父对象的子对象列表中删除。
这种机制在 GUI 程序开发过程中是相当实用的。有一个很明显的现象就是我们会在窗口中new很多控件,但是却没有delete,因为在父控件销毁时这些子控件以及布局管理器对象会一并销毁。
值得注意的是,如果在构造时设置父对象为 NULL,那么当前实例不会有父对象存在,Qt 也不会自动析构该实例,除非实例超出作用域导致析构函数被调用,或者用户在恰当时机使用 delete 操作符或者使用 deleteLater 方法。
QWidget
QWidget 也是 QObject 的子类,所以在 parent 机制上是没有区别的。然而实际使用时,对于 QWidget 和其派生类来说,在内存管理上要稍微复杂一些。因为 QWidget 需要和 QEventLoop 高度配合才能完成工作,我们举个例子来说明一下。
例如 QWidget 的关闭流程,首先用户点击关闭按钮触发 close() 槽,然后Qt向 widget 发送 QCloseEvent,默认的 QCloseEvent 会将 widget 隐藏起来,也就是hide()。我们可以看到,widget 的关闭实际是将其隐藏,而没有释放内存,虽然我们有时会重写 closeEvent 但也不会手动释放 widget。
所以需要设置 Qt::WA_DeleteOnClose 属性,那么会在 close 之后接着调用 widget 的析构函数,或者手动 delete。
二、对象树模型存在的小问题
示例代码1:
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPushButton btn("button");
QWidget w;
btn.setParent(&w);
w.show();
return a.exec();
}
运行结果: 关闭 widget 后程序崩溃,没有正常结束
程序异常结束。
原因:
btn.setParent(&w);
btn
对象,通过setParent()
函数,将自己挂到对象树上。当程序结束时,根据栈区的特性,应该是先析构父对象–w
,再析构btn
。可是由于Qt中的对象树自动析构原理,我们析构父对象会自动析构子对象。也就是说,在析构父对象–w
时,会自动调用子对象btn
的析构函数。然后,栈区继续销毁,按照顺序还要再一次析构btn
。但是这时候已经是第二次调用 子对象的析构函数了,C++中不允许调用两次析构函数,因此,程序会崩溃。
三、正确写法–尽量在堆上创建子对象
示例代码2:
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPushButton *btn = new QPushButton ("button");
QWidget *w = new QWidget;
btn->setParent(w);
w->show();
return a.exec();
}
w和btn被分配在堆上。w析构时,不会影响在栈区中的对象,程序正常运行。
由此我们可以看到,Qt 的对象树机制虽然在内存管理上很方便,但是也会带来一些麻烦,为了避免这些麻烦我们可以这么做:
- 先创建父对象再创建子类对象,并且在创建子对象时就指定父对象;
- 尽量在堆上创建子对象;
参考链接:
Qt 对象树
Qt中的内存泄漏
[Qt源码阅读(三) 对象树管理]