C++智能指针
引用计数:对于一块内存,多一个指针指向它,指针的引用计数就会+1。
对于指针指针赋值来说,=号右边的引用计数+1,=左边的引用计数-1.下图中sp1 -1 ,sp4+1
1. RAII¶
RAII 是 resource acquisition is initialization 的缩写,意为“资源获取即初始化”。它是 C++ 之父 Bjarne Stroustrup 提出的设计理念,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。在 RAII 的指导下,C++ 把底层的资源管理问题提升到了对象生命周期管理的更高层次。
2. unique_ptr¶
unique_ptr的原理很简单,就是一个“得不到就毁掉”的理念,直接把拷贝和赋值禁止了。
对于用不上赋值拷贝的场景的时候,我们选择unique_ptr也是一个不错的选择。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
3. shared_ptr¶
我们可以对一个资源添加一个计数器,让所有管理该资源的智能共用这个计数器,倘若发生拷贝,计数器加一,倘若有析构发生, 计数器减一,当计数器等于0的时候,就把对象析构掉。 所有的智能指针共同维护着两个地址:一个时对象资源的地址,一个是引用计数的地址(不是一个shared_ptr有一个int计数,而是共同维护一个int*)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
- 赋值拷贝
赋值拷贝需要注意两点:
- 在被赋值之前的对象需要将自己析构,也就是放弃当前资源的管理权,然后再去被赋值,取得新的管理权。
- 避免自己对自己赋值,按照1中的机制,如果自己对自己赋值,会造成无谓的操作,或者误析构资源。
4. weak_ptr¶
如果你仔细思考 std::shared_ptr
就会发现依然存在着资源无法释放的问题。看下面这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b
,这使得 a,b
的引用计数均变为了 2,而离开作用域时,a,b
智能指针被析构,却只能造成这块区域的引用计数-1,这样就导致了 a,b
对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露。
那么A节点资源什么时候析构呢, 当B->pointer析构,也就是当B节点资源析构,那么B节点资源什么时候析构呢,当A->pointer析构,也就是当A节点资源析构…此时形成了一个类似于“死锁”的情况。
如图 :
解决这个问题的办法就是使用弱引用指针 std::weak_ptr
,std::weak_ptr
是一种弱引用(相比较而言 std::shared_ptr
就是一种强引用)。弱引用不会引起引用计数增加,当换用弱引用时候,最终的释放流程如图所示(A计数为1,B计数为2,离开作用域时a,b销毁,由于此时A的计数为0,B计数为1,A销毁,所以A->pointer销毁,B的计数-1,变为0,B也销毁):
1 2 3 4 5 6 |
|
在上图中,最后一步只剩下 B,而 B 并没有任何智能指针引用它,因此这块内存资源也会被释放。
std::weak_ptr
没有 *
运算符和 ->
运算符,所以不能够对资源进行操作,它可以用于检查 std::shared_ptr
是否存在,其 expired()
方法能在资源未被释放时,会返回 false
,否则返回 true
;除此之外,它也可以用于获取指向原始对象的 std::shared_ptr
指针,其 lock()
方法在原始对象未被释放时,返回一个指向原始对象的 std::shared_ptr
指针,进而访问原始对象的资源,否则返回nullptr
。
weak_ptr 不是一个RALL智能指针,它不参与资源的管理,他是专门用来解决引用计数的,我们可以使用一个shared_ptr 来初始化一个weak_ptr,但是weak_ptr 不增加引用计数,不参与管理,但是也像指针一样访问修改资源。
实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
自定义删除器¶
不管是我们自己实现的shared_ptr还是库中的shared_ptr,我们在析构的时候默认都是 delete _ptr,如果我们托管的类型是 new T[] ,或者 malloc出来的话,就导致类型不是匹配的,无法析构。
为此,shared_ptr提供了 定制删除器,我们可以在构造的时候作为参数传入。如果我们不传参,就默认使用delete
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
智能指针作为形参和返回值¶
作为形参¶
- 引用传递 (
std::shared_ptr
&std::unique_ptr
):当你想在函数内部修改指针或其指向的对象,并影响调用者持有的指针时。1 2 3 4 5 6 7
void processShared(const std::shared_ptr<MyClass>& ptr) { // 读取ptr指向的对象 } void modifyUnique(std::unique_ptr<MyClass>& ptr) { // 修改ptr指向的对象 }
- 值传递 (
std::shared_ptr
):适用于需要拷贝指针的情况,通常用于std::shared_ptr
,因为它通过引用计数来共享所有权。
1 2 3 4 5 6 7 |
|
- 传递常量引用 (
const std::shared_ptr<T>&
):当你不需要修改指针但又不想拷贝(避免增加引用计数)时,适用于只读访问。
作为返回值¶
- 创建新对象:使用
std::make_unique
或std::make_shared
在函数内创建新对象,并返回这个智能指针。
1 2 3 4 5 6 7 |
|
std::unique_ptr
,当你想将一个对象的所有权从一个函数传递给另一个函数时。
- 共享所有权:对于
std::shared_ptr
,当多个所有者需要共享对同一对象的访问时。