Qt如何删除对象?存储QObject的最佳方法是什么?

23
我听说在Qt中,对象会自动删除它们的子对象,我想知道在这些情况下会发生什么。
#include <QApplication>
#include <QLabel>
#include <QHBoxLayout>
#include <QWidget>

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
/*
    QLabel label("label");      //  Program will crash. Destruct order is 1. widget, 2. layout, 3. label
    QHBoxLayout layout;         //  But layout will be deleted twice
    QWidget widget;
*/
    QWidget widget;             //  Program doesn't seem to crash but is it safe ? Does Qt use
    QHBoxLayout layout;         //  delete to operate on already destructed children ?
    QLabel label("label");

    layout.addWidget(&label);   //  layout is label's parent
    widget.setLayout(&layout);  //  widget is layout's parent
    widget.show();
    return app.exec();
}

这在Qt中允许吗?当销毁子对象时,Qt会做什么?
顺便说一下,我考虑使用shared_ptr等智能指针。但我认为Qt也会删除已经被智能指针销毁的对象。
我知道你想使用new为对象分配动态内存。但我不觉得这是可靠的,请告诉我是否存在任何情况(例如异常)依赖于Qt的对象树来处理动态内存会导致内存泄漏。
如果我使用对象而不是指针来动态分配对象,只要它们拥有所有权,我就必须考虑对象销毁的顺序,这很繁琐。 我不知道在Qt中使用动态内存是否是良好的实践。
你有什么建议或更好的解决方案吗?

1
如果 shared_ptr 删除了子对象,那么父对象会被通知,这不是问题。问题在于另一种情况,当父对象删除子对象时,shared_ptr 不会注意到,从而导致双重删除。 - Frank Osterfeld
2个回答

42

经过Qt的多个版本测试,QObject实现了组合模式

该模式要求组合对象拥有子对象的所有权,因此只要进行了父子关系的绑定,您可以确信当父对象被销毁时,子QObjects也将被销毁。

通常做法是在堆内存中创建子对象并立即赋予父级。如果您没有立即指定父级,可以使用setParent()函数明确指定,否则当您使用addWidget()addLayout()将小部件添加到父级小部件时,将自动进行绑定。

QLayout对象是其他QLayoutsQWidgets的大小和布局管理器。它们不拥有它们管理的对象。实际上,父级为QLayout的那个QWidget就是这些对象的父级。

您可以选择在堆内存或栈内存中创建根父级。

如果您更喜欢智能指针,有两个专门用于QObjects的类:QPointerQSharedPointer。每个类都有其优缺点。

#include <QApplication>
#include <QLabel>
#include <QHBoxLayout>
#include <QWidget>

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    QWidget widget;             // Root parent so can create as a auto-deleting object on the stack
    QHBoxLayout *layout = new QHBoxLayout(&widget);         // Create on the heap and parent immediately
    QLabel *label = new QLabel("label", &widget);           // Create on the heap and parent immediately

    layout->addWidget(label);   // widget remains label's parent
    widget.setLayout(layout);   // widget is changed to layout's parent if necessary, as well 
                                // as any widgets that layout manages
    widget.show();
    return app.exec();

    // layout and label are destroyed when widget is destroyed
}

6
概括而言:将根QObject对象分配到堆栈中,其余所有内容都分配到堆中。 - tlonuk
2
@tlonuk 不完全是这样。当您可以通过值持有对象时,堆分配可能是一种过早的悲观优化。 QObject 作为值就可以了;它们不能移动或复制,但在其他方面没有任何特殊之处。 - Kuba hasn't forgotten Monica

23

在RobbiE的回答基础上,QPointerQSharedPointer是两个互补的类,分别用于不同的功能。

QPointer及其注意事项

QPointer是指向QObject的弱指针。当所指向的对象被销毁时,它会将自身重置为零。它不是拥有指针:它永远不会删除对象本身,并且不能保证对象的存在。使用它可以避免出现悬空指针,该指针指向在其他地方管理所有权的对象。在每次使用前,请检查指针是否为空。如果对象在另一个线程中被销毁,则可能会遇到竞争条件:

