为什么要有智能指针?
1.什么是智能指针?
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。
2.为什么需要智能指针?
指针在C++的学习和使用中是必不可少的,重要性可想而知,如果对指针的理解不是很深入,很容易产生野指针,造成内存泄漏等问题。这时有人就会想到用一个“有思想”的指针,知道自己什么时候该释放,问题不就解决了吗。智能指针正好是针对这些问题所存在的,因为它是存放在栈上的模板对象,在栈内部包了一层指针。栈的生命周期结束时,它里面的指针自然也就释放了。这样一个“有思想”的指针就实现了。
3.智能指针和普通指针的区别在哪里?
智能指针实际是利用RAll(资源获取及初始化)的技术对普通指针加了一层封装机制,目的是为了使智能指针可以方便的管理一个对象的生命期。如果是普通指针,使用这个对象之后我们需要删除它。如果忘记删除,会造成一个野指针、内存泄漏等问题,在调用运行这个函数的时候就会抛出异常。
智能指针的实现。
1.基于RAII思想的SmartPtr类
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源的简单技术,是C++语言的一种管理资源、避免泄漏的惯用法。
这种思想是在对象构造的时候获取资源,然后控制对资源的访问,使之在对象生命周期始终保持有效,最有在对象析构的时候释放资源。这种做法不需要显式地释放资源并且对象所需要的资源在其生命周期内始终保持有效。
template<class T>
class SmartPtr{
public:SmartPtr(T* ptr = nullptr) :_ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}//SmartPtr写到这里还不能算是一个智能指针,这个指针现在是符合了RAII机制,//但是还不具有指针的行为,需要将*和->重载才可以T& operator*(){return *_ptr;}T& operator->(){return _ptr}
};
2.auto_ptr
在C++98中就提出了智能指针auto_ptr
auto_ptr的实现原理:管理权转移的思想。
接下来通过简单地模拟实现auto_ptr来了解它的原理(命名为Auto_Ptr)
template<class T>
class Auto_Ptr
{
public:AutoPtr(T* ptr = nullptr) :_ptr(ptr){}~AutoPtr(){if (_ptr)delete _ptr;}//一旦发生了拷贝,就将sp中的资源转移到当前对象中,然后sp与它所管理的资源断开练习//这样就解决了一块空间被多个对象使用造成的程序崩溃问题。AutoPtr(AutoPtr<T>& sp){//检查是不是自己给自己赋值if (this != &sp){//释放当前对象中的资源if (_ptr)delete _ptr;//转移sp中资源到当前对象_ptr = sp._ptr;sp._ptr = nullptr;}return *this;}private:T* _ptr;
};
auto_ptr存在的问题:
当对象拷贝或者赋值后,前面的对象就悬空了,再使用前面对象访问资源就会出现问题
C++98中设计的auto_ptr问题是非常明显的,所以在实际工作中很多公司直接明确规定了不能使用auto_ptr这个智能指针,已被C++11明确声明不再支持使用。
3. unique_ptr
C++11中开始提供更靠谱的智能指针unique_ptr
unique的实现原理:防止被拷贝
接下来通过简单地模拟实现auto_ptr来了解它的原理(命名为Unique_Ptr)
template<class T>
class Unique_Ptr
{
public:Unique_Ptr(T* ptr = nullptr) : _ptr(ptr){}~Unique_Ptr(){if (_ptr)delete _ptr;}Unique_Ptr& operator*(){return *_ptr;}Unique_Ptr& operator->(){return _ptr;}
private:Unique_Ptr(Unique_Ptr<T> const &);Unique_Ptr operator=(Unique_Ptr<T> const &);//上述方法是C++98中给出的防止被使用方法:只声明不实现,并将其生命为私有//下面是C++11中给出的防止被使用的方法:=delete;Unique_Ptr(Unique_Ptr<T> const &)=delete;Unique_Ptr operator=(Unique_Ptr<T> const &)=delete;T* _ptr;
};
4.shared_ptr
基于前面智能指针不能拷贝的缺陷,C++11提供了更为可靠并且可以拷贝的shared_ptr
shared_ptr的实现原理是通过引用计数的方式来实现多个shared_prt对象之间共享资源,这就和一个宿舍都要去上课,每个人走的时候都会通知让最后一个走的把门锁了。
shared_prt在内部给每个资源都维护者一份计数,用来记录这份资源被多少个对象所共享。在对象调用析构函数的时候,说明这个对象不在使用这份资源了,他的引用计数要减一。如果他是最后一个使用者(引用计数减一后为零),他就得释放这块资源。如果不是最后一个使用者(引用计数减一后不为零),就不能释放这块资源。
接下来通过简单地模拟实现auto_ptr来了解它的原理(命名为Shared_Ptr)
#include <thread>
#include <mutex>template<class T>
class Shared_Ptr
{
public:Shread_Ptr(T* ptr = nullptr): _ptr(ptr), _pCount(new int(1)), _pMutex(new mutex){if (_ptr == nullptr)*_ptrCount = 0;}~Shread_Ptr(){Release();}//拷贝构造函数Shared_Ptr(Shared_Ptr<T>& sp):_ptr(sp._ptr), _pCount(sp._pCount), _pMutex(sp._pMutex){if (_ptr) //如果不是空指针,增加引用计数AddCount();}//重载=运算符Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp){if (_prt != sp._ptr) //检查是不是自己给自己赋值{Release(); //释放所管理的旧资源_ptr = sp._ptr;_pCount = sp._pCount;_pMutex = sp._pMutex;if (_ptr) //如果不是空指针,增加引用计数AddCount();}return *this;}Shared_Ptr<T>& operator*(){return *_ptr;}Shread_Ptr<T>& operator->(){return _ptr;}T* Get(){return _ptr;}
private://加减引用计数使用锁对操作进行加锁或者使用原子操作int AddCount(){_pMutex->lock();++(*_pCount);_pMutex->unlock();return *_pCount;}int SubCount(){_pMutex->lock();--(*_pCount);_pMutex->unlock();return *_pCount;}void Release(){//如果指向的指针不为空,并且这个对象是最后一个使用这个指针的人,开始释放这份资源if (_ptr && 0 == SubCount()){delete _ptr;delete _pCount;}}private:T* _ptr; //指向管理资源的指针int *_pCount; //引用计数mutex* _pMutex; //互斥锁
};
4.1 shared_ptr的线程安全问题。
假设在上面代码中对引用计数进行加减时没有加锁或者进行原子操作,由于智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时进行加或减操作时,这
个操作不是原子的,引用计数原来是1,加了两次,可能还是2。这样引用计数就错乱了,会导致资源没有按照约定释放或者程序崩溃的问题。所以智能指针中对引用计数的加减操作是必须要要加锁的,也就是说引用计数的操作是线程安全的。
4.2 shared_ptr的循环引用问题。
先实现一个循环引用的场景:
#include <memory>
#include <iostream>using namespace std;struct ListNode
{int _data;shared_ptr<ListNode> _prev;shared_ptr<ListNode> _next;~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{shared_ptr<ListNode> node1(new ListNode);shared_ptr<ListNode> node2(new ListNode);//打印这两个节点的引用计数cout << node1.use_count() << endl;cout << node2.use_count() << endl;node1->_next = node2;node2->_prev = node1;//打印这两个节点的引用计数cout << node1.use_count() << endl;cout << node2.use_count() << endl;return 0;
}
打印结果如下:
很明显,没有调用析构函数,画图解析:
分析:
- <>a.node1和node2两个智能指针对象指向两个节点,引用计数变为1。
- node1的_next指向node2,node2的_prev指向node1,引用计数变为2。
- 当两个智能指针释放空间时,引用计数减到1,但是_next还指向着下一个节点,_prev还指向着上一个节点。
- 也就是说,如果想释放node1中的_next,就先得释放它指向的node2,但是如果想释放node2就得将node2的_next和_prev先释放,而_prev指向了node1,得先将node1释放了才能释放_prev。这样一来,如果想释放node1就得先释放node2,要释放node2就得先释放node1。这样就构成了循环引用,谁也不会释放 针对这一问题,解决方案是在引用计数的场景下,把节点中的_prev和_next换成weak_ptr就可以了。
- 其中的原理是:当node1->_next= node2;和node2->_prev = node1时,weak_ptr的_next和_prev不会增加 node1和node2的引用计数。