目录
C++ 标准一共有四种智能指针:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr。其中 auto_ptr 在 C++11 已被摒弃,C++17 中被移除不可用了。
什么是智能指针?
什么时候用它们?
QT中什么时候使用智能指针?
智能指针是一种抽象数据类型,具有标准指针的所有功能,并另外提供自动垃圾回收。智能指针有助于动态内存操作。它们的主要优点是减少了由于内存管理不善而导致的内存泄漏和错误。
从使用的层面来讲,智能指针其实是对裸指针(普通指针)的类封装,这使得智能指针实质是一个对象,使用感觉就像一个指针。利用 RAII(资源获取即初始化)技术来实现智能指针的特性。使用智能指针的目的,最主要的任务就是解决内存泄漏的问题。通过智能指针来执行垃圾回收的任务。智能指针的这种能力赋予了C++语言本身进行内存垃圾清理的实现。通俗的讲,智能指针就是帮你做了你忘记delete操作的工作。
按照这种思路,好像只要申请的内存空间(包含堆和栈)资源都应该使用智能指针来工作。否则,随着项目代码量的增加、算法的复杂,我们绝对会忘记delete,或者不好delete。例如两个对象彼此都拥有着对方的指针,当其中一个对象完成任务后,你将不确定是否delete。再例如,在处理一个可以预期的delete过程中,如果发生异常,那delete将永不会执行,而且这种到处充满捕获异常的代码异常的难看。理论上,如果非特殊需要,我们应该把裸指针全部替换成智能指针。
QT框架有自己的垃圾回收系统,通过父与子的树体系来管理内存。每个创建的小部件都注册进父子树结构内。当某个节点的部件完成工作进行销毁,那么QT将销毁这个部件下的所有子部件。这一切都是自动执行的,我们不需要关心去手动做什么。QT框架对C++的扩展,就像Java垃圾回收一样。当然,因为C++与生俱来对指针的操作能力,QT框架不能帮我们做完一切关于内存的管理。所有不是继承QT的对象,或是动态创建的对象,或是没有父QT对象的都应该手动处理。(我不确定这样理解是否正确,有待慢慢验证。)
但,我在QT的博客讨论区看到这样一段话:千万别让UI指针和智能指针混在一起。否则将使灾难。
这里是摘抄的另一位大牛的留言:
如果您觉得需要智能指针来确保正确的对象破坏,您应该首先重新检查您的设计。
我属于这样一个学派,即开发者应该理解对象何时被创建,何时被销毁。这是代码设计的一部分。
聪明的指针突然提示您不再需要这样做。但这是错误的:指针可能被称为“智能”,但它仍然不知道您打算做什么。结果可能是悬空的对象和不稳定的程序行为。
设计您的对象生命周期。在某些情况下,您无法知道销毁对象的正确时间,在这种情况下,使用智能指针可能是一个很好的设计决策。但是,在使用它之前,请准确了解它的行为,并为手头的作业使用正确的智能指针。
在Qt中我们有很多的new,可是却很少看见delete,这是因为QT也有自己的垃圾回收机制,当然不同于java的引用计数,Qt是以对象树的形式来实现对垃圾回收的。父类拥有并维护着一颗对象树。如果delete使用不当,程序就非常容易崩溃。所以,如果是QT垃圾回收所管理的对象,我们是不需要手动处理的。
unique_ptr 是 auto_ptr 的继承者,对于同一块内存只能有一个持有者,而 unique_ptr 和 auto_ptr 唯一区别就是 unique_ptr 不允许赋值操作,也就是不能放在等号的左边(函数的参数和返回值例外),这一定程度上避免了一些误操作导致指针所有权转移,然而 unique_str 依然有提供所有权转移的方法: std::move。调用 move 后,原 unique_ptr 就会失效,再用其访问裸指针也会发生和 auto_ptr 相似的 crash。所以,即使使用了 unique_ptr,也要慎重使用 move 方法,防止指针所有权被转移导致的 crash。当然,我们应该牢记move后,废弃的指针。永远不要访问它。在浏览博客时,曾经看到一篇文章介绍的move后的指针依旧在使用,这段代码出现在网络低层代码中。具体未研究。
shared_ptr 是目前工程内使用最多最广泛的智能指针,它使用引用计数实现对同一块内存的多个引用,在最后一个引用被释放时,指向的内存才释放,这也是和 unique_ptr 最大的区别。
使用 shared_ptr 过程中有几点需要注意:
QSharedPointer是C++中的自动共享指针。出于正常目的,它的行为与普通指针完全相同,包括对常量的尊重。如果没有其他QSharedPointer对象引用它,当它超出范围时,QSharedPointer将删除它所持有的指针。QSharedPointer对象可以从普通指针、另一个QSharedPointer对象或通过将QWeakPointer对象提升为强引用来创建。
QSharedPointer和QWeakPointer是可重入类。这意味着,通常,如果没有同步,多个线程无法同时访问给定的QSharedPointer或QWeakPointer对象。
多个线程可以同时安全地访问不同的QSharedPointer和QWeakPointer对象。这包括它们持有指向同一对象的指针的情况;引用计数机制是原子的,不需要手动同步。
需要注意的是,虽然指针值可以通过这种方式访问(即,由多个线程同时访问,无需同步),但QSharedPointer和QWeakPointer不能保证所指向的对象。该对象的特定线程安全和可重入规则仍然适用。
Qt还提供了另外两个指针包装类:QPointer和QSharedDataPointer。它们彼此不兼容,因为每个都有非常不同的用例。
QSharedPointer通过外部引用计数(即,放置在对象外部的引用计数器)保存共享指针。正如其名称所示,指针值在QSharedPointer和QWeakPointer的所有实例之间共享。但是,指针指向的对象的内容不应被视为共享:只有一个对象。因此,QSharedPointer不提供分离或复制指向对象的方法。
另一方面,QSharedDataPointer持有指向共享数据(即,从QSharedData派生的类)的指针。它通过放置在QSharedData基类中的内部引用计数来实现。因此,该类可以根据对被保护数据的访问类型进行分离:如果是非常量访问,它会自动创建一个副本,以便操作完成。
QExplicitlySharedDataPointer是QSharedDataPointer的一个变体,除了它仅在显式调用QExplicitySharedDatapointer::detach()时分离(因此而得名)。
QScopedPointer只保存一个指向堆分配对象的指针,并在其析构函数中删除它。当需要对对象进行堆分配和删除时,该类非常有用,但仅此而已。QScopedPointer是轻量级的,它不使用额外的结构或引用计数。
最后,QPointer持有指向QObject派生对象的指针,但它的作用很弱。QWeakPointer具有相同的功能,但不推荐使用该功能。
QSharedPointer的一个特性是指针跟踪机制,可以在编译时启用以进行调试。启用后,QSharedPointer在全局集中注册它跟踪的所有指针。这允许您捕获错误,例如将同一指针分配给两个QSharedPointer对象。
通过在包含QSharedPointer头之前定义QT_SHAREDPOINTER_TRACK_POINTERS宏来启用此功能。
即使在没有该功能的情况下编译代码,也可以安全地使用该功能。QSharedPointer将确保从跟踪器中删除指针,即使是在没有指针跟踪的情况下编译的代码。
但是,请注意,指针跟踪功能对多继承或虚拟继承有限制(即,在两个不同指针地址可以引用同一对象的情况下)。在这种情况下,如果指针被转换为不同的类型,并且其值发生变化,QSharedPointer的指针跟踪机制可能无法检测到被跟踪的对象是相同的。
手动管理堆分配的对象很难且容易出错,常见的结果是代码泄漏内存并很难维护。QScopedPointer是一个很小的实用程序类,它通过将基于堆栈的内存所有权分配给堆分配大大简化了这一过程,更通常称为资源获取初始化(RAII)。
QScopedPointer保证当当前范围消失时,所指向的对象将被删除。
手动管理堆分配的对象既困难又容易出错,常见的结果是代码泄漏内存并且难以维护。 QScopedPointer 是一个小型实用程序类,它通过将基于堆栈的内存所有权分配给堆分配来大大简化这一点,更普遍地称为资源获取是初始化 (RAII)。 QScopedPointer 保证当当前作用域消失时,指向的对象将被删除。
如果我们指向的内存数据是一个数组,这时可以用 QScopedArrayPointer。QScopedArrayPointer 与 QScopedPointer 类似,用于简单的场景。
自 Qt5 以来一直是围绕 QWeakPointer 的包装器。该类曾经计划被弃用,但改为保留以支持遗留代码。
保护指针QPointer
当您需要存储一个指向其他人拥有的QObject的指针时,保护指针非常有用,因此在您仍然持有对QObject的引用时可能会被销毁。您可以安全地测试指针的有效性。
请注意,Qt5在使用QPointer时引入了行为的轻微变化。当在QWidget(或QWidgets的子类)上使用QPointer时,以前QPointer将被QWidgetDestructor清除。现在,QPointer被QObject析构函数清除(因为这是QWeakPointer对象被清除的时候)。在QWidget析构函数销毁正在跟踪的小部件的子部件之前,不会清除跟踪小部件的任何QPointer。
QPointer中可用的函数和运算符与普通无保护指针中可用的相同,但指针算术运算符(+、-、++和-)除外,它们通常仅用于对象数组。
像普通指针一样使用Qpointer,您无需阅读此类文档。
为了创建保护指针,可以从T*或相同类型的另一个保护指针构造或分配保护指针。您可以使用运算符==()和运算符将它们相互比较=(),或使用isNull()测试nullptr。您可以使用*x或x->member符号取消对它们的引用。
保护指针将自动转换为T*,因此您可以自由混合保护指针和非保护指针。这意味着,如果您有一个QPointer<QWidget>,则可以将其传递给需要qwidgets*的函数。因此,声明函数以QPointer作为参数没有什么价值;只使用普通指针。在存储指针时使用QPointer。
注意,类T必须继承QObject,否则将导致编译或链接错误。
QPointer中可用的函数和运算符与普通无保护指针中可用的相同,但指针算术运算符(+、-、++和-)除外,它们通常仅用于对象数组。
像普通指针一样使用Qpointer,您无需阅读此类文档。
为了创建保护指针,可以从T*或相同类型的另一个保护指针构造或分配保护指针。您可以使用运算符==()和运算符将它们相互比较=(),或使用isNull()测试nullptr。您可以使用*x或x->member符号取消对它们的引用。
保护指针将自动转换为T*,因此您可以自由混合保护指针和非保护指针。这意味着,如果您有一个QPointer<QWidget>,则可以将其传递给需要qwidgets*的函数。因此,声明函数以QPointer作为参数没有什么价值;只使用普通指针。在存储指针时使用QPointer。
注意,类T必须继承QObject,否则将导致编译或链接错误。
QSharedDataPointer<T>使编写自己的隐式共享类变得容易。QSharedDataPointer实现线程安全引用计数,确保向可重入类添加QSharedData指针不会使它们不可重入。
QSharedDataPointer 这个类是帮我们实现数据的隐式共享的。我们知道 Qt 中大量的采用了隐式共享和写时拷贝技术。
许多Qt类使用隐式共享来将指针的速度和内存效率与类的易用性结合起来。
QWeakPointer 是为配合 QSharedPointer 而引入的一种智能指针,它更像是 QSharedPointer 的一个助手(因为它不具有普通指针的行为,没有重载operator*和->)。它的最大作用在于协助 QSharedPointer 工作,像一个旁观者一样来观测资源的使用情况。QWeakPointer就像一个观察者来辅助QSharedPointer 工作。一旦我们需要QSharedPointer权限,我们可以提升QWeakPointer的权限,升级为QSharedPointer使用。