为什么需要智能指针
代码中途退出,也能保证资源的合理释放,在c++中没有垃圾回收机制的情况下,智能指针就可以保证我们申请的资源,最后忘记释放的问题,防止内存泄露,也帮我们减少了一定的负担,不用再在所有可能退出的地方都进行是否释放的检测。
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句 柄、网络连接、互斥量等等)的简单技术
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的 时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象
两个好处
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
利用RAII的思想创建一个smartptr类替我们管理资源
template<class T>class smartptr
{
public:smartptr( T* ptr = nullptr):_ptr(ptr){}~smartptr(){if (_ptr){delete _ptr;}}
private:T *_ptr;
};void TestSmartPtr()
{smartptr<int>sp(new int);throw 1;
}int main()
{try{TestSmartPtr();}catch (int err){cout << err << endl;}system("pause");return 0;
}
这个类虽然能利用自身的生命周期帮助我们管理资源的释放,但它本身还不能称作为一个智能指针,因为它并不能解引用,所以,我们要重载解引用运算符*
和箭头运算符->
T& operator*() //重载解引用运算符{return *_ptr; //返回当前内容}
T* operator->(){return &(operator*()); //把指针所指向空间的地址返回}
struct A
{int a;int b;int c;
};
void TestSmartPtr()
{smartptr<int>sp1(new int);*sp1 = 100;cout << *sp1 << endl;smartptr<A>sp2(new A);//我们自己的这个类,也可以用指针的方式访问数据sp2->a = 10;sp2->b = 20;sp2->c = 30;throw 1;
}
这基本就是一个简单的智能指针,但是这个“指针”有一个巨大的缺陷,它并没有处理浅拷贝问题
smartptr<A>sp3(sp2);
sp3和sp2他们管理的资源都是一样的,但是我们这里并不能用深拷贝的方式来解决这个问题,因为资源是外部的用户提供的,本身写的类并没有申请空间的权力
智能指针基本原理
RAII(资源可以自动释放)+operator*()/operator->()(当成指针使用)+解决浅拷贝
C++98提供的 auto_ptr
RAII(资源可以自动释放)+operator*()/operator->()(当成指针使用)+资源管理权限的转移
资源管理权限的转移的图解
namespace bite //自己写一个命名空间,为了防止和系统冲突
{//auto_ptr 利用资源管理权限的转移来解决浅拷贝template<class T>class auto_ptr{//RALLpublic:auto_ptr(T *_ptr = nullptr):_ptr(_ptr){}//资源的转移auto_ptr(auto_ptr<T>& ap) //要修改内容,所以不用加const:_ptr(ap._ptr){ap._ptr = nullptr;}~auto_ptr(){if (_ptr){delete _ptr;}}//像指针一样的方式使用T& operator*(){return *_ptr;}T* operator->(){//将指针所指向空间的地址返回就可以return &(operator*());}protected:T *_ptr;};
}void TestAuto_ptr()
{bite::auto_ptr<int>ap1(new int);bite::auto_ptr<int>ap2(ap1);
}
此时我们可以看到,ap1和ap2已经不再指向同一块内存空间了,但是赋值操作也是一个浅拷贝,所以我们还要重载赋值运算符
auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap){ //是不是自己给自己赋值if (_ptr) //当前对象有没有管理资源delete _ptr; //如果有,释放_ptr = ap._ptr; //当前对象接受ap的资源ap._ptr = nullptr; //ap 指空}return *this;}
缺陷
因为auto_ptr用的是资源的转移,所以不能同时对两个对象进行操作,就像下面这样
改进后的auto_ptr
//改进:资源管理权转移
namespace bite //自己写一个命名空间,为了防止和系统冲突
{//auto_ptr 利用资源管理权限的转移来解决浅拷贝template<class T>class auto_ptr{//RAIIpublic:auto_ptr(T *_ptr = nullptr):_ptr(_ptr), _owner(false){if (_ptr)delete _ptr;}//资源的转移auto_ptr(auto_ptr<T>& ap) //要修改内容,所以不用加const:_ptr(ap._ptr), _owner(ap._owner){ap._owner = false;}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap){ //是不是自己给自己赋值if (_ptr && _owner) //当前对象有没有管理资源的权限delete _ptr; //如果有,释放_ptr = ap._ptr; //当前对象接受ap的资源_owner = ap._owner;ap._owner = false;}return *this;}~auto_ptr(){if (_ptr && _owner){delete _ptr;_owner = false;}}//像指针一样的方式使用T& operator*(){return *_ptr;}T* operator->(){//将指针所指向空间的地址返回就可以return &(operator*());}protected:T *_ptr;bool _owner; //资源真正的管理权限};
}void TestAuto_ptr()
{bite::auto_ptr<int>ap1(new int);bite::auto_ptr<int>ap2(ap1);ap1 = ap2;
}int main()
{TestAuto_ptr();system("pause");return 0;
}
但是这个改进后又有一个更大的缺陷???,就是造成野指针,所以还是尽量不要用这个智能指针
c++ 11 unique_ptr
一个对象独占资源
实现原理
- unique_ptr 禁止调用拷贝构造函数
- unique_ptr 禁止调用赋值运算符重载
- 完成一份资源被一个对象独占
- c++98 中的方式:拷贝构造函数以及赋值运算符重载只声明不定义,访问权限设置为private,为了防止友员,不能定义,只能声明
- c++11 中的方式:通过delet 禁止编译器生成默认成员函数
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
实现
namespace bite
{template<class T>class unique_ptr{public:unique_ptr(T *ptr = nullptr):_ptr(ptr){}~unique_ptr(){if (_ptr) //如果提供资源delete _ptr; //释放掉资源}//具有指针的行为T &operator*(){return *_ptr;}T *operator->(){return _ptr;}///c++98:拷贝构造函数以及赋值运算符重载只声明不定义,访问权限设置为private//如果定义,如果用户把函数当成这个类的友员函数的话,私有权限就不复存在/*private:unique_ptr(const unique_ptr<T>&);unique_ptr<T>& operator=(const unique_ptr<T>&)*///c++11:将默认成员函数删除掉//delete:用来释放new申请出来的空间// 禁止编译器生成默认的成员函数unique_ptr(const unique_ptr<T>&) = delete;unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;private:T * _ptr;};
}
c++ 11:shared_ptr
共享资源
RALL+operator()/operator->()+引用计数*
实现原理
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指 针了
模拟实现
namespace bite
{template<class T>class shared_ptr{public:shared_ptr(T *ptr = nullptr):_ptr(ptr), _pCount(nullptr){if (_ptr){_pCount = new int(1);}}~shared_ptr(){if (_ptr && 0 == --*_pCount){delete _ptr;delete _pCount;}}T & operator*(){return *_ptr;}T * operator->(){return _ptr;}//采用引用计数的方式解决浅拷贝的问题shared_ptr(const shared_ptr<T>&sp):_ptr(sp._ptr), _pCount(sp._pCount){++ *_pCount;}//sp2 = sp1//sp2 自己是否有资源---->没有----->直接共享// 有 1.资源的计数是1 2.计数>1(多个资源共享)shared_ptr<T>& operator=(const shared_ptr<T>&sp){if (this != &sp) //如果不是自己给自己赋值{//将this从资源中分离开if (_ptr && 0 == --*_pCount){delete _ptr;delete _pCount;}//共享资源_ptr = sp._ptr;_pCount = sp._pCount;++*_pCount;}return *this;}private:T *_ptr;int *_pCount;};
}void TestSharedPtr()
{bite::shared_ptr<int >sp1(new int);bite::shared_ptr<int>sp2(sp1);//当前对象没有资源bite::shared_ptr<int>sp3;sp3 = sp2;//当前对象独立拥有资源bite::shared_ptr<int>sp4(new int);sp4 = sp3;bite::shared_ptr<int>sp5(new int);bite::shared_ptr<int>sp6(sp5);sp5 = sp4;
}
可以看到他们共用一份资源,计数也正常,sp6用的是sp5的资源,计数变成1了。释放的时候从后往前释放
只是我们现在实现的智能指针有一些缺陷
缺陷
- 不能管理任意类型的指针,比如:文件指针 析构函数:资源采用delete,而文件需要fclose进行关闭,但我们的析构函数已经写死了
- 没有保证线程安全
- 存在循环引用----怎么解决?
解决任意类型的指针
定制删除器
//new出来资源的释放
template<class T>
struct DFDel
{void operator()(T*& ptr){if (ptr){delete ptr;ptr = nullptr;}}
};
//malooc出来的资源的释放
template<class T>
struct Free
{void operator()(T*& ptr){if (ptr){free(ptr);ptr = nullptr;}}
};//处理文件指针的释放
struct Fclose
{void operator()(FILE *&pf){if (pf){fclose(pf);pf = nullptr;}}
};
namespace bite
{template < class T, class DF = DFDel<T>>class shared_ptr{public:shared_ptr(T *ptr = nullptr):_ptr(ptr), _pCount(nullptr){if (_ptr){_pCount = new int(1);}}~shared_ptr(){Release();}T & operator*(){return *_ptr;}T * operator->(){return _ptr;}//采用引用计数的方式解决浅拷贝的问题shared_ptr(const shared_ptr<T>&sp):_ptr(sp._ptr), _pCount(sp._pCount){++ *_pCount;}//sp2 = sp1//sp2 自己是否有资源---->没有----->直接共享// 有 1.资源的计数是1 2.计数>1(多个资源共享)shared_ptr<T>& operator=(const shared_ptr<T>&sp){if (this != &sp) //如果不是自己给自己赋值{//将this从资源中分离开if (_ptr && 0 == --*_pCount){Release();}//共享资源_ptr = sp._ptr;_pCount = sp._pCount;++*_pCount;}return *this;}private:void Release(){if (_ptr && 0 == --*_pCount){//delete _ptr;DF()(_ptr); //无名对象delete _pCount;}}private:T *_ptr;int *_pCount;};
}void TestSharedPtr()
{bite::shared_ptr<int>sp1(new int);bite::shared_ptr<int,Free<int>>sp2((int *)malloc(sizeof(int)));bite::shared_ptr<FILE,Fclose>sp3(fopen(("List.h"), "r"));
}int main()
{TestSharedPtr();return 0;
}
解决线程安全
- 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时
++
或--
,这 个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未 释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是 线程安全的。 - 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
我们需要加锁解决
#include<mutex>
namespace bite
{template < class T, class DF = DFDel<T>>class shared_ptr{public:shared_ptr(T *ptr = nullptr):_ptr(ptr), _pCount(nullptr), _pMutex(nullptr){if (_ptr){_pMutex = new mutex;_pCount = new int(1);}}~shared_ptr(){Release();}T & operator*(){return *_ptr;}T * operator->(){return _ptr;}//采用引用计数的方式解决浅拷贝的问题shared_ptr(const shared_ptr<T>&sp):_ptr(sp._ptr),_pCount(sp._pCount), _pMutex(sp._pMutex){AddRef();}//sp2 = sp1//sp2 自己是否有资源---->没有----->直接共享// 有 1.资源的计数是1 2.计数>1(多个资源共享)shared_ptr<T>& operator=(const shared_ptr<T>&sp){if (this != &sp) //如果不是自己给自己赋值{//将this从资源中分离开if (_ptr && 0 == SubRef()){Release();}//共享资源_ptr = sp._ptr;_pCount = sp._pCount;AddRef();}return *this;}size_t use_count()const{return *_pCount;}T *get(){return _ptr; //返回地址}private:void Release(){if (_ptr && 0 == SubRef()){//delete _ptr;DF()(_ptr); //无名对象delete _pCount;if (_pMutex)delete _pMutex;}}void AddRef() //引用计数变更时需要进行加锁和解锁{_pMutex->lock();++*_pCount;_pMutex->unlock();}int SubRef(){_pMutex->lock();--*_pCount;_pMutex->unlock();return *_pCount;}private:T *_ptr;int *_pCount;mutex* _pMutex;};
}
加的锁可以保证shared_ptr是安全的,但无法保证shared_ptr所指向的内容是不是安全的,如果想要保证,必须用户自己来进行保证
解决循环引用
什么是循环引用
我们来看这个代码
struct ListNode
{
public:ListNode(int data):_data(data), _pPre(nullptr), _pNext(nullptr){cout << "ListNode(int)" << this << endl;}~ListNode(){cout << "~ListNode(int):" << this << endl;}int _data;shared_ptr<ListNode>_pPre;shared_ptr<ListNode>_pNext;//ListNode * _pPre;//ListNode * _pNext;
};
void TestSharedPtr()
{shared_ptr<ListNode>sp1(new ListNode(10));shared_ptr<ListNode>sp2(new ListNode(20));//查看引用计数cout << sp1.use_count() << endl;cout << sp2.use_count() << endl;sp1->_pNext = sp2;sp2->_pPre = sp1;//查看引用计数cout << sp1.use_count() << endl;cout << sp2.use_count() << endl;
}
解决方法
weak_ptr
标准库引入了新的智能指针weak_ptr:RAII+operator*()/operator->()+引用计数,原理跟shared_ptr一样
- 作用
专门解决shared_ptr存在得循环引用问题,除此之外,没有其他别得用途 - 限制
不能单独管理资源,必须与shared_ptr配合使用
所以
- 解决方案:在引用计数的场景下,把结点中的
_prev
和_next
改成weak_ptr
就可以了 - 原理就是,
node1->_next = node2;
和node2->_prev = node1;
时weak_ptr
的_next
和_prev
不会增加node1
和node2
的引用计数。
struct ListNode
{
public:ListNode(int data):_data(data)//, _pPre(nullptr)//, _pNext(nullptr){cout << "ListNode(int)" << this << endl;}~ListNode(){cout << "~ListNode(int):" << this << endl;}int _data;weak_ptr<ListNode>_pPre;weak_ptr<ListNode>_pNext;//ListNode * _pPre;//ListNode * _pNext;
};void TestSharedPtr()
{shared_ptr<ListNode>sp1(new ListNode(10));shared_ptr<ListNode>sp2(new ListNode(20));//查看引用计数cout << sp1.use_count() << endl;cout << sp2.use_count() << endl;sp1->_pNext = sp2;sp2->_pPre = sp1;//查看引用计数cout << sp1.use_count() << endl;cout << sp2.use_count() << endl;
}
int main()
{TestSharedPtr();return 0;
}
shared_ptr 原型
在编译器中,shared_ptr 就是根据下图的方式定义
C++11和boost中智能指针的关系
- C++ 98 中产生了第一个智能指针auto_ptr.
- C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
- C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
- C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的 scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
RAII解决死锁
RAII思想除了可以用来设计智能指针,还可以用来设计守卫锁,防止异常安全导致的死锁问题
#include <thread>
#include <mutex>
// C++11的库中也有一个lock_guard,下面的LockGuard造轮子其实就是为了学习他的原理 template<class Mutex>
class LockGuard
{public: LockGuard(Mutex& mtx) :_mutex(mtx) { _mutex.lock(); }~LockGuard() { _mutex.unlock(); } LockGuard(const LockGuard<Mutex>&) = delete;private:
// 注意这里必须使用引用,否则锁的就不是一个互斥量对象 Mutex& _mutex;
};mutex mtx;
static int n = 0;void Func()
{ for (size_t i = 0; i < 1000000; ++i) { LockGuard<mutex> lock(mtx); ++n; }
}
int main()
{ int begin = clock(); thread t1(Func); thread t2(Func);t1.join(); t2.join();int end = clock();cout << n << endl; cout <<"cost time:" <<end - begin << endl; return 0;
}