shared_ptr浅析

前言

这个指针近乎完美,原来出现在boost库中,C++11时引入了标准库,解决了auto_ptr对内部对象独占的机制,转而采用引用计数的方式,每增加一次赋值,则引用计数加1,每析构一个智能指针对象,则引用计数减1,当引用计数为1时销毁智能指针对象的同时,也析构内部对象。这种采用引用计数方式避免了对象所有权转移,所以作为函数返回值,函数参数,容器的元素都不会有问题,但是因为引用计数的加入,相应的会带来对引用计数维护的开销。

auto_ptr一样,shared_ptr本身也是一个模板类,那么一般情况下直接用它来定义一个智能指针的对象,例如std::shared_ptr<Test> pa(new Test);需要注意的是pa虽然叫智能指针,但是它是一个对象,在它的内部保存着一个原始的对象的指针。查看shared_ptr的代码时发现,它主要有getswapresetuniqueuse_countoperator booloperator*operator->operator=几个函数,与auto_ptr相比少了release函数,但是多了swapuniqueuse_countoperator bool四个函数,下面通过一些例子来了解一下shared_ptr的具体用法。

使用环境

  1. VS2015 + Windows7(应该是C++11标准)
  2. 头文件#include <memory>
  3. 命名空间using namespace std;

测试过程

