[7] 智能指针总结
【1】STL中的auto_ptr、unique_ptr、shared_ptr的联系
1.1 共性
(1)智能指针模板位于名称空间std中,所有智能指针模板类的应用语法相同。
举例如下:
1 #include <iostream> 2 #include <memory> 3 #include <string> 4 5 class Report 6 { 7 private: 8 std::string str; 9 public: 10 Report(const std::string s) : str(s) 11 { 12 std::cout << "Object Created!\n"; 13 } 14 ~Report() 15 { 16 std::cout << "Object Deleted!\n"; 17 } 18 void comment() const 19 { 20 std::cout << str << "\n"; 21 } 22 }; 23 24 void main() 25 { 26 { 27 std::auto_ptr<Report> ps(new Report("using auto_ptr")); 28 ps->comment(); 29 } 30 31 { 32 std::unique_ptr<Report> ps(new Report("using unique_ptr")); 33 ps->comment(); 34 } 35 36 { 37 std::shared_ptr<Report> ps(new Report("using shared_ptr")); 38 ps->comment(); 39 } 40 41 std::cin.get(); 42 } 43 // Run out 44 /* 45 Object Created! 46 using auto_ptr 47 Object Deleted! 48 Object Created! 49 using unique_ptr 50 Object Deleted! 51 Object Created! 52 using shared_ptr 53 Object Deleted! 54 */
(2)所有的智能指针类都只有一个explicit构造函数,这个构造函数将指针作为参数。因此不需要自动(隐式)将常规指针转换为智能指针对象,举例如下:
1 std::shared_ptr<double> pd; 2 double *p_reg = new double; 3 // pd = p_reg; // not allowed. Error: no operator"="matches these operands 4 pd = std::shared_ptr<double>(p_reg); 5 // std::shared_ptr<double> pshared = p_reg; // not allowed. 6 std::shared_ptr<double> pshared(p_reg);
(3)所有智能指针对象的构建都不可使用非堆内存指针作为实参。程序可通过编译,但运行将崩溃。
举例如下:
1 std::string tempObject("I am programmer."); 2 std::shared_ptr<std::string> pt(&tempObject);
(4)为了规避两个智能指针对象指向同一块特定内存引起的析构隐患问题,主要设计策略分为三种模型:深复制模型,所有权模型,引用计数模型。
深复制模型。定义拷贝构造函数、赋值运算符时,使之执行深复制。这样两个指针将指向不同的对象,其中一个是另一个的副本。
所有权模型。建立所有权的概念,对于特定的对象,只能有一个智能指针可拥有它。然后,让拷贝操作和赋值操作转让所有权。这是用于auto_ptr和unique_ptr的策略。
引用计数模型。通过跟踪指向特定对象的智能指针数,创建智能更高的指针。这是shared_ptr的策略。
1.2 区别
(1)既然auto_ptr和unique_ptr都是所有权模型,那么两者有何不同?请看如下应用示例:
1 #include <iostream>
2 #include <string>
3 #include <memory>
4 using namespace std;
5
6 void main()
7 {
8 auto_ptr<string> names[5] =
9 {
10 auto_ptr<string> (new string("Liu")),
11 auto_ptr<string> (new string("Sun")),
12 auto_ptr<string> (new string("Chen")),
13 auto_ptr<string> (new string("Wang")),
14 auto_ptr<string> (new string("Qin")),
15 };
16
17 auto_ptr<string> pcopy;
18 pcopy = names[3];
19
20 cout << "Start print names[5]: "<< endl;
21 for (int i = 0; i < 5; ++i)
22 {
23 cout << *names[i] << endl;
24 }
25 cout << "End print names[5]: "<< endl;
26
27 cin.get();
28 }
示例运行结果截图如下:
打印到第四个元素时,程序直接崩溃了!提示:auto_ptr 无引用对象,解引用失败。
导致失败的主要原因在于语句:pcopy = names[3];
在auto_ptr放弃对象的所有权后,再使用它来访问对象,才幡然醒悟它已经是一个空指针。
当把auto_ptr换成unique_ptr时,发现直接编译就通不过,因为unique_ptr不允许赋值操作。
到这里,突然想起顾城的诗《避免》:你不愿意种花 你说 我不愿看见它 一点点凋落 是的 为了避免结束 你避免了一切开始
unique_ptr为了避免与auto_ptr犯同样的错误,在程序的编译阶段就直接限制了运行崩溃可能发生的隐患。
其实,由编译错误分析可知,unique_ptr把复制构造函数和赋值操作符均声明为了私有成员:
结论1:unique_ptr 比 auto_ptr 更安全(一个经典的编译阶段错误比运行阶段错误更安全实例)。
退一步思考,未必把一个对象赋给另个对象时一定会留下危险的悬挂指针。比如下面示例,两个函数一切运行正常:
1 #include <iostream>
2 #include <string>
3 #include <memory>
4 using namespace std;
5
6 auto_ptr<string> getTempAuto_Ptr(const char* s)
7 {
8 auto_ptr<string> tempPtr(new string(s));
9 return tempPtr;
10 }
11
12 unique_ptr<string> getUnique_Ptr(const char* s)
13 {
14 unique_ptr<string> tempPtr(new string(s));
15 return tempPtr;
16 }
17
18 void main()
19 {
20 auto_ptr<string> ptr_auto;
21 ptr_auto = getTempAuto_Ptr("hao hao xue xi.");
22
23 unique_ptr<string> ptr_unique;
24 ptr_unique = getUnique_Ptr("tian tian xiang shang.");
25
26 cin.get();
27 }
返回临时对象被剥夺所引用对象的所有权后很快被销毁,根本没有机会再操纵它。编译器很智能的允许这样做。
那说明什么呢?说明一点:当程序试图将一个unique_ptr赋给另一个时,如果源unique_ptr是个临时右值,编译器允许这样做;如果unique_ptr将还存在一段时间,编译器禁止这样做。
验证代码如下:
1 unique_ptr<string> uPtr(new string("C++")); 2 unique_ptr<string> uPtr2; 3 //uPtr2 = uPtr; // error 4 unique_ptr<string> uPtr3; 5 uPtr3 = unique_ptr<string>(new string("Java")); // allow
结论2:如果在容器对象中,建议使用unique_ptr(此建议非彼建议!)。
unique_ptr为什么如此智能?到底为她注入了何种神功?
其实也不要见怪不怪,因为她使用了C++11新增的右值引用和移动构造(详情参见随笔《左值引用、右值引用和移动语义》)。
哦,oh,难怪呢?那也就是说,如果非要把一个unique_ptr对象赋给另外一个时,可以通过move方法搞定了?OK,你才是真正智能。请看下面示例:
1 unique_ptr<string> uPtr(new string("C++")); 2 unique_ptr<string> uPtr2; 3 4 uPtr2 = move(uPtr); // 使用move allow 5 6 unique_ptr<string> uPtr3; 7 uPtr3 = unique_ptr<string>(new string("Java")); // allow
结论3:unique_ptr使用了右值引用和移动构造。
智能指针最终负责释放申请的内存块,我们已经知道C++语法中释放内存块分为两种场景情况:
一种由new申请的内存空间,必须使用delete与其配对进行释放;另一种由new []申请的空间,必须使用delete []与其配对进行释放。
那么,是否每种智能指针都可以支持两种情况呢?答案是否定的。
相比于auto_ptr和shared_ptr,unique_ptr有个可用于数组的变体,即有对new []和delete []的支持:
1 std::unique_ptr<double []> pda(new double[5]); // will use delete[]
结论4:只有unique_ptr可以配对delete []这种情况。
模板shared_ptr包含一个显式的构造函数,用于将右值unique_ptr转换为shared_ptr。
shared_ptr将接管原来归unique_ptr所有的对象。举例如下:
1 #include <memory> 2 using namespace std; 3 4 unique_ptr<int> get_int(int n = 10) 5 { 6 return unique_ptr<int>(new int(n)); 7 } 8 9 void main() 10 { 11 unique_ptr<int> uPtr(get_int(1)); 12 // shared_ptr<int> sPtr(uPtr); // error uPtr is an lvalue 13 shared_ptr<int> sPtr2(get_int(2)); 14 }
结论5:在unique_ptr为右值时,可将其赋给shared_ptr。
【2】如何选择智能指针
智能指针的选择需要根据具体使用情境进行抉择,基本选用原则:
1. 程序要使用多个智能指针指向同一个内存对象时(即指向对象的所有权不可能归一个智能指针所有时),应选择shared_ptr。
2. STL中的许多算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器禁止)和auto_ptr(行为不确定性)。如果编译器没有提供shared_ptr,可使用Boost库中提供的shared_ptr。
3. 程序不要多个智能指针指向同一个内存对象时,可使用unique_ptr。
4. 如果函数内部使用new分配内存,并返回指向该内存的指针,将返回类型声明为unique_ptr。
5. 如果容器对象中存储智能指针,最好选择unique_ptr。
6. 如果编译器不支持unique_ptr,请考虑使用Boost库中提供的scoped_ptr(与unique_ptr类似)。
温馨提示:想了解更多智能指针详细请参考以下随笔:
《 [1] More Effective C++ (智能指针) 》
《 [2] 智能指针 》
Good Good Study, Day Day Up.
顺序 选择 循环 总结