if (pointer) /* another thread can destruct it here */ pointer->method();
QPointer本身是线程安全的,但由于QPointer提供的API不足,使用它的代码永远无法保证线程安全。

QPointer在主线程中始终可以与小部件对象一起使用,并且与已建立父子关系的小部件对象所拥有的对象一起使用。对象和其用户位于同一线程,因此在指针空检查和指针使用之间,对象不会被另一个线程处置:

QPointer<QLabel> label(...);
if (label) label->setText("I'm alive!");

如果您正在重新进入事件循环,需要小心。假设我们有以下代码:

QPointer<QLabel> label(...);
...
if (label) {
   label->setText(...)
   QFileDialog::getOpenFileName(...);
  // Here the event loop is reentered, and essentially any other code in your
  // application can run, including code that could destruct the widget that
  // you're using. The `deleteLater` calls won't do it, since they defer to
  // the main event loop, but it's not always obvious that nothing else
  // will. The line below can thus dereference a null pointer (IOW: crash). 
  label->setText(...);
}

至少,在您调用主要不相关的代码(例如,发出信号(任何人都可以对其做出任何反应!),返回一个事件循环重新进入的调用,如 exec 等)后,您需要重新检查 QPointer 。这也是为什么阻塞调用不好的原因:您永远不应该使用它们。

QPointer<QWidget> widget(...);
...
if (label) {
  label->setText(...);
  QFileDialog::getOpenFileName(...);
  // Reenters the event loop, the widget may get deleted.
}
// Not re-checking the pointer here would be a bug.
if (label) {
  label->setText(...);
  ...
}

QSharedPointer和QWeakPointer

本节内容仅供参考。在现代代码中,应该使用std::shared_ptrstd::weak_ptr,不需要任何保留。截至2018年,它们已经存在于C++中7年。

QSharedPointer是一个拥有指针。它的工作方式类似于Java和CPython中的变量,或者类似于std::shared_ptr。只要有至少一个QSharedPointer指向一个对象,该对象就会被保留。当最后一个QSharedPointer被销毁时,对象也会被销毁和删除。

QWeakPointerQSharedPointer的表兄弟。它是非拥有型指针。它跟踪由QSharedPointer持有的对象是否仍然存活。当拥有对象的最后一个QSharedPointer消失时,它将重置为nullptr。它可以被看作是对非QObject类的QPointer的一般化。使用QWeakPointer的唯一安全方法是将其转换为QSharedPointer。当您持有共享指针时,对象将被保证保持活动状态。

QPointer类似于QObjectQWeakPointer,但不需要存在QSharedPointer

在堆上分配的对象和由其他机制管理生命周期的对象使用QSharedPointer是错误的。例如,对于具有父项的QObject,拥有QSharedPointer是错误的。对象的父项将删除它,并且您将得到一个悬空的QSharedPointer!Qt具有一些内置检查,当发生这种情况时会发出警告,但此时为时已晚,未定义的行为已经发生。

QScopedPointer

本节内容仅供参考。应该使用std::unique_ptr,没有任何保留。截至2018年,它已经存在于C++中7年。

QScopedPointerstd::unique_ptr一样,是一个独占型指针。它的工作就是在其超出范围时删除所持有的对象。C++11的unique_ptr名称非常适合:它是一个唯一的指针,试图复制这样的指针是错误的。始终只有一个QScopedPointer拥有一个给定的对象,并且它不与其他智能指针类型合作。您可以通过调用data方法获取底层对象的原始指针。

std::auto_ptr

这个指针是为了解决C++98/03中缺乏移动语义而尝试的。由于其破碎的复制语义,使用这个类应该被视为一个错误。使用std::unique_ptrstd::shared_ptr——如果只需要可移动性,则使用前者;如果需要多个副本共存,则使用后者。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接