C++的智能指针(shared_ptr,weak_ptr)
一:概述
参考资料:《C++ Primer中文版 第五版》
我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。
在C++中,动态内存的管理是用一对运算符完成的:new和delete,new:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,delete:指向一个动态独享的指针,销毁对象,并释放与之关联的内存。
动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。
为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同,
shared_ptr允许多个指针指向同一个对象。属于强引用类型,他会维护引用计数的信息,即会记录当前有多少个存活的 shared_ptrs 正持有该对象. 共享的对象会在最后一个强引用离开的时候销毁( 也可能释放).
unique_ptr则“独占”所指向的对象。
标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,他不控制对象生命周期, 虽然指向shared_ptr指针指向的对象的内存,却并不拥有该内存。
这三种智能指针都定义在memory头文件中。
二:解析
为什么把上面这段话全部粘贴下来,因为真的说的太好了,都是干货,不仅简单阐述了智能指针的应用场景和原理,还帮我们回忆了一些基本知识,所以提取出来的知识点有
1,局部变量都是桟里的,所以没事。所以并不是说所有的指针都要担心他的释放而使用智能指针,而是当指针指向是一块堆中分配的内存的时候
2,new和delete是一对,delete释放的内存是级联的么?这个疑问先保留。
3, 首先分配的堆内存就那么一块,一旦这块内存被shared_ptr类型的指针指向,那么他在拥有这块存储的操作权的同时,还同时还知晓此时还有多少个像他一样的指针也拥有这块内存
更要命的是,这个指针并不是单纯的指向,他还涵盖一个天大的动作,即最后一个这样类型的指针发现就剩他自己指向该对象的时候,他会在自己消亡之前把这块内存也释放了,
即会调用对应的delete函数。
注意:什么叫自己消亡,如果这个指针只是一个局域变量,或者自己所属的类被释放了....
4.weak_ptr就是一个旁观者,可以看到这块内存被多少个shared_ptr引用了,而不能通过本指针去操作这块内存
三:实践
通过以上的逻辑,多说无意,一起实践以下,先简单介绍一下我的整体框架:
核心类为Ele,其内部有个变量是个指针,指针指向的也是一块内存,
这块内存是一个类的实例,并且还存在多态,即根据不同的情况实际创建的是不同的类实例,于是:
1.核心工作类
typedef shared_ptr<UDPProtocol> SProtocol; //SProtocol是一个指向UDPProtocol类型内存的指针,并且是一个强引用,即他拥有访问该存储的权限,并且他的指向会增加计数引用,并并且他有权释放该内存 typedef weak_ptr<UDPProtocol> WProtocol; //WProtocol是指向该内存的弱引用,他可以看作是就是一个快照!!! struct Ele{ ... SProtocol NewUDPProtocol(int pType); SProtocol GetUDPProtocol();private: void FreeEle(); private: WProtocol pUdpProtocol; //核心类只记录申请内存的快照而以,这样他的存在就不影响对象的释放,如果是强引用,那么他只要存在,申请的内存就永远无法释放。 }; vector<SProtocol> sProtocols; //但是在对象的生命周期中,总得有个强指针一直引用着吧,因为在核心类中的WProtocol类型的变量只是个快照,他的存在是依赖强引用sProtocols。
2.共享指针的用法:创建获取与检查,与其他
1)创建共享指针(强引用)方式如下:
SProtocol sh = make_shared<SIPProtocol>();
(2)强引用指针shared_ptr有如下可用的工具函数:
.get():可以用该函数获取底层对象的普通指针,或者说是该指针包含的裸指针。注意该裸指针的值就是底层对象的指针,也就是等同于最开始new的那块内存的地址,因此他的获取可以用来判断,具体用法见下面
shared_ptr还定义了自己的类型转换操作符:static_pointer_cast, dynamic_pointer_cast, const_pointer_cast ,具体用法见下面代码。
(3)弱引用指针weak_ptr不能直接访问对象,但是他有一些工具函数可以用来用
.expired()用来查看自己监视的对象是否还在,或者他依赖的那些强引用指针们还有没有了
.use_count()用来看看我依赖的强引用指针们现在有多个,即目前有多少个指针指向底层对象
.lock()会检查weak_ptr指向的对象是否存在。如果存在,返回一个指向共享对象的shared_ptr,如果不存在,lock将返回一个空指针。
代码:
//0.核心类调用,即智能指针使用的开始 Ele ingress; ingress.NewUDPProtocol(APP_PROTOCOL_SIP); //1. 创建底层实例函数,以及其他 SProtocol Ele::NewUDPProtocol(int pType){ SProtocol sh; if (pType==APP_PROTOCOL_SIP || port == 5060 ||portSer == 5060 || port == 5061 ||portSer == 5061){ sh =make_shared<SIPProtocol>(); //根据不同的情况,创建不同的底层实例 }else{ sh =make_shared<UDPProtocol>(); } pUdpProtocol=sh; appType=pType; sProtocols.push_back(sh); //将强引用指针添加到vector中,维持这对象的生命周期 return sh; } //2.获取核心类中的底层实例对应的强引用指针 SProtocol Ele::GetUDPProtocol(){ /* if(pUdpProtocol.expired()){ //只是一个检查,看看依赖的强引用指针还有没有 return NULL; }*/ return pUdpProtocol.lock(); //.lock()函数获取对应的强引用指针 } //3.释放核心类时候,需要做的底层释放 void Ele::FreeEle(){ if(!pUdpProtocol.expired()){ SProtocol p = pUdpProtocol.lock(); for(auto iter = sProtocols.begin();iter!=sProtocols.end();iter++){ if(iter->get()==p.get()){ //指向的是同一块底层解构体 sProtocols.erase(iter); //从容器中把强引用拿出来,从此底层就可以随时被清除了,相当于把他的根给坎了 break; } } log_Debug("FreeEle:: after erase(if exist) the origin share pUdpProtocol,now the use_count is:%d",pUdpProtocol.use_count()); } }
3. 附底层类和其子类,所创建的底层存储就是这些类型的实例:
class UDPProtocol{ public: mutex mutex_protocol; UDPProtocol(int pType) : protocolType{PROXY_PROTOCOL_NAME[pType]}{} UDPProtocol() : protocolType{PROXY_PROTOCOL_NAME[PROXY_PROTOCOL_UDP]}{} ~UDPProtocol(){} }; class SIPProtocol : public UDPProtocol{ public: SIPProtocol():UDPProtocol(PROXY_PROTOCOL_SIP),msg(NULL){} ~SIPProtocol(){UACMap.clear(); if(msg)FreeSIPMsg();} mutex mutex_protocol; };
4. 在由多态的场景下,智能指针的强转用法如下
Ele ingress; shared_ptr<SIPProtocol> sip; //首先声明子类的强引用指针 SProtocol sp=ingress.GetUDPProtocol(); //获取到父类的强引用指针 sip=dynamic_pointer_cast<SIPProtocol>(sp); //强转得到子类的强引用指针 ingress.NewUDPProtocol(APP_PROTOCOL_SIP);