首先什么是智能指针?
RAII:资源分配即初始化,通俗点来讲,就是定义一个类来封装资源的分配和释放,再构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
实现机制:是利用类的构造和析构函数(释放资源)是由编译器自动调用的。
智能指针不仅管理执行对象的释放问题,还可以像指针一样的使用。
C++标准库中主要有四个智能指针,分别是auto_ptr,shared_ptr,scoped_ptr和weak_ptr。auto_ptr是C++98标准化才引入的,scoped_ptr,shared_ptr和weak_ptr是C++11标准化才引入的。auto_ptr是对资源进行了转移,虽说简单,但使用起来都是坑,因此在任何情况下建议都不要使用;scoped_ptr是资源独占,防拷贝和赋值,将其拷贝函数和赋值操作的重载只给出私有的声明(面试中若写智能指针,可写scoped_ptr简单不易出错);shared_ptr是对资源的共享,可以拷贝和赋值(与weak_ptr结合使用),weak_ptr主要与shared_ptr配合使用,自己不能直接管理动态开辟的空间。
下面我主要想谈一下shared_ptr。
首先我们来看一下shared_ptr的代码实现:
template<class T> class del { public: void operator()(T* _ptr) { delete _ptr; _ptr = NULL; } }; class Fclose { public: FILE* fclose(FILE* ptr) { cout << "fclose" << endl; fclose(ptr); } }; template<class T> class Free { public: T* free(T* ptr) { free(ptr); ptr = NULL; } }; template<class T,class Des=del> class shared_ptr { public: shared_ptr(T* ptr) :ptr(_ptr) , pcount(_pcount) { if (ptr != NULL) { *ptr = new int; *pcount = 1; } } shared_ptr(shared_ptr& sp) { _ptr = sp._ptr; _pcount = sp._pcount; if (_pcount) { (*_pcount)++; } } ~shared_ptr() { cout << "~shared_ptr()" << endl; if (_ptr&&!(--*_pcount)) { destroy(); } } shared_ptr& operator=(shared_ptr& sp) { if (!this == &sp) { if (_ptr && (--*pcount)) { destroy(); } _ptr = sp._ptr; _pcount = sp._pcount; (*_pcount)++; } return *this; } T* operator->() { return _ptr; } T& operator*() { return *_ptr; } private: void destroy() { del<T>()(_ptr); delete _pcount; _pcount = NULL; } T* _ptr; int* _pcount; }; 虽然看起来好像没有什么问题了,但shared_ptr还存在三个问题:线程安全问题,需要定制删除器以及循环引用问题。
线程安全问题:
1.一个shared_ptr实体可被各个线程同时读取;
2.两个的shared_ptr实体可以被两个线程同时写入,“析构”算写操作;
3.如果要从各个线程读写同一个shared_ptr对象,那么需要加锁。
定制删除器:
在shared_ptr中被管理的资源都需要用delete来释放,当被管理的资源若是文件类型,或是用malloc开辟出来的动态空间,则无法使用delete来释放,若不做任何的处理,程序将奔溃,所以,我们要为其定制删除器。上面给出的代码就为其定制了删除器,使程序书写变得麻烦。
循环引用问题:
我们先来看一段代码:
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; #include"boost/shared_ptr.hpp" struct Node { Node(int value) :_value(value) { cout << "Node()" << endl; } ~Node() { cout << "~Node()" << endl; } shared_ptr<Node> _prev; shared_ptr<Node> _next; int _value; }; void Test2() { shared_ptr<Node> sp1(new Node(1)); shared_ptr<Node> sp2(new Node(2)); cout << sp1.use_count() << endl; cout << sp2.use_count() << endl; sp1->_next = sp2; sp2->_prev = sp1; cout << sp1.use_count() << endl; cout << sp2.use_count() << endl; } int main() { Test2(); return 0; }
构建shared_ptr的两个对象sp1,sp2,先构造的后释放,后构造的先释放,先释放的是sp2,那么它的引用计数为2,减去1之后成为1,不能进行释放,因为sp1还在管理这段空间,但是sp2这个变量已经被销毁,因为它是栈上的变量,但sp2管理的堆上的空间并没有释放。接下来,释放sp1,同样,先将引用计数减去1,引用计数变成了1,因此也不会释放sp1管理的动态空间。只有当sp1,sp2引用计数变成0时,空间才允许被释放。也就是说sp2要释放,必须要sp1释放,但sp1要释放,必须要等sp2释放。最终它们两个对象都有释放空间,造成了内存的泄露。
下面根据下图,我们来理解一下sp1,sp2的构造过程,以及它们是如何利用shared_ptr智能指针来管理空间的。
这是使用shared_ptr所构建出来的对象。一部分用来管理引用计数,另一部分用来管理节点。
如图,便是上述双向链表最终构建出来的节点指针的指向。
首先是构造智能指针sp1,先对其引用计数开辟空间,并初始化user和weaks均为1,然后将节点的空间交给__ptr来管理,引用计数的空间交给_Ref来管理。然后在构建sp2,构造过程与sp1相同。代码中sp2->prev=sp1与sp1->next=sp2调用过程相同,都是将各自的引用计数空间中的user加至2,然后sp1中的_next节点中的_Ref管理sp2的引用计数;sp2中的_prev节点中的_Ref管理的sp1的引用计数,_ptr管理sp1节点。析构时,先析构sp2,但引用计数中的user为1,无法释放空间,再去析构sp1,与sp2相同,也无法对空间进行释放。所以,开辟出来的四块动态内存空间,都没有释放。
这就是shared_ptr的循环引用问题。
以上就是对shared_ptr进行的讨论,若还有哪些地方没有涉及到欢迎补充。