当我们在使用Qt时不可避免得需要接触到内存的分配和使用,即使是在使用Python,Golang这种带有自动垃圾回收器(GC)的语言时我们仍然需要对Qt的内存管理机制有所了解,以更加清楚的认识Qt对象的生命周期并在适当的时机加以控制或者避免进入陷阱。

这篇文章里我们将学习QObject & parent对象管理机制,以及QWidget与内存管理这两点Qt的基础知识。

QObject和内存管理

在Qt中,我们可以大致把对象分为两类,一类是QObject和它的派生类;另一类则是普通的C++类。

对于第二种对象,它的生命周期与管理和普通的C++类基本没有区别,而QObject和它的派生类则有以下的显著区别:

  • QObject和其派生类可以使用SIGNAL/SLOT机制
  • 它们一般会有一个parent父对象的指针,用于内存管理(后面重点说明)
  • 对于QWidget和其派生类来说,内存管理要稍微复杂一些,因为QWidget需要和eventloop高度配合才能工作(后面也会重点说明)

signal和slot一般来说并不会对内存管理产生影响,但是对close()槽的处理会对QWidget产生一些影响,所以我们放在后面讲解。

那么先来看一下QObject和parent机制。

QObject的parent

我们时常能看到QWidget或者其他的控件的构造函数中有一项参数parent,默认值都为NULL,例如:

QLineEdit(const QString &contents, QWidget *parent = nullptr);
QWidget(QWidget *parent = nullptr, Qt::WindowFlags f = ...);

这个parent的作用就在于使当前的对象实例加入parent指定的QObject及其派生类的children中,当一个QObject被delete或者调用了它的析构函数时,所有加入的children也会全部被析构。

如果parent设置为NULL,会有如下的情况:

  • 如果是构造时直接指定了NULL,那么当前实例不会有父对象存在,Qt也不能自动析构该实例除非实例超出作用域导致析构函数被调用,或者用户在恰当的实际使用delete操作符或者使用deleteLater方法;
  • 如果已经指定了非NULL的parent,这时将它设置成了NULL,那么当前实例会从父对象的children中删除,不再受到QObject & parent机制的影响;
  • 对于QWidgetparent为NULL时代表其为一个顶层窗口,也可以就是独立于其他widget在系统任务栏单独出现的widget,对于永远都是顶层窗口的widget,例如QDialog,当parent不为NULL时他会显示在父widget中心区域的上层;
  • 如果QWidgetparent为NULL或是其他值,在其加入布局管理器或者QMainWindow设置widget时,会自动将parent设置为相应的父widget,在父控件销毁时这些子控件以及布局管理器对象会一并销毁。

所以我们可以看出,QObject对象实际上拥有一颗类实例关系树,在树中保存了所有通过指定parent注册的子对象,而子对象里又保存有其子对象的关系树,所以当一个父对象被销毁时,所有依赖或间接依赖于它的对象都会被正确的释放,使用者无需手动管理这些资源的释放操作。

基于此原理,我们可以放心的让Qt管理资源,这里有几个建议:

  1. 对于QObject及其派生类,如果彼此之间存在一定联系,则应该尽量指定parent,对于QWidget应该指定parent或者加入布局管理器由管理器自动设置parent。
  2. 对象只需要在局部作用域存在时可以选择不进行内存分配,利用局部作用域变量的生命周期自动清理资源。
  3. 对于非QWidget的对象来说,如果不指定非NULLparent,则需要自己管理对象资源。QWidget比较特殊,我们在下一节讲解。
  4. 对于在局部作用域上创建的父对象及其子对象,要注意对象销毁的顺序,因为父对象销毁时也会销毁子对象,当子对象会在父对象之后被销毁时会引发double free。

QWidget和内存的释放

QWidget也是QObject的子类,所以在parent机制上是没有区别的,然而实际使用时我们更多的是使用“关闭”(close)而不是delete去删除控件,所以差异就出现了。

