【精华】详解Qt中的内存管理机制

前言

内存管理,是对软件中内存资源的分配与释放进行有效管理的方法和理论。

众所周知,内存管理是软件开发的一个重要的内容。软件规模越大,内存管理可能出现的问题越多。如果像C语言一样手动地管理内存,一会给开发人员带来巨大的负担,二是手动管理内存的可靠性较差。

Qt为软件开发人员提供了一套内存管理机制,用以替代手动内存管理。

下面开始逐条讲述Qt中的内存管理机制。

一脉相承的栈与堆的内存管理

了解C语言的同学都知道,C语言中的内存分配有两种形式:栈内存、堆内存。

栈内存

栈内存的管理是由编译器来做的,栈上申请的内存变量,生存期由所在作用域决定,超出作用域的栈内存变量会被编译器自动释放。

值得一提的是,作用域的显著标志是一对大括号,大括号内部即为作用域内部,大括号外部即为作用域外部。

参考下列代码:

int main()
{int a = 0;return 1;
}

变量a在栈内存上,main函数返回时,作用域结束,a的内存自动被释放。

从以上描述也可以看出,栈内存的使用是在编译器严密监管之下进行的,遵循严格的作用域规则,所以栈内存的大小、申请时机、释放时机都能在编译的时候确定。

堆内存

堆内存是另外一种管理方式。堆内存最大的特点是可以动态分配,即在运行时可以根据需要进行申请。当然随之而来的弊端也显而易见:需要开发人员对堆内存的释放进行严格管理,稍有疏漏会导致内存泄漏,甚至软件崩溃等问题。

参考下列代码:

int main()
{// 申请堆内存int *intArray = (int *)malloc(100);// 使用堆内存...// 释放堆内存free(intArray);return 1;   
}

如上述代码,堆内存分配的写法区别于栈内存。C语言中,堆内存使用malloc分配,使用free释放。C++中可以使用new分配,使用delete释放。

至此,我们介绍了C语言中的内存管理方式。我们知道Qt是C++的框架,C++是对C语言的扩展,所以C语言中的内存管理方式(堆、栈)和动态内存管理(堆内存释放问题)存在的问题,在C++中仍然存在。所以Qt中自然而然也有相同的问题。说起来可能有点乱,下面用一张图来说明它们的关系:

请添加图片描述

那么,Qt是如何为我们解决动态内存管理问题的呢?下面开始正式讲解。

使用对象父子关系进行内存管理

使用对象父子关系进行内存管理的原理,简述为:

在创建类的对象时,为对象指定父对象指针。当父对象在某一时刻被销毁释放时,父对象会先遍历其所有的子对象,并逐个将子对象销毁释放。

为了直观理解上述过程,以如下代码为例进行说明:

#include <QApplication>
#include <QLabel>int main(int argc, char *argv[])
{QApplication a(argc, argv);// 创建主窗口QWidget mainWidget;mainWidget.resize(400, 300);// 创建文字标签QLabel *label = new QLabel("Hello World!", &mainWidget);// 显示主窗口mainWidget.show();return a.exec();
}

运行结果如下:

请添加图片描述

上述代码中,mainWidget为主窗口对象,类型为QWidgetlabel为子窗口对象,类型为QLabel *。

注意代码第13行,在创建label文本标签窗口对象时,new QLabel的第二个参数即为父对象地址(参考Qt Assistant中QLabel的说明文档),这里给的值是主窗口的地址。

main函数退出时,mainWidget超出main函数作用域会析构,析构时会自动删除label窗口对象,所以这里,我们不需要再写一行:delete label; 来释放label的内存,很方便而且又能节省时间精力。

使用引用计数对内存进行管理

引用计数

引用计数可以说是软件开发人员必知必会的知识点,它在内存管理领域的地位是数一数二的。

引用计数的原理,还是力所能及地用最简单的话来描述:

引用计数需要从三个方面来全面理解:

  1. 使用场景:一个资源,多处使用(使用即引用)。

  2. 问题:到底谁来释放资源。

  3. 原理:使用一个整形变量来统计,此资源在多少个地方被使用,此变量称为引用计数。当某处使用完资源以后,将引用计数减1。当引用计数为0时,即没有任何地方再使用此资源时,真正释放此资源。这里的资源,在动态内存管理中就是指堆内存。

用一句话描述就是:谁最后使用资源,谁负责释放资源

我们很容易联想到现实中的例子,就是日常生活中的刷碗问题的解决方案,即谁最后吃完谁刷碗。

需要说明的是,引用计数不仅仅是在内存管理中使用,它是一个通用的机制,凡是涉及到资源管理的问题,都可以考虑使用引用计数。

下面将要介绍基于引用计数原理的两种衍生的机制:显式共享和隐式共享。

