从之前智能指针auto_ptr的极度不安全,到scoped_ptr的无法赋值拷贝,我们看到的是功能的缺失,安全性的思考,那么是否有一种智能指针能两者兼得呢?shared_ptr无疑成为了首选之选,它是最像指针的智能指针,也是Boost库中最有价值、最重要的组成部分之一。
shared_ptr的设计思想
共享指针,核心思想是通过引用计数来实现的,引入一个变量指针pCount来指向一块内存空间,当调用构造函数、赋值运算符的重载以及拷贝构造函数时都会对其引入的计数器pCount+1,只有当仅有一个指针指向这块空间时,即pCount=1时,才可以释放空间,否则引用计数pCount-1。
大家从这一点想到了什么?没错,之前STL中的string类的写时拷贝也是引入的这个思想,两者有异曲同工之妙,具体看源代码。
存在缺陷的版本源代码
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
#include <iostream> using namespace std; template<class T> class SharedPtr { public: SharedPtr(); SharedPtr(T* ptr); SharedPtr(const SharedPtr<T>& ap); ~SharedPtr(); SharedPtr<T>& operator=(SharedPtr<T> ap); T& operator*(); T& operator*()const; T* operator->(); T* operator->()const; long GetCount()const; T* GetPtr()const; private: T* _ptr; long* _pCount; }; template<class T>//构造函数 SharedPtr<T>::SharedPtr() :_ptr(NULL) ,_pCount(new long(1)) {} template<class T>//构造函数(带参数) SharedPtr<T>::SharedPtr(T* ptr) :_ptr(ptr) ,_pCount(new long(1)) {} template<class T>//拷贝构造函数 SharedPtr<T>::SharedPtr(const SharedPtr<T>& ap) :_ptr(ap._ptr) ,_pCount(ap._pCount) { ++(*this->_pCount); } template<class T>//析构函数 SharedPtr<T>::~SharedPtr() { if (--(*this->_pCount) == 0) { delete this->_ptr; delete this->_pCount; } } //方法一 template<class T>//赋值运算符的重载 SharedPtr<T>& SharedPtr<T>::operator=(SharedPtr<T> ap) { if (this != &ap) { if (--(*this->_pCount) == 0) { delete this->_ptr; delete this->_pCount; } this->_ptr = ap._ptr; this->_pCount = ap._pCount; ++(*this->_pCount); } return *this; } //方法二 //template<class T> //SharedPtr<T>& SharedPtr<T>::operator=(SharedPtr<T> ap) //{ // swap(this->_ptr, ap->_ptr); // swap(this->_pCount, ap->_pCount); // return *this; //} template<class T>//重载*运算符 T& SharedPtr<T>::operator*() { return *(this->_ptr); } template<class T>//重载*运算符const T& SharedPtr<T>::operator*()const { return *(this->_ptr); } template<class T>//重载->运算符 T* SharedPtr<T>::operator->() { return this->_ptr; } template<class T>//重载->运算符const T* SharedPtr<T>::operator->()const { return this->_ptr; } template<class T>//获取当前引用计数的个数 long SharedPtr<T>::GetCount()const { return *(this->_pCount); } template<class T> T* SharedPtr<T>::GetPtr()const//当前的对象指针 { return this->_ptr; } int main() { SharedPtr<int> ap1(new int(2)); SharedPtr<int> ap2(ap1); SharedPtr<int> ap3 = ap2; system("pause"); return 0; } |
问题1->循环引用
比如下面的程序,我们定义一个双向循环链表的结构体,在结构体外定义两个节点,让p1的后继指向p2,p2的前驱指向p1,那么程序是否会正常运行呢?
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 30 31 32 33 34 35 36 37 |
#include<iostream> #include <memory> using namespace std; struct ListNode { ListNode(int value) :_value(value) { cout << "ListNode()" << endl; } ~ListNode() { cout << "~ListNode()" << endl; } int _value; shared_ptr<ListNode> _prev; shared_ptr<ListNode> _next; }; void FunTest() { shared_ptr<ListNode> p1(new ListNode(1)); shared_ptr<ListNode> p2(new ListNode(2)); cout << p1.use_count() << endl; cout << p2.use_count() << endl; p1->_next = p2; p2->_prev = p1; cout << p1.use_count() << endl; cout << p2.use_count() << endl; } int main() { FunTest(); system("pause"); return 0; } |
当资源要释放时,p1节点释放的前提是p2释放,而p2的释放又依赖于p1,就形成了一个互相等待的局面,上升到操作系统的话,就等于进程之间形成了死锁,只不过这里是资源释放的依赖关系,而操作系统是资源竞争的关系。最终程序形成了循环引用,两个节点都无法释放资源,内存泄漏也就顺理成章。
解决方案(循环引用)
为了解决shared_ptr带来的循环引用的问题,我们引入了weak_ptr这个弱引用指针,它不能单独使用,用来辅助shared_ptr。weak_ptr智能指针会对引用计数做出特殊的处理,对上述情况不在加1。比如下面的代码:
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 30 31 32 33 34 35 36 37 |
#include<iostream> #include <memory> using namespace std; struct ListNode { ListNode(int value) :_value(value) { cout << "ListNode()" << endl; } ~ListNode() { cout << "~ListNode()" << endl; } int _value; weak_ptr<ListNode> _prev; weak_ptr<ListNode> _next; }; void FunTest() { shared_ptr<ListNode> p1(new ListNode(1)); shared_ptr<ListNode> p2(new ListNode(2)); cout << p1.use_count() << endl; cout << p2.use_count() << endl; p1->_next = p2; p2->_prev = p1; cout << p1.use_count() << endl; cout << p2.use_count() << endl; } int main() { FunTest(); system("pause"); return 0; } |
weak_ptr是一种不控制所指向对象生存周期的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被销毁,即使有weak_ptr指向对象,对象还是会被释放。
你会发现当我们将链表结构体中的前驱指针与后继指针定义为weak_ptr时,引用计数不仅没有增加,而且也成功调用了两个节点的析构函数,防止了内存泄漏以及循环引用计数的问题。
问题2->线程安全性问题
shared_ptr 对象提供与内建类型一样的线程安全级别。一个 shared_ptr 实例可以同时被多个线程“读”(仅使用不变操作进行访问)。 不同的 shared_ptr 实例可以同时被多个线程“写入”(使用类似 operator= 或 reset 这样的可变操作进行访问)(即使这些实 例是拷贝,而且共享下层的引用计数),任何其它的同时访问的结果会导致未定义行为。总结一下主要有3个方面:
①同一个shared_ptr被多个线程“读”是安全的。
②同一个shared_ptr被多个线程“写”是不安全的。
③共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的。
问题3->内存泄漏问题
解决了循环引用问题,尴尬的是还有一个内存泄漏问题,也就是说,当我们用malloc申请出来的空间,是无法释放的,因为malloc申请的空间只能用free去释放。当我们打开一个文件的时候,相应的也会维护一个文件指针,当程序运行完毕的时候,就必须关闭这个文件,否则会造成内存泄漏。
解决方案(内存泄漏)
定置删除器。如果指针是一个指向文件类型的,在析构函数中只需要关闭文件即可,而不是释放空间。如果空间是malloc出来的,那么析构函数中必须要调用free来释放资源,而不是delete来释放空间。这里我们会用到仿函数,具体的实现见代码。
删除器版本的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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
#include <iostream> using namespace std; #pragma warning (disable:4996) //关闭文件 struct Fclose { void operator()(void* ptr) { fclose((FILE*)ptr); cout << "fclose()" << endl; } }; //释放malloc struct Free { void operator()(void *ptr) { free(ptr); cout << "free()" << endl; } }; //默认为delete struct DefaultDelete { void operator()(void *ptr) { delete ptr; cout << "delete()" << endl; } }; template<class T,class Deleter = DefaultDelete> class SharedPtr { public: SharedPtr(T* ptr); SharedPtr(T* ptr, Deleter del); SharedPtr(const SharedPtr<T, Deleter>& ap); ~SharedPtr(); SharedPtr<T, Deleter>& operator=(SharedPtr<T, Deleter> ap); T& operator*(); T& operator*()const; T* operator->(); T* operator->()const; long GetCount()const; T* GetPtr()const; private: T* _ptr; long* _count; Deleter _del; }; template<class T,class Deleter = DefaultDelete> SharedPtr<T, Deleter>::SharedPtr(T* ptr)//构造函数 :_ptr(ptr) ,_count(new long(1)) {} template<class T, class Deleter = DefaultDelete> SharedPtr<T, Deleter>::SharedPtr(T* ptr,Deleter del)//构造函数 : _ptr(ptr) , _count(new long(1)) ,_del(del) {} template<class T,class Deleter = DefaultDelete> SharedPtr<T, Deleter>::SharedPtr(const SharedPtr<T, Deleter>& sp)//拷贝构造 :_ptr(sp._ptr) ,_count(sp._count) ,_del(sp._del) { ++(*_count); } template<class T,class Deleter = DefaultDelete> SharedPtr<T, Deleter>::~SharedPtr()//析构函数 { if (--(*_count) == 0) { _del(_ptr); delete _count; } } template<class T, class Deleter = DefaultDelete>//赋值运算符的重载 SharedPtr<T, Deleter>& SharedPtr<T, Deleter>::operator=(SharedPtr<T, Deleter> sp) { std::swap(_ptr, sp._ptr); std::swap(_count, sp._count); std::swap(_del, sp._del); return *this; } template<class T,class Deleter = DefaultDelete> T& SharedPtr<T, Deleter>::operator*()//重载接引用 { return *_ptr; } template<class T, class Deleter = DefaultDelete> T& SharedPtr<T, Deleter>::operator*()const { return *_ptr; } template<class T, class Deleter = DefaultDelete> T* SharedPtr<T, Deleter>::operator->()//重载间接运算符 { return _ptr; } template<class T, class Deleter = DefaultDelete> T* SharedPtr<T, Deleter>::operator->()const { return _ptr; } template<class T,class Deleter = DefaultDelete> long SharedPtr<T, Deleter>::GetCount()const//获取引用计数 { return *_count; } template<class T,class Deleter = DefaultDelete> T* SharedPtr<T, Deleter>::GetPtr()const//获取对象指针 { return _ptr; } int main() { SharedPtr<int> sp1(new int(1));//默认为delete SharedPtr<FILE, Fclose> sp2(fopen("1.txt", "w+"), Fclose());//文件操作 SharedPtr<string, Free> sp3((string *)malloc(sizeof(string)), Free());//字符串操作 system("pause"); return 0; } |
智能指针总结
如果程序要使用多个指向同一个对象的指针,应该选择shared_ptr。这样的情况包括:有一个指针数组,并使用一些辅助指针来表示特定的元素,如最大的元素和最小的元素;两个对象包含都指向第三个对象的指针;STL容器包含指针。
如果程序不需要多个指向同一个对象的指针,则可使用scoped_ptr(unique_ptr)。如果函数使用new分配内存,并返回指向该内存的指针,将其返回类型声明为scoped_ptr(unique_ptr)是不错的选择。这样,所有权将转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。