首先我们先编写一些测试类,用来测试智能指针各个函数的作用,以及可能出现的问题,测试类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Example
{
public:
Example(int param = 0)
{
number = param;
cout << "Example: " << number << endl;
}

~Example() { cout << "~Example: " << number << endl; }

void test_print() { cout << "in test print: number = " << number << endl; }

void set_number(int num) { number = num; }

int get_number() { return number; }

private:
int number;
};

  1. 测试函数get reset operator* operator->
    这几个函数与auto_ptr智能指针的用法一样,可以参考auto_ptr用法get函数可以获得智能指针包装的原始指针,可以用来判断被包装对象的有效性,也可以用来访问被包装对象,operator*可以直接对智能指针包装的原始指针解引用,获得被包装的对象,operator->用来取得原始对象的指针,引用成员时与get函数作用相同,reset函数用于重新设置内部对象,若参数为空,则表示取消对内部对象的引用,此时若引用计数大于1则进行减1操作,否则直接析构内部对象。需要注意的是普通的对象指针是无法隐式转换成shared_ptr的,需要利用构造函数实现,示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    void test1()
    {
    //error C2440: “初始化”: 无法从“Example *”转换为“std::shared_ptr<Example>”
    //shared_ptr<Example> ptr1 = new Example(1);

    shared_ptr<Example> ptr1(new Example(1)); // Example: 1(输出内容)
    if (ptr1.get()) // 调用函数get,获取原始指针,判断有效性
    {
    cout << "ptr1 is valid" << endl; // 原始指针有效
    }
    ptr1->test_print(); // in test print: number = 1(输出内容),调用operator->

    ptr1.reset(); // ~Example: 1(输出内容),调用函数reset,设置为空,释放原内部对象

    ptr1.reset(new Example(2)); // Example: 2(输出内容),重新申请对象并设置

    (*ptr1).test_print(); // in test print: number = 1(输出内容),调用operator*
    } // ~Example: 1(输出内容),出定义域,释放内部对象
  2. 测试函数operator bool用法
    operator bool函数其实就是用来判断内部对象是否有效的,若内部对象不为空则返回true,否则返回false,大概的实现就是return this->get() != nullptr;,测试代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void test2()
    {
    shared_ptr<Example> ptr2(new Example(2)); // Example: 2(输出内容)
    if (ptr2) // 调用operator bool
    cout << "ptr2 is valid" << endl; // ptr2 is valid(输出内容),说明ptr2是有效的

    ptr2.reset(); // ~Example: 2(输出内容),设置内部对象为空

    if (ptr2) // 调用operator bool
    cout << "ptr2 is valid" << endl; // 没有输出,说明ptr2已经无效
    }
  3. 测试函数swap用法
    从这个名字就可以看出,这个函数用于交换,那么是用来交换什么的呢?实际上是用来交换内部对象的,看下面的例子一试便知,代码运行过后,通过打印可以发现智能指针对象ptr3和ptr4的内部对象进行了交换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void test3()
    {
    shared_ptr<Example> ptr3(new Example(3)); // Example: 3(输出内容)
    shared_ptr<Example> ptr4(new Example(4)); // Example: 4(输出内容)
    ptr3->test_print(); // in test print: number = 3(输出内容)
    ptr4->test_print(); // in test print: number = 4(输出内容)

    ptr3.swap(ptr4); // 调用函数swap

    ptr3->test_print(); // in test print: number = 4(输出内容)
    ptr4->test_print(); // in test print: number = 3(输出内容)
    } // ~Example: 3(输出内容),出定义域,释放内部对象
    // ~Example: 4(输出内容),出定义域,释放内部对象
  4. 测试函数unique use_count operator=用法
    为什么把这几个函数放到一起来说,因为他们是息息相关的,首先函数operator=是用来处理赋值操作的,而赋值操作就会影响引用计数的变化,也就是赋值操作后,use_count函数查询到的引用计数会发生变化,而当use_count返回引用计数是1时,用来表明是否独自引用内部对象的函数unique也会返回true,换句话说unique函数的实现基本就是return this->use_count() == 1,测试代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    void test4()
    {
    shared_ptr<Example> ptr4(new Example(4)); // Example: 4(输出内容)
    if (ptr4.unique())
    {
    cout << "ptr4 is unique" << endl; // ptr4 is unique(输出内容)
    cout << "ptr4 use count : " << ptr4.use_count() << endl;// ptr4 use count : 1(输出内容)
    }

    shared_ptr<Example> ptr5 = ptr4;
    if (ptr4)
    cout << "ptr4 is valid" << endl;// ptr4 is valid(输出内容)说明赋值之后两个智能指针对象都有效

    if (ptr5)
    cout << "ptr5 is valid" << endl;// ptr5 is valid(输出内容)说明赋值之后两个智能指针对象都有效

    if (ptr4.unique())
    cout << "ptr4 is unique" << endl; // 没有输出,说明ptr4不是唯一管理内部对象的智能指针了

    cout << "ptr4 use count : " << ptr4.use_count() << endl; // ptr4 use count : 2(输出内容)
    cout << "ptr5 use count : " << ptr5.use_count() << endl; // ptr4 use count : 2(输出内容)
    } // ~Example: 4(输出内容),出定义域,释放内部对象
  5. 测试用同一个对象指针生成两个shared_ptr对象
    auto_ptr一样,我测试的结果是崩溃,官方标准网站上说是结果未定义,基本上就是说不靠谱,别这样干,仔细想想也能理解,虽说shared_ptr是通过引用计数方式实现,但也不是无所不能,比如这种情况,两个对象都是通过构造生成的,对内部对象的指针p都是“唯一”引用的,也就是两个对象的内部引用计数都是1,当第一个智能指针对象销毁时,会析构内部对象,当第二个智能指针对象销毁时,同样会析构内部对象,这样就造成了崩溃,测试如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void test5()
    {
    Example *p = new Example(5); // Example: 5(输出内容)
    shared_ptr<Example> ptr5(p);
    shared_ptr<Example> ptr6(p);

    cout << "ptr4 use count : " << ptr5.use_count() << endl;// ptr4 use count : 1(输出内容)
    cout << "ptr5 use count : " << ptr6.use_count() << endl;// ptr5 use count : 1(输出内容)
    } // ~Example: 3(输出内容),出定义域,ptr5释放内部对象
    // ~Example : -572662307(输出内容),出定义域,ptr6释放内部对象,程序崩溃
  6. 测试shared_ptr作为函数参数和返回值
    因为shared_ptr内部是引用计数,而不是独占所有权,所以在赋值的时候只改变引用计数,不会发生所有权转移,所以这两种用法基本没有问题,发生在auto_ptr上的崩溃惨剧也不会在这里上演,测试代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    void test6_inner1(shared_ptr<Example> ptr6_1)
    {
    ptr6_1->test_print(); // in test print: number = 6(输出内容)
    cout << "ptr6_1 use count : " << ptr6_1.use_count() << endl;// ptr6 use count : 2(输出内容)
    }

    shared_ptr<Example> test6_inner2()
    {
    shared_ptr<Example> ptr6_2(new Example(62)); // Example:62(输出内容)
    ptr6_2->test_print(); // in test print: number = 62(输出内容)
    cout << "ptr6_2 use count : " << ptr6_2.use_count() << endl;// ptr6_2 use count : 1(输出内容)
    return ptr6_2;
    }

    void test6()
    {
    shared_ptr<Example> ptr6(new Example(6)); // Example:6(输出内容)
    ptr6->test_print(); // in test print: number = 6(输出内容)
    cout << "ptr6 use count : " << ptr6.use_count() << endl;// ptr6 use count : 1(输出内容)
    test6_inner1(ptr6);
    cout << "ptr6 use count : " << ptr6.use_count() << endl;// ptr6 use count : 1(输出内容)

    ptr6 = test6_inner2(); // ~Example: 6(输出内容),ptr6接管新的对象,原来对象被析构
    cout << "ptr6 use count : " << ptr6.use_count() << endl;// ptr6 use count : 1(输出内容)
    } // ~Example: 62(输出内容),出定义域,ptr6释放内部对象
  7. 测试shared_ptr作为容器元素
    在这里也不存在auto_ptr作为容器元素时的争议,同样是引用计数的机制发挥了作用,使得他满足的容器的要求——其元素对象的拷贝与原对象相同或者等价,所以这里也不会出现问题,同时那些针对于容器的算法在shared_ptr上也可以大显身手,比如下面这个排序的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 一般会写成只读引用类型,这里为了说明问题才这样定义
    bool comp(shared_ptr<Example> a, shared_ptr<Example> b)
    {
    return a->get_number() > b->get_number();
    }

    void test7()
    {
    vector<shared_ptr<Example>> v(10);
    for (int i = 0; i < 10; i++)
    {
    v[i] = shared_ptr<Example>(new Example(70+i));
    }// 依次输出Example:70,Example:71,Example:72...Example:79

    // 循环调用
    for (int i = 0; i < 10; i++)
    {
    v[i]->test_print();
    }// 依次输出in test print: number = 70....in test print: number = 79

    sort(v.begin(), v.end(), comp); // 这可以正常运行,但是使用auto_ptr会死的很难看

    // 循环调用
    for (int i = 0; i < 10; i++)
    {
    v[i]->test_print();
    }// 依次输出in test print: number = 79....in test print: number = 70
    }// 依次输出~Example: 79,~Example: 78...~Example: 70
  8. 测试使用指针或者引用作为参数
    虽然shared_ptr作为参数、返回值、容器元素貌似没有丝毫问题了,但是有时还是使用shared_ptr对象的指针或者引用比较好,因为这样可以减少对对象的拷贝,毕竟对象的拷贝是需要消耗时间的,用更好的方式为什么不用呢,参考下面的用法,没有任何问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    void test8_inner1(shared_ptr<Example>* ptr8_1)
    {
    (*ptr8_1)->test_print(); // in test print: number = 8(输出内容)
    cout << "ptr8_1 use count : " << (*ptr8_1).use_count() << endl;// ptr8_1 use count : 1(输出内容)
    }

    void test8_inner2(shared_ptr<Example>& ptr8_2)
    {
    ptr8_2->test_print(); // in test print: number = 8(输出内容)
    cout << "ptr8_2 use count : " << ptr8_2.use_count() << endl;// ptr8_2 use count : 1(输出内容)
    }

    void test8()
    {
    shared_ptr<Example> ptr8(new Example(8)); // Example:8(输出内容)
    ptr8->test_print(); // in test print: number = 8(输出内容)
    cout << "ptr8 use count : " << ptr8.use_count() << endl;// ptr8 use count : 1(输出内容)
    test8_inner1(&ptr8);
    cout << "ptr8 use count : " << ptr8.use_count() << endl;// ptr8 use count : 1(输出内容)
    test8_inner2(ptr8);
    cout << "ptr8 use count : " << ptr8.use_count() << endl;// ptr8 use count : 1(输出内容)
    } // ~Example: 8(输出内容),出定义域,ptr8释放内部对像