显式共享

显式共享,是仅仅使用引用计数控制资源的生命周期的一种共享管理机制。这种机制下,无论资源在何处被引用,自始至终所有引用指向资源都是同一个。

之所以叫显式共享,是因为这种共享方式很直接,没有隐含的操作,如:Copy on Write写时拷贝(见隐式共享的相关说明)。如果想要拷贝并建立新的引用计数,必须手动调用detach()函数。

从使用者的角度看,从头到尾资源只有一份,一个地方修改了,另一个地方就能读取到修改后的资源。

**相关Qt类:**QExplicitlySharedDataPointer,更加深入的用法和编码,需要参考Qt文档中的相关说明及Demo。

隐式共享

隐式共享,也是一种基于引用计数的控制资源的生命周期的共享管理机制。

隐式共享,对不同的操作有不同的处理:

  • 读取时,在所有引用的地方使用同一个资源;

  • 在写入、修改时自动复制一份资源出来做修改,自动脱离原始的引用计数,因为是新的资源,所以要建立新的引用计数。这种操作叫Copy on Write写时复制技术,是自动隐含进行的。

从使用者的角度看,每个使用者都像是拥有独立的一份资源。在一个地方修改,修改的只是原始资源的拷贝,不会影响原始资源的内容,自然就不会影响到其他使用者。所以这种共享方式称为隐式共享。

相关Qt类有QString、QByteArray、QImage、QList、QMap、QHash等。

推荐阅读:Qt文档中的Implicit Sharing专题。

智能指针

智能指针是对C/C++指针的扩展,同样基于引用计数。

智能指针和显示共享和隐式共享有何区别?它们区别是:智能指针是轻量级的引用计数,它将显式共享、隐式共享中的引用计数实现部分单独提取了出来,制作成模板类,形成了多种特性各异的指针。

例如,QString除了实现引用计数,还实现了字符串相关的丰富的操作接口。QList也实现了引用计数,还实现了列表这种数据结构的各种操作。可以说,显式共享和隐式共享一般是封装在功能类中的,不需要开发者来管理。

智能指针将引用计数功能剥离出来,为Qt开发者提供了便捷的引用计数基础设施。

强(智能)指针

Qt中的强指针实现类是:QSharedPointer,此类是模板类,可以指向多种类型的数据,主要用来管理堆内存。关于QSharedPointer在Qt Assistant中有详细描述。

它的原理和显式共享一样:最后使用的地方负责释放删除资源,如类对象、内存块。

强指针中的“强”,是指每多一个使用者,引用计数都会老老实实地**+1**。而弱指针就不同,下面就接着讲解弱指针。

弱(智能)指针

Qt中的弱指针实现类是QWeakPointer,此类亦为模板类,可以指向多种类型的数据,同样主要用来管理堆内存。关于QWeakPointer在Qt Assistant中有详细描述。

弱指针只能从强指针QSharedPointer转化而来,获取弱指针,不增加引用计数,它只是一个强指针的观察者,观察而不干预。只要强指针存在,弱指针也可以转换成强指针。可见弱指针和强指针是一对形影不离的组合,通常结合起来使用。

局部指针

局部指针,是一种超出作用域自动删除、释放堆内存、对象的工具。它结合了栈内存管理和堆内存管理的优点。

Qt中的实现类有:QScopedPointer,QScopedArrayPointer,具体可以参考Qt Assistant。

观察者指针

上面说弱指针的时候,讲到过观察者。观察者是指仅仅做查询作用的指针,不会影响到引用计数。

Qt中的观察者指针是QPointer,它必须指向QObject的子类对象,才能对对象生命周期进行观察。因为只有QObject子类才会在析构的时候通知QPointer已失效。

QPointer是防止悬挂指针(即野指针)的有效手段,因为所指对象一旦被删除,QPointer会自动置空,在使用时,判断指针是否为空即可,不为空说明对象可以使用,不会产生内存访问错误的问题。

总结

本篇文章讲解了Qt中的各种内存管理机制,算是做了一个比较全面的描述。

之所以说是必读,是因为笔者在工作中发现,内存管理确实非常重要。Qt内存管理机制是贯穿整个Qt中所有类的核心线索之一,搞懂了内存管理

  • 能在脑海中形成内存中对象的布局图,写代码的时候才能下笔如有神,管理起项目中众多的对象才能游刃有余,提高开发效率;
  • 能够减少bug的产生。有经验的开发者应该知道,内存问题很难调试定位到具体的位置,往往导致奇怪的bug出现。
  • 能够帮助理解Qt众多类的底层不变的逻辑,学起来更容易。

