前言
内存管理,是对软件中内存资源的分配与释放进行有效管理的方法和理论。
众所周知,内存管理是软件开发的一个重要的内容。软件规模越大,内存管理可能出现的问题越多。如果像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为主窗口对象,类型为QWidget;label为子窗口对象,类型为QLabel *。
注意代码第13行,在创建label文本标签窗口对象时,new QLabel的第二个参数即为父对象地址(参考Qt Assistant中QLabel的说明文档),这里给的值是主窗口的地址。
在main函数退出时,mainWidget超出main函数作用域会析构,析构时会自动删除label窗口对象,所以这里,我们不需要再写一行:delete label; 来释放label的内存,很方便而且又能节省时间精力。
使用引用计数对内存进行管理
引用计数
引用计数可以说是软件开发人员必知必会的知识点,它在内存管理领域的地位是数一数二的。
引用计数的原理,还是力所能及地用最简单的话来描述:
引用计数需要从三个方面来全面理解:
使用场景:一个资源,多处使用(使用即引用)。
问题:到底谁来释放资源。
原理:使用一个整形变量来统计,此资源在多少个地方被使用,此变量称为引用计数。当某处使用完资源以后,将引用计数减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未来工程师”,欢迎关注。