一、简介

Qt内存管理机制:Qt 在内部能够维护对象的层次结构。对于可视元素,这种层次结构就是子组件与父组件的关系;对于非可视元素,则是一个对象与另一个对象的从属关系。在 Qt 中,在 Qt 中,删除父对象会将其子对象一起删除。

C++中delete 和 new 必须配对使用(一 一对应):delete少了,则内存泄露,多了麻烦更大。Qt中使用了new却很少delete,因为QObject的类及其继承的类,设置了parent(也可在构造时使用setParent函数或parent的addChild)故parent被delete时,这个parent的相关所有child都会自动delete,不用用户手动处理。但parent是不区分它的child是new出来的还是在栈上分配的。这体现delete的强大,可以释放掉任何的对象,而delete栈上对象就会导致内存出错,这需要了解Qt的半自动的内存管理。另一个问题:child不知道它自己是否被delete掉了,故可能会出现野指针。那就要了解Qt的智能指针QPointer。

二、关联图

(1)Linux内存图,主要了解堆栈上分配内存的不同方式。

(2)在Qt中,最基础和核心的类是:QObject,QObject内部有一个list,会保存children,还有一个指针保存parent,当自己析构时,会自己从parent列表中删除并且析构所有的children。

三、详解

1、Qt的半自动化的内存管理

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

(2)QWidget及其派生类的对象,可以设置 Qt::WA_DeleteOnClose 标志位(当close时会析构该对象)。

(3)QAbstractAnimation派生类的对象,可以设置 QAbstractAnimation::DeleteWhenStopped。

(4)QRunnable::setAutoDelete()、MediaSource::setAutoDelete()。

(5)父子关系:父对象、子对象、父子关系。这是Qt中所特有的,与类的继承关系无关,传递参数是与parent有关(基类、派生类,或父类、子类,这是对于派生体系来说的,与parent无关)。

2、内存问题例子

  • 例子一

  1. #include <QApplication>
  2. #include <QLabel>
  3. int main(int argc, char *argv[])
  4. {
  5. QApplication a(argc, argv);
  6. QLabel *label = new QLabel("Hello Qt!");
  7. label->show();
  8. return a.exec();
  9. }

分析:(1)label 既没有指定parent,也没有对其调用delete,所以会造成内存泄漏。书中的这种小例子也会出现指针内存的问题。
改进方式:(1)分配对象到栈上而不是堆上

  1. #include <QApplication>
  2. #include <QLabel>
  3. int main(int argc, char *argv[])
  4. {
  5. QApplication a(argc, argv);
  6. QLabel label("Hello Qt!");
  7. label.show();
  8. return a.exec();
  9. }

(2)设置标志位,close()后会delete label。

  1. label->setAttribute(Qt::WA_DeleteOnClose);

(3)new后手动delete

  1. #include <QApplication>
  2. #include <QLabel>
  3. int main(int argc, char *argv[])
  4. {
  5. int ret = 0;
  6. QApplication a(argc, argv);
  7. QLabel *label = new QLabel("Hello Qt!");
  8. label->show();
  9. ret = a.exec();
  10. delete label;
  11. return ret;
  12. }
  • 例子二

  1. #include <QApplication>
  2. #include <QLabel>
  3. int main(int argc, char *argv[])
  4. {
  5. QApplication app(argc, argv);
  6. QLabel label("Hello Qt!");
  7. label.show();
  8. label.setAttribute(Qt::WA_DeleteOnClose);
  9. return app.exec();
  10. }

运行:

分析:程序崩溃,因为label被close时,delete &label;但label对象是在栈上分配的内存空间,delete栈上的地址会出错。

有些朋友理解为label被delete两次而错误,可以测试QLabel label("Hello Qt!"); label.show();delete &label;第一次delete就会出错。

  • 例子三

  1. #include <QApplication>
  2. #include <QLabel>
  3. int main(int argc, char* argv[])
  4. {
  5. QApplication app(argc, argv);
  6. QLabel label("Hello Qt!");
  7. QWidget w;
  8. label.setParent(&w);
  9. w.show();
  10. return app.exec();
  11. }

分析:Object内部有一个list,会保存children,还有一个指针保存parent,当自己析构时,会自己从parent列表中删除并且析构所有的children。

w比label先被析构,当w被析构时,会删除chilren列表中的对象label,但label是分配到栈上的,因delete栈上的对象而出错。

改进方式:(1)调整一下顺序,确保label先于其parent被析构,label析构时将自己从父对象的列表中移除自己,w析构时,children列表中就不会有分配在stack中的对象了。

  1. #include <QApplication>
  2. #include <QLabel>
  3. int main(int argc, char* argv[])
  4. {
  5. QApplication app(argc, argv);
  6. QWidget w;
  7. QLabel label("Hello Qt!");
  8. label.setParent(&w);
  9. w.show();
  10. return app.exec();
  11. }