先提一下widget关闭的流程,首先用户触发close()槽,然后Qt向widget发送QCloseEvent,默认的QCloseEvent会做如下处理:

  1. 将widget隐藏,也就是hide()
  2. 如果有设置Qt::WA_DeleteOnClose,那么会接着调用widget的析构函数

我们可以看到,widget的关闭实际是将其隐藏,而没有释放内存,虽然我们有时会重写closeEvent但也不会手动释放widget。

看一个因为close机制导致的内存泄漏的例子,我们在button被单击后弹出某个自定义对话框:

button.ConnectClicked(func (_ bool) {
dialog := NewMyDialog()
dialog.Exec()
})

因为dialog在close时会被隐藏,而且没有设置DeleteOnClose,所以Qt不会去释放dialog,而用户也无法回收dialog的资源,也行你会说golang的GC不是能处理这种情况吗,然而遗憾的是GC并不能处理cgo分配的资源,所以如果你期望GC做善后的话恐怕要失望了,每次点击按钮后内存用量都会增加一点,没错,内存泄露了。

那么给dialog设置一个parent,像这样,会如何呢?

dialog.SetParent(self)

遗憾的是,并没有什么区别,因为这样只是把dialog加入父控件的children,并没有删除dialog,只有父对象被销毁时内存才会真正释放。

解决办法也有三个。

第一种是使用deleteLater,例如:

dialog.DeleteLater()

这会通知Qt的eventloop在下次进入主循环的时候析构dialog,这样一来确实解决了内存泄露,不过缺点是会有不可预测的延迟存在,有时候延迟是难以接受的。

第二种是手动删除widget,适用于parent为NULL的场合:

C++:

delete dialog;

golang:

dialog.DestroyMyDialog()

说明一下,DestroyType也是qtmoc生产的帮助函数,因为golang没有析构函数的概念,所以goqt使用生成的该帮助函数显示调用底层C++对象的析构函数。

第三种比较简单,对于单纯显示而不需要和父控件做交互的widget,直接设置DeleteOnClose即可,close时widget会被自动析构。

当然对于PyQt5来说并不会存在如上的问题,sip库能很好的与python的GC一起工作。唯一需要注意的是有时底层C++对象已经被释放,但是上层python对象依然存在,这时使用该对象将导致抛错。

总结

Qt提供了一套方便的机制帮助我们进行内存和资源管理,使我们从繁重的劳动中得到了部分的解放,但同时也要注意到那些很容易坑,这样才能写出健壮的正确执行的程序。

如有错误之处,欢迎批评指正。

参考:

http://doc.qt.io/qt-5/qwidget.html

http://doc.qt.io/qt-5/qobject.html

http://doc.qt.io/qt-5/objecttrees.html

https://stackoverflow.com/questions/20164015/is-deletelater-necessary-in-pyqt-pyside

