【精华】详解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未来工程师”,欢迎关注。
请添加图片描述

  • 7
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
QtSerialPort模块是用于与串口进行通信的模块,可以在Qt应用程序实现串口通信功能。下面是一个简单的使用SerialPort模块的示例代码: ```cpp #include <QCoreApplication> #include <QtSerialPort/QSerialPort> #include <QtSerialPort/QSerialPortInfo> #include <QDebug> int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); // 获取可用的串口列表 QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts(); qDebug() << "Available ports:"; for (const QSerialPortInfo &port : ports) { qDebug() << port.portName(); } // 打开串口 QSerialPort serial; serial.setPortName("COM1"); // 设置串口名字 serial.setBaudRate(QSerialPort::Baud9600); // 设置波特率 serial.setDataBits(QSerialPort::Data8); // 设置数据位数 serial.setParity(QSerialPort::NoParity); // 设置校验位 serial.setStopBits(QSerialPort::OneStop); // 设置停止位 serial.setFlowControl(QSerialPort::NoFlowControl); // 设置流控制 if (!serial.open(QIODevice::ReadWrite)) { qDebug() << "Failed to open serial port!"; return -1; } // 读取数据 QObject::connect(&serial, &QSerialPort::readyRead, [&]() { QByteArray data = serial.readAll(); qDebug() << "Received data:" << data; }); // 发送数据 QByteArray sendData = "Hello, SerialPort!"; qint64 bytesWritten = serial.write(sendData); qDebug() << "Bytes written:" << bytesWritten; return a.exec(); } ``` 上述代码,首先通过`QSerialPortInfo::availablePorts()`获取可用的串口列表,并打印出来。然后创建一个`QSerialPort`对象,设置串口的参数,如串口名字、波特率、数据位数、校验位、停止位和流控制。接着打开串口,如果打开失败则输出错误信息并返回。然后使用`QObject::connect()`连接`readyRead`信号,当串口有数据可读时触发该信号,并读取数据并输出。最后使用`serial.write()`发送数据到串口。 注意:在使用SerialPort模块前,需要在.pro文件添加`QT += serialport`以启用该模块。 希望以上代码对你有所帮助,如果有任何问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

撬动未来的支点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值