概述
看多了Qt的代码就会发现,很多人的代码里大量使用new,却很少看到delete,以至于我刚学Qt时也养成了这个习惯,在写C++时也不用delete,结果造成很多麻烦。
原因在于Qt采用半自动的内存管理,不像c++那种全需要自己delete堆内存对象。Qt对象继承自QObject, QObject内部有一个list,会保存children,即QObjectList* children
;还有一个指针保存parent,即QObject *parent
。当此Qt对象执行析构函数时,它会将自己从parent的列表中删除,并且析构掉所有的children。
objParent
有两个子对象objChild
和objChild2
,三者都继承自QObject,那么关系如下图:
半自动化的内存管理规则
QObject及其派生类的对象,如果其parent非0,那么其parent析构时会析构该对象。
QWidget及其派生类的对象,可以设置
Qt::WA_DeleteOnClose
标志位,当close时会调用QWidgetPrivate::close_helper
,进而调用deleteLater
析构该对象。QAbstractAnimation
派生类的对象,可以设置QAbstractAnimation::DeleteWhenStopped
。QRunnable::setAutoDelete()
、MediaSource::setAutoDelete()
。父子关系: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);