本文只是对Qt中内存管理进行了梳理,无法涵盖很多细节问题,读者需要花一些时间去详细阅读Qt助手文档,最好是写几个demo测试验证。花时间是值得的,因为技术是日新月异的,但是核心的原理变化是不大的。Qt中的内存管理思想和方法,在很多语言、框架中(Python、Objective C、JavaScript等等)都有类似的应用。

值得一提的是,之所以Qt中具有各种各样的内存管理方式,是因为它能够减轻开发者的负担,更加专注于业务代码的实现,而不是被内存问题折腾的焦头烂额。不使用Qt中的内存管理,只用C的手动内存管理仍然可以写可以运行的代码!前提是不考虑成本问题,并假设开发者在内存问题上不会犯错。总之一句话,不要对立各种技术,每种技术都有适用的场景,抛开场景谈方法都是不理智的。

后面会根据需要专门讲解一些细节问题,敬请关注!


本文首发自公众号“Qt未来工程师”,欢迎关注。
请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/436848.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【转】2.1【MySQL】运行原理(一):查询sql的执行过程及MySQL架构分析

MySQL的发展历史和版本分支&#xff1a; 时间里程碑1996 年MySQL1.0 发布。它的历史可以追溯到 1979 年&#xff0c;作者 Monty 用 BASIC 设计的一个报表工具。1996 年 10 月3.11.1 发布。MySQL 没有 2.x 版本。2000 年ISAM 升级成 MyISAM 引擎。MySQL 开源。2003 年MySQL4.0 …

【转】2.2【MySQL】运行原理(二):InnoDB 内存结构、磁盘结构及update sql执行过程分析

前一篇讲完了查询流程&#xff0c;我们是不是再讲讲更新流程、插入流程和删除流程&#xff1f;在数据库里面&#xff0c;我们说的update操作其实包括了更新、插入和删除。如果大家有看过MyBatis的源码&#xff0c;应该知道Executor里面也只有doQuery()和doUpdate()的方法&#…

【转】2.3【MySQL】运行原理(三)InnoDB 逻辑存储结构

MySQL的存储结构分为5级&#xff1a;表空间、段、簇、页、行。 1.表空间 TableSpace 上篇【MySQL】从InnoDB的内存结构、磁盘结构到update sql执行过程分析 在磁盘结构部分就说过了&#xff0c;表空间可以看做是InnoDB 存储引擎逻辑结构的最高层&#xff0c;所有的数据都存放在…

【转】【MySQL】运行原理(四):重做日志(redo log),回滚日志(undo log),二进制日志(binlog)

MySQL中有六种日志文件&#xff0c;分别是&#xff1a;重做日志&#xff08;redo log&#xff09;、回滚日志&#xff08;undo log&#xff09;、二进制日志&#xff08;binlog&#xff09;、错误日志&#xff08;errorlog&#xff09;、慢查询日志&#xff08;slow query log&…

python 读中文乱码_python字符乱码的解决小结

引言无论学习什么程序语言&#xff0c;字符串这种数据类型总是着有非常重要。然而最近在学习python这门语言&#xff0c;想要显示中文&#xff0c;总是出现各种乱码。于是在网上查了很多资料&#xff0c;各说纷纭&#xff0c;我也尝试了许多的方法&#xff0c;有时候可以正常显…

ntnub原理怎么看_老电工由浅入深带你入门学PLC的工作原理和梯形图的编程规则...

PLC编程怎么学&#xff1f;很难吗&#xff1f;工控小白怎么入门学习PLC&#xff1f;需要为学习PLC编程做哪些准备&#xff1f;学习PLC编程时&#xff0c;前期一定要积累相关的理论知识&#xff0c;有了一定的基础&#xff0c;基础打扎实之后就是多练习了。今天推荐的重点&#…

【转】国密加密算法SM系列的C#实现方法

http://www.zhimengzhe.com/bianchengjiaocheng/Javabiancheng/22144.html 在网上搜索SM实现方法&#xff0c;按照上面网站提供方法总是出错&#xff0c;经过调试终于修改好了&#xff0c;给大家以参考&#xff0c;不走弯路了 base64修改&#xff0c;这个看需求&#xff0c;如…

1盒子刷webpad_拉宽带送的盒子也有春天:一招解放各种束缚限制

序言&#xff1a;故事要从一年多前开始说起了&#xff0c;话说...装了宽带之后&#xff0c;移动送了个电视盒子&#xff0c;一次未使用&#xff0c;角落吃灰一年多了&#xff0c;最近有个大胆的想法&#xff01;其实是被逼迫无奈&#xff0c;孩子总是喜欢拿我手机刷抖音&#x…

php udp发送和接收_63、php利用原生socket创建udp服务

