Как Qt удаляет объекты? И как лучше всего хранить QObjects?

Я слышал, что объекты в 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 parent
    widget.setLayout(&layout);  //  widget is layout parent
    widget.show();
    return app.exec();
}

Разрешено ли это в Qt? Что делает Qt при уничтожении ребенка?

Кстати, я рассматривал использование интеллектуальных указателей, таких как shared_ptr. Но я думаю, что Qt также удалит объект, который уже был уничтожен интеллектуальным указателем тоже.

Я знаю, что вы хотели бы использовать new для распределения динамической памяти для объектов. Но я не чувствую его обнадеживающим, скажите, пожалуйста, если есть какие-либо ситуации (например, исключения), которые приведут к утечкам памяти, когда они будут полагаться на дерево объектов Qt для обработки динамической памяти?

Если я использую объекты, а не указатели, для динамического выделения объектов, я должен учитывать порядок уничтожения объектов, если они имеют право собственности, что является утомительным. Я не знаю, хорошо ли использовать динамическую память в Qt.

Есть ли у вас предложения или лучшие решения?

Ответ 1

Реализация Композитного шаблона дизайна QObject была проверена и протестирована через множество версий Qt.

В шаблоне требуется, чтобы составной объект владел дочерними элементами, поэтому, пока родительский процесс был выполнен, вы можете быть уверены, что дочерний элемент QObjects будет уничтожен при уничтожении родителя.

Стандартная практика заключается в создании дочерних объектов в кучевой памяти и немедленной родительской. Если вы не родитель немедленно, вы можете явно родительский, используя функцию setParent(), иначе родительский процесс будет выполняться автоматически при добавлении виджета в родительский виджет, используя addWidget() или addLayout().

QLayout объекты - это менеджеры размера и компоновки других QLayouts и QWidgets. Они не владеют объектами, которыми они управляют. Родитель фактически является QWidget, что QLayout является дочерним.

У вас есть выбор для создания родительского корня в стеке или в памяти кучи.

Если вы чувствуете себя более комфортно с умными указателями, есть два класса, специально предназначенные для QObjects: QPointer и QSharedPointer. У каждого есть свои плюсы и минусы.

#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 parent
    widget.setLayout(layout);   // widget is changed to layout 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
}

Ответ 2

Добавив ответ RobbiE, QPointer и QSharedPointer - это два класса, которые выполняют разные функции.

QPointer и его оговорки

A QPointer является слабым указателем на a QObject. Он сбрасывается до нуля, когда объект, на который указывает объект, уничтожается. Это не владеющий указатель: он никогда не удаляет сам объект и не гарантирует существование объекта. Используйте его, чтобы избежать обвинчивания указателя на объект, владение которым управляется в другом месте. Проверьте, не указана ли указатель перед каждым использованием. Вы столкнетесь с условиями гонки, если объект будет уничтожен в другом потоке:

if (pointer) /* another thread can destruct it here */ pointer->method();

Сам QPointer является потокобезопасным, но используемый код не может быть потокобезопасным из-за недостаточного API, предоставляемого QPointer.

QPointer всегда безопасен для использования из основного потока с объектами виджетов и с объектами, принадлежащими объектам виджетов, где установлены отношения родитель-ребенок. Объекты и их пользователи находятся в одном потоке, поэтому объект не будет удален другим потоком между нулевой проверкой указателя и использованием указателя:

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

Вам нужно быть осторожным, если вы повторно входите в цикл событий. Предположим, что:

QPointer<QTcpSocket> socket(...);
...
if (socket) {
  socket->write(...);
  socket->waitForBytesWritten();
  // Here the event loop is reentered, and essentially any other code in your
  // application can run, including code that could destruct the socket that
  // you're using. The line below can thus dereference a null pointer
  // (IOW: crash). Even worse, in such a case after the event loop returns
  // back to `waitForBytesWritten`, `this` is a dangling pointer. So, before
  // the line below crashes, something in `waitForBytesWritten` may do silly
  // things, like formatting your hard drive.
  socket->write(...);
}

По крайней мере, вам нужно повторно проверить QPointer каждый раз после того, как вы вернетесь от блокировки, повторного вызова цикла-события, такого как waitForXxx или exec. Именно поэтому блокирование вызовов является злым: вы никогда не должны их использовать.

QPointer<QTcpSocket> socket(...);
...
if (socket) {
  socket->write(...);
  socket->waitForBytesWritten();
  // Reenters the event loop, the socket may get deleted.
}
// Not re-checking the pointer here would be a bug.
if (socket) {
  socket->write(...);
  ...
}

QSharedPointer и QWeakPointer

A QSharedPointer является владеющим указателем. Он работает как переменные в Java и Python, или как std::shared_ptr. Пока существует хотя бы один QSharedPointer, указывающий на объект, объект хранится вокруг. Когда уничтожается последний QSharedPointer, объект разрушается и удаляется.

QWeakPointer - кузена QSharedPointer. Он не владеет. Он отслеживает, остаются ли объекты, хранящиеся в QSharedPointer. Он сбрасывается до nullptr, когда последний QSharedPointer, который владеет объектом, уходит. Его можно рассматривать как обобщение классов QPointer для не QObject. Единственный безопасный способ использования QWeakPointer - преобразовать его в QSharedPointer. Когда вы держите общий указатель, объект будет оставаться в живых.

A QPointer как a QWeakPointer для QObject s, но для него не требуется существование a QSharedPointer.

Ошибка использования QSharedPointer для объекта, который не выделен в куче, а также для объекта, время жизни которого управляется другими механизмами. Например, это ошибка, чтобы иметь QSharedPointer в QObject, у которого есть родитель. Родитель объекта удалит его, и в итоге вы получите оборванную QSharedPointer!

QScopedPointer

QScopedPointer, как и std::unique_ptr, является единственным владеющим указателем. Его задача - удалить удерживаемый объект, когда он выходит за рамки. Имя С++ 11 unique_ptr очень подходит: это уникальный указатель, в том смысле, что это ошибка, чтобы попытаться скопировать такие указатели. Всегда существует только один QScopedPointer, которому принадлежит данный объект, и он не взаимодействует с другими типами указателей. Вы можете получить указатель на базовый объект, вызвав метод data.

станд:: auto_ptr

Из-за своей сломанной семантики копирования использование этого класса должно рассматриваться как ошибка. Используйте std::unique_ptr или QScopedPointer.