(2)将label分配到堆上

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

或者QLabel *label = new QLabel("Hello Qt!",this);

  • 例子四:野指针

  1. #include <QApplication>
  2. #include <QLabel>
  3. int main(int argc, char* argv[])
  4. {
  5. QApplication app(argc, argv);
  6. QWidget *w = new QWidget;
  7. QLabel *label = new QLabel("Hello Qt!");
  8. label->setParent(w);
  9. w->show();
  10. delete w;
  11. label->setText("go");     //野指针
  12. return app.exec();
  13. }

(上述程序不显示Label,仅作测试)

分析:程序异常结束,delete w时会delete label,label成为野指针,调用label->setText("go");出错。

改进方式:QPointer智能指针

  1. #include <QApplication>
  2. #include <QLabel>
  3. #include <QPointer>
  4. int main(int argc, char* argv[])
  5. {
  6. QApplication app(argc, argv);
  7. QWidget *w = new QWidget;
  8. QLabel *label = new QLabel("Hello Qt!");
  9. label->setParent(w);
  10. QPointer<QLabel> p = label;
  11. w->show();
  12. delete w;
  13. if (!p.isNull()) {
  14. label->setText("go");
  15. }
  16. return app.exec();
  17. }
  • 例子五:deleteLater

当一个QObject正在接受事件队列时如果中途被你销毁掉了,就是出现问题了,所以QT中建大家不要直接Delete掉一个QObject,如果一定要这样做,要使用QObject的deleteLater()函数,它会让所有事件都发送完一切处理好后马上清除这片内存,而且就算调用多次的deletelater也不会有问题。

发送一个删除事件到事件系统:

  1. void QObject::deleteLater()
  2. {
  3. QCoreApplication::postEvent(this, new QEvent(QEvent::DeferredDelete));
  4. }

3、智能指针

如果没有智能指针,程序员必须保证new对象能在正确的时机delete,四处编写异常捕获代码以释放资源,而智能指针则可以在退出作用域时(不管是正常流程离开或是因异常离开)总调用delete来析构在堆上动态分配的对象。

Qt家族的智能指针:

智能指针

 

引入

QPointer

Qt Object 模型的特性(之一)
注意:析构时不会delete它管理的资源

 

QSharedPointer

带引用计数

Qt4.5

QWeakPointer

 

Qt4.5

QScopedPointer

 

Qt4.6

QScopedArrayPointer

QScopedPointer的派生类

Qt4.6

QSharedDataPointer

用来实现Qt的隐式共享(Implicit Sharing)

Qt4.0

QExplicitlySharedDataPointer

显式共享

Qt4.4

     

std::auto_ptr

   

std::shared_ptr

std::tr1::shared_ptr

C++0x

std::weak_ptr

std::tr1::weak_ptr

C++0x

std::unique_ptr

boost::scoped_ptr

C++0x

(1)QPointer

QPointer是一个模板类。它很类似一个普通的指针,不同之处在于,QPointer 可以监视动态分配空间的对象,并且在对象被 delete 的时候及时更新。

QPointer的现实原理:在QPointer保存了一个QObject的指针,并把这个指针的指针(双指针)交给全局变量管理,而QObject 在销毁时(析构函数,QWidget是通过自己的析构函数的,而不是依赖QObject的)会调用QObjectPrivate::clearGuards 函数来把全局 GuardHash 的那个双指针置为*零,因为是双指针的问题,所以QPointer中指针当然也为零了。用isNull 判断就为空了。

  1. // QPointer 表现类似普通指针
  2. QDate *mydate = new QDate(QDate::currentDate());
  3. QPointer mypointer = mydata;
  4. mydate->year();    // -> 2005
  5. mypointer->year(); // -> 2005
  6. // 当对象 delete 之后,QPointer 会有不同的表现
  7. delete mydate;
  8. if(mydate == NULL)
  9. printf("clean pointer");
  10. else
  11. printf("dangling pointer");
  12. // 输出 dangling pointer
  13. if(mypointer.isNull())
  14. printf("clean pointer");
  15. else
  16. printf("dangling pointer");
  17. // 输出 clean pointer

(2)std::auto_ptr

  1. // QPointer 表现类似普通指针
  2. QDate *mydate = new QDate(QDate::currentDate());
  3. QPointer mypointer = mydata;
  4. mydate->year();    // -> 2005
  5. mypointer->year(); // -> 2005
  6. // 当对象 delete 之后,QPointer 会有不同的表现
  7. delete mydate;
  8. if(mydate == NULL)
  9. printf("clean pointer");
  10. else
  11. printf("dangling pointer");
  12. // 输出 dangling pointer
  13. if(mypointer.isNull())
  14. printf("clean pointer");
  15. else
  16. printf("dangling pointer");
  17. // 输出 clean pointe

