Qt - 内存回收机制

1. 内存回收机制

Qt中有内存回收机制, 但是不是所有被new出的对象被自动回收, 满足条件才可以回收

如果想要在Qt中实现内存的自动回收, 需要满足以下两个条件:

  1. 创建的对象必须是QObject类的子类(间接子类也可以)
  2. 创建出的类对象, 必须要指定其父对象是谁, 一般情况下有两种操作方式

对于1:

QObject类是没有父类的, Qt中有很大一部分类都是从这个类派生出去的

Qt中使用频率很高的窗口类和控件都是 QObject 的直接或间接的子类

对于2:

// 方式1: 通过构造函数
// parent: 当前窗口的父对象, 找构造函数中的 parent 参数即可
QWidget::QWidget(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags());
QTimer::QTimer(QObject *parent = nullptr);

// 方式2: 通过setParent()方法
// 假设这个控件没有在构造的时候指定符对象, 可以调用QWidget的api指定父窗口对象
void QWidget::setParent(QWidget *parent);
void QObject::setParent(QObject *parent);

对于窗口类,QWidget是所有类的父类,使用QWidget *parent 父类指针指向子类对象

对于其他类,QObject 是所有类的父类,使用QObject *parent父类指针指向子类对象

主要就是这两种父类指针,父类指针创建的目的很大程度上就是为了资源回收。

当父对象析构的时候,先把所有的子对象析构,最后在析构父对象。

 

2. 内存回收本质

当你创建一个QObject对象时,会看到QObject的构造函数接收一个QObject指针作为参数,这个参数就是 parent,也就是父对象指针。这相当于,在创建QObject对象时,可以提供一个其父对象,我们创建的这个QObject对象会自动添加到其父对象的children()列表。当父对象析构的时候,这个列表中的所有对象也会被析构。

任何对象树中的 QObject对象 delete 的时候,如果这个对象有 parent,则自动将其从 parent 的children()列表中删除;如果有孩子,则自动 delete 每一个孩子。Qt 保证没有QObject会被 delete 两次,这是由析构顺序决定的。

当父对象析构的时候,先把所有的子对象析构,最后在析构父对象。

 

3. 测试代码

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include "subwindows.h"
#include "mybutton.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    SubWindows* subwin = new SubWindows(this);
    subwin->show();

    MyButton* btn = new MyButton(subwin,"我的按钮");
    btn->show();
}

MainWindow::~MainWindow()
{
    delete ui;
    qDebug()<<"MainWindow的析构函数"<<endl;
}

mybutton.h

#ifndef MYBUTTON_H
#define MYBUTTON_H

#include <QPushButton>

class MyButton: public QPushButton
{
public:
    MyButton(QWidget *parent = 0,QString text ="");
    ~MyButton();
};

#endif // MYBUTTON_H

mybutton.cpp

#include "mybutton.h"
#include <QDebug>


MyButton::MyButton(QWidget *parent,QString text):QPushButton(parent)
{
    this->setFixedSize(80,30);
    this->setText(text);
}

MyButton::~MyButton()
{
    qDebug()<<"MyButton的析构函数";
}

subwindows.h

#ifndef SUBWINDOWS_H
#define SUBWINDOWS_H

#include <QDialog>

namespace Ui {
class SubWindows;
}

class SubWindows : public QDialog
{
    Q_OBJECT

public:
    explicit SubWindows(QWidget *parent = 0);
    ~SubWindows();

private:
    Ui::SubWindows *ui;
};

#endif // SUBWINDOWS_H

subwindows.cpp

#include "subwindows.h"
#include "ui_subwindows.h"
#include <QDebug>

SubWindows::SubWindows(QWidget *parent) :QDialog(parent),ui(new Ui::SubWindows)
{
    ui->setupUi(this);
}

SubWindows::~SubWindows()
{
    delete ui;
    qDebug()<<"subwindows的析构函数"<<endl;
}

