C++11新特性全面解析(三):智能指针与死锁

本篇是c++中比较重要的章节,补充了前面碰到的有关智能指针却没有讲解的语法及特性

目录

智能指针

1.为什么需要智能指针?

2.智能指针的使用及原理

[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

相关推荐
REDcker2 小时前
JS 与 C++ 语言绑定技术详解
开发语言·javascript·c++
认真敲代码的小火龙2 小时前
【JAVA项目】基于JAVA的医院管理系统
java·开发语言·课程设计
曼巴UE53 小时前
UE5 C++ 动态多播
java·开发语言
小小晓.3 小时前
Pinely Round 4 (Div. 1 + Div. 2)
c++·算法
SHOJYS3 小时前
学习离线处理 [CSP-J 2022 山东] 部署
数据结构·c++·学习·算法
steins_甲乙3 小时前
C++并发编程(3)——资源竞争下的安全栈
开发语言·c++·安全
煤球王子3 小时前
学而时习之:C++中的异常处理2
c++
请一直在路上4 小时前
python文件打包成exe(虚拟环境打包,减少体积)
开发语言·python
luguocaoyuan4 小时前
JavaScript性能优化实战技术学习大纲
开发语言·javascript·性能优化