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
,查看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
- 命名空间
using namespace std;
首先我们先编写一个测试类,用来测试智能指针各个函数的作用,以及可能出现的问题,测试类的代码如下:
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; }
private:
int number;
};
-
测试函数
get
、release
、reset
、operator*
、operator->
、swap
、operator bool
这些函数在解释auto_ptr
的时候基本都提到过,swap
、operator bool
作为两个新的函数在解释shared_ptr
的时候也演示过,所以此处就不花过多的篇幅举例了,这里写到一个测试函数中,体会一下用法就好:void test1() { unique_ptr
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 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
的对象直接赋值的话,会在编译期间就提示错误:void test2() { //unique_ptr
ptr2 = new example(2);// 编译错误,不支持原始指针到智能指针的隐式转换 unique_ptr ptr2(new example(2)); // example: 2(输出内容) //unique_ptr ptr3 = ptr2; // 编译错误,...: 尝试引用已删除的函数 //unique_ptr ptr4(ptr2); // 编译错误,...: 尝试引用已删除的函数 unique_ptr ptr5(std::move(ptr2)); // 正常编译,使用move移动语义,符合预期效果 ptr5->test_print(); // in test print: number = 2(输出内容) } // ~example: 2(输出内容) // 出作用域被析构 -
测试
unique_ptr
作为参数和返回值
unique_ptr
是可以作为参数和返回值的,不过因为operator=
不允许使用,所以在作为参数的时候需要使用函数std::move()
,但是作为返回值却不需要,这里留个疑问,最后分析一下:void test3_inner1(unique_ptr
ptr3_1) { ptr3_1->test_print(); // in test print: number = 3(输出内容) } // ~example: 3(输出内容) // 出作用域被析构 unique_ptr test3_inner2() { unique_ptr ptr3_2(new example(32));// example:32(输出内容) ptr3_2->test_print(); // in test print: number = 32(输出内容) return ptr3_2; } void test3() { unique_ptr 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
在内在这种用法的情况下都不会发生问题:void test4_inner1(unique_ptr
* ptr4_1) { (*ptr4_1)->test_print(); // in test print: number = 4(输出内容) } // 指针传递没有析构 void test4_inner2(unique_ptr & ptr4_2) { ptr4_2->test_print(); // in test print: number = 4(输出内容) } // 引用传递没有析构 void test4() { unique_ptr 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
这一句,是不是感觉很神奇,居然不报编译错误,我感觉和作为返回值时是相同的处理。(new example(56)); void test5() { vector
> v(7); for (int i = 0; i < 6; i ) { v[i] = unique_ptr (new example(50 i)); // 依次输出example:70,...example:75 } // 直接赋值,迷之成功,不是不能operator=吗,这里实际上调用的还是std::move类似的移动语义? v[6] = unique_ptr (new example(56));// example:56(输出内容) // 直接将unique_ptr对象push_back v.push_back(unique_ptr (new example(57))); // example:57(输出内容) // 利用移动语义push_back v.push_back(std::move(unique_ptr (new example(58)))); // example:58(输出内容) // 利用make_unique创建unique_ptr,c 14才支持 v.push_back(make_unique (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
对象的“删除器”,如果不手动指定就会获得默认的删除器,否则就返回你指定的,举个例子一看就明白了,代码如下:// a custom deleter class custom_deleter { int flag; public: custom_deleter(int val) : flag(val) {} template
void operator()(t* p) { std::cout << "use custom deleter, flag=" << flag ; delete p; } }; void test6() { custom_deleter dlter(666); unique_ptr ptr6(new example(6), dlter); // example:6(输出内容) ptr6->test_print(); // in test print: number = 6(输出内容) // 调用get_deleter unique_ptr ptr7(new example(7), ptr6.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
相同的错误。
示例传送门:unique_ptr用法