关闭主窗口:

 

 

4. 拓展知识

首先看这个代码

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QPushButton>
#include "mypushbutton.h"

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;

    QPushButton* btn1 ;
    QPushButton* btn2 ;
    QPushButton* btn3 ;
    myPushButton* mybtn1;
};
#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QHBoxLayout>


MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    btn1 = new QPushButton("btn1");
    btn2 = new QPushButton("btn2");
    btn3 = new QPushButton("btn3");
    mybtn1 = new myPushButton("mybtn1");
    btn1->setFixedSize(60,36);
    btn2->setFixedSize(60,36);
    btn3->setFixedSize(60,36);
    mybtn1->setFixedSize(60,36);

    QHBoxLayout *hlayout = new QHBoxLayout;
    hlayout->addWidget(btn1);
    hlayout->addWidget(btn2);
    hlayout->addWidget(btn3);
    hlayout->addWidget(mybtn1);
    hlayout->setSpacing(5);

    // 创建中央部件并设置布局
    QWidget *centralWidget = new QWidget(this);
    centralWidget->setLayout(hlayout);
    this->setCentralWidget(centralWidget); // 关键步骤!
}

MainWindow::~MainWindow()
{
    delete ui;
}

mypushbutton.h

#ifndef MYPUSHBUTTON_H
#define MYPUSHBUTTON_H

#include <QPushButton>
#include <QEvent>

class myPushButton : public QPushButton
{
    Q_OBJECT  // 必须包含宏以支持信号和槽

public:
    // 构造函数
    explicit myPushButton(QWidget *parent = nullptr);  // 默认构造函数
    explicit myPushButton(const QString &text, QWidget *parent = nullptr); // 带文本的构造函数
    ~myPushButton();

    // 可选:自定义功能函数
    void setHoverStyle(const QString &style); // 设置悬停样式

protected:
    // 覆盖事件处理函数
    void enterEvent(QEvent *event) override;  // 鼠标进入按钮区域
    void leaveEvent(QEvent *event) override;  // 鼠标离开按钮区域

private:
    QString m_normalStyle;   // 正常状态样式
    QString m_hoverStyle;    // 悬停状态样式
};

#endif // MYPUSHBUTTON_H

mypushbutton.cpp

#include "mypushbutton.h"
#include <QEvent>
#include <QDebug>

// 默认构造函数
myPushButton::myPushButton(QWidget *parent) : QPushButton(parent)
{
    // 初始化默认样式
    m_normalStyle = "QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px; border-radius: 4px; }";
    m_hoverStyle = "QPushButton:hover { background-color: #45a049; }";

    // 应用初始样式
    this->setStyleSheet(m_normalStyle + m_hoverStyle);
}

// 带文本的构造函数
myPushButton::myPushButton(const QString &text, QWidget *parent) : QPushButton(text, parent)
{
    // 同上,初始化样式
    m_normalStyle = "QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px; border-radius: 4px; }";
    m_hoverStyle = "QPushButton:hover { background-color: #45a049; }";
    this->setStyleSheet(m_normalStyle + m_hoverStyle);
}

myPushButton::~myPushButton()
{
    qDebug()<<"~myPushButton析构函数";
    qDebug()<< "myPushButton destroyed:" << this->text();
}

// 设置悬停样式
void myPushButton::setHoverStyle(const QString &style)
{
    m_hoverStyle = style;
    this->setStyleSheet(m_normalStyle + m_hoverStyle);
}

// 鼠标进入事件
void myPushButton::enterEvent(QEvent *event)
{
    Q_UNUSED(event);
    // 可以在此处添加额外逻辑(例如播放动画)
    this->setCursor(Qt::PointingHandCursor); // 改变鼠标指针形状
}

// 鼠标离开事件
void myPushButton::leaveEvent(QEvent *event)
{
    Q_UNUSED(event);
    this->unsetCursor(); // 恢复默认鼠标指针
}

