三大智能指针

智能指针的思想

c++ 要求程序员自己管理内存,为程序员提供了更高自由度,但更高的自由度同时意味着更多责任。为了减少c++程序员在使用裸指针时可能带来的内存泄露,c++11 引入智能指针帮助程序员管理内存。智能指针背后的设计思想是RAII

unique_ptr

unique_ptr 设计的目的是保证指针变量只指向一个实体,避免出现有其他指针变量指向相同实体,或者此指针变量指向同类型的其他实体。可以理解为指针变量与实体之间是一对一的关系。
为了实现上述设计目标,unique_ptr 的拷贝构造函数以及赋值构造函数都声明为deleted(c++11引入,和将拷贝构造函数以及赋值构造函数声明为private实现的效果一致)。所以unique_ptr 只允许使用裸指针初始化或者使用其他unique_ptr移动构造。比较特殊的是,一个新的unique_ptr变量可以从返回值为unique_ptr的函数构造。

unique_ptr<int> p(new int(2));   // allowed
unique_ptr<int> p1(p);     // not allowed
unique_ptr<int> p2 = p;  // not allowed
unique_ptr<int> p3(std::move(p)); // allowed
unique_ptr<int> p4 = std::move(p) // allowed

unique_ptr<int> foo()
{
    unique_ptr<int> p(new int(2));
    return p;
}

unique_ptr<int> p5 = foo();  // allowed

在引入 unique_ptr 之前,存在一个叫做auto_ptr 的智能指针,auto_ptr 与 unique_ptr 不同点是auto_ptr没有禁止拷贝构造和赋值构造,并且在执行拷贝构造和赋值构造后,之前的指针变量指向null,再次解引用时会产生运行时错误,这就是auto_ptr被广为诟病的地方。unique_ptr只有在被显示移动的时候才会将实体的所有权交给其他变量。现auto_ptr已经弃用。

shared_ptr

unique_ptr 实现指针变量与实体之前一一对应关系,但是在实际应用中,我们经常存在多个指针变量共同指向一个实体的场景,当所有指针变量的生命周期结束时再释放相应的资源。shared_ptr 就是为了解决此问题而引入。
简单的来讲,shared_ptr 实现的原理是在内部使用一个引用计数,每当有一个新的指针变量绑定到实体上时内部的引用计数加一,如果有指针变量生命周期结束引用计数减一,当引用计数为0时,释放相应资源。
shared_ptr 可以使用 use_count() 方法查看引用计数大小。
在多线程的环境下使用shared_ptr的const成员方法不需要外部的同步机制,使用shared_ptr的非const方法时需要重载shared_ptr的原子方法防止数据竞争。
需要注意的是,shared_ptr的引用计数内部的实现原理是一个原子变量,因此在多线程的环境下有大量的shared_ptr变量的复制以及销毁的话会带来比较大的性能损耗,所以如果新的shared_ptr 变量不需要改变指向的实体的内容时应该按照 const reference的方式使用。这个问题在我实际开发中遇到过,问题比较隐蔽,花费了大量时间才定位到。

weak_ptr

weak_ptr 的定义

weak_ptr 是为了配合shared_ptr而引入,weak_ptr 指向shared_ptr 管理的对象但是不会增加shared_ptr内部的引用计数,也就是说,不管是否有weak_ptr 指向对象,只要是shared_ptr的引用计数为0时,对象也会被销毁。weak_ptr 没有重载 * 以及 -> 方法。weak_ptr 提供了use_count 查看观察的shared_ptr的引用计数,使用expired 方法判断shared_ptr 指向的对象是否被销毁,使用lock方法创建一个新的shared_ptr 变量。

shared_ptr 循环引用问题

考虑以下代码

#include <memory>
#include <iostream>

class B;

class A
{
  public:
    A()
    {
        std::cout << "A construct.\n";
    }

    ~A()
    {
        std::cout << "A destruct.\n";
    }

    std::shared_ptr<B> pb;

};

class B
{
  public:
    B()
    {
        std::cout << "B construct.\n";
    }

    ~B()
    {
        std::cout << "B destruct.\n";
    }

    std::shared_ptr<A> pa;

};

int main()
{
    std::shared_ptr<A> a(new A());
    std::shared_ptr<B> b(new B());

    a->pb = b;
    b->pa = a;

    return 0;
}

编译后执行结果如下

A construct.
B construct.

可以看到,由于互相引用,shared_ptr a 以及 b 所管理的对象都没有释放。我们把上述代码中的类A 以及类B中成员变量都声明为weak_ptr 类型后重新编译运行后的结果如下

A construct.
B construct.
B destruct.
A destruct.

由此可见,weak_ptr 不会增加shared_ptr 的互相引用的问题,可以保证shared_ptr所管理的资源可以正确释放。

weak_ptr 与观察者模式

利用weak_ptr的特性,使用weak_ptr 以及shared_ptr配合可以实现观察者模式