前言
现代C++中,涉及的智能指针主要有:unique_ptr
, shared_ptr
, weak_ptr
三个,智能指针并非线程安全的,需要额外的互斥手段。
unique_ptr
这是独占指针,有以下特性:
- 两个指针不能指向同一资源
- 无法进行拷贝赋值与拷贝构造,只有移动赋值和移动构造
- 离开作用域自动释放资源
- 容器内可使用指针
#include <memory> // 实际上是在 unique_ptr.h 中
下面是API的详解:
- 构造函数
unique_ptr(); // 空构造函数
template<typename T> unique_ptr(T* ptr); // 使用ptr构造
template<typename T,typename D> unique_ptr(T* ptr, D deleter); // 使用ptr构造,然后使用deleter来析构,否则是用默认的 delete ptr析构
- 成员函数
T* get(); // 获取指针
T* release(); // 不在管理指针,并返回指针,需要手动 delete
void reset(T*); // 释放掉原来的资源,并管理新的资源,也可以设置为 nullptr ,就只释放原来的资源
auto p = make_unique(const T& value); // 创建一个智能指针
T& operator*(); // 可以使用 *p 来获取对象
T* operator->(); // 可以使用 p-> 来获取指针
void swap(unique_ptr& other); // 与另一个指针交换
shared_ptr
共享指针,以下特性:
- 所有管理同一个对象的引用计数是共同的,称为总引用计数,可简单认为是一个
long
类型的地址保存了ptr
地址资源的引用计数。 - 尽量使用
make_shared
创建,不得使用一个指针来初始多个shared_ptr
对象,会造成重复释放 - 拷贝赋值,拷贝构造都会增加总的引用计数
- 移动赋值,移动构造不会增加总的引用计数,会把原来的指针指针完全移动给新的指针
- 析构或者释放等只会计数减一,当总引用计数清零后,才会释放资源
- 避免循环引用
- 尽量不要用原始指针,防止智能指针自动释放资源导致原始指针使用异常
- 不要使用数组来初始化
下面是API的详解:
- 构造与赋值
auto ptr = make_shared<T>(const T& value); // 最好使用这个
auto ptr2 = ptr; // 拷贝构造函数
auto ptr3 = std::move(ptr2); // 移动构造,ptr2不再管理对象
shared_ptr<T> ptr4, ptr5;
ptr4 = ptr3; // 拷贝赋值
ptr5 = std::move(ptr3); // ptr3不再管理对象
- 成员函数
T* get(); // 获取指针
bool owner_before(const shared_ptr<T>& ptr); // 如果当前智能指针排在ptr之前,返回true(即小于它)
void reset(T* ptr, Deleter deleter = nullptr); // 释放当前资源,重新管理另一个资源
void swap(shared_ptr<T>& other); // 与另一个交换
long use_count(); // 返回总的引用计数
operator bool(); // 返回资源是否存在
T& operator*(); // 返回对象
T* operator->(); // 返回指针
weak_ptr
问题引出
weak_ptr
是为了辅助shared_ptr
而出现的,shared_ptr
会存在成环问题。
经典的成环问题如下:
class A;
class B;
class A(){
public:
std::shared_ptr<B> a2b;
}
class B(){
public:
std::shared_ptr<A> b2a;
}
const auto a = make_shared<A>();
const auto b = make_shared<B>();
a->a2b = b;
b->b2a = a;
// 如果 a 要释放掉 -> a2b 也需要释放掉 b 释放掉 -> b2a要释放掉 -> a要释放掉
// 形成闭环
可以把上述中的a2b
或者b2a
换成weak_ptr
。
解决方案
STL中引用计数分为强引用计数和弱引用计数,shared_ptr
是增加强引用计数的,而weak_ptr
只会增加弱引用计数。
不管是shared_ptr
还是weak_ptr
,其基类都会维持着两个引用计数,一个是强引用,一个是弱引用。
- 强引用:拥有或共享资源的“所有权”,即如果该智能指针不回收资源,这个资源一定会存在,哪怕其他智能指针回收了资源
- 弱引用:共享资源的“使用权”,如果资源的强引用归零,资源会消失,不具备管理资源的权力
weak_ptr
有着以下特性:
- 不控制资源的生命周期,只访问资源,但是资源可能会被强引用指针释放,一定要注意资源的生命周期
- 只能从
shared_ptr
或者另一个weak_ptr
中构造 - 如果需要管理生命周期了,可以生成一个强引用
- 只会增加弱引用计数
- 拷贝赋值,拷贝构造都会增加总的弱引用计数
- 移动赋值,移动构造不会增加总的弱引用计数,会把原来的指针指针完全移动给新的指针
- 并不重载
operator*,operator->
,需要使用转换成强引用 - 除非在知道会造成引用循环的时候才使用这个来打破引用循环,否则会导致资源被释放掉
一些API详解:
#include <memory>
shared_ptr<T> lock(); // 尝试获取一个强引用的指针,但是如果资源不在了,就会返回空,注意使用 if(l) 判断资源是否存在
bool expired(); // 检查资源是否释放了,相当于 use_count() == 0
long use_count(); // 返回强引用计数,注意是强引用计数,也就是关联的 shared_ptr 个数
void swap(weak_ptr& other); // 交换
void reset(); // 重置,减少响应的弱引用计数,没有其他重载
回到问题
class A;
class B;
class A(){
public:
std::weak_ptr<B> a2b;
}
class B(){
public:
std::shared_ptr<A> b2a;
}
const auto a = make_shared<A>();
const auto b = make_shared<B>();
a->a2b = b; // 变成弱引用了,但是 a->a2b 不会管理生命周期,真正的生命周期由 b 管理
b->b2a = a;
上述代码可以解决循环引用问题的,但是仍然会存在问题,就是如果b
生命周期结束,那么它管理的对象B
也会结束,a2b
不具备资源管理能力,因此再访问会造成空地址错误。
弱引用的出现,不仅仅是解决上述问题的,还有其他许多问题需要弱引用来解决,其重点在于解决临时“使用权”,这个使用需要lock
转换成强引用才能使用,所以一定需要搭配强引用来使用。