auto_ptr被销毁时会自动删除它指向的对象。

std::auto_ptr<QLabel> label(new QLabel("Hello Dbzhang800!"));

(3)其他的类参考相应文档。

4、自动垃圾回收机制

(1)QObjectCleanupHandler

Qt 对象清理器是实现自动垃圾回收的很重要的一部分。QObjectCleanupHandler可以注册很多子对象,并在自己删除的时候自动删除所有子对象。同时,它也可以识别出是否有子对象被删 除,从而将其从它的子对象列表中删除。这个类可以用于不在同一层次中的类的清理操作,例如,当按钮按下时需要关闭很多窗口,由于窗口的 parent 属性不可能设置为别的窗口的 button,此时使用这个类就会相当方便。

  1. #include <QApplication>
  2. #include <QObjectCleanupHandler>
  3. #include <QPushButton>
  4. int main(int argc, char* argv[])
  5. {
  6. QApplication app(argc, argv);
  7. // 创建实例
  8. QObjectCleanupHandler *cleaner = new QObjectCleanupHandler;
  9. // 创建窗口
  10. QPushButton *w = new QPushButton("Remove Me");
  11. w->show();
  12. // 注册第一个按钮
  13. cleaner->add(w);
  14. // 如果第一个按钮点击之后,删除自身
  15. QObject::connect(w, SIGNAL(clicked()), w, SLOT(deleteLater()));
  16. // 创建第二个按钮,注意,这个按钮没有任何动作
  17. w = new QPushButton("Nothing");
  18. cleaner->add(w);
  19. w->show();
  20. // 创建第三个按钮,删除所有
  21. w = new QPushButton("Remove All");
  22. cleaner->add(w);
  23. QObject::connect(w, SIGNAL(clicked()), cleaner, SLOT(deleteLater()));
  24. w->show();
  25. return app.exec();
  26. }

在上面的代码中,创建了三个仅有一个按钮的窗口。第一个按钮点击后,会删除掉自己(通过 deleteLater() 槽),此时,cleaner 会自动将其从自己的列表中清除。第三个按钮点击后会删除 cleaner,这样做会同时删除掉所有未关闭的窗口。

(2)引用计数
  应用计数是最简单的垃圾回收实现:每创建一个对象,计数器加 1,每删除一个则减 1。

  1. class CountedObject : public QObject
  2. {
  3. Q_OBJECT
  4. public:
  5. CountedObject()
  6. {
  7. ctr=0;
  8. }
  9. void attach(QObject *obj)
  10. {
  11. ctr++;
  12. connect(obj, SIGNAL(destroyed(QObject*)), this, SLOT(detach()));
  13. }
  14. public slots:
  15. void detach()
  16. {
  17. ctr--;
  18. if(ctr <= 0)
  19. delete this;
  20. }
  21. private:
  22. int ctr;
  23. };

利用Qt的信号槽机制,在对象销毁的时候自动减少计数器的值。但是,我们的实现并不能防止对象创建的时候调用了两次attach()。

(3)记录所有者

