CPP 引入 Smart Pointer 的原因,以及最佳实践
Smart Pointer 引入原因
使用 Raw Pointer 管理动态内存时,经常会遇到这样的问题:
- 忘记delete内存,造成内存泄露
- 出现异常时,不会执行delete,造成内存泄露
下面的代码解释了,当一个操作发生异常时,会导致delete不会被执行:
1 | void func() { |
在 C++98 中我们需要用一种笨拙的方式,写出异常安全的代码:
1 | void func() { |
C++11 使用 Smart Pointer 能轻易写出异常安全的代码,因为当对象退出作用域时, Smart Pointer 将自动调用对象的析构函数,避免内存泄露:
1 | void func() { |
Smart Pointer 使用方法
Smart Pointer 在
在大多数情况下,初始化 Raw Pointer 或资源句柄以指向实际资源时,请立即将指针传递给Smart Pointer。在现代C++中,Raw Pointer 仅用于有限范围,循环或辅助函数的小代码块,其中性能至关重要,并且不存在对所有权混淆的可能性。
以下示例将 Raw Pointer 声明与 Smart Pointer 声明进行比较。
1 | // 使用 Raw Pointer |
如示例中所示, Smart Pointer 是您在堆栈中声明的类模板,并使用指向堆分配对象的 Raw Pointer 进行初始化。 Smart Pointer 初始化后,它拥有 Raw Pointer 。这意味着 Smart Pointer 负责删除 Raw Pointer 指定的内存。 Smart Pointer 析构函数包含对删除的调用,并且因为 Smart Pointer 在堆栈中声明,所以当 Smart Pointer 超出作用域时,即使在堆栈之后的某处抛出异常,也会调用析构函数。
通过使用熟悉的指针操作符 -> 和 * 来访问封装的指针, Smart Pointer 类重载该指针来返回封装的 Raw Pointer 。
C++ Smart Pointer 的对象创建:您创建对象,然后让系统在正确的时间删除它。不同之处在于没有单独的垃圾收集器在后台运行; 内存通过标准C++范围规则进行管理,以便运行时环境更快,更高效。
总是在单独的代码行上创建Smart Pointer,而不是在参数列表中创建,以便由于某些参数列表分配规则而不会发生细微的资源泄漏。
以下示例显示了如何使用标准模板库中的unique_ptrSmart Pointer类型来封装指向大对象的指针。
1 | class LargeObject { |
该示例演示了使用 Smart Pointer 的以下基本步骤:
- 将Smart Pointer声明为自动(本地)变量
– 不要在Smart Pointer本身上使用new 或 malloc 表达式 - 在类型参数中,指定封装指针的指向类型
- 将一个Raw Pointer传递给Smart Pointer构造函数中的新对象
– Some utility functions or smart pointer constructors do this for you - 使用重载的 -> 和 * 运算符来访问该对象
- 让 Smart Pointer 删除对象
Smart Pointer 的设计在内存和性能方面尽可能高效。 例如,unique_ptr中唯一的数据成员是封装的指针。 这意味着 unique_ptr 与该指针的大小完全相同,可以是四个字节,也可以是八个字节。 通过使用Smart Pointer重载的 * 和 -> 操作符访问封装的指针不会比直接访问 Raw Pointer 慢得多。
Smart Pointer有自己的成员函数,可以用“.”符号来访问。 例如,一些STL Smart Pointer有一个 reset member function ,释放指针的所有权。 如果您想在 Smart Pointer 超出范围之前释放 Smart Pointer 所拥有的内存,这非常有用,如下例所示。
1 | void SmartPointerDemo2() { |
Smart Pointer 类型
- ** unique_ptr **
- 仅允许一个底层指针的所有者
- 可以移动到新的所有者,但不能复制或共享
- unique_ptr小而高效, 大小是一个指针
- 它支持右值引用,以便从STL集合中快速插入和检索
- 头文件:
<memory>
- ** shared_ptr **
- 引用计数的Smart Pointer
- 当您想要将一个Raw Pointer分配给多个所有者时使用,例如,当您从容器中返回指针的副本但想保留Raw Pointer时。 直到所有shared_ptr所有者超出范围或放弃所有权之后,才会删除Raw Pointer
- 大小是两个指针, 一个用于对象,另一个用于包含引用计数的共享控制块
- 头文件:
<memory>
- ** weak_ptr **
- 与shared_ptr结合使用的特例Smart Pointer
- weak_ptr 提供对一个或多个shared_ptr实例拥有的对象的访问权限,但不参与引用计数
- 当你想观察一个物体时使用,但不要求它保持活着。在某些情况下需要中断shared_ptr实例之间的循环引用。
- 头文件:
<memory>
创建和使用unique_ptr实例
A unique_ptr不会共享它的指针。 无法将它复制到另一个unique_ptr,(除非它是可修改rvalue) 通过值传递给函数,或需要对其进行复制的任何标准模板库 (STL) 算法中使用。 A unique_ptr只能移动。 这意味着内存资源的所有权将转移到新的unique_ptr和原始unique_ptr不再拥有它。 我们建议您将一个对象限制为一个所有者,因为拥有多个程序逻辑增加复杂性。 因此,当您需要为普通的 C++ 对象的 Smart Pointer ,使用unique_ptr。
下图说明了两个转让所有权unique_ptr实例。
移动 unique_ptr 的所有权, unique_ptr在中定义
** 示例 **
下面的示例演示如何创建unique_ptr实例,并在函数之间传递它们。
1 | // |
1 | unique_ptr<Song> SongFactory(std::wstring artist, std::wstring title) { |
这些例子演示了unique_ptr的基本特征:它可以被移动,但不能被复制。 “移动”将所有权转移到新的unique_ptr并重置旧的unique_ptr。
以下示例显示如何创建unique_ptr实例并在矢量中使用它们
1 | void SongVector() { |
在for_each循环中,请注意unique_ptr是在lambda表达式中通过引用传递的。 如果你尝试在这里传值,编译器会抛出一个错误,因为unique_ptr拷贝构造函数被禁用。
以下示例显示如何初始化一个类成员unique_ptr。
1 | class MyClass |
- 仅允许一个底层指针的所有者
- 可以移动到新的所有者,但不能复制或共享
创建和使用shared_ptr实例
shared_ptr 类型是 Smart Pointer 在为方案设计多个所有者可能必须管理对象生存期内存中的 C++ 标准库中。 在初始化可以将它复制的 shared_ptr 后,将它在函数参数的值,并将其分配给其他 shared_ptr 实例。 所有实例指向同一对象,并且,对“的共享访问控制块”该引用计数的增量和减量,每当新 shared_ptr 添加,超出范围或重新设置。 当引用计数达到零时,控制块删除内存资源和自身。
下图显示了指向个内存位置的几 shared_ptr 实例。
1 | // |
OUTPUT:
sp.use_count(): 1
gp.use_count(): 2
gp.use_count(): 1
创建和使用weak_ptr实例
有时一个对象必须存储一个访问shared_ptr的底层对象的方法,而不会导致引用计数增加。通常情况下,如果在shared_ptr实例之间有循环引用,则会发生这种情况。
最好的设计是尽可能避免指针的共享所有权。但是,如果您必须共享shared_ptr实例的所有权,请避免它们之间的循环引用。当循环引用是不可避免的,或者由于某种原因更可取的时候,使用weak_ptr来给一个或多个所有者一个弱引用给另一个shared_ptr。通过使用weak_ptr,您可以创建一个shared_ptr,它将连接到现有的一组相关实例,但前提是基础内存资源仍然有效。 weak_ptr本身不参与引用计数,因此它不能阻止引用计数变为零。但是,可以使用weak_ptr尝试获取初始化的shared_ptr的新副本。如果内存已被删除,则抛出bad_weak_ptr异常。如果内存仍然有效,那么只要shared_ptr变量保持在作用域内,新的共享指针就会增加引用计数并保证内存有效。
下面的代码示例演示了weak_ptr用于确保正确删除具有循环依赖关系的对象的情况。当你检查这个例子时,假设它只是在考虑了其他解决方案之后才创建的。 Controller对象表示机器进程的某些方面,它们独立运行。每个控制器必须能够随时查询其他控制器的状态,并且每个控制器都包含一个专用矢量<weak_ptr <Controller>>
。每个向量包含一个循环引用,因此,使用weak_ptr实例而不是shared_ptr。
1 | // |
OUTPUT
Creating Controller0
Creating Controller1
Creating Controller2
Creating Controller3
Creating Controller4
push_back to v[0]: 1
push_back to v[0]: 2
push_back to v[0]: 3
push_back to v[0]: 4
push_back to v[1]: 0
push_back to v[1]: 2
push_back to v[1]: 3
push_back to v[1]: 4
push_back to v[2]: 0
push_back to v[2]: 1
push_back to v[2]: 3
push_back to v[2]: 4
push_back to v[3]: 0
push_back to v[3]: 1
push_back to v[3]: 2
push_back to v[3]: 4
push_back to v[4]: 0
push_back to v[4]: 1
push_back to v[4]: 2
push_back to v[4]: 3
use_count = 1
Status of 1 = On
Status of 2 = On
Status of 3 = On
Status of 4 = On
use_count = 1
Status of 0 = On
Status of 2 = On
Status of 3 = On
Status of 4 = On
use_count = 1
Status of 0 = On
Status of 1 = On
Status of 3 = On
Status of 4 = On
use_count = 1
Status of 0 = On
Status of 1 = On
Status of 2 = On
Status of 4 = On
use_count = 1
Status of 0 = On
Status of 1 = On
Status of 2 = On
Status of 3 = On
Destroying Controller4
Destroying Controller3
Destroying Controller2
Destroying Controller1
Destroying Controller0
Press any key