在MainWindow的构造函数中创建了四个按钮:三个QPushButton和一个myPushButton实例。这些按钮都被添加到一个水平布局中,而布局设置到了中央部件上。中央部件又被设置为MainWindow的中央部件。此时,用户没有显式地为这些按钮指定父对象,但布局的addWidget函数是否会影响父对象呢?

 

在 Qt 中,对象的父子关系是内存管理的关键机制。针对你的代码,以下是详细分析:


结论

你的代码目前不会造成内存泄漏,但需要理解其背后的原因。关键点在于:

  1. 当控件被添加到布局(hlayout->addWidget(...))时,布局会将这些控件的父对象自动设置为布局所在的父部件(即 centralWidget)。

  2. 当父部件(centralWidget)被销毁时,Qt 会自动递归销毁其所有子对象(包括按钮)。

因此,即使你在创建 QPushButton 和 myPushButton 时没有显式指定父对象,它们的父对象最终会被正确设置为 centralWidget,从而由 Qt 自动管理内存。


详细分析

1. 控件创建时未指定父对象

你的代码中按钮的创建方式:

btn1 = new QPushButton("btn1");  // 没有指定 parent 参数,默认 parent = nullptr
mybtn1 = new myPushButton("mybtn1"); // 同样 parent = nullptr

此时这些按钮的父对象为 nullptr,理论上需要手动 delete 才能释放内存,否则会导致内存泄漏。

2. 控件被添加到布局后

当调用 hlayout->addWidget(...) 时,布局会执行以下操作:

void QLayout::addWidget(QWidget *widget) {
    widget->setParent(parentWidget()); // 关键步骤:将控件的父对象设为布局的父部件
    // ...其他操作
}

由于 hlayout 被设置到 centralWidget(通过 centralWidget->setLayout(hlayout)),所有添加到布局中的控件的父对象会被自动设置为 centralWidget

3. 父部件销毁时的自动清理

当 centralWidget 被销毁时(例如主窗口关闭时),Qt 会自动销毁其所有子对象(包括按钮和布局),因此无需手动释放内存。

 

代码验证

你可以在 MainWindow 的析构函数中添加调试输出,观察按钮是否被销毁:

MainWindow::~MainWindow() 
{
    delete ui;
    qDebug() << "MainWindow destroyed";
}

// 在 myPushButton 析构函数中添加输出
myPushButton::~myPushButton() 
{
    qDebug() << "myPushButton destroyed:" << this->text();
}

// 在 QPushButton 子类化(如果允许)或通过事件过滤器验证

运行程序后关闭窗口时,会看到所有按钮的析构函数被调用,证明内存已正确释放。


最佳实践建议

尽管当前代码没有内存泄漏,但为了代码的清晰性和可维护性,建议在创建控件时显式指定父对象:

// 直接指定父对象为 centralWidget
QWidget *centralWidget = new QWidget(this);
btn1 = new QPushButton("btn1", centralWidget); // 显式设置父对象
mybtn1 = new myPushButton("mybtn1", centralWidget);

这样做有以下好处:

  1. 明确所有权:直观表明控件的生命周期由 centralWidget 管理。

  2. 避免意外错误:防止在布局被设置前控件未被正确关联父对象的情况。


潜在风险

以下情况可能导致内存泄漏:

  1. 控件未被添加到布局或父部件:如果某个按钮被创建但未添加到布局或设置父对象,则必须手动 delete

  2. 动态移除控件:如果通过 removeWidget() 从布局中移除控件,需要显式调用 delete 或重新指定父对象。


总结

  • 代码是安全的,因为 Qt 的布局机制自动设置了父对象。

  • 显式指定父对象是更好的编程习惯,尤其是在复杂项目中。

  • 使用 Qt 的内存管理机制可以有效减少手动 new/delete 的错误。

posted @ 2022-04-15 10:51  [BORUTO]  阅读(337)  评论(0)    收藏  举报