前言
这个指针近乎完美,原来出现在boost库中,C++11时引入了标准库,解决了auto_ptr
对内部对象独占的机制,转而采用引用计数的方式,每增加一次赋值,则引用计数加1,每析构一个智能指针对象,则引用计数减1,当引用计数为1时销毁智能指针对象的同时,也析构内部对象。这种采用引用计数方式避免了对象所有权转移,所以作为函数返回值,函数参数,容器的元素都不会有问题,但是因为引用计数的加入,相应的会带来对引用计数维护的开销。
与auto_ptr
一样,shared_ptr
本身也是一个模板类,那么一般情况下直接用它来定义一个智能指针的对象,例如std::shared_ptr<Test> pa(new Test);
需要注意的是pa
虽然叫智能指针,但是它是一个对象,在它的内部保存着一个原始的对象的指针。查看shared_ptr
的代码时发现,它主要有get
、swap
、reset
、unique
、use_count
、operator bool
、operator*
、operator->
、operator=
几个函数,与auto_ptr
相比少了release
函数,但是多了swap
、unique
、use_count
、operator bool
四个函数,下面通过一些例子来了解一下shared_ptr
的具体用法。
使用环境
- VS2015 + Windows7(应该是C++11标准)
- 头文件
#include <memory>
- 命名空间
using namespace std;
测试过程
首先我们先编写一些测试类,用来测试智能指针各个函数的作用,以及可能出现的问题,测试类的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class 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;
};
测试函数
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
18void 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(输出内容),出定义域,释放内部对象测试函数
operator bool
用法operator bool
函数其实就是用来判断内部对象是否有效的,若内部对象不为空则返回true,否则返回false,大概的实现就是return this->get() != nullptr;
,测试代码如下:1
2
3
4
5
6
7
8
9
10
11void 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已经无效
}测试函数
swap
用法
从这个名字就可以看出,这个函数用于交换,那么是用来交换什么的呢?实际上是用来交换内部对象的,看下面的例子一试便知,代码运行过后,通过打印可以发现智能指针对象ptr3和ptr4的内部对象进行了交换:1
2
3
4
5
6
7
8
9
10
11
12
13void 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(输出内容),出定义域,释放内部对象测试函数
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
22void 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(输出内容),出定义域,释放内部对象测试用同一个对象指针生成两个
shared_ptr
对象
与auto_ptr
一样,我测试的结果是崩溃,官方标准网站上说是结果未定义,基本上就是说不靠谱,别这样干,仔细想想也能理解,虽说shared_ptr
是通过引用计数方式实现,但也不是无所不能,比如这种情况,两个对象都是通过构造生成的,对内部对象的指针p
都是“唯一”引用的,也就是两个对象的内部引用计数都是1,当第一个智能指针对象销毁时,会析构内部对象,当第二个智能指针对象销毁时,同样会析构内部对象,这样就造成了崩溃,测试如下:1
2
3
4
5
6
7
8
9
10void 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释放内部对象,程序崩溃测试
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
25void 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释放内部对象测试
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测试使用指针或者引用作为参数
虽然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
22void 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_ptr
与auto_ptr
相比要优秀的多,这得益于其内部引用计数的实现,正是这种非独占所有权的方式,使其摆脱了auto_ptr
的种种限制,并将其踢出了C++标准(auto_ptr
在C++17中被移除),但是shared_ptr
也不是完美无缺的,引用计数不能解决所的问题,并且可能会带来一些问题,比如“循环引用问题”,这个得靠后面我们即将说到的weak_ptr
来解决,所以说没有什么结构是完美的,选择合适的就是最好的,综合前面多个测试的例子,可以得到一些经验。
总结
shared_ptr
作为目前最优秀的指针,取代auto_ptr
是必然的,所以能使用shared_ptr
的地方还是尽量使用shared_ptr
。- 不要使用同一个原始对象的指针生成多个
shared_ptr
对象,这样使用会导致未定义的行为,比如test5
这个函数就导致了崩溃和错误的输出。 shared_ptr
不是万能的,如果不加思考的把原始指针都替换成shared_ptr
,虽然大部分能防止内存泄露,但是还会造成其他的问题,比如循环引用,这种情况需要使用weak_ptr
来解决问题,如果不解决就会造成另一种形式的内存泄漏。- 不要使用
get
返回的指针来初始化一个shared_ptr
对象,这种的做法的本质与第2点一样,会造成未定义的行为。 - 尽量不要保存
get
函数返回的指针,因为你不知道什么时候这个指针对应的对象就被析构掉了,所以请“随用随取”。