智能指针
unique_ptr
- 独占资源所有权的指针。由于没有引用计数,因此性能较好。
- 离开
unique_ptr
对象的作用域时,会自动释放资源。 unique_ptr
本质是一个类,将复制构造函数和赋值构造函数声明为delete
就可以实现独占式,
只允许移动构造和移动赋值。unique_ptr
所持有的对象只能通过转移语义(move)
将所有权转移
到另外一个unique_ptr
。
1 | // 自定义实现 unique_ptr |
1 | std::unique_ptr<int> uptr = std::make_unique<int>(200); |
std::unique_ptr
是move-only
的。
1 | std::unique_ptr<int> uptr = std::make_unique<int>(200); |
std::unique_ptr
可以指向一个数组。
1 | std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10); |
shared_ptr
std::shared_ptr
其实就是对资源做引用计数——当引用计数为0
的时候,自动释放资源。
C++ 智能指针的实现(手撕 shared_ptr)
1 |
|
std::shared_ptr 的实现原理
shared_ptr
需要维护的信息有两部分:- 指向共享资源的指针
- 引用计数等共享资源的控制信息——实现上是维护一个指向控制信息的指针。
- 所以,
shared_ptr
对象需要保存两个指针(shared_ptr 大小为 16 )。shared_ptr
的deleter
是保存在控制信息中,所以,是否有自定义deleter
不影响shared_ptr
对象的大小。 - 当我们创建一个 shared_ptr 时,其实现一般如下:
1 | std::shared_ptr<T> sptr1(new T); |
- 复制一个 shared_ptr:
1 | std::shared_ptr<T> sptr2 = sptr1; |
- 为什么控制信息和每个
shared_ptr
对象都需要保存指向共享资源的指针?可不可以去掉shared_ptr
对象中指向共享资源的指针,以节省内存开销? - 答案是:不能。 因为
shared_ptr
对象中的指针指向的对象不一定和控制块中的指针指向的对象一样。 - 来看一个例子:
1 | struct Fruit |
- 另外,
std::shared_ptr
支持aliasing constructor
1 | template< class Y > |
Aliasing Constructor
: 简单说就是构造出来的shared_ptr
对象和参数r
指向同一个控制块(会影响r
指向的资源的生命周期),但是指向共享资源的指针是参数ptr
。看下面这个例子。
1 | using Vec = std::vector<int>; |
- 看上面的例子,使用
std::shared_ptr
时,会涉及两次内存分配:一次分配共享资源对象;一次分配控制块。C++ 标准库提供了std::make_shared
函数来创建一个shared_ptr
对象,只需要一次内存分配。
- 这种情况下,不用通过控制块中的指针,我们也能知道共享资源的位置——这个指针也可以省略掉。
shared_ptr 是线程安全的吗
shared_ptr
在 C++ 中是部分线程安全的, 这意味着它不是在所有情况下都是安全的
线程安全的部分
std::shared_ptr
的引用计数(即管理对象的共享所有权的计数)是线程安全的。因为std::shared_ptr
的内部引用计数是原子的,这意味着多个线程可以安全地对同一个std::shared_ptr
对象进行引用计数的操作(如shared_ptr
的拷贝构造和赋值)。这些操作不会导致数据竞争。
1 | std::shared_ptr<int> p = std::make_shared<int>(0); |
- 初始引用计数:
p
初始时有一个引用计数,因为它本身就是一个std::shared_ptr
。因此,初始的引用计数是1
。 - 线程
t1
和t2
的操作:每个线程将p
赋值给一个包含10000
个元素的向量中的每个元素。每次赋值操作都会增加p
的引用计数。由于有两个线程,每个线程都会增加10000
次引用计数。因此,总的引用计数增加量是10000 + 10000 = 20000
- 最终引用计数:初始引用计数
1
加上两个线程增加的引用计数20000
,总引用计数为1 + 20000 = 20001
- 这里的关键在于每次赋值操作都会原子地增加引用计数。因此,即使两个线程同时执行
sp_arr[i] = p;
,也不会导致数据竞争或未定义行为。
线程不安全的部分
对象的访问
- 尽管
std::shared_ptr
的引用计数是线程安全的,但对所管理对象的访问并不是线程安全的。如果多个线程同时访问同一个shared_ptr
管理的对象,并且至少有一个线程在修改该对象,那么就需要额外的同步机制(如互斥锁)来确保线程安全。
1 | std::shared_ptr<int> p1 = std::make_shared<int>(0); |
- 上面的代码运行,输出的结果不是预想的
20000
,每次运行输出的结果都会发生变化。因此同时修改shared_ptr
指向的对象不是线程安全的。
直接修改 shared_ptr 对象本身的指向
- 如果多个线程同时修改同一个
std::shared_ptr
对象的指向(例如,使用赋值操作或重置操作),这将导致数据竞争。 - 数据竞争可能导致以下问题:
- 引用计数的损坏:如果一个线程在修改 shared_ptr 的指向时,另一个线程也在修改它,可能会导致引用计数不一致,从而导致内存泄漏或双重释放。
- 未定义行为:访问已释放的内存或访问无效的指针。
1 |
|
- 多次运行上面的代码会发现,输出的
value
值有时候会是一个乱码数字,不是预期的100
。为了避免这些问题,需要使用互斥锁(std::mutex)
来同步对sharedPtr
的修改。
1 | std::shared_ptr<int> sharedPtr = std::make_shared<int>(42); // 创建一个 shared_ptr |
weak_ptr
std::weak_ptr
要与std::shared_ptr
一起使用。 一个std::weak_ptr
对象看做是std::shared_ptr
对象管理的资源的观察者,它不影响共享资源的生命周期:- 如果需要使用
weak_ptr
正在观察的资源,可以将weak_ptr
提升为shared_ptr
- 当
shared_ptr
管理的资源被释放时,weak_ptr
会自动变成nullptr
- 如果需要使用
1 | void Observe(std::weak_ptr<int> wptr) |
- 当
shared_ptr
析构并释放共享资源的时候,只要weak_ptr
对象还存在,控制块就会保留,weak_ptr
可以通过控制块观察到对象是否存活。
enable_shared_from_this
- 一个类的成员函数如何获得指向自身(
this
)的shared_ptr
?
1 | class Foo |
- 上面的代码其实会生成两个独立的
shared_ptr
,他们的控制块是独立的,最终导致一个Foo
对象会被delete
两次。
- 成员函数获取
this
的shared_ptr
的正确的做法是继承std::enable_shared_from_this
。
1 | class Bar : public std::enable_shared_from_this<Bar> |
- 一般情况下,继承了
std::enable_shared_from_this
的子类,成员变量中会增加一个指向this
的weak_ptr
。这个weak_ptr
在第一次创建shared_ptr
的时候会被初始化,指向this
。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 SleepyLoser's Blog!
评论