概述

众所周知 ,指针是C/C++ 最大的特点,同时也是C/C++ 程序员最头疼的地方,因为你总是会在内存释放、内存泄露这些老生常谈的问题上面出问题,java最受我羡慕的就是它有一个完整的内存管理器,不用我们去头疼内存的管理问题。C++ 中智能指针的出现就是为了缓解我们对于内存管理方面的焦虑。

auto指针

从C++98开始便推出了auto_ptr,对裸指针进行封装,让程序员无需手动释放指针指向的内存区域,在auto_ptr生命周期结束时自动释放,然而,由于auto_ptr在转移指针所有权后会产生野指针,导致程序运行时crash。

如下例子:

auto_ptr<int> p1(new int(10));
auto_ptr<int> p2 = p1; //转移控制权
*p1 += 10; //crash,p1为空指针,可以用p1->get判空做保护

C++11 智能指针

unique_ptr

unique_ptr是auto_ptr的继承者,对于同一块内存只能有一个持有者,而unique_ptr和auto_ptr唯一区别就是unique_ptr不允许赋值操作,也就是不能放在等号的右边(函数的参数和返回值例外),这一定程度避免了一些误操作导致指针所有权转移,然而,unique_str依然有提供所有权转移的方法move,调用move后,原unique_ptr就会失效,再用其访问裸指针也会发生和auto_ptr相似的crash,如下面示例代码,所以,即使使用了unique_ptr,也要慎重使用move方法,防止指针所有权被转移。

unique_ptr<int> up(new int(5));
//auto up2 = up; // 编译错误
auto up2 = move(up);
cout << *up << endl; //crash,up已经失效,无法访问其裸指针

shared_ptr 引用型

auto_ptr和unique_ptr都有或多或少的缺陷,因此C++11还推出了shared_ptr,这也是目前工程内使用最多最广泛的智能指针,他使用引用计数(感觉有参考Objective-C的嫌疑),实现对同一块内存可以有多个引用,在最后一个引用被释放时,指向的内存才释放,这也是和unique_ptr最大的区别。

另外,使用shared_ptr过程中有几点需要注意:

构造shared_ptr的方法,如下示例代码所示,我们尽量使用shared_ptr构造函数或者make_shared的方式创建shared_ptr,禁止使用裸指针赋值的方式,这样会shared_ptr难于管理指针的生命周期。

// 使用裸指针赋值构造,不推荐,裸指针被释放后,shared_ptr就野了,不能完全控制裸指针的生命周期,失去了智能指针价值

int *p = new int(10);
shared_ptr<int>sp = p;
delete p; // sp将成为野指针,使用sp将crash


// 将裸指针作为匿名指针传入构造函数,一般做法,让shared_ptr接管裸指针的生命周期,更安全
shared_ptr<int>sp1(new int(10));


// 使用make_shared,推荐做法,更符合工厂模式,可以连代码中的所有new,更高效;方法的参数是用来初始化模板类
shared_ptr<int>sp2 = make_shared<int>(10);

禁止使用指向shared_ptr的裸指针,也就是智能指针的指针,这听起来就很奇怪,但开发中我们还需要注意,使用shared_ptr的指针指向一个shared_ptr时,引用计数并不会加一,操作shared_ptr的指针很容易就发生野指针异常。

shared_ptr<int>sp = make_shared<int>(10);
cout << sp.use_count() << endl; //输出1
shared_ptr<int> *sp1 = &sp;
cout << (*sp1).use_count() << endl; //输出依然是1
(*sp1).reset(); //sp成为野指针
cout << *sp << endl; //crash

使用shared_ptr创建动态数组,在介绍unique_ptr时我们就讲过创建动态数组,而shared_ptr同样可以做到,不过稍微复杂一点,如下代码所示,除了要显示指定析构方法外(因为默认是T的析构函数,不是T[]),另外对外的数据类型依然是shared_ptr,非常有迷惑性,看不出来是数组,最后不能直接使用下标读写数组,要先get()获取裸指针才可以使用下标。所以,不推荐使用shared_ptr来创建动态数组,尽量使用unique_ptr,这可是unique_ptr为数不多的优势了。

template <typename T>
shared_ptr<T> make_shared_array(size_t size) {
    return shared_ptr<T>(new T[size], default_delete<T[]>());
}

shared_ptr<int>sp = make_shared_array(10); //看上去是shared<int>类型,实际上是数组
sp.get()[0] = 100; //不能直接使用下标读写数组元素,需要通过get()方法获取裸指针后再操作

用shared_ptr实现多态,在我们使用裸指针时,实现多态就免不了定义虚函数,那么用shared_ptr时也不例外,不过有一处是可以省下的,就是析构函数我们不需要定义为虚函数了,如下面代码所示:

class A {
public:
    ~A() {
        cout << "dealloc A" << endl;
    }
};
class B : public A {
public:
    ~B() {
        cout << "dealloc B" << endl;
    }
};
int main(int argc, const char * argv[]) {
    A *a = new B();
    delete a; //只打印dealloc A
    shared_ptr<A>spa = make_shared<B>(); //析构spa是会先打印dealloc B,再打印dealloc A
    return 0;
}

