一、问题
在C++中中,我们都知道:delete 和 new 必须配对使用(一 一对应):delete少了,则内存泄露。
为什么Qt使用new来创建一个控件,但是却没有使用delete来进行释放?
二、对象树
Qt中使用对象树来组织和管理所有的QObject类及其子类的对象。
当创建一个QObject对象时,会看到QObject的构造函数接收一个QObject指针作为参数,这个参数就是 parent,也就是父对象指针。
每个 QObject 内部都有一个list,用来保存所有的 children,还有一个指针,保存自己的parent。当它自己被析构时,如果这个对象有 parent,则自动将其从 parent 的children()列表中删除;如果有孩子,则自动 delete 每一个孩子。
比如Widget中的组件,如果是在堆上创建(使用new操作符),那么只要指定Widget为其父窗口(创建时指定parent参数为this)就可以了,也不需要进行delete操作。当整个应用程序关闭时,会去销毁该Widget对象,而此时又会自动销毁它的所有子组件,这些都是Qt的对象树所完成的。
三、对象树的实例
我们来分析一个Button对象的创建到销毁的过程。
创建一个MyPushButton对象,继承QPushButton
#ifndef MYPUSHBUTTON_H
#define MYPUSHBUTTON_H
#include <QPushButton>
class MyPushButton : public QPushButton
{
Q_OBJECT
public:
explicit MyPushButton(QWidget *parent = nullptr);
~MyPushButton();
};
#endif // MYPUSHBUTTON_H
#include "mypushbutton.h"
#include<QDebug>
MyPushButton::MyPushButton(QWidget *parent) : QPushButton(parent)
{
qDebug()<<"MyPushButton construct";
}
MyPushButton::~MyPushButton()
{
qDebug()<<"MyPushButton destory";
}
main函数
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
创建mainWindow
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mypushbutton.h"
#include<QtDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
qDebug()<<"MainWindow construct";
ui->setupUi(this);
MyPushButton *mybtn=new MyPushButton(this);
mybtn->resize(120,50);
mybtn->setText("MyPushButton");
mybtn->move(200,20);
}
MainWindow::~MainWindow()
{
delete ui;
qDebug()<<"MainWindow destory";
}
MainWindow类的析构函数中默认已经有了销毁ui的语句,这里又添加了输出语句。
当MainWindow窗口被销毁时,将输出信息。下面运行程序,然后关闭窗口,在Qt Creator
的应用程序输出栏中的输出信息为:
可能有读者问这里为什么是MyPushButton后销毁,难道不应该先销毁MyPushButton,再销毁MainWindows吗?
因为这里与Qt的实现有关,堆栈信息如下,deleteChildren 在MainWindows的父类QWidget的析构函数中,C++是子类对象先析构,再进行父类对象的析构,而MyPushButton在QWidget的析构函数中调用,所以顺序与我们预想的不一致。
四、总结
Qt 引入对象树的概念,在一定程度上解决了内存问题,但是也存在风险。
比如下面这段代码,程序崩溃了!!!
{
QPushButton quit("Quit");
QWidget window;
quit.setParent(&window);
}
分析:作为父对象的 window 会首先被析构,因为它是最后一个创建的对象。在析构过程中,它会调用子对象列表中每一个对象的析构函数,也就是说, quit 此时就被析构了。然后,代码继续执行,在 window 析构之后,quit 也会被析构,因为 quit 也是一个局部变量,在超出作用域的时候当然也需要析构。但是,这时候已经是第二次调用 quit 的析构函数了,C++ 不允许调用两次析构函数,因此,程序崩溃了。
所以,对于规范的Qt程序,我们在main()函数中将主窗口部件创建在栈上,比如“Widget w;”,而不要在堆上进行创建(使用new操作符),避免内存泄漏。对于子窗口部件,可以使用new操作符在堆上进行创建,同时一定要指定其父部件(parent),这样就不需要再使用delete操作符来销毁该对象了。
参考: