【问题标题】:Are destructors necessary in QDialogs?QDialogs 中是否需要析构函数?
【发布时间】:2015-06-30 13:55:29
【问题描述】:

我正在关注 Qt 示例(如 TabDialog),我注意到所有 UI 项都是作为指针创建的 - 但我看不到 delete 和析构函数。

对吗?这不会导致内存泄漏吗?

我正在尝试添加析构函数

~TabDialog()
{
    delete tabWidget;
    delete buttonBox;
}

在调用者上

TabDialog *tabDialog = new TabDialog();
tabDialog->setAttribute(Qt::WA_DeleteOnClose);
tabDialog->exec();

但是当我关闭对话框时程序崩溃了。

析构函数和delete 都是指针项,是不必要的还是我做错了?

【问题讨论】:

  • 如果您构造对话框的小部件并将对话框设置为父级,则无需在对话框的析构函数中删除它们:Qt 会处理它们的删除。
  • 对不起,这不会重现。你的代码是正确的。删除是不必要的,但无害。
  • @KubaOber 谢谢,我不确定,因为我希望使用 new + delete 的模式,并且我不想造成内存泄漏
  • 您确定紧跟在tabDialog->exec() 之后的代码没有尝试访问tabDialog 吗?请记住,当exec() 返回时,tabDialog 是一个悬空指针。也许你在它下面有delete tabDialog,你在向我们隐藏它?!
  • tabDialog->exec() 后面没有代码... 声明的函数上没有其他代码,在上下文菜单事件中称为menu.addAction(showPropertiesAct);

标签: c++ qt memory-leaks


【解决方案1】:

我认为您会因为以下几行而感到困惑:

tabWidget = new QTabWidget;//and so on

您看不到明确的父级(如new QTabWidget(this);),但这里没有必要。看这里:

QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(tabWidget);
mainLayout->addWidget(buttonBox);
setLayout(mainLayout);

setLayout 将重新设置您的 QVBoxLayoutQVBoxLayout 将重新设置其中的所有小部件,因此现在您的小部件有一个父级,它们将在您的对话后被销毁。

正如doc所说:

当你使用一个布局时,你不需要传递一个父级 构建子小部件。布局将自动重新设置 小部件(使用 QWidget::setParent()),以便它们是 安装布局的小部件。

注意:布局中的小部件是在其上的小部件的子级。 布局已安装,而不是布局本身。小部件只能有 其他小部件作为父级,而不是布局。

【讨论】:

  • 添加tabDialog->setAttribute(Qt::WA_DeleteOnClose); 是个好主意吗?还是默认的?
  • @Thalia 当用户关闭它时它会删除对话框。如果您需要这个并且如果您使用tabDialog->show(); 调用对话框非常好,因为在您的情况下exec()“停止”执行并等待结果,因此您可以编写类似tabDialog->exec(); delete tabDialog;
  • 您的回答具有误导性。破坏 Qt 内存控制下的小部件不是无效的。这里的问题是 delete 传递了悬空指针。 Thalia 不会删除存在的小部件——这将是一件非常有效的事情。 “Qt”不会删除你已经删除的小部件,那会很糟糕。 Qt 实际上跟踪 QObjects 与父母的生命周期,如果您先删除对象,则行为非常合适。 同样,这里的错误是 delete 传递的悬空指针不指向有效的 QObject 实例,而是指向垃圾!
  • 如果你是对的,这段代码将无法工作:int main(int argc, char ** argv) { QApplication app(argc, argv); QWidget w; QVBoxLayout l(&w); QLabel label("Hello"); QPushButton button("Button"); l.addWidget(&label); l.addWidget(&button); w.show(); return app.exec(); } 尽管在从main 退出时调用了以下析构函数,但它仍然有效,顺序为:1. button.~QPushButton(), 2 .label.~QLabel(),3.l.~QVBoxLayout(),4.w.~QWidget(),5.app.~QApplication()。这些调用是有效的 C++,顺便说一句。
  • @Chernobyl 在 C++ 中,对象按从大多数派生类到基类的顺序被破坏。 TabDialog::~TabDialog() 将在 TabDialog::~QObject() 之前执行。
【解决方案2】:

对不起,但这只是无法重现。测试用例如下。您可能需要添加一些额外的代码才能使其重现。

Qt 的内存管理会处理所有事情,因为所有的小部件最终都有父母。即:

  1. 标签一旦被传递给addTab,就会成为父标签。
  2. tabWidgetbuttonBox 在添加到布局后立即成为父级。

由于您在 Qt 尝试删除它们之前删除了 tabWidgetbuttonBox,所以一切都很好。一旦您删除它们,QObject 的内存管理就会收到通知,并将它们从TabDialog 的子列表中删除。我已经在析构函数代码中明确说明了这一点。

Q_ASSERT 的含义是:“在运行时的这一点上,以下必须为真”。如果我们错了,调试构建将中止。因为它没有,所以断言是正确的。因此,在delete tabWidget 之前,对话框同时具有QTabWidgetQDialogButtonBox 子级。在delete tabWidget 之后,对话框不应再有任何QTabWidget 子级。以此类推。

#include <QApplication>
#include <QDialog>
#include <QTabWidget>
#include <QDialogButtonBox>
#include <QVBoxLayout>

