目录
1.1RAII(资源获取几初始化)
1.2auto_ptr
1.3unique_ptr
1.5weak_ptr
我们在在动态开辟空间的时候,malloc出来的空间如果没有进行释放,那么回传在内存泄漏问题。或者在malloc与free之间如果存在抛异常,那么还是有内存泄漏安全。因此我们在这里引入了智能指针来对资源进行管理。(内存泄漏)
1.1RAII(资源获取及初始化)
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法的好处:
- 不需要显示的释放资源。
- 采用这种方式,对象所需要的资源在其生命期内始终保持有效。
总结:RAII就是一种管理资源自动释放的一种机制,初步看来,他通过类将资源包装起来。在进行资源初始化时,巧妙地利用编译器会自动调用构造函数预计析构函数的特性,来完成对资源的自动释放。在构造方法中,将资源放入,让对象进行释放,在析构方法中,将资源释放掉。
#include<iostream>
using namespace std;
//智能指针的原理:RAII+具有指针类似的行为
//我们在这里自己进行封装
template<class T>
class Smartptr{
public:Smartptr(T* p = nullptr) :ptr(p){}~Smartptr(){if (ptr){//此时指针如果不为空且具有释放的权利的时候,则将其释放,且将owner重新职位falsedelete ptr;ptr = nullptr;}}//在使用指针是我们有*与->的使用,因此在这里要对齐进行运行算符重载//重载*T& operator*(){return *ptr;}//他只能在指针指向的是对象或者是结构体的时候来使用T& operator->(){return ptr;}//某些情况下使用原生态指针T* get(){return ptr;}
private:T* ptr;//采用类进行指针管理
};
int main(){Smartptr<int> st1(new int);Smartptr<int> at2(st1);//此时调用拷贝构造函数,但是这个类里面没有,因此只能使用默认的拷贝构造//因此是浅拷贝return 0;
}
根据上面代码,我们先简单的模拟了一下智能指针发现了存在这一个致命的问题,如果当一个对象对另一个对象进行拷贝构造时,由于没有定义拷贝构造函数,那么就会使用到默认的拷贝构造函数,产生浅拷贝问题。又因为所有的智能指针都是一样的,那如何解决浅拷贝问题呢?我们在前面学习string类时,对浅拷贝的解决方式时使用深拷贝,但是在这里我们不能使用深拷贝,在string类中,因为其内部要存字符串,需要申请空间,而string类中的空间是自己申请与维护的,而智能指针的资源是用户提供的,如下图:
智能指针不能申请资源只能提用户来管理资源,因此此处不能使用深拷贝的方式来解决问题。
1.2auto_ptr
资源完全转移
我们参考C++98版本的库中就提供了auto_ptr的智能指针是如何解决浅拷贝问题的。
namespace bite{template<class T>class auto_ptr{public:// RAII : 保证资源可以自动释放auto_ptr(T* ptr = nullptr): _ptr(ptr){}~auto_ptr(){if (_ptr){delete _ptr;_ptr = nullptr;}}// 解决浅拷贝方式:资源转移// auto_ptr<int> ap2(ap1)auto_ptr(auto_ptr<T>& ap): _ptr(ap._ptr){ap._ptr = nullptr;}// ap1 = ap2;auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap){// 此处需要将ap中的资源转移给this// 但是不能直接转移,因为this可能已经管理资源了,否则就会造成资源泄漏if (_ptr){delete _ptr;}// ap就可以将其资源转移给this_ptr = ap._ptr;ap._ptr = nullptr; // 让ap与之前管理的资源断开联系,因为ap中的资源已经转移给this了}return *this;}// 对象具有指针类似的行为T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* Get(){return _ptr;}private:T* _ptr;};
}
int main(){auto_ptr<int> st1(new int);auto_ptr<int> at2(st1);return 0;
}
我们观察上述代码,虽然他解决了浅拷贝问题,但是他又引入了新的问题,。当对象拷贝或者赋值后,前面的对象就悬空了。它的缺陷就是当我们想访问或者修改st1对象的时候,代码会崩溃。
资源管理权限转移
为了解决上面的问题有使用了转移资源管理权限的思想。
#include<iostream>
using namespace std;
//智能指针的原理:RAII+具有指针类似的行为
//我们在这里自己进行封装
template<class T>
class autoptr{
public:autoptr(T* p = nullptr) :ptr(p), owner(true){}~autoptr(){if (ptr && owner){//此时指针如果不为空且具有释放的权利的时候,则将其释放,且将owner重新职位falsedelete ptr;owner = false;}}//在使用指针是我们有*与->的使用,因此在这里要对齐进行运行算符重载//重载*T& operator*(){return *ptr;}//他只能在指针指向的是对象或者是结构体的时候来使用T& operator->(){return ptr;}//某些情况下使用原生态指针T* get(){return ptr;}//因此在这里解决浅拷贝问题//资源管理权限的转移autoptr(autoptr<T>& p) :ptr(p.ptr), owner(p.owner){p.owner = false;}T& operator=(autoptr<T>& p){//赋值运算符的重载if (this == p){//首先判断是否是自己给自己复制return p;}if (ptr && owner){//如果此时ptr不为空且具有权限,那么此时就将现在的资源释放掉,顺便拿到p的权限delete ptr;ptr = p.ptr;owner = p.owner;p.owner = false;}}//某些情况下使用原生态指针
private:T* ptr;//采用类进行指针管理
};
int main(){autoptr<int> st1(new int);autoptr<int> at2(st1);//此时调用拷贝构造函数,但是这个类里面没有,因此只能使用默认的拷贝构造函数//因此是浅拷贝return 0;
}
如上面代码,当发生拷贝构造或者赋值时,将被拷贝对象中资源转移给新对象,然后让被拷贝对象与资源断开联系,这样就解决了一块空间被多个对象使用而造成程序崩溃问题。但是在这里存在着致命缺陷。再对st1进行拷贝后将其的指针赋值为空,导致了st1对象悬空,通过st1对象访问资源就会出现问题,会造成野指针,使代码崩溃。因此要在这里说明什么情况下对不要使用auto_ptr。
1.3unique_ptr
上面的问题都是因为发生了拷贝构造然后造成的,因此unique_ptr在这里采用的方式是禁止拷贝。也就是说,一份资源只能被一个对象来进行管理,对象之见不能共享资源(资源独占)。解决浅拷贝方式--资源独占,防止拷贝,在这里有两种方案,第一种:C++98中的方案,将拷贝构造函数以及赋值运算符重载方法只进行声明不进行定义,并且将其权限给成私有的,这样就防止其被拷贝。第二种:C++11种的方案:可以让编译器不生成默认的拷贝构造以及赋值运算符delete,delete关键字它的扩展功能就是从堆上进行释放资源,用其修饰默认的构造函数,表明编译器不会生成了。
#include<iostream>
using namespace std;
//智能指针的原理:RAII+具有指针类似的行为
//我们在这里自己进行封装
template<calss T>
class DF_new{
public:void operatr()(T*& ptr){if(ptr){delete ptr;ptr = nullptr;}}
};
template<calss T>
class DF_free{
public:void operatr()(T*& ptr){if(ptr){free(ptr);ptr = nullptr;}}
};
//关闭文件指针
template<calss T>
class DF_close{
public:void operatr()(FILE*& ptr){if(ptr){fclose(ptr);ptr = nullptr;}}
};
//T:资源中所放的数据的类型
//DF:资源的释放方式
template<class T,class DF = DF_new<T>>//DF释放的方式
class uniqueptr{
public:uniqueptr(T* p = nullptr) :ptr(p){}~uniqueptr(){if (ptr){//对于ptr管理的资源,有可能是从堆上申请的内存空间,文件指针,malloc空间...//因此他在释放的是否是要进行考虑的,是不同的,解决的方式就是对这个类再加上一个模板参数列表即可ptr = nullptr;}}//在使用指针是我们有*与->的使用,因此在这里要对齐进行运行算符重载//重载*T& operator*(){return *ptr;}//他只能在指针指向的是对象或者是结构体的时候来使用T& operator->(){return ptr;}//某些情况下使用原生态指针T* get(){return ptr;}//解决浅拷贝方式--资源独占,防止拷贝,在这里有两种方案//第一种:C++98中的方案:
private:uniqueptr(const uniqueptr<T,DF>&);uniqueptr<T&>operator=(const uniqueptr<T,DF>&);//第二种:C++11中的方案:可以让编译器不生成默认的拷贝构造以及赋值运算符--deleteuniqueptr(const uniqueptr<T,DF>&) = delete;//表明编译器不会生成默认的赋值运算符重载uniqueptr<T,DF>& operator=(const uniqueptr<T,DF>&) = delete;
private:T* ptr;//采用类进行指针管理
};
在这里说明一下为什么在C++98中对其拷贝构造函数与赋值运算符重载只进行定义,不声明不定义,且将其权限给成私有的。如果没有将其设置为私有的,那么用户就会在外部对其方法进行定义。
unique_ptr指针适用于资源被一个对象管理并且不会被共享。他的缺陷就是多个对象中资源无法进行共享,因此使用到了shared_ptr指针。
1.4shared_ptr
共享指针,对个对象之间可以共享资源。在这里采用引用计数的方式来进行浅拷贝的。引用计数实际上就是一个整形空间,记录使用资源的对象的个数,在释放之前,让最后一个使用资源的的对象来进行释放。
#include<iostream>
using namespace std;
//智能指针的原理:RAII+具有指针类似的行为
//我们在这里自己进行封装
template<class T,class DF = DF_new<T>>
class sharedptr{
public:sharedptr(T* p = nullptr) :ptr(p),p_count(nullptr){if(ptr){//此时只有当前建好的一个对象在使用该份资源p_count = new int(1);}}~sharedptr(){if (ptr && 0 == --(*count)){DF df;df(ptr);delete p_count;p_count = nullptr;}}//在使用指针是我们有*与->的使用,因此在这里要对齐进行运行算符重载//重载*T& operator*(){return *ptr;}//他只能在指针指向的是对象或者是结构体的时候来使用T& operator->(){return ptr;}//某些情况下使用原生态指针T* get(){return ptr;}//用户可能需要获取引用计数int use_count()const{return *p_count;}//解决浅拷贝方式,引用计数sharedptr(const sharedptr<T,DF>& sp):ptr(sp.ptr),p_count(sp.p_count){if(ptr){++(*p_count);}}sharedptr<T,DF>& operator=(const sharedptr<T,DF>& sp){if(this != &sp){//在sp共享之前,需要将之前的资源进行释放if(ptr && 0 == --*(p_count)){//如果此时之前的内容只有他一个进行管理,那么直接进行释放DF df;df(ptr);delete p_count;}//this就可以与sp进行共享了ptr = sp->ptr;p_count = sp->p_count;if(p_count){p_count++;}}return *this;}private:T* ptr;//采用类进行指针管理int* p_count;//指向的是使用资源的对象的个数
};
释放的操作:先检测是否有资源,有资源即是pcount>=1,先给计数器进行-1操作,然后检测计数器是否为0,如果是0,则说明当前对象是最后使用资源的对象,,需要将资源以及计数空间进行释放,当为非0的时候,说明还有其他对象在使用资源,当前资源不需要释放。
我们观察上面的代码,可以判断吹他在单线程下是没有出现问题的,但是在多线程下可能是有问题的。多线程下有多个执行流,CPU也是多核的,多个线程同时往下执行,假设现在连个线程中的智能指针共享的是同一份资源,两个线程结束时,需要将其管理的资源释放掉。也有情况下,线程同事进行判断,使得最后导致资源没有进行释放,而引起资源泄漏。因此,在遇到共享的资源,变量等等之类的,需要考虑多线程环境下的安全性。因此最常见的方式是对其进行加锁。在这里进行加锁,是为了保证自身的安全性。
#include<iostream>
using namespace std;
//智能指针的原理:RAII+具有指针类似的行为
//我们在这里自己进行封装
template<class T,class DF = DF_new<T>>
class sharedptr{
public:sharedptr(T* p = nullptr) :ptr(p),p_count(nullptr),mutex(new mutex){if(_ptr){p_count = new int(1);}}~sharedptr(){reldef();}//在使用指针是我们有*与->的使用,因此在这里要对齐进行运行算符重载//重载*T& operator*(){return *ptr;}//他只能在指针指向的是对象或者是结构体的时候来使用T& operator->(){return ptr;}//某些情况下使用原生态指针T* get(){return ptr;}//用户可能需要获取引用计数int use_count()const{return *p_count;}//解决浅拷贝方式,引用计数sharedptr(const sharedptr<T,DF>& sp):ptr(sp.ptr),p_count(sp.p_count),_pmutex(sp._pmutex){Addref();}sharedptr<T,DF>& operator=(const sharedptr<T,DF>& sp){if(this != &sp){//在sp共享之前,需要将之前的资源进行释放reldef();//this就可以与sp进行共享了ptr = sp->ptr;p_count = sp->p_count;_pmutex = sp._pmutex;Addref();}return *this;}
private:void Addref(){//对加法进行处理if(!ptr) return;_pmutex->lock();++(*p_count);_pmutex->unlock();}//此时我们还需要判断锁是否需要释放void reldef(){//对减法进行处理if(ptr) return; bool isdelete = false;_pmutex->lock();if (ptr && 0 == --(*count)){DF df;df(ptr);delete p_count;p_count = nullptr;//当资源释放完毕后,对其进行标记isdelete = true;}_pmutex->unlock();if(isdelete){delete(_pmutex);}}
private:T* ptr;//采用类进行指针管理int* p_count;//指向的是使用资源的对象的个数mutex* _pmutex;//加上锁的原因是要保证在这里引用计数的操作是原子性的
};
虽然shared_ptr在这里是可以避免拷贝构造带来的错误,但是他自身也有缺陷。在使用shared_ptr时可能会引起循环引用。什么是循环引用呢?我们先举个例子。
#incldue<memory>
struct ListNode{shared_ptr<ListNode*> next;shared_ptr<ListNode*> prve;int data;shared(int x):next(nullptr),prev(nullptr),data(x){cout<<"ListNode(int)"<<this<<endl;}~ListNode(){cout<<"~ListNode():"<<this<<endl;}
};
void Looptest(){//将两个节点分别交给智能指针来管理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->next = sp2;sp2->prev = sp1;cout<<sp1.use_count()<<endl;cout<<sp2.use_count()<<endl;
}
int main(){Looptest();
}
当shared_ptr管理的资源在相互指向的时候,我们看上面代码的运行情况:在结果中,我们发现运行时并未出现调用析构函数的结果,在这里没有释放掉资源,因此会引起资源泄露问题。也就是说,循环引用是指两个对象之间形成了环路,在智能指针shared_ptr中存在这个问题,他的引用计数不为0。也就是两份资源分别等待对方先进行释放,最后导致了内存泄漏。处理这种现象十分简单,只需要只使用一个weak_ptr即可。
1.5weak_ptr
weak_ptr的实现原理是使用了引用计数进行实现的,他不可以进行资源的管理,唯一的作用就是配合shared_ptr解决循环引用的问题。
#incldue<memory>
struct ListNode{weak_ptr<ListNode*> next;weak_ptr<ListNode*> prve;int data;shared(int x):next(nullptr),prev(nullptr),data(x){cout<<"ListNode(int)"<<this<<endl;}~ListNode(){cout<<"~ListNode():"<<this<<endl;}
};
void Looptest(){//将两个节点分别交给智能指针来管理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->next = sp2;sp2->prev = sp1;cout<<sp1.use_count()<<endl;cout<<sp2.use_count()<<endl;
}
int main(){Looptest();
}
我们看上面的代码,此时析构函数执行了,并没有发生引用循环。
question:为什么weak_ptr可以解决循环引用?
原因是在他的引用计数上。如上图代码,我们进行分析:
在标准库中,weak_ptr的引用计数维护了两份,由图可知,当开始执行时,use=weak=1;此时在执行sp1->next=sp2,因为sp1->next的类型是一个weak_ptr,因此此时的sp2的引用计数的weak++,再执行sp2->prve=sp1,因为sp2->prve的类型也是一个weak_ptr,因此此时的sp1的引用计数weak++;此时sp1指向空间中的计数use=1,weak=2,sp2指向的资源空间的计数也是一样。
现在要对资源进行释放。首先释放sp2,因为sp2的类型是一个shared_ptr,use--等于0,说明此时资源是可以进行释放的,因此就要对对象内部的每一个资源进行释放掉,sp2->prev是weak_ptr类型,将其销毁,那么左面资源的中的引用计数weak--,然后sp2->prve与sp1断开,next指针也销毁掉了,因此此时的节点也销毁掉了,所以sp2的pcount与资源的引用计数断开,右面的资源的引用计数weak--。
现在进行释放sp1,因为sp1的类型是一个shared_ptr,use--等于0,说明此时资源是可以进行释放的,因此就要对对象内部的每一个资源进行释放掉,sp1->next是weak_ptr类型,将其销毁,那么右面资源的中的引用计数weak--,此时右面的引用计数的weak=0,因此就可以将右面资源的引用计数进行释放;左面资源的prve指针此时也销毁了,此时节点进行销毁,所以sp1的pcount与资源的引用计数断开,左面的weak--等于0,此时将左面的资源的引用计数进行销毁。
总结:当一个资源被shared_ptr共享时,use++;当一个资源被weak_ptr共享时,weak++。且只有shared_ptr可以独立的管理资源。
question:unique_ptr与shared_ptr能否可以管理一块连续空间?
可以。如果要管理里一段连续的空间,我们必须自己实现删除器,operator()(T*&ptr){delete[] ptr;ptr=nullptr;}。但是没有什么意义,对于连续空间,一般是不会直接交给智能指针进行管理的,因为在STL中已经有了vector。