Qt的内存管理机制的更多相关文章

  1. Qt 内存管理机制(转)

      许转载http://devbean.blog.51cto.com/448512/526734 强类型语言在创建对象时总会显式或隐式地包含对象的类型信息.也就是说,强类型语言在分配对象内存空间时,总 ...

  2. Qt 内存管理机制

    这篇文章首先发布于我的主页 http://www.devbean.info,以后也会直接发布在那里.现在有 Flex 4 的一篇和 <从 C++ 到 Objective-C>系列,感谢大家 ...

  3. Qt的内存管理

    在QT的程序中经常会看到只有new而不delete的情况,其实是因为QT有一套回收内存的机制,主要的规则如下: 1.所有继承自QOBJECT类的类,如果在new的时候指定了父亲,那么它的清理时在父亲被 ...

  4. MFC与Qt的内存管理

    最近在做MFC向Qt的移植,在内存管理方面遇到了很头疼的问题,虽然不知道问题到底出在哪,先了解下这两个库的内存管理方式.于是转载两篇关于内存管理的文章. 一. Qt内存管理: 在Qt的程序中经常会看到 ...

  5. 浅谈Linux内存管理机制

    经常遇到一些刚接触Linux的新手会问内存占用怎么那么多?在Linux中经常发现空闲内存很少,似乎所有的内存都被系统占用了,表面感觉是内存不够用了,其实不然.这是Linux内存管理的一个优秀特性,在这 ...

  6. ARC内存管理机制详解

    ARC在OC里面个人感觉又是一个高大上的牛词,在前面Objective-C中的内存管理部分提到了ARC内存管理机制,ARC是Automatic Reference Counting---自动引用计数. ...

  7. 深入了解C#系列:谈谈C#中垃圾回收与内存管理机制

    今天抽空来讨论一下.Net的垃圾回收与内存管理机制,也算是完成上个<WCF分布式开发必备知识>系列后的一次休息吧.以前被别人面试的时候问过我GC工作原理的问题,我现在面试新人的时候偶尔也会 ...

  8. 【Cocos2d-x 3.x】内存管理机制与源码分析

    侯捷先生说过这么一句话 :  源码之前,了无秘密. 要了解Cocos2d-x的内存管理机制,就得阅读源码. 接触Cocos2d-x时, Cocos2d-x的最新版本已经到了3.2的时代,在学习Coco ...

  9. Spark 1.6以后的内存管理机制

     Spark 内部管理机制 Spark的内存管理自从1.6开始改变.老的内存管理实现自自staticMemoryManager类,然而现在它被称之为"legacy". " ...

随机推荐

  1. C. Neko does Maths

    time limit per test1 second memory limit per test256 megabytes inputstandard input outputstandard ou ...

  2. centos7下搭建高匿HTTP代理

    一.一般适用情况1.两台都有外网IP,一台服务器请求资源通过另外一个服务器,本文重点讲第一种.2.两台服务器,其中一台服务器只有内网IP,另外一台服务器有公网和内网IP. 二.前提 # 确认服务器端i ...

  3. DOS命令(一)

    1. echo 输出内容,用来输出文字. [例如:echo hello] 2. titile 标题,用来修改标题. 3. color 背景色前景色,用来设置背景色和前景色 0 = 黑色 8 = 灰色 ...

  4. [转]jenkins2 插件安装

    文章来自:http://www.ciandcd.com 文中的代码来自可以从github下载: https://github.com/ciandcd Jenkins的安装包和插件在7个国家有20多个镜 ...

  5. 依赖注入[6]: .NET Core DI框架[编程体验]

    毫不夸张地说,整个ASP.NET Core框架是建立在一个依赖注入框架之上的,它在应用启动时构建请求处理管道过程中,以及利用该管道处理每个请求过程中使用到的服务对象均来源于DI容器.该DI容器不仅为A ...

  6. WEB安全系列之如何挖掘任意用户登录漏洞

    WEB安全系列之如何挖掘任意用户登录漏洞 0x01  前言        每周两篇文章打卡.坏蛋100块钱都不给我,好坏好坏的. 0x02  什么是任意用户登录漏洞   几乎每个网站都有自己的会员系统 ...

  7. 基本类型数据转换(int,char,byte)

    public class DataUtil { public static void main(String[] args) { int a = 8; int value = charToInt(by ...

  8. 2种不同方式实现背景图里加入文字的简单CSS样式

    如果让你实现下图的样式(图片里面插入文字),你会怎么做呢? 我总结了2种方式 ①:用 img src属性直接引入图片 + 定位 ②:用背景图且不使用定位 第一种: HTML <div class ...

  9. [Swift]LeetCode3. 无重复字符的最长子串 | Longest Substring Without Repeating Characters

    Given a string, find the length of the longest substring without repeating characters. Examples: Giv ...

  10. [Swift]LeetCode561. 数组拆分 I | Array Partition I

    Given an array of 2n integers, your task is to group these integers into n pairs of integer, say (a1 ...