循环引用,笔者最先接触引用计数的语言就是Objective-C,而OC中最常出现的内存问题就是循环引用,如下面代码所示,A中引用B,B中引用A,spa和spb的强引用计数永远大于等于1,所以直到程序退出前都不会被退出,这种情况有时候在正常的业务逻辑中是不可避免的,而解决循环引用的方法最有效就是改用weak_ptr,具体可见下一章。

class A {
public:
    shared_ptr<B> b;
};
class B {
public:
    shared_ptr<A> a;
};
int main(int argc, const char * argv[]) {
    shared_ptr<A> spa = make_shared<A>();
    shared_ptr<B> spb = make_shared<B>();
    spa->b = spb;
    spb->a = spa;
    return 0;
} //main函数退出后,spa和spb强引用计数依然为1,无法释放

weak_ptr

正如上一章提到,使用shared_ptr过程中有可能会出现循环引用,关键原因是使用shared_ptr引用一个指针时会导致强引用计数+1,从此该指针的生命周期就会取决于该shared_ptr的生命周期,然而,有些情况我们一个类A里面只是想引用一下另外一个类B的对象,类B对象的创建不在类A,因此类A也无需管理类B对象的释放,这个时候weak_ptr就应运而生了,使用shared_ptr赋值给一个weak_ptr不会增加强引用计数(strong_count),取而代之的是增加一个弱引用计数(weak_count),而弱引用计数不会影响到指针的生命周期,这就解开了循环引用,上一章最后的代码使用weak_ptr可改造为如下代码。

class A {
public:
    shared_ptr<B> b;
};
class B {
public:
    weak_ptr<A> a;
};
int main(int argc, const char * argv[]) {
    shared_ptr<A> spa = make_shared<A>();
    shared_ptr<B> spb = make_shared<B>();
    spa->b = spb; //spb强引用计数为2,弱引用计数为1
    spb->a = spa; //spa强引用计数为1,弱引用计数为2
    return 0;
} //main函数退出后,spa先释放,spb再释放,循环解开了

使用weak_ptr也有需要注意的点,因为既然weak_ptr不负责裸指针的生命周期,那么weak_ptr也无法直接操作裸指针,我们需要先转化为shared_ptr,这就和OC的Strong-Weak Dance有点像了,具体操作如下:

shared_ptr<int> spa = make_shared<int>(10);
weak_ptr<int> spb = spa; //weak_ptr无法直接使用裸指针创建
if (!spb.expired()) { //weak_ptr最好判断是否过期,使用expired或use_count方法,前者更快
    *spb.lock() += 10; //调用weak_ptr转化为shared_ptr后再操作裸指针
}
cout << *spa << endl; //20

智能指针原理

使用栈对象管理堆对象

在C++中,内存会分为三部分,堆、栈和静态存储区,静态存储区会存放全局变量和静态变量,在程序加载时就初始化,而堆是由程序员自行分配,自行释放的,例如我们使用裸指针分配的内存;而最后栈是系统帮我们分配的,所以也会帮我们自动回收。因此,智能指针就是利用这一性质,通过一个栈上的对象(shared_ptr或unique_ptr)来管理一个堆上的对象(裸指针),在shared_ptr或unique_ptr的析构函数中判断当前裸指针的引用计数情况来决定是否释放裸指针。

shared_ptr引用计数的原理

一开始笔者以为引用计数是放在shared_ptr这个模板类中,但是细想了一下,如果这样将shared_ptr赋值给另一个shared_ptr时,是怎么做到两个shared_ptr的引用计数同时加1呢,让等号两边的shared_ptr中的引用计数同时加1?不对,如果还有第二个shared_ptr再赋值给第三个shared_ptr那怎么办呢?或许通过下面的类图便清楚个中奥秘。

智能指针UML.jpg
[ boost中shared_ptr与weak_ptr类图 ]

我们重点关注shared_ptr的类图,它就是我们可以直接操作的类,这里面包含裸指针T*,还有一个shared_count的对象,而shared_count对象还不是最终的引用计数,它只是包含了一个指向sp_counted_base的指针,这应该就是真正存放引用计数的地方,包括强应用计数和弱引用计数,而且shared_count中包含的是sp_counted_base的指针,不是对象,这也就意味着假如shared_ptr a = b,那么a和b底层pi_指针指向的是同一个sp_counted_base对象,这就很容易做到多个shared_ptr的引用计数永远保持一致了。

多线程安全

本章所说的线程安全有两种情况:

多个线程操作多个不同的shared_ptr对象

C++11中声明了shared_ptr的计数操作具有原子性,不管是赋值导致计数增加还是释放导致计数减少,都是原子性的,这个可以参考sp_counted_base的源码,因此,基于这个特性,假如有多个shared_ptr共同管理一个裸指针,那么多个线程分别通过不同的shared_ptr进行操作是线程安全的。

多个线程操作同一个shared_ptr对象

同样的道理,既然C++11只负责sp_counted_base的原子性,那么shared_ptr本身就没有保证线程安全了,加入两个线程同时访问同一个shared_ptr对象,一个进行释放(reset),另一个读取裸指针的值,那么最后的结果就不确定了,很有可能发生野指针访问crash。

作者:腾讯技术工程
窥见C++11智能指针