更合适的实现是,不仅仅记住有几个对象持有引用,而且要记住是哪些对象。例如:

  1. class CountedObject : public QObject
  2. {
  3. public:
  4. CountedObject() {}
  5. void attach(QObject *obj) {
  6. // 检查所有者
  7. if(obj == 0)
  8. return;
  9. // 检查是否已经添加过
  10. if(owners.contains(obj))
  11. return;
  12. // 注册
  13. owners.append(obj);
  14. connect(obj, SIGNAL(destroyed(QObject*)), this, SLOT(detach(QObject*)));
  15. }
  16. public slots:
  17. void detach(QObject *obj) {
  18. // 删除
  19. owners.removeAll(obj);
  20. // 如果最后一个对象也被 delete,删除自身
  21. if(owners.size() == 0)
  22. delete this;
  23. }
  24. private:
  25. QList owners;
  26. ;

现在我们的实现已经可以做到防止一个对象多次调用 attach() 和 detach() 了。然而,还有一个问题是,我们不能保证对象一定会调用 attach() 函数进行注册。毕竟,这不是 C++ 内置机制。有一个解决方案是,重定义 new 运算符(这一实现同样很复杂,不过可以避免出现有对象不调用 attach() 注册的情况)。

四、总结

Qt 简化了我们对内存的管理,但是,由于它会在不太注意的地方调用 delete,所以,使用时还是要当心。

五、参考

http://blog.csdn.net/taiyang1987912/article/details/29271549#comments

Qt浅谈之一:内存泄露(总结),对于QWidget可以setAttribute(Qt::WA_DeleteOnClose),而且绝对不能手动删除栈上的对象的更多相关文章

  1. 浅谈 JS 内存泄露方式与避免方法(二)

    Concept WHAT : 内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束.正常情况下,垃圾回收器在DOM元素和event处理器不被引用或访问的时候回收它们.但是,IE的早些 ...

  2. 浅谈Java内存泄露

    一.引言 先等等吧……累了

  3. Qt浅谈内存泄露(总结)

    Qt浅谈内存泄露(总结) 来源 http://blog.csdn.net/taiyang1987912/article/details/29271549 一.简介 Qt内存管理机制:Qt 在内部能够维 ...

  4. Qt浅谈之一:内存泄露(总结)

    一.简介       Qt内存管理机制:Qt 在内部能够维护对象的层次结构.对于可视元素,这种层次结构就是子组件与父组件的关系:对于非可视元素,则是一个对象与另一个对象的从属关系.在 Qt 中,在 Q ...

  5. Qt浅谈之总结(整理)

    Qt浅谈之总结(整理) 来源 http://blog.csdn.net/taiyang1987912/article/details/32713781 一.简介 QT的一些知识点总结,方便以后查阅. ...

  6. 浅谈C++ 内存泄漏及其检测工具

    浅谈C++ 内存泄漏及其检测工具 http://wenku.baidu.com/link?url=1DGkOOvd_ITZyB8IHAwfhCOx2tfO6id8UfuyQkAMHZU6sasaAXz ...

  7. Qt应用中检测内存泄露——VLD

    本文简要描述一下在Qt应用中使用VLD来检测内存泄露.本次测试环境:QtCreator2.3 + Qt4.7.4-vs2008 + VS2008 Express. 1.下载并安装:VLD-2.2: h ...

  8. 浅谈OC内存管理

    一.基本原理 (一)为什么要进行内存管理. 由于移动设备的内存极其有限,所以每个APP所占的内存也是有限制的,当app所占用的内存较多时,系统就会发出内存警告,这时需要回收一些不需要再继续使用的内存空 ...

  9. 浅谈iOS内存管理机制

    iOS内存管理机制的原理是引用计数,引用计数简单来说就是统计一块内存的所有权,当这块内存被创建出来的时候,它的引用计数从0增加到1,表示有一个对象或指针持有这块内存,拥有这块内存的所有权,如果这时候有 ...

随机推荐

  1. 配置Lotus Domino集群视频详解

    IBM Lotus Domino Server 有个最重要的高可用特新就是集群,它对于任何使用 Domino 服务器的组织,让用户能够持续访问它们的数据库,保证业务不间断运行.下面通过两个视频来学习如 ...

  2. Codefroces Educational Round 27 845G Shortest Path Problem?

    Shortest Path Problem? You are given an undirected graph with weighted edges. The length of some pat ...

  3. Activemq开启JMX及问题排查

    0.服务端开启JMX服务 一.修改${ ActiveMQ_HOME} /conf/activemq.xml文件 在<broker>标签中添加如下属性:useJMX="true&q ...

  4. 今日题解------codeforce 893d

    题意:给你一个数列,小于零表示表示信用卡里取出钱,大于零表示信用卡里存钱,等于零表示要查询信用卡, 如果被查到信用卡里的钱小于零,那你就GG,或者在任何时候你的信用卡里的钱大于d的话(不需要找ai等于 ...

  5. jQuery过滤选择器具体解释

    基本过滤选择器 选取第一个元素(:first) //选择第一个div元素. $('#btn1').click(function(){ $('div:first').css("backgrou ...

  6. 35.Intellij IDEA设置忽略部分类编译错误

    转自:https://www.aliyun.com/jiaocheng/290360.html 有些时候我们的项目中有些错误,但这些错误并不影响项目的整体运行(或许是没有使用到),默认情况下idea是 ...

  7. 1.4 Ecosystem官网剖析(博主推荐)

    不多说,直接上干货! 一切来源于官网 http://kafka.apache.org/documentation/ 1.4 Ecosystem 生态系统 There are a plethora of ...

  8. 3、Task.Factory属性

    3.Task.Factory属性 Task类提供了一个Factory静态属性,这个属性返回一个TaskFactory对象. Task task = Task.Factory.StartNew(Task ...

  9. go reflect 调用方法

    package main import ( "fmt" "reflect" ) type A struct { } func (A) Test() { fmt. ...

  10. scrapy爬取知乎某个问题下的所有图片

    前言: 1.仅仅是想下载图片,别人上传的图片也是没有版权的,下载来可以自己欣赏做手机背景但不商用 2.由于爬虫周期的问题,这个代码写于2019.02.13 1.关于知乎爬虫 网上能访问到的理论上都能爬 ...