Qt内存管理(一) 半自动内存管理机制

概述

看多了Qt的代码就会发现,很多人的代码里大量使用new,却很少看到delete,以至于我刚学Qt时也养成了这个习惯,在写C++时也不用delete,结果造成很多麻烦。

原因在于Qt采用半自动的内存管理,不像c++那种全需要自己delete堆内存对象。Qt对象继承自QObject, QObject内部有一个list,会保存children,即QObjectList* children;还有一个指针保存parent,即QObject *parent。当此Qt对象执行析构函数时,它会将自己从parent的列表中删除,并且析构掉所有的children。

objParent有两个子对象objChildobjChild2,三者都继承自QObject,那么关系如下图:

半自动化的内存管理规则

  1. QObject及其派生类的对象,如果其parent非0,那么其parent析构时会析构该对象。

  2. QWidget及其派生类的对象,可以设置Qt::WA_DeleteOnClose标志位,当close时会调用QWidgetPrivate::close_helper,进而调用deleteLater析构该对象。

  3. QAbstractAnimation派生类的对象,可以设置QAbstractAnimation::DeleteWhenStopped

  4. QRunnable::setAutoDelete()MediaSource::setAutoDelete()

  5. 父子关系:Qt特有的性质,与类的继承关系不同,注意二者要在同一线程。

使用如下:

QObject *p = new QObject();
MySon1 *ps1=new MySon1(p);
MySon2* ps2 = new MySon2();
delete ps2;

类MySon2只有一个无参的构造函数,这种情况下ps2不会自动析构,需要调用delete或者deleteLater

源码分析

构造函数

QObject构造函数部分代码:

if (parent) {
    QT_TRY {
        if (!check_parent_thread(parent, parent ? parent->d_func()->threadData : 0, d->threadData)  )
            parent = 0;
        if (d->isWidget) {
            if (parent) {
                d->parent = parent;
                d->parent->d_func()->children.append(this);
            }
            // no events sent here, this is done at the end of the QWidget constructor
        } else {
            setParent(parent);
        }
    }
    ......

如果指定了parent,那么先判断parent和当前对象是否在同一线程,如果不在就令parent=0,如果当前对象是widget且存在parent(在同一线程),那么赋给parent指针,而且后者添加本对象到子对象的QList,在QWidget构造函数最后发送事件QEvent::ChildAdded
如果当前对象不是widget就调用setParnet,这个函数的源码太长,大致还是对两个指针做了类似的操作,最后同步向事件循环发送了QEvent::ChildAdded事件。

析构函数

QObject析构函数大致如下:

QObject::~QObject()
{
    Q_D(QObject);
    d->wasDeleted = true;
    d->blockSig = 0; // unblock signals so we always emit destroyed()
    ......
    if (!d->isWidget && d->isSignalConnected(0)) {
        emit destroyed(this);   // 发射destroyed信号,且无法blocked,一般情况下没有槽来响应
    }
    ......
    if (d->connectionLists || d->senders) {
        ......
        /* disconnect all receivers  Disconnect all senders */
    }
    // 析构掉所有的children
    if (!d->children.isEmpty())
        d->deleteChildren();

#if QT_VERSION < 0x60000
    qt_removeObject(this);
#endif

    if (Q_UNLIKELY(qtHookData[QHooks::RemoveQObject]))
        reinterpret_cast<QHooks::RemoveQObjectCallback>(qtHookData[QHooks::RemoveQObject])(this);
    if (d->parent)        // remove it from parent object,将自己从parent的列表中删除
        d->setParent_helper(0);
}

其中deleteChildren代码如下:

void QObjectPrivate::deleteChildren()
{
    Q_ASSERT_X(!isDeletingChildren, "QObjectPrivate::deleteChildren()", "isDeletingChildren already set, did this function recurse?");
    isDeletingChildren = true;
    // delete children objects, don't use qDeleteAll as the destructor of the child might delete siblings
    for (int i = 0; i < children.count(); ++i) {
        currentChildBeingDeleted = children.at(i);
        children[i] = 0;
        delete currentChildBeingDeleted;    //删除子对象
    }
    children.clear();
    currentChildBeingDeleted = 0;
    isDeletingChildren = false;
}

函数清除所有子类指针,当然每个子类指针清除时又会清除它的所有子类,因此Qt中new出来的指针很少有显示对应的delete,因为只要最上面的指针被框架删除了,以前版本的源码用的是qDeleteAll,而且直接放在QObject析构函数里。

setParent_helper实际执行的代码如下:

QObjectPrivate::setParent_helper(QObject *o)
{
    parentD->children.removeAt(index);
    if (sendChildEvents && parentD->receiveChildEvents) {
        QChildEvent e(QEvent::ChildRemoved, q);
        QCoreApplication::sendEvent(parent, &e);
    }
}

向事件循环发送了事件QEvent::ChildRemoved,再看事件循环部分的源码:

bool QObject::event(QEvent *e)
{
    switch (e->type()) {
    ......
    case QEvent::ChildRemoved:
        childEvent((QChildEvent*)e);
        break;
    }
}
//虚函数
void QObject::childEvent(QChildEvent * /* event */)
{
}

Qt内存管理机制的缺陷

Qt半自动内存机制的缺陷在于parent不区分它的child是分配在stack上还是heap上,结果会出现parent析构stack上的子对象。因此,Qt对象析构的核心原则是保证子对象先析构,这样它会把自己从父对象列表中删除,二者取消关联,那么父对象析构时就不会再次析构子对象了。

看这样的代码:

    QApplication app(argc, argv);
    QLabel label("Hello Qt!");
    QWidget w;
    label.setParent(&w);
    w.show();
    return app.exec();

C++规定,本地对象的析构函数的调用顺序与他们的构造顺序相反。所以是QWidget对象先析构,也就是父对象先析构,它会删除子对象label,但label却不是通过new分配在heap中,而是在stack中,当然会出问题。运行之后正常,但关闭窗口会报错:

改进方法一:只要把QLabel和QWidget的构造语句交换一下就可以了,这也是一般人的代码习惯。

改进方法二:让QLabel对象创建在heap上:

    QLabel *label = new QLabel("Hello Qt!");
    QWidget w;
    label->setParent(&w);

猜你喜欢

转载自blog.csdn.net/yao5hed/article/details/81092128