【Example】C++ 标准库智能指针 unique_ptr 与 shared_ptr
【概念直接搬运Docs】C 样式编程的一个主要 bug 类型是内存泄漏。 泄漏通常是由于为分配的内存的调用失败引起的 delete
new
。 现代 C++ 强调“资源获取即初始化”(RAII) 原则。 其理念很简单。 资源(堆内存、文件句柄、套接字等)应由对象“拥有”。 该对象在其构造函数中创建或接收新分配的资源,并在其析构函数中将此资源删除。 RAII 原则可确保当所属对象超出范围时,所有资源都能正确返回到操作系统。
为了支持对 RAII 原则的简单采用,C++ 标准库提供了三种智能指针类型:std::unique_ptr、std::shared_ptr 和 std::weak_ptr。 智能指针可处理对其拥有的内存的分配和删除。 下面的示例演示了一个类,其中包含一个数组成员,该成员是在调用 make_unique()
时在堆上分配的。 对和的 new
调用 delete
由 unique_ptr
类封装。 当 widget
对象超出范围时,将调用 unique_ptr 析构函数,此函数将释放为数组分配的内存。
在现代 c + + 编程中,标准库包含智能指针,这些指针用于帮助确保程序不会出现内存和资源泄漏,并具有异常安全。
====================================
unique_ptr
unique_ptr 类型智能指针在设计上最显著的特点是内部托管的指针一旦被创建就不能被任何形式的复制给另一个unique_ptr,只可以被移动给另一个unique_ptr。unique_ptr 没有拷贝构造函数,因此不能用于赋值。该指针最常用的情况是单例模式和编译防火墙的封装。当一个 std::unique 生命周期结束时,将释放所托管的内存,也就意味着,如果它作为一个局部变量托管一个对象,对象的生命周期也仅限于当前作用域范围,调用 reset() 或 operator= 时也会释放原有内存。
// 演示创建 unique_ptr unique_ptr<Brain> u_brain = make_unique<Brain>(); u_brain->HelloWorld(); // 移动 unique_ptr unique_ptr<Brain> um_barin = std::move(u_brain); um_barin->HelloWorld();
// 移动方法2
std::swap(u_brain, um_brain);
// 错误 // um_barin = u_brain; // u_brain->HelloWorld(); // C26800
// 可以使用以下方法判断是否为空指针 if (um_brain == nullptr) { std::cout << "um_brain is nullptr" << std::endl; }
// 可以释放资源将指针恢复空指针
um_brain.reset();
成员函数表:
名称 | 作用 |
operator= | 赋值,如果已经存在托管的内存,会释放掉原有内存。 |
operator[] | 数组下标形式访问。 |
release | 返回一个指向被管理对象的裸指针,并释放所有权。 |
reset | 释放原有对象内存,如果传递了参数,则替换为新指定的指针。 |
swap | 交换两个 std::unique_ptr 对象 |
get | 返回所托管对象的裸指针。 |
get_deleter | 返回所托管对象的析构删除器。 |
operator bool | 检查是否有关联的被管理对象。 |
operator* | 解引用。 |
operator-> | 解引用。 |
额外注解:
只有非 const 的
unique_ptr
能转移被管理对象的所有权给另一unique_ptr
。若对象的生存期为 const std::unique_ptr 所管理,则它被限定在创建指针的作用域中。
std::unique_ptr
常用于管理对象的生存期,包含:
- 通过正常退出和经由异常退出两者上的受保证删除,提供异常安全,给处理拥有动态生存期的对象的类和函数
- 传递独占的拥有动态生存期的对象的所有权到函数
- 从函数获得独占的拥有动态生存期对象的所有权
- 作为具移动容器的元素类型,例如保有指向动态分配对象的指针的 std::vector (例如,若想要多态行为)
std::unique_ptr
可为不完整类型T
构造,例如用于改善用作 pImpl 手法中柄的用途。若使用默认删除器,则T
必须在代码中调用删除器点处完整,这发生于析构函数、移动赋值运算符和std::unique_ptr
的reset
成员函数中。(相反地, std::shared_ptr 不能从指向不完整类型的裸指针构造,但可于T
不完整处销毁)。注意若T
是类模板特化,则以unique_ptr
为运算数的使用,如 !p ,因 ADL 而要求T
的形参完整。若
T
是某基类B
的导出类,则 std::unique_ptr<T> 可隐式转换为 std::unique_ptr<B>。产生的 std::unique_ptr<B> 的默认删除器将使用B
的 operator delete ,这导致未定义行为,除非B
的析构函数为虚。注意 std::shared_ptr 表现有别: std::shared_ptr<B> 将使用类型T
的 operator delete ,而且即使B
的析构函数非虚,也会正确删除被占有对象。不同于 std::shared_ptr ,
std::unique_ptr
可通过任何满足可空指针 (NullablePointer) 的定制柄类型管理对象。例如,这允许管理位于共享内存,但提供定义typedef boost::offset_ptr pointer;
或其他缀饰指针的Deleter
的对象。
---- 《C++ Reference》
====================================
shared_ptr
和 unique 不同的是,它允许自身对象(shared_ptr)被复制,复制出来的 shared_ptr 所托管的指针都指向同一块内存空间。而它的每一份拷贝(shared_ptr自身)都会有一个引用计数,资源的释放由生命周期中最后一个 shared_ptr 负责,调用最后一个 shared_ptr 的 reset() 或 operator= 时也会释放原有内存。因此 shared_ptr 是最常用的智能指针,也是最容易出问题的智能指针。
使用它时应当注意:
1,不要将已存在裸指针交由 shared_ptr,任何形式的智能指针都不应该去托管已有的裸指针。
2,作为函数参数传递时,请传递引用。因为作为值传递时,将产生大量无意义的引用计数。
3,共享所有权性质的对象往往比限定作用域的对象生存时间更久、资源开销更大,尤其是多线程下。
// 创建 shared_ptr<Brain> s_brain = make_shared<Brain>(); s_brain->HelloWorld();
// 复制 shared_ptr<Brain> c_brain = s_brain; shared_ptr<Brain> d_brain = s_brain; // 检查唯一性 std::cout << s_brain.unique() << std::endl; // 检查引用计数数量 std::cout << s_brain.use_count() << std::endl;
成员函数表:
名称 | 作用 |
operator= | 赋值。 |
operator[]【C++17】 | 数组下标形式访问。 |
reset |
替换所管理的对象。 如果是最后一个管理该对象的 shared_ptr,对象将被释放。 |
swap | 与另一个 shared_ptr 交换所管理的对象指针。 |
use_count | 返回当前引用计数。 |
get | 返回已托管对象的裸指针。 |
owner_before | 提供基于拥有者大小顺序的比较。 |
operator bool | 检查是否有关联的被管理对象。 |
operator* | 解引用。 |
operator-> | 解引用。 |
有关线程安全性的注解:
多个线程能在
shared_ptr
的不同实例上调用所有成员函数(包含复制构造函数与复制赋值)而不附加同步,即使这些实例是副本,且共享同一对象的所有权。若多个执行线程访问同一shared_ptr
而不同步,且任一线程使用shared_ptr
的非 const 成员函数,则将出现数据竞争;原子函数的 shared_ptr 特化能用于避免数据竞争。
---- 《C++ Reference》
与 shared_ptr 搭配的 weak_ptr
weak_ptr 设计上与 shared_ptr 搭配使用,因为 shared_ptr 存在一个问题,就是循环引用计数递增而导致的内存泄漏:
比如说:
【伪代码】
class node{ shared_ptr<node> start; shared_ptr<node> end; } shared_ptr<node> nn = make; shared_ptr<node> mm = make; nn->end = mm; mm->start = nn;
这种情况,两个node对象互相引用着对方,最终导致资源无法被释放。所以有时候需要访问 shared_ptr 自身,而不是它所托管的资源。
所以 weak_ptr 的作用就来了:
【伪代码】 class node{ weak_ptr<node> start; weak_ptr<node> end; } shared_ptr<node> nn = make; shared_ptr<node> mm = make; nn->end = mm; mm->start = nn;
为什么呢?
因为 share_ptr 是强引用,强引用是只要被引用的对象还存活那么这个引用也一定会存在。
而 weak_ptr 是弱引用,弱引用是虽然对象还活着,但是这个引用则可有可无。
所以,weak_ptr 的作用就是作为一个 "观察者" 访问 shared_ptr 本身,而不是 shared_ptr 所托管的资源。
同时也意味着,weak_ptr 只能访问它所观察的shared_ptr 本身,而不能访问 share_ptr 托管的资源,所以,它不会增加 shared_ptr 的引用计数。
shared_ptr<Brain> r_brain = make_shared<Brain>(); // 创建 weak_ptr weak_ptr<Brain> w_brain(r_brain); // 检查 shared 引用计数数量 std::cout << w_brain.use_count() << std::endl; // 返回一个原始 shared 的拷贝(增加引用计数) shared_ptr<Brain> e_brain(w_brain.lock()); std::cout << w_brain.use_count() << std::endl;
成员函数表:
名称 | 作用 |
operator= | 赋值 |
reset | 释放被管理对象的所有权 |
swap | 交换被管理对象 |
use_count | 返回当前所管理的对象引用计数。 |
expired | 检查被引用的对象是否已删除 |
lock | 创建管理被引用的对象的shared_ptr |
owner_before | 提供基于拥有者大小顺序的比较 |
额外补充:
std::bad_weak_ptr
是 std::shared_ptr 以 std::weak_ptr 为参数的构造函数,在 std::weak_ptr 指代已被删除的对象时,作为异常抛出的对象类型。#include <memory> #include <iostream> int main() { std::shared_ptr<int> p1(new int(42)); std::weak_ptr<int> wp(p1); p1.reset(); try { std::shared_ptr<int> p2(wp); } catch(const std::bad_weak_ptr& e) { std::cout << e.what() << '\n'; } }
---- 《C++ Reference》
std::enable_shared_from_this
它是一个可以继承的类,该机制的作用是安全地生成一个对象 this 指针的 std::shared_ptr 对象。
一个类继承它以后,将会拥有 shared_from_this() 和 weak_from_this() 两个函数,分别返回当前 this 指针的 std::shared_ptr 与 std::weak_ptr。
当在一个类内部,上级类(子类、内部类)的 this 指针需要以 std::shared_ptr 的形式传递时,就需要使用 enable_shared_from_this。因为如果直接使用 std::shared_ptr 包装 this 指针,会产生多个无关联且托管同一指针的 std::shared_ptr 对象,进而导致内存的重复释放。同时还有一个问题,就是外部获取该类的 this 指针后,仅凭裸指针无法判断该对象当前是否存活,而通过 shared_ptr 则可以判断当前指针是否有效,尽管把对象本身的 this 指针丢出去是非常不规范的做法。(...)
PS:此机制最大的作用是解决在类内部创建并分离线程,在线程当中通过 this 访问该对象的资源,却无法判断该对象是否存活的问题,导致调用一个无效指针进而产生异常。
#include <iostream> #include <memory> class Brain : public std::enable_shared_from_this<Brain> { public: auto GetSharedThis() { return shared_from_this(); } auto GetWeakThis() { return weak_from_this(); } }; int main() { std::shared_ptr<Brain> pBrain_1 = std::make_shared<Brain>(); std::shared_ptr<Brain> pBrain_2 = pBrain_1->GetSharedThis(); if (pBrain_1.get() == pBrain_2.get()) { std::cout << "True" << std::endl; } return EXIT_SUCCESS; }
====================================
make_unique 与 make_shared
这两个标准库函数是用于创建智能指针的函数。
【以下懒得打字了直接抄的Docs,重点我划出来】
使用make_shared
作为创建对象的简单、更高效的方法,以及一个shared_ptr
来同时管理对对象的共享访问。 在语义上,这两个语句是等效的:
auto sp = std::shared_ptr<Example>(new Example(argument)); auto msp = std::make_shared<Example>(argument);
但是,第一条语句进行了两个分配,如果在shared_ptr
对象的分配成功后,Example
的分配失败,则未命名的Example
对象将被泄漏。 使用make_shared
的语句更简单,因为只涉及到一个函数调用。 这样会更有效,因为库可能会对对象和智能指针进行一个分配。 此函数的速度更快,导致内存碎片更少,但在一次分配时不存在异常,而不是在另一种分配上。 通过使引用对象和更新智能指针中的引用计数的代码具有的更好的地址来提高性能。
make_unique如果不需要对对象的共享访问权限,请考虑使用。 allocate_shared 如果需要为对象指定自定义分配器,请使用。 make_shared
如果对象需要自定义删除器,则不能使用,因为无法将删除器作为参数传递。
unique_ptr<Brain> u_brain = make_unique<Brain>();
shared_ptr<Brain> s_brain = make_shared<Brain>();
====================================
智能指针的数组形式及默认删除策略 std::default_delete:
智能指针支持数组形式,数组形式的智能指针在某种程度上比容器对象更高效,但是也意味着提供的功能更少,操作更加原始:
#include <iostream> #include <memory> int main() {
// std::unique_ptr 默认就可以回收数组 std::unique_ptr<int[]> int_unique(new int[10]); for (int i = 0; i < 10; i++) { int_unique[i] = i; } for (int i = 0; i < 10; i++) { auto item = int_unique.get() + i; std::cout << "Unique Value: " << *item << std::endl; } // std::shared_ptr 则需要指定删除策略 std::shared_ptr<int[]> int_shared(new int[10], std::default_delete<int[]>()); for (int i = 0; i < 10; i++) { auto item = int_shared.get() + i; *item = i; } for (int i = 0; i < 10; i++) { auto item = int_shared.get() + i; std::cout << "Shared Value: " << *item << std::endl; } return EXIT_SUCCESS; }
请注意,std::default_delete 为指定默认删除策略,其中 std::unique_ptr 可以正确析构一个数组,但 std::shared_ptr 默认情况下调用 delete 无法析构一个数组。
因此,当 shared_ptr 托管的内存为一块数组时,在内存申请时就需要通过 std::default_delete 为其指定回收策略:
std::shared_ptr<int[]> int_shared(new int[10], std::default_delete<int[]>());