前言
unique_ptr
这个指针是C++11标准时被引入标准库的,有一种说法称它是boost::scoped_ptr
的一个分身,并且它在C++11的时候“转正”了,但是scoped_ptr
还被留在boost库中,看来没有转正的机会了,不过unique_ptr
与scoped_ptr
确实很像,unique_ptr
只比scoped_ptr
多了一个移动语义,可以通过std::move()
函数来转移内部对象的所有权。
其实在我看来,unique_ptr
与auto_ptr
是最像的,他设计之初就是为了替代auto_ptr
,其实两者基本上没有区别,如果把auto_ptr
限制一下,使其不能通过拷贝构造和赋值获得所有权,但是可以通过std::move()
函数获得所有权,那基本上就变成了unique_pr
,这一点通过下面的函数分析也可以看出,两者的函数基本一致。
unique_pr
作为一个模板类,可以直接用它来定义一个智能指针的对象,例如std::unique_pr<Test> pa(new Test);
,查看unique_pr
的代码时发现,它主要有get
、release
、reset
、operator*
、operator->
、operator=
、swap
、operator bool
、get_deleter
几个函数,相比于auto_ptr
常用函数来说,只多了swap
、operator bool
、get_deleter
这三个函数,基本上没什么变化,不过get_deleter
这个函数值的详细解释一下,下面通过一些例子来了解一下unique_pr
的具体用法。
使用环境
- 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
18class 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; }
private:
int number;
};
测试函数
get
、release
、reset
、operator*
、operator->
、swap
、operator bool
这些函数在解释auto_ptr
的时候基本都提到过,swap
、operator bool
作为两个新的函数在解释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
29
30void test1()
{
unique_ptr<Example> ptr1(new Example(1)); // Example: 1(输出内容)
if (ptr1.get()) // 调用get函数,判断内部指针的有效性
{
ptr1.get()->test_print(); // in test print: number = 1(输出内容)
ptr1->set_number(2); // 调用了operator->
(*ptr1).test_print(); // in test print: number = 2(输出内容)
}
if (ptr1) // 调用operator bool 检测内部对象的有效性
cout << "ptr1 is valid\n"; // ptr1 is valid(输出内容)
Example *p = ptr1.release(); // 调用release函数,取出内部对象
if (!ptr1) // 调用operator bool 检测内部对象的有效性
cout << "ptr1 is invalid\n"; // ptr1 is invalid(输出内容)
ptr1.reset(p); // 调用reset函数,重新设置内部对象
if (ptr1) // 调用operator bool 检测内部对象的有效性
cout << "ptr1 is valid\n"; // ptr1 is valid(输出内容)
ptr1->test_print(); // in test print: number = 2(输出内容)
unique_ptr<Example> ptr2(new Example(20)); // Example: 20(输出内容)
ptr1.swap(ptr2); // 调用swap函数,重新设置内部对象
ptr1->test_print(); // in test print: number = 20(输出内容)
ptr2->test_print(); // in test print: number = 2(输出内容)
ptr1.reset(); // ~Example: 20(输出内容)// 重置内部对象被销毁
} // ~Example: 2(输出内容) // 出作用域被析构测试函数
operator=
operator=
这个函数是unique_ptr
与auto_ptr
最大的区别,因为在auto_ptr
中,这个操作函数往往是导致问题出现的罪魁祸首,赋值之后所有权转移,原智能指针对象无效,这样往往会导致程序崩溃,所以在unique_ptr
中operator=
被禁止使用了,取而代之的是具有移动语义的std::move()
函数,如果unique_ptr
的对象直接赋值的话,会在编译期间就提示错误:1
2
3
4
5
6
7
8
9
10void test2()
{
//unique_ptr<Example> ptr2 = new Example(2);// 编译错误,不支持原始指针到智能指针的隐式转换
unique_ptr<Example> ptr2(new Example(2)); // Example: 2(输出内容)
//unique_ptr<Example> ptr3 = ptr2; // 编译错误,...: 尝试引用已删除的函数
//unique_ptr<Example> ptr4(ptr2); // 编译错误,...: 尝试引用已删除的函数
unique_ptr<Example> ptr5(std::move(ptr2)); // 正常编译,使用move移动语义,符合预期效果
ptr5->test_print(); // in test print: number = 2(输出内容)
} // ~Example: 2(输出内容) // 出作用域被析构测试
unique_ptr
作为参数和返回值unique_ptr
是可以作为参数和返回值的,不过因为operator=
不允许使用,所以在作为参数的时候需要使用函数std::move()
,但是作为返回值却不需要,这里留个疑问,最后分析一下: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
26void test3_inner1(unique_ptr<Example> ptr3_1)
{
ptr3_1->test_print(); // in test print: number = 3(输出内容)
} // ~Example: 3(输出内容) // 出作用域被析构
unique_ptr<Example> test3_inner2()
{
unique_ptr<Example> ptr3_2(new Example(32));// Example:32(输出内容)
ptr3_2->test_print(); // in test print: number = 32(输出内容)
return ptr3_2;
}
void test3()
{
unique_ptr<Example> ptr3(new Example(3)); // Example:3(输出内容)
ptr3->test_print(); // in test print: number = 3(输出内容)
//test3_inner1(ptr3); // 直接作为参数传递会报编译错误,不存在拷贝构造
test3_inner1(std::move(ptr3)); // 但是可以使用std::move的移动语义来实现
if (!ptr3)
cout << "ptr3 is invalid\n"; // ptr1 is valid(输出内容),移动之后ptr3无效
ptr3 = test3_inner2(); // 由于不允许调用构造或者赋值,此处使用了移动语义move
ptr3->test_print(); // in test print: number = 32(输出内容)
} // ~Example: 32(输出内容),出定义域ptr3释放内部对象测试
unique_ptr
类型的指针或者引用作为参数
这一点没有什么问题,因为不会发生所有权的转移和引用计数的增加,所有的智能指针,包括auto_ptr
在内在这种用法的情况下都不会发生问题:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18void test4_inner1(unique_ptr<Example>* ptr4_1)
{
(*ptr4_1)->test_print(); // in test print: number = 4(输出内容)
} // 指针传递没有析构
void test4_inner2(unique_ptr<Example>& ptr4_2)
{
ptr4_2->test_print(); // in test print: number = 4(输出内容)
} // 引用传递没有析构
void test4()
{
unique_ptr<Example> ptr4(new Example(4)); // Example:4(输出内容)
ptr4->test_print(); // in test print: number = 4(输出内容)
test4_inner1(&ptr4); // 取地址作为参数
test4_inner2(ptr4); // 引用作为参数
} // ~Example: 4(输出内容),出定义域ptr4释放内部对象测试
unique_ptr
作为容器元素
前面分析auto_ptr
的时候已经说过,auto_ptr
在作为容器元素时,是不具有跨平台性质的,因为在有的平台表现很正常,有的环境下直接编译报错,原因就是使用auto_ptr
很容易出错,不是说一定会出错,而是可能出问题,所以个别平台直接在编译期报错,防止后续的错误。而unique_ptr
作为容器元素时,表现很统一,没有任何问题,但是我感觉这里就有点牵强,后续再说,注意v[6] = unique_ptr<Example>(new Example(56));
这一句,是不是感觉很神奇,居然不报编译错误,我感觉和作为返回值时是相同的处理。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
28void test5()
{
vector<unique_ptr<Example>> v(7);
for (int i = 0; i < 6; i++)
{
v[i] = unique_ptr<Example>(new Example(50 + i)); // 依次输出Example:70,...Example:75
}
// 直接赋值,迷之成功,不是不能operator=吗,这里实际上调用的还是std::move类似的移动语义?
v[6] = unique_ptr<Example>(new Example(56));// Example:56(输出内容)
// 直接将unique_ptr对象push_back
v.push_back(unique_ptr<Example>(new Example(57))); // Example:57(输出内容)
// 利用移动语义push_back
v.push_back(std::move(unique_ptr<Example>(new Example(58)))); // Example:58(输出内容)
// 利用make_unique创建unique_ptr,C++14才支持
v.push_back(make_unique<Example>(59)); // Example:59(输出内容)
// 循环调用
for (int i = 0; i < 10; i++)
{
v[i]->test_print();
}// 依次输出in test print: number = 50....in test print: number = 59
}// 依次输出~Example: 50,~Example: 51...~Example: 59测试函数
get_deleter
这个函数还是第一次提到,作用就是获得unique_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// a custom deleter
class custom_deleter {
int flag;
public:
custom_deleter(int val) : flag(val) {}
template <class T>
void operator()(T* p)
{
std::cout << "use custom deleter, flag=" << flag ;
delete p;
}
};
void test6()
{
custom_deleter dlter(666);
unique_ptr<Example, custom_deleter> ptr6(new Example(6), dlter); // Example:6(输出内容)
ptr6->test_print(); // in test print: number = 6(输出内容)
unique_ptr<Example, custom_deleter> ptr7(new Example(7), ptr6.get_deleter()); // 调用get_deleter
// 重置智能指针,内部对象使用自定义删除器删除
ptr6.reset(); // 输出:use custom deleter, flag = 666~Example: 6
ptr7->test_print(); // in test print: number = 7(输出内容)
} // 输出:use custom deleter, flag = 666~Example: 7
现象分析
上面的几个例子都很简单,基本上看一遍就知道怎么用了,但是有一点让人很迷惑,就是operator=
的使用,最开始已经说过了,unique_ptr
中的operator=
已经被禁止使用了,但是例子中有两处很有争议,就是unique_ptr
作为函数返回值和直接把unique_ptr
赋值给vector元素,一开始我也不是太清楚,后来找资料时发现了一些线索,和大家分享一下:
当函数返回一个对象时,理论上会产生临时变量,那必然是会导致新对象的构造和旧对象的析构,这对效率是有影响的。C++编译针对这种情况允许进行优化,哪怕是构造函数有副作用,这叫做返回值优化(RVO),返回有名字的对象叫做具名返回值优化(NRVO),就那RVO来说吧,本来是在返回时要生成临时对象的,现在构造返回对象时直接在接受返回对象的空间中构造了。假设不进行返回值优化,那么上面返回unique_ptr会不会有问题呢?也不会。因为标准允许编译器这么做:
1.如果支持move构造,那么调用move构造。
2.如果不支持move,那就调用copy构造。
3.如果不支持copy,那就报错吧。
很显然,unique_ptr
本身是支持move
构造的,所以unique_ptr
对象可以被函数返回,另外我推测将unique_ptr
直接赋值给vector元素也利用了相似的操作,这里不太确定,希望了解的小伙伴能告知一下其中的原因。
说到这里,我们对unique_ptr
也有了整体的认识,说unique_ptr
是auto_ptr
的替代品,可是unique_ptr
真的优秀了吗?
我看未必,它并非不会再犯错,只是犯错的成本大了一些,如果使用std::move()
转移了所有权之后,再直接使用原来的智能指针对象,同样会使得程序崩溃。
其实auto_ptr
和unique_ptr
给我的感觉就是就好比租房子,租房时有些人喜欢看一下房东的房产证,有的人则无所谓,来个人说是房东他就敢跟人签合同,房屋所有权是通过房产证来转移的,使用auto_ptr
就好像两个人可以私下交易,把钱和房产证直接交换,房产证的转移很随便,使用unique_ptr
就好比在转移房产的时候需要放鞭炮、然后在全世界广播一下,比较麻烦,并且有可能被租房的人看到,但是本质是一样的,都是拿钱来转移房的所有权,关键还是看租房的人,如果租房先看房产证,即使是房产证的转移很随便(也就是使用auto_ptr
),也不会出问题,如果租房根本不看房产证,即使房产证交易通知了世界上所有人(即使用unique_ptr
),也会租到没证的房子(程序崩溃)。
所以说unique_ptr
并没有消除错误,仅仅是提高了犯错的成本。
总结
- 对比
auto_ptr
和unique_ptr
后发现,unique_ptr
几乎只是将auto_ptr
的operator=
改为std::move()
函数。 - 现在标准库中只剩下了
shared_ptr
、weak_ptr
和unique_ptr
三个智能指针,weak_ptr
是为了解决shared_ptr
的循环引用问题而存在的,有其特定的使用情况,所以只剩下了shared_ptr
和unique_ptr
的选择,选择的标准就是看是否需要对原对象共享所有权,如果需要使用shared_ptr
,如果不需要是独占所有权的使用unique_ptr
。 unique_ptr
并没有从根本上消除可能错误,仅仅是提高了犯错的成本,并且给出移动所有权的提示,但是在容器vector元素赋值时依然很隐晦,可能造成auto_ptr
相同的错误。