看智能指针前要看下面四个问题
1.为什么需要智能指针?
答:在申请资源时通常有借有还,例如new了一份地址空间,就要delete回来,malloc分配,free归还,而如果由程序员来写,不仅费时费力,更重要的是,可能会有突发事件,如new和delete中间抛异常或者return返回值,导致内存泄漏,这个时候我们就需要一个能够自动管理指针,释放资源的对象,避免内存泄漏,智能指针就是这个对象。
2.什么是智能指针?
答:智能指针是一个类,能够管理指针自动释放资源实现
3.什么是智能指针的原理?
原理为RAII+类似指针的行为(重定义operator*()和operator->())+解决浅拷贝问题(防止重复释放资源导致程序崩溃)
4.解决方法
从C++98到C++11官方提出了很多的方法
auto_ptrunique_ptrshare_ptrshare_ptr及其配套的weak_ptr 智能指针要解决的最重要的问题就是浅拷贝问题,在这里深拷贝不管用了,因为假如我们要设置多个智能指针管理同一块内存,那么指针肯定要指向同一块内存位置。 但是假如有多个指针指向同一块内存,怎么确定那一个指针结束时释放资源。 针对这一问题,有了第一代的auto_ptr namespace wang_1708//写命名空间是为了防止调用官方的auto_ptr { template<class T>//用模板类是因为有多种的指针 class auto_ptr { public: auto_ptr(T* ptr= nullptr):_ptr(nullptr) { _ptr = ptr; } auto_ptr(auto_ptr &sp)//拷贝构造不需要判断是不是自己 { if(&(*sp))//看看有没有资源 ,因为不一定是内置数据类型,所以要 //先解引用再求值 { _ptr = sp._ptr; sp._ptr = nullptr; } } bool operator!=(auto_ptr& sp) { if(sp._ptr != _ptr) return true; } auto_ptr& operator=(auto_ptr& sp) { if(*sp && sp != (*this))//看看有没有资源且看是不是自己 { _ptr = sp._ptr; sp._ptr = nullptr; } return *this; }//返回的必须为引用否则会崩溃 ,原因很复杂。 T& operator*() { return *_ptr; } T* operator->() { return _ptr;//注意这个地方传的是指针虽然调用看起来有问题 //,但是编译器会加上->,所以没问题 } ~auto_ptr() { if(_ptr) delete _ptr; } //private: T* _ptr; } ; }这个指针的用法时每次用拷贝构造函数和operator=如果原来的智能指针对象有资源且不是自己,就将原来的智能指针对象的储存的指针赋值给新的指针,并且将原来的指针置为nullptr。 析构函数时判断是否为非空指针,如果时进行资源的释放。 但是第一代的auto_ptr有一个问题,每次拷贝构造和赋值后就有一个对象里的指针被置为空,如果使用者不了解,继续对空指针进行操作就会导致程序崩溃。 针对这个问题就有了二代auto_ptr
namespace wang_1708//写命名空间是为了防止调用官方的auto_ptr { template<class T>//用模板类是因为有多种的指针 class auto_ptr { public: auto_ptr(T* ptr= nullptr) :_ptr(nullptr), owner_ptr(true) { _ptr = ptr; } auto_ptr(auto_ptr &sp)//拷贝构造不需要判断是不是自己 { if (&(*sp))//看看有没有资源 { _ptr = sp._ptr; owner_ptr = true; sp.owner_ptr = false; } } bool operator!=(auto_ptr& sp) { if (sp._ptr != _ptr) return true; } auto_ptr& operator=(auto_ptr& sp) { if (*sp && sp != (*this))//看看有没有资源且看是不是自己 { _ptr = sp._ptr; owner_ptr = true; sp.owner_ptr = false; } return *this; }//返回的必须为引用否则会崩溃 ,原因很复杂。 T& operator*() { return *_ptr; } T* operator->() { return _ptr;//注意这个地方传的是指针虽然调用看起来有问题 //,但是编译器会加上->,所以没问题 } ~auto_ptr() { if (owner_ptr == true) delete _ptr; } private: T* _ptr; bool owner_ptr; }; }比起上一个多了一个bool成员变量owner_ptr用来表明这块资源的释放管理权,析构函数来判断是否为拥有权限的成员变量,这样就不需要担心程序崩溃的问题,但是这种做法同样有隐患如。
void text_auto_ptr() { wang_1708::auto_ptr<int> sp1(new int(2)); if(true) wang_1708::auto_ptr<int> sp2 = sp1; }很明显sp1的管理权交给了sp2,但是sp2是if的作用域内的临时,当出if作用域时会调用sp2的析构函数,如果之后sp1就成了野指针了。 所以auto_ptr的隐患是很多的。 C++标准里规定:不论在什么情况下,都不要用auto_ptr。 2.unque_ptr是另一个思路的智能指针,因为析构函数和浅引用的问题,所以干脆让一份资源只能被一个智能指针对象管理,要做到这一点,需要把拷贝构造函数和赋值重载运算符禁掉就行了。一共有两种方法可以做到
将拷贝构造函数和赋值重载运算符的声明private并且不实现
在C++11里在函数后面加上=delete;意味着删掉这个函数,也能达到效果。 因为实现简单这里就不写具体代码了。 3.shared_ptr:shared_ptr是通过引用计数来达到资源释放的。 :shared_pt比auto_ptr多了一个int*指针——pcount用来记录指向同一块资源的指针数。
namespace wang_1708//写命名空间是为了防止调用官方的auto_ptr { template<class T>//用模板类是因为有多种的指针 class shared_ptr { public: shared_ptr(T* ptr= nullptr) :_ptr(nullptr), _pcount(new int(0)) { if (ptr) { _ptr = ptr; (*_pcount)++; } } shared_ptr(shared_ptr &sp) { if (&(*sp))//看看有没有资源 { _ptr = sp._ptr; _pcount = sp._pcount; (*_pcount)++; } } bool operator!=(shared_ptr& sp) { if (sp._ptr != _ptr) return true; } shared_ptr& operator=(shared_ptr& sp) { if (&(*sp) && sp._ptr != _ptr)//看看有没有资源且看是不是指向同一块资源 { if ((*_pcount) == 0) { _pcount = sp._pcount; (*_pcount)++; _ptr = sp._ptr; } if ((*_pcount) >= 1) { (*_pcount)--; resele(_pcount); _pcount = sp._pcount; (*_pcount)++; _ptr = sp._ptr; } } return *this; } //最复杂的一步,当sp1 = sp2 在sp2有资源的情况下 有三种情况,sp1为管理资源 直接与sp2共享 //sp1单独管理资源-----在与sp2共享前必须先释放自己的资源 //sp1 与其他shared_ptr的对象管理资源,自身的计数减一,与sp2共享 T& operator*() { return *_ptr; }//注意要返回引用,否则程序会崩溃 T* operator->() { return _ptr;//注意这个地方传的是指针虽然调用看起来有问题 //,但是编译器会加上->,所以没问题 } ~shared_ptr() { if ((*_pcount) == 1) delete _ptr; (*_pcount)--; } void resele(int *&pcount) { if ((*pcount) == 0) { delete _ptr; _ptr = nullptr;//用来确定是否释放资源的。 } } private: T* _ptr; int* _pcount; }; }share_ptr看起来很美好,解决了auto_ptr的隐患,并且有unique_ptr的优点–可以有多个对象管理同一块资源,但是从上面的代码中可以看出,这个share_ptr的析构函数是通过delete来释放资源的,所以当我们传入malloc分配的资源,甚至是个FILE类型的资源时,就会有问题,针对这点我们需要知道使用者传入的对象是什么类型的,就要用仿函数来获取对象的类型。
template <class T> class Free { public: void operator()(T*& _ptr) { if (_ptr) { free(_ptr); _ptr = nullptr; } } }; class Fclose { public: void operator()(FILE*& _ptr) { if (_ptr) { fclose(_ptr); _ptr = nullptr; } } }; template <class T> class delete_2 { public: void operator()(T*& _ptr) { delete _ptr; _ptr = nullptr; } }; namespace wang_1708//写命名空间是为了防止调用官方的auto_ptr { template<class T, class DF = delete_2<T> >//用模板类是因为有多种的指针 class shared_ptr { public: shared_ptr(T* ptr = nullptr) :_ptr(nullptr), _pcount(new int(0)) { if (ptr) { _ptr = ptr; (*_pcount)++; } } shared_ptr(shared_ptr &sp) { if (&(*sp))//看看有没有资源 { _ptr = sp._ptr; _pcount = sp._pcount; (*_pcount)++; } } bool operator!=(shared_ptr& sp) { if (sp._ptr != _ptr) return true; } shared_ptr& operator=(shared_ptr& sp) { if (&(*sp) && sp._ptr != _ptr)//看看有没有资源且看是不是指向同一块资源 { if ((*_pcount) == 0) { _pcount = sp._pcount; (*_pcount)++; _ptr = sp._ptr; } if ((*_pcount) >= 1) { (*_pcount)--; resele(_pcount); _pcount = sp._pcount; (*_pcount)++; _ptr = sp._ptr; } } return *this; } //最复杂的一步,当sp1 = sp2 在sp2有资源的情况下 有三种情况,sp1为管理资源 直接与sp2共享 //sp1单独管理资源-----在与sp2共享前必须先释放自己的资源 //sp1 与其他shared_ptr的对象管理资源,自身的计数减一,与sp2共享 T& operator*() { return *_ptr; }//注意要返回引用,否则程序会崩溃 T* operator->() { return _ptr;//注意这个地方传的是指针虽然调用看起来有问题 //,但是编译器会加上->,所以没问题 } ~shared_ptr() { if ((*_pcount) == 1) DF(_ptr); (*_pcount)--; } void resele(int *&pcount) { if ((*pcount) == 0) { DF(_ptr); //_ptr = nullptr;不需要重复两次nullptr } } private: T* _ptr; int* _pcount; }; }这里提醒一点,重载()是void operator()(形参列表),而不是void operator(形参列表)(),这里卡了半天。
以后调用shared_ptr智能指针要在类模板列表里写两个类型,第一个是数据类型,第二个是你希望的释放资源的函数名。 如果是一个自定义类型的数据,可以专门写一个仿函数用来释放资源。 从这里看shared_ptr好像已经成功了,但是还要考虑线程安全问题,做法是在计数的地方加锁,使其成为原子操作,还有一种更简便的方法,就是讲计数的类型变为atomic_int因为这里比较简单就不贴代码了。
还要注意的是:虽然智能指针能保证自己的线程安全,但是如果它管理的对象有线程安全问题,智能指针也无法让该对象线程安全。 最后的最后,还要说一个问题,就是share_ptr的循环引用问题,假设有一个双向链表listnode每个节点有两个成员next和pre假设我们有两个节点指针p1,p2用share_ptr来管理这两个指针 然后 让 p1->next = p2 p2->pre = p1 next和pre也用share_ptr来保存,就会出问题了,下面是图
汗,博主的画图水平比较捉急,请见谅。 当程序接受调用析构函数时,就会发生循环引用问题,具体是指,开始有各有两个指针指向两块空间,调用析构时,p1和p2完成了它们的使命,使引用计数减一,但是到了next和pre就有问题了,要让next释放资源必须让pre不再管理next的地址,同样让pre释放资源必须让next不再管理pre的地址,就像两个拿着枪的人谁都不想走第一步,所以就在那里停着,这个就是循环引用,是shared_ptr的缺陷。 针对这个问题,又有了weak_ptr指针,和之前的修改方式不同,weak_ptr并不是shared_ptr的优化版本,而是配合shared_ptr使用,解决了循环引用问题。 具体做法是 weak_ptr<node> pre; weak_ptr<node> next; 因为weak_ptr是一种弱引用指针,所谓弱引用指针是指当引用的对象活着时不一定存在,仅仅只是引用对象的存在的引用。 不会修改计数的值,更像是一个一般的指针能够判断指向的空间是否被释放了。 美中不足的是,虽然弱引用指针有效的解决了循环引用问题,但是程序员必须预先想到循环引用的问题,才会使用weak_ptr属于编译期的方法,如果在运行时出现了循环引用,会造成内存泄漏,不要认为智能指针就绝对安全,毕竟C++可没有垃圾回收机制。