首页 > 代码库 > C++智能指针剖析(下)boost::shared_ptr&其他
C++智能指针剖析(下)boost::shared_ptr&其他
1. boost::shared_ptr
前面我已经讲解了两个比较简单的智能指针,它们都有各自的优缺点。由于 boost::scoped_ptr 独享所有权,当我们真真需要复制智能指针时,需求便满足不了了,如此我们再引入一个智能指针,专门用于处理复制,参数传递的情况,这便是如下的boost::shared_ptr。
boost::shared_ptr 属于 boost 库,定义在 namespace boost 中,包含头文件#include<boost/smart_ptr.hpp> 便可以使用。在上面我们看到 boost::scoped_ptr 独享所有权,不允许赋值、拷贝,boost::shared_ptr 是专门用于共享所有权的,由于要共享所有权,其在内部使用了引用计数。boost::shared_ptr 也是用于管理单个堆内存对象的。
这是比较完善的一个智能指针,他是通过指针保持某个对象的共享拥有权的智能指针。若干个shared_ptr对象可以拥有同一个对象,该对象通过维护一个引用计数,记录有多少个shared_ptr指针指向该对象,最后一个指向该对象的shared_ptr被销毁或重置时,即引用计数变为0时,该对象被销毁。销毁对象时使用的是delete表达式或是在构造shared_ptr时传入的自定义删除器(delete),这后面会有详细讲解,但是shared_ptr指针同样拥有缺陷,那就是循环引用,和线程安全问题,这也在后面讲解。先来模拟实现一下shared_ptr指针。
1 template <class T> 2 class SharedPtr 3 { 4 public: 5 SharedPtr(T* ptr = NULL) 6 :_ptr(ptr) 7 ,_count(new int(0)){ 8 if (_ptr != NULL) { 9 ++(*_count); 10 } 11 } 12 SharedPtr(const SharedPtr<T>& sp) 13 :_ptr(sp._ptr) 14 ,_count(sp._count){ 15 if (_ptr != NULL) { 16 ++(*_count); 17 } 18 } 19 SharedPtr<T>& operator=(const SharedPtr<T>& sp) { 20 if (this != &sp) { //排除对象本身自己给自己赋值 21 if (--(*_count) <= 0) { 22 delete[]_ptr; 23 delete[]_count; 24 } 25 else //指向同一个对象的指针互相赋值 26 { } 27 _ptr = sp, _ptr; 28 _count = sp._count; 29 *(_count)++; 30 } 31 return *this; 32 } 33 ~SharedPtr() { 34 if (--(*count) == 0) { 35 delete[] _ptr; 36 delete[] _count; 37 } 38 } 39 T& operator*(){ 40 return *_ptr; 41 } 42 T* operator->() { 43 return _ptr; 44 } 45 bool operator ==(const SharedPtr<T>& sp) { 46 return (_ptr == sp._ptr); 47 } 48 bool operator !=(const SharedPtr<T>& sp) { 49 return (_ptr != sp._ptr); 50 }
51 int UseCount() {
52 return *_count;
53 }
51 private: 52 T* _ptr; 53 int* _count; 54 };
1.1 问题1:线程安全
因为使用引用计数值位判定指标,所以在多线程的环境下是不安全的。会因线程调用的先后顺序不同导致错误产生。对于这种问题,解决方法一般是加锁,对引用计数进行加锁,保证操作是互斥的。
1.2 问题2:循环引用
针对循环引用,从实际的例子来大分析问题,以便能更好的理解。看下面代码:
1 struct ListNode 2 { 3 int _data; 4 SharedPtr<ListNode> _next; 5 SharedPtr<ListNode> _prev; 6 7 ListNode(int x) 8 :_data(x), _next(NULL), _prev(NULL) 9 {} 10 ~ListNode() 11 { 12 cout << "~ListNode()" << endl; 13 } 14 }; 15 16 void test() 17 { 18 SharedPtr<ListNode> A(new ListNode(1)); 19 SharedPtr<ListNode> B(new ListNode(2)); 20 21 if (A && B) { 22 A->_next = B; 23 B->_prev = A; 24 } 25 26 cout << "A._count:" << A.UseCount() << endl; 27 cout << "B._count:" << B.UseCount() << endl; 28 } 29 30 int main() 31 { 32 test(); 33 getchar(); 34 return 0; 35 }
输出结果:从结果可以看出两个节点的引用计数都是2,且一直没有调用析构函数,这将造成内存泄漏,下面我将图解原理:而要解决循环引用的问题,就需要用boost库的另一个智能指针,即boost::weak_ptr。后面在详细讲解。
1.3 定制删除器(仿函数)
经上面分析,我们可以看到,上面的指针不能用于文件的关闭,也不能用于管理malloc和new[]开辟的动态内存的释放,所以我们可以运用仿函数来定制删除器。如下:
1 template<class T> 2 struct DeleteArray //用于new[]开辟的动态内存释放 3 { 4 void operator()(T* ptr) 5 { 6 cout << "A" << endl; 7 delete[] ptr; 8 } 9 }; 10 struct Fclose //用于文件关闭 11 { 12 void operator()(FILE* ptr) 13 { 14 cout << "B" << endl; 15 16 fclose(ptr); 17 } 18 }; 19 template<class T> 20 struct Free //用于malloc开辟的动态内存的释放 21 { 22 void operator()(T* ptr) 23 { 24 cout << "C" << endl; 25 free(ptr); 26 } 27 }; 28 int main() 29 { 30 shared_ptr<string> ap1(new string[10], DeleteArray<string>()); 31 shared_ptr<FILE> ap2(fopen("test.txt", "w"),Fclose()); 32 shared_ptr<int> ap3((int*)malloc(sizeof(int)), Free<int>()); 33 return 0; 34 }
2. boost::weak_ptr
boost::weak_ptr 属于 boost 库,定义在 namespace boost 中,包含头文件#include<boost/smart_ptr.hpp> 便可以使用。
在讲 boost::weak_ptr 之前,让我们先回顾一下前面讲解的内容。似乎boost::scoped_ptr、boost::shared_ptr 这两个智能指针就可以解决所有单个对象内存的管理了,这儿还多出一个 boost::weak_ptr,是否还有某些情况我们没纳入考虑呢?答案是有。首先 boost::weak_ptr 是专门为 boost::shared_ptr 而准备的。有时候,我们只关心能否使用对象,并不关心内部的引用计数。boost::weak_ptr 是 boost::shared_ptr 的观察者(Observer)对象,观察者意味着 boost::weak_ptr 只对 boost::shared_ptr 进行引用,而不改变其引用计数,当被观察的 boost::shared_ptr 失效后,相应的 boost::weak_ptr 也相应失效。weak_ptr其实是一个辅助性的智能指针,结合shared_ptr指针使用,它的本质就是弱引用,并不增加引用计数值。他没有实现->和*运算符的重载,所以不能直接用它访问对象。针对循环引用这个问题,就是因为不会引起引用计数值的改变,所以我们可以将_next和_prev定义为weak_ptr指针,这样就很好地解决了循环引用的问题。
1 struct ListNode 2 { 3 int _data; 4 weak_ptr<ListNode> _next; //定义为weak_ptr指针 5 weak_ptr<ListNode> _prev; 6 7 ListNode(int x) 8 :_data(x), _next(NULL), _prev(NULL) 9 {} 10 ~ListNode() 11 { 12 cout << "~ListNode()" << endl; 13 } 14 };
其实 boost::weak_ptr 主要用在软件架构设计中,可以在基类(此处的基类并非抽象基类,而是指继承于抽象基类的虚基类)中定义一个 boost::weak_ptr,用于指向子类的boost::shared_ptr,这样基类仅仅观察自己的 boost::weak_ptr 是否为空就知道子类有没对自己赋值了,而不用影响子类 boost::shared_ptr 的引用计数,用以降低复杂度,更好的管理对象。
boost库剩余的两个指针:auto_arr和shared_arr.这两个都是管理数组的,因为之前几个指针的析构函数中都是delete,不能对数组进行释放,所以我们自己定制删除器,而这两个指针就是专门管理指针的。下面来模拟实现一下。
3. 模拟实现auto_arr
1 template<class T> 2 class AutoArr 3 { 4 public: 5 AutoArr(T* ptr = NULL) 6 :_ptr(ptr) 7 {} 8 9 ~AutoArr() 10 { 11 delete[] _ptr; 12 } 13 14 AutoArr(const AutoArr<T>& s) 15 { 16 _ptr = s._ptr; 17 s._ptr = NULL; 18 } 19 20 AutoArr<T>& operator=(const AutoArr<T>& s) 21 { 22 if (this != &s) 23 { 24 _ptr = s._ptr; 25 s._ptr = NULL; 26 } 27 return *this; 28 } 29 30 T& operator[](size_t pos) 31 { 32 if (_ptr == NULL) 33 { 34 throw a; 35 } 36 return *(_ptr+pos); 37 } 38 39 void set(T* ptr) 40 { 41 int i = 0; 42 while (*(ptr + i)) 43 { 44 *(_ptr + i) = *(ptr + i); 45 i++; 46 } 47 } 48 49 protected: 50 T* ptr; 51 };
4. 模拟实现shared_arr
1 template<class T> 2 3 class SharedArr 4 { 5 public: 6 SharedArr(T* ptr = NULL) 7 :_ptr(ptr),_count(new int(0)) 8 { 9 (*_count)++; 10 } 11 12 ~SharedArr() 13 { 14 delete[] _ptr; 15 } 16 17 SharedArr(const SharedArr<T>& s) 18 { 19 _ptr = s._ptr; 20 (*_count)++; 21 } 22 23 SharedArr<T>& operator=(const SharedArr<T>& s) 24 { 25 if (this != &s) 26 { 27 if (--(*_count) <= 0) 28 { 29 delete _ptr; 30 delete _count; 31 } 32 else 33 { } 34 _ptr = s._ptr; 35 _count = s._count; 36 (*_count)++; 37 } 38 } 39 40 T& operator[](size_t pos) 41 { 42 if (_ptr == NULL) 43 { 44 throw 1; 45 } 46 return *(_ptr + pos); 47 } 48 49 protected: 50 T* _ptr; 51 int* _count; 52 };
5. 如何选择智能指针?
在掌握了这几种智能指针后,大家可能会想另一个问题:在实际应用中,应使用哪种智能指针呢?下面给出几个使用指南。
(1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:
- 有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;
- 两个对象包含都指向第三个对象的指针;
- STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。
(2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())。例如,可在程序中使用类似于下面的代码段。
1 unique_ptr<int> make_int(int n) 2 { 3 return unique_ptr<int>(new int(n)); 4 } 5 void show(unique_ptr<int> &p1) 6 { 7 cout << *a << ‘ ‘; 8 } 9 int main() 10 { 11 ... 12 vector<unique_ptr<int> > vp(size); 13 for(int i = 0; i < vp.size(); i++) 14 vp[i] = make_int(rand() % 1000); // copy temporary unique_ptr 15 vp.push_back(make_int(rand() % 1000)); // ok because arg is temporary 16 for_each(vp.begin(), vp.end(), show); // use for_each() 17 ... 18 }
其中push_back调用没有问题,因为它返回一个临时unique_ptr,该unique_ptr被赋给vp中的一个unique_ptr。另外,如果按值而不是按引用给show()传递对象,for_each()将非法,因为这将导致使用一个来自vp的非临时unique_ptr初始化pi,而这是不允许的。前面说过,编译器将发现错误使用unique_ptr的企图。
在unique_ptr为右值时,可将其赋给shared_ptr,这与将一个unique_ptr赋给一个需要满足的条件相同。与前面一样,在下面的代码中,make_int()的返回类型为unique_ptr<int>:
1 unique_ptr<int> pup(make_int(rand() % 1000)); // ok 2 shared_ptr<int> spp(pup); // not allowed, pup as lvalue 3 shared_ptr<int> spr(make_int(rand() % 1000)); // ok
模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptr。shared_ptr将接管原来归unique_ptr所有的对象。
在满足unique_ptr要求的条件时,也可使用auto_ptr,但unique_ptr是更好的选择。如果你的编译器没有unique_ptr,可考虑使用Boost库提供的scoped_ptr,它与unique_ptr类似。
C++智能指针剖析(下)boost::shared_ptr&其他