class TabDialog : public QDialog
{
   QTabWidget *tabWidget;
   QDialogButtonBox *buttonBox;
public:
   TabDialog() :
      tabWidget(new QTabWidget),
      buttonBox(new QDialogButtonBox(QDialogButtonBox::Ok | 
                                     QDialogButtonBox::Cancel))
   {
      tabWidget->addTab(new QWidget, tr("General"));
      tabWidget->addTab(new QWidget, tr("Permissions"));
      tabWidget->addTab(new QWidget, tr("Applications"));
      QVBoxLayout *layout = new QVBoxLayout;
      layout->addWidget(tabWidget);
      layout->addWidget(buttonBox);
      setLayout(layout);
      connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
      connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
   }
   ~TabDialog() {
      Q_ASSERT(findChild<QTabWidget*>());
      Q_ASSERT(findChild<QDialogButtonBox*>());
      delete tabWidget;
      Q_ASSERT(! findChild<QTabWidget*>());
      Q_ASSERT(findChild<QDialogButtonBox*>());
      delete buttonBox;
      Q_ASSERT(! findChild<QTabWidget*>());
      Q_ASSERT(! findChild<QDialogButtonBox*>());
   }
};

int main(int argc, char *argv[])
{
   QApplication a(argc, argv);
   TabDialog *tabDialog = new TabDialog();
   tabDialog->setAttribute(Qt::WA_DeleteOnClose);
   tabDialog->exec();
   return 0;
}

如果您尝试了以下操作,它会崩溃的唯一方法:

int main(int argc, char *argv[])
{
   QApplication a(argc, argv);
   TabDialog *tabDialog = new TabDialog();
   tabDialog->setAttribute(Qt::WA_DeleteOnClose);
   tabDialog->exec();
   // At this point `tabDialog` is a dangling pointer.
   delete tabDialog; // crash
   return 0;
}

不幸的是,Qt 示例是毫无意义的过早悲观化的例子。 Qt 的类广泛使用PIMPL 成语。因此,QTabWidget 的大小并不比QObject 的大小大多少(在我的 64 位平台上为 48 对 16 字节)。通过在堆上分配您的类的固定成员,您正在执行 两个 堆分配:一个小的用于QObject 派生类,然后另一个用于它的 PIMPL。您无缘无故地将分配数量增加了一倍。

以下是避免这种悲观情绪的方法:

#include <QApplication>
#include <QDialog>
#include <QTabWidget>
#include <QDialogButtonBox>
#include <QVBoxLayout>

class TabDialog : public QDialog
{
   QVBoxLayout m_layout;
   QTabWidget m_tabWidget;
   QDialogButtonBox m_buttonBox;
   QWidget m_generalTab, m_permissionsTab, m_applicationsTab;
public:
   TabDialog() :
      m_layout(this),
      m_buttonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel)
   {
      m_tabWidget.addTab(&m_generalTab, tr("General"));
      m_tabWidget.addTab(&m_permissionsTab, tr("Permissions"));
      m_tabWidget.addTab(&m_applicationsTab, tr("Applications"));
      m_layout.addWidget(&m_tabWidget);
      m_layout.addWidget(&m_buttonBox);
      connect(&m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
      connect(&m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
   }
};

int main(int argc, char *argv[])
{
   QApplication app(argc, argv);
   auto tabDialog = new TabDialog();
   tabDialog->setAttribute(Qt::WA_DeleteOnClose);
   tabDialog->show(); // NOT tabDialog->exec()!!
   return app.exec();
}

明确的堆分配越少越好。这样,您甚至不会被 delete 所诱惑,因为不涉及任何指针。编译器会自动为您生成必要的析构函数调用。

此外,如果您在main 中只显示一个窗口,那么显式堆分配也没有意义:

int main(int argc, char *argv[])
{
   QApplication app(argc, argv);
   TabDialog tabDialog;
   tabDialog.show();
   return app.exec();
}

【讨论】:

  • 不确定能否删除子对象 - 当我尝试删除子项目时,我的程序冻结了。但我现在明白为什么我不需要删除它们了。
  • @Thalia 根据 SO 的规则,如果您对特定代码有疑问,必须将其发布在问题本身中。您需要以 Qt 为例并开始将其最小化以找出失败的原因。这是你的工作。就目前而言,您的问题因此而偏离主题。或者您可以从我的答案中获取最小化的示例,然后开始将 Qt 示例中的代码添加到其中,看看它会在什么时候失败。我的预感是您将无法重现该问题。尝试擦除您的构建目录并重新构建。我敢打赌它会正常工作。
【解决方案3】:

Qt 上的内存处理

Qt 将小部件作为树处理,每个小部件都有一个父级,每个父级都有 释放子内存的义务,如果小部件没有父小部件,您应该使用操作员 delete 手动删除它。

【讨论】:

  • 这是误导。这里的错误是 delete 传递了悬空指针:指向不再存在的对象的指针。销毁或删除有父母的QObjects 是完全有效的(并且很常见)!
猜你喜欢
  • 2018-06-19
  • 1970-01-01
  • 1970-01-01
  • 2011-10-11
  • 2013-05-22
  • 1970-01-01
  • 2021-05-30
  • 1970-01-01
  • 2020-12-27
相关资源
最近更新 更多