前言

现代C++中,涉及的智能指针主要有:unique_ptr, shared_ptr, weak_ptr三个,智能指针并非线程安全的,需要额外的互斥手段。

unique_ptr

这是独占指针,有以下特性:

  • 两个指针不能指向同一资源
  • 无法进行拷贝赋值与拷贝构造,只有移动赋值和移动构造
  • 离开作用域自动释放资源
  • 容器内可使用指针
#include <memory> // 实际上是在 unique_ptr.h 中

下面是API的详解:

  1. 构造函数
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析构
  1. 成员函数
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的详解:

  1. 构造与赋值
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不再管理对象
  1. 成员函数
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转换成强引用才能使用,所以一定需要搭配强引用来使用。