现象分析

shared_ptrauto_ptr相比要优秀的多,这得益于其内部引用计数的实现,正是这种非独占所有权的方式,使其摆脱了auto_ptr的种种限制,并将其踢出了C++标准(auto_ptr在C++17中被移除),但是shared_ptr也不是完美无缺的,引用计数不能解决所的问题,并且可能会带来一些问题,比如“循环引用问题”,这个得靠后面我们即将说到的weak_ptr来解决,所以说没有什么结构是完美的,选择合适的就是最好的,综合前面多个测试的例子,可以得到一些经验。

总结

  1. shared_ptr作为目前最优秀的指针,取代auto_ptr是必然的,所以能使用shared_ptr的地方还是尽量使用shared_ptr
  2. 不要使用同一个原始对象的指针生成多个shared_ptr对象,这样使用会导致未定义的行为,比如test5这个函数就导致了崩溃和错误的输出。
  3. shared_ptr不是万能的,如果不加思考的把原始指针都替换成shared_ptr,虽然大部分能防止内存泄露,但是还会造成其他的问题,比如循环引用,这种情况需要使用weak_ptr来解决问题,如果不解决就会造成另一种形式的内存泄漏。
  4. 不要使用get返回的指针来初始化一个shared_ptr对象,这种的做法的本质与第2点一样,会造成未定义的行为。
  5. 尽量不要保存get函数返回的指针,因为你不知道什么时候这个指针对应的对象就被析构掉了,所以请“随用随取”。

测试源码

示例传送门:shared_ptr用法

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客