1、案例函数汇总2、案例通过socket创建udp服务&#xff0c;获取对端的ip和port信息。并进行打印2.1、udp服务源码/*** Copyright(C) Iamasb* project : 3、workerman相关知识点* explain : 原生socket创建创建udp服务* filename : socket_udp.php* author : Iamasb*/// 创建udp…

【转】C#实现SM3国密加密

C#实现SM3国密加密 本文主要讲解“国密加密算法”SM系列之SM3的C#实现方法&#xff0c;加密规则请详阅国密局发布的文档。 首先需第三方Nuget包&#xff1a;Portable.BouncyCastle &#xff08;源码来自http://www.bouncycastle.org/csharp/&#xff09; 1.1常规处理 /// &l…

mq集群要建传输队列吗_面试官:消息队列这些我必问!

作者&#xff1a;mousycodersegmentfault.com/a/1190000021054802消息队列连环炮项目里怎么样使用 MQ 的&#xff1f;为什么要使用消息队列&#xff1f;消息队列有什么优点和缺点&#xff1f;kafka,activemq,rabbitmq,rocketmq 都有什么去呗&#xff1f;如何保证消息队列高可用…

【转】国密算法sm4 CBC模式加解密

一.什么是CBC模式? CBC模式的全称是Cipher Block Chaining模式&#xff08;密文分组链接模式&#xff09;&#xff0c;之所以叫这个名字&#xff0c;是因为密文分组像链条一样相互连接在一起。 在CBC模式中&#xff0c;首先将明文分组与前一个密文分组进行异或运算&#xff0c…

【转】对称加密和分组加密中的四种模式(ECB、CBC、CFB、OFB)

版权声明&#xff1a;本文为作者原创&#xff0c;如需转载&#xff0c;请注明出处https://blog.csdn.net/weixin_42940826注&#xff1a;以下图片来自于《图解密码学》&#xff0c;这本书讲的更全面细致&#xff0c;建议阅读&#xff0c;在我资源库中有此书&#xff0c;还有使用…

中发生数据丢失_如何防止Redis脑裂导致数据丢失?

所谓的脑裂&#xff0c;就是指在主从集群中&#xff0c;同时有两个主节点&#xff0c;它们都能接收写请求。而脑裂最直接的影响&#xff0c;就是客户端不知道应该往哪个主节点写入数据&#xff0c;结果就是不同的客户端会往不同的主节点上写入数据。而且&#xff0c;严重的话&a…

【转】TransactionScope事务简介

在.NET 1.0/1.1 版本我们使用SqlTransaction.处理事务 string connString ConfigurationManager.ConnectionStrings["db"].ConnectionString; using (var conn new SqlConnection(connString)) { conn.Open(); using (IDbTransaction tran conn.BeginTransact…

网络通道数2的倍数_限流笔记-通道限流(二)

在工作中的时候&#xff0c;由于我负责的一个系统需要调用很多的第3方的系统&#xff0c;可是呢&#xff0c;这些个第3方的系统的性能完全不一致&#xff0c;有的好有的坏&#xff0c;还成本都不一样&#xff0c;当然了平时把&#xff0c;直接使用成本低的就行了&#xff0c;但…

mysql题目_MySQL练习题

创建下列表并创建相关约束问题1&#xff1a;查询出成绩表&#xff0c;而且student_id 后面要有对应的学生名&#xff0c;course_id 后面要有对应的课程名.1 SELECT2 score.sid,3 score.student_id,4 student.sname,5 score.course_id,6 course.cname,7 score.number8 FROM scor…

查看mysql数据库的死锁日志_【MySQL】mysql死锁以及死锁日志分析

1.死锁的概念死锁&#xff1a;死锁一般是事务相互等待对方资源&#xff0c;最后形成环路造成的。对于死锁&#xff0c;数据库处理方法&#xff1a;牺牲一个连接&#xff0c;保证另外一个连接成功执行。发生死锁会返回ERROR&#xff1a;1213 错误提示&#xff0c;大部分的死锁In…

【转】二进制文件和ASCII文件有何区别

二进制文件和ASCII文件&#xff08;即文本文件&#xff09;的区别&#xff0c;对于和计算机亲近时间尚短的同学是个难题。本文用简单的例子&#xff0c;试图展示其中的道道&#xff0c;希望能对菜鸟们有些帮助。 1、一个例子&#xff1a;两种100000 有程序&#xff1a; #includ…

【转】为什么不能使用字符流读取非文本的二进制文件?

读取文件 刚学Java的IO流部分时&#xff0c;书上说只能使用字节流去读取图片、视频等非文本二进制文件&#xff0c;不能使用字符流&#xff0c;否则文件会损坏。所以我就一直记住这一点了&#xff0c;但是为什么不能使用&#xff0c;这一直是我的一个疑惑。今天&#xff0c;我…