本篇是c++中比较重要的章节,补充了前面碰到的有关智能指针却没有讲解的语法及特性
目录
[2.1 使用](#2.1 使用)
[2.2 RAII](#2.2 RAII)
[2.3 智能指针的坑](#2.3 智能指针的坑)
[3. 三种解决方案解决智能指针的坑](#3. 三种解决方案解决智能指针的坑)
[3.1 c++98 auto_ptr](#3.1 c++98 auto_ptr)
[3.2 c++11 unique_ptr](#3.2 c++11 unique_ptr)
[3.3 c++11 shared_ptr](#3.3 c++11 shared_ptr)
[3.31 share_ptr的拷贝和赋值的线程安全问题](#3.31 share_ptr的拷贝和赋值的线程安全问题)
[3.32 share_ptr的缺陷:循环引用问题](#3.32 share_ptr的缺陷:循环引用问题)
[3.33 weak_ptr解决share_ptr的循环引用缺陷](#3.33 weak_ptr解决share_ptr的循环引用缺陷)
[4. 智能指针的发展历史](#4. 智能指针的发展历史)
[1. 为什么死锁无法使用智能指针管理](#1. 为什么死锁无法使用智能指针管理)
[2. 基于RAII思想设计的锁管理守卫](#2. 基于RAII思想设计的锁管理守卫)
[3. lock_guard和unique_lock的区别](#3. lock_guard和unique_lock的区别)
智能指针
1.为什么需要智能指针?
c++由于没有Java的gc,new、malloc、socket网络套接字,fopen等等出来的资源,需要我们手动去释放。
两种情况:1.忘记释放 2.发生异常安全问题
这些最终都会导致资源的泄漏
对于之前的异常安全我们有提到:解决方案是再抛(但是你完全不知道不清楚这个函数会不会抛异常,别人写的函数并不规范,不在函数后面添加相关是否会抛异常的信息)
cpp
void f1(){
int* p=new int;
try{
cout<<div()<<endl;
}
catch(...){
delete p;
throw;
}
delete p;
}
难道对于每个这样的函数我都需要再抛???
所以,综上提出了智能指针
2.智能指针的使用及原理
2.1 使用
智能指针简单来说就是通过一个类对象的生命周期来管理资源
cpp
template<class T>
class SmartPtr
{
public:
// RAII
// 保存资源
SmartPtr(T* ptr)
:_ptr(ptr)
{}
// 释放资源
~SmartPtr()
{
if(_ptr){
cout<<"delete:"<<_ptr<<endl;
delete _ptr;
}
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return return _ptr;
}
private:
T* _ptr;
};
注意:我们这里必须要重载*和->,因为我们要模仿原生指针的行为
cpp
void f1(){
int* p=new int;
SmartPtr<int> sp(p);
cout<<div()<<endl;
}
int main(){
try{
f1();
}
catch(exception&e){
cout<<e.what()<<endl;
}
return 0;
}
把指针交给SmartPtr管理,无论是函数正常结束还是抛异常,都会导致sp的生命周期到了之后自动调用析构函数进行释放
2.2 RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的****时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效
总的来说:是智能指针采用了RAII的思想(一种托管资源的思想),并不是智能指针就是RAII,智能指针式依靠这种RAII实现的,unique_lock/lock_guard也是。
2.3 智能指针的坑
对于原生指针,我们可以
cpp
int* p1 = new int;
int* p2=p1;
但是如果是智能指针
cpp
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2=sp1;
这样会出错,由于我们没有实现拷贝构造,这里会直接复制,然后释放两次就会出现问题

一块空间被析构两次
智能指针智能就是因为它比原生指针能够自己释放资源,但是这里的问题就是不能指向同一块资源,原生指针可以,所以后面就演变出来如何解决这个问题
3. 三种解决方案解决智能指针的坑

c++98:提出了auto_ptr(注意,这个最好不要用,有问题)
c++11:提出了unique_ptr和shared_ptr(这两个可以用)

这里对于类对象string的拷贝,是需要开辟两块空间,单独指向的,因为这都是string的资源,都是属于你自己想要开辟出的资源
但是智能指针不行,我是让你去托管资源,而不是另外开辟出来资源再指向,这个资源不属于智能指针(所以这种解决方案完全不行)
3.1 c++98 auto_ptr
对于拷贝构造函数,直接实现管理权转移,把管理权交给新的,旧的设为nullptr
cpp
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap.ptr) {
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(const auto_ptr<T>& ap) {
if (this != &ap) {
if (_ptr) {
delete _ptr;
}
_prt = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
设计存在缺陷:你完全不注意的话,原来的智能指针变为了nullptr,如果解引用就会出错,导致空指针的错误,程序被终止,一般公司都明令禁止使用
3.2 c++11 unique_ptr
简单粗暴:直接禁掉拷贝构造函数和operator=

简单粗暴,推荐使用
缺陷:如果有需要拷贝的场景,他就没法使用
3.3 c++11 shared_ptr
增加引用计数,只有当计数为0的时候才是真正的删除
方法一:int count;
这个不正确,因为这样的话计数器就不是同一个,各自拥有单独的计数器,修改只会影响字节的


修改sp的计数器和修改_count的计数器是不同的,对于原指针,我们想的是ptr1=ptr2,管理同一块内存空间,并且计数器是同一个,这里的计数器不同,导致最后想要析构的时候,计数器还剩1,析构失败
方法二:使用static int _count;
这个也不正确,因为static虽然是大家一起共用一个,但是这个是类专属,不是实例化之后的专属,因为你有可能有很多智能指针(share_ptr<int>,share_ptr<double>),大家共用一个释放肯定有问题

我们需要实现的是这个样子
方法三:使用指针 int*_count;(注意不使用引用的原因是外部可以改变)
这样可以记录有多少个对象共享管理资源,最后一个析构释放资源即可


3.31 share_ptr的拷贝和赋值的线程安全问题
对于多线程而言,当对(*pcount)++的时候就会出现线程安全问题

_count应该变为3,但是由于多线程问题,大家同时+,可能会导致变为2

因为++不是原子性的,所以如果原来是1,我放到寄存器当中,加到2,但是此时时间片到了,我没有把寄存器中的值放回去内存,换别的线程来加,直接拿内存当中的1到寄存器中又+
这种概率出现的概率小,但是+的次数多了,线程多了就可能出现问题

这里模拟的是多线程安全问题:for循环是一个语句块,里面是临时变量,里面sp1是拷贝构造函数,出了一次循环还析构一次,这样大概率是有线程安全问题的

直接崩溃掉了,引用计数存在线程安全问题
解决方法:前面篇章有讲解
1.使用atomic(原子性操作)(c++库实现的主要利用原子操作)
2.互斥锁
注意:互斥锁的开销远大于原子操作,这里我们模拟实现使用互斥锁
注意:互斥锁这里需要使用指针类型,和前面的原因一样,
如果是普通变量就导致智能指针的锁不一样,锁不住
如果是static就导致不同的智能指针的锁都一样,锁的粒度太大了
使用mutex* _pmtx; (std::atomic<int>* _pcount;(原子操作就需要用这个))
这里我们抽象出两个函数
一个是add_ref_count(); //实现_count++
一个是release(); //实现_count--,并且如果减到0,要释放
cpp
void add_ref_count()
{
_pmtx->lock();
++(*_pcount);
_pmtx->lock();
}
void release()
{
bool flag=false;
//增加这个是判断锁什么时候释放,否则再if里面释放的话后面没有人释放锁
//并且这个flag是在每个线程的独立的栈帧的,是没问题的
_pmtx->lock();
if(--(*_pcount)==0){
delete _ptr;
_ptr=nullptr;
delete _pcount;
_pcount=nullptr;
flag=true;
}
_pmtx->unlock();
if(flag==true){
delete _pmtx;
_pmtx=nullptr;
}
}
凡是用到--和++的地方全都换成这两个函数即可
库当中实现的是线程安全的,这个是单单引用计数的增减是线程安全的
如果是多线程读写share_ptr智能指针本身是不安全的(拷贝、赋值)
多线程通过share_prt智能指针访问资源是不安全的(*sp)
3.32 share_ptr的缺陷:循环引用问题

注意:这个结构体的成员变量是share_ptr<ListNode>,如果你是ListNode* next的话
后面去赋值,比如spn1->next=node2是不可以的,因为你spn2是一个智能指针类型,而你的spn1->next是一个ListNode*类型,不一样赋值不了,所以这里我们需要处理的是成员变量得是share_ptr<ListNode>类型,才能赋值
也就是说 _next 析构了, node2 就释放了。
也就是说 _prev 析构了, node1 就释放了。
但是 _next 属于 node 的成员, node1 释放了, _next 才会析构,而 node1 由 _prev 管理, _prev 属于 node2


这样是会出现循环引用问题的,看图分析:
如果spn1释放了,此时左边的_count变为1,spn2释放了,此时右边的_count也变为1
但是注意左右两边的空间还存在,并且互相捏着对象空间的管理(智能指针),此时就会导致spn1和spn2都释放了,但是空间还在,并且由对方来管理,结点里的智能指针是跟结点的生命周期一样的,由结点决定,只有结点空间销毁了,智能指针才会销毁,但是互相捏着对方,除非main函数结束,否则一直不会释放,这就是循环引用(share_ptr的缺陷)
问题就是:对于长时间运行的程序(如服务器、后台服务)中,循环引用会导致内存持续占有且无法释放,从而导致资源泄漏
为了解决这个问题,提出了weak_ptr解决循环引用
3.33 weak_ptr解决share_ptr的循环引用缺陷
严格来说weak_ptr不是智能指针,因为它没有使用到RAII的资源管理机制
这个是专门解决share_ptr的循环引用问题的

当遇到像之前遇到的结点当中有循环引用的问题,我们此时不增加引用计数,直接交给weak_ptr,因为weak_ptr是专门用来接受share_ptr类型的
对于会出现循环引用的地方使用weak_ptr不增加引用计数即可
注意:weak_ptr不提供*和->,因为无法保证你的对象是否还存在,可能会导致野指针的风险,你可以通过lock()函数转换成shared_ptr后判断
4. 智能指针的发展历史


官方的智能指针在<memory>头文件当中
对于array版本是针对new[]的 析构函数的时候使用delete[] 并且重载operator[]
但是c++11没有实现[]版本
那这样new[]出来的对象怎么办???

对于234的场景如何解决???因为share_ptr中的是delete,并不是delete[] 、free、fclose
解决方案:定制删除器:写一个仿函数给它调用

官方是支持一个仿函数传参的

对于访问我们可以使用get()函数先获得指针,int*arr =sp.get();arr[]即可
死锁问题
由于有异常就会有异常安全,那就需要RAII
由于构造函数和析构函数是自动调用,也就是出了作用会自动调用析构函数即使你异常了
由于c++没有gc(gc就是垃圾回收器,后台专门有一个线程来定期清理垃圾,有损耗,性能会降低)
但java和c++都无法解决一个问题:死锁问题
1. 为什么死锁无法使用智能指针管理
锁的本质是保证多线程安全,但是这个是由你编写程序的人来决定的,也就是说它不是像资源一样,你锁不用了智能指针可以帮我释放,但是锁,程序怎么知道你什么时候解锁,怎么知道你用不用,锁是应该由程序员来控制的
如果使用智能指针,它仅仅是保证了资源的所有权正常释放,但是你的锁是管理临界区,是加解锁,两个管理的资源都不一样
2. 基于RAII思想设计的锁管理守卫

这个写法有问题:_lk(lock),这里使用了拷贝构造函数,锁本身是不支持拷贝的,因为你拷贝出来了一把锁,那这把拷贝出来的锁究竟是不是我的呢???
所以成员变量应该使用引用,这样就是同一把锁(很少有成员变量定义成引用,这是很少的场景下才会使用的)

注意:同时需要禁掉拷贝构造和赋值构造
3. lock_guard和unique_lock的区别
两个都是RAII思想
但是lock_guard只有构造函数和析构函数
而unique_lock可以手动的加锁/解锁,场景比较灵活
内存泄漏相关问题
什么是内存泄漏
内存泄漏的危害是什么
如何解决内存泄漏
内存泄漏:程序已动态分配的堆内存不再使用,但未释放,导致系统内存被持续占用,最终可能引发内存耗尽、程序卡顿甚至崩溃。危害:对于一般程序如果内存泄漏,重启之后就ok了(重启之后os会自动回收进程的所有资源),但是对于长期运行,不能随便重启的程序,碰到内存泄漏危害非常大,比如os,服务器,这些长期运行,不用的内存没有释放,会导致可用的内存越来越少,导致服务很多操作失败(容器存数据,打开文件,创建套接字,发送数据等等都是需要内存的),对于这些服务如果出现内存泄漏,都是事故
如何解决:
a.写c/c++代码时小心谨慎一点
b.不好处理的地方多用智能指针等等去管理(事前预防)
c.如果怀疑存在内存泄漏,或者已经出现,可以使用内存泄漏工具去检测(事后解决)
内存泄漏检测工具:valgrind