【C++】:智能指针 -- RAII思想&shared_ptr剖析

【C++】:智能指针 -- RAII思想&shared_ptr剖析

目录

点击跳转上一篇文章: 【C++】:错误处理机制 – 异常

一,内存泄漏

(1) 什么是内存泄漏

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费

(2) 内存泄漏的危害

长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死
voidMemoryLeaks(){// 1.内存申请了忘记释放int* p1 =(int*)malloc(sizeof(int));int* p2 = new int;// 2.异常安全问题int* p3 = new int[10];// 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.Func(); delete[] p3;}

二,智能指针的使用及原理

2.1 RAII思想

RAII 是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象

这种做法有两大好处

(1) 不需要显式地释放资源
(2) 采用这种方式,对象所需的资源在其生命期内始终保持有效
// 使用RAII思想设计的SmartPtr类 template<class T> class SmartPtr { public:SmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if(_ptr) delete _ptr;} T& operator*(){return*_ptr;} T* operator->(){return _ptr;} private: T* _ptr;};intdiv(){int a, b; cin >> a >> b;if(b ==0) throw invalid_argument("除0错误");return a / b;}voidFunc(){ Shard_Ptr<int>sp1(new int); Shard_Ptr<int>sp2(new int); cout <<div()<< endl;}intmain(){ try {Func();}catch(const exception& e){ cout<<e.what()<<endl;}return0;}

总结一下智能指针的原理

(1) RAII特性
(2) 重载operator*和opertaor->,具有像指针一样的行为

2.2 auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针,auto_ptr 支持拷贝,但是拷贝时,管理权限转移,会造成被拷贝指针悬空。

使用方法如下

structDate{int _year;int _month;int _day;Date(int year =1,int month =1,int day =1):_year(year),_month(month),_day(day){}~Date(){ cout <<"~Date()"<< endl;}};intmain(){ auto_ptr<Date>ap1(new Date);//拷贝时,管理权限转移,被拷贝(ap1)指针悬空 auto_ptr<Date>ap2(ap1);// 此时ap1为空指针了,访问直接报错!//ap1->_year++;return0;}
结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr

2.3 unique_ptr

C++11中开始提供更靠谱的unique_ptrunique_ptr的实现原理:简单粗暴的防拷贝。

使用方法如下

structDate{int _year;int _month;int _day;Date(int year =1,int month =1,int day =1):_year(year),_month(month),_day(day){}~Date(){ cout <<"~Date()"<< endl;}};intmain(){ unique_ptr<Date>up1(new Date);//不支持拷贝//unique_ptr<Date> up2(up1);return0;}

三,shared_ptr(重点)

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

3.1 shared_ptr的原理及使用

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

原理实现的具体细节

(1) shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享
(2) 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
(3) 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
(4) 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

使用方法如下

structDate{int _year;int _month;int _day;Date(int year =1,int month =1,int day =1):_year(year),_month(month),_day(day){}~Date(){ cout <<"~Date()"<< endl;}};intmain(){ shared_ptr<Date>sp1(new Date); shared_ptr<Date>sp2(sp1); shared_ptr<Date>sp3(sp2);return0;}

3.2 shared_ptr的模拟实现

1. 基本框架

namespace bit { template<class T> class shared_ptr { public:// 构造,拷贝等其他接口..... T*get()const{return _ptr;}intuse_count()const{return*_pcount;}// 重载运算符,模拟指针的行为 T& operator*(){return*_ptr;} T* operator->(){return _ptr;} private: T* _ptr ;int* _pcount;//引用计数 function<void(T* ptr)> _del =[](T* ptr){delete ptr;};// function的默认构造中没有可调用对象,不给缺省值会报错}

2. 引用计数的设计

每个资源都要配一个引用计数,是用来记录有多少个对象共同指向这块资源的。

不是每个对象都配一个计数,也不能直接使用static静态变量,这样所有对象都用一个计数了,显然不合理

所以我们要在堆上开一块空间保存计数,用一个指针指向这个计数,当每次有对象指向同一块空间时,就可以找到这个指针指向的计数++,析构时计数- -每次构造的时候就出现新资源,所以要在构造的时候申请
shared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1))//每个资源给一个计数{}
在这里插入图片描述

3. 拷贝构造

把一个对象拷贝给另一个对象,说明这个对象的资源与另一个对象共享了,计数++

// sp2(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount)++;}

4. 析构函数

如果引用计数到0,说明已经没有对象指向这块资源了,就要释放该资源

voidrelease(){if(--(*_pcount)==0){//delete _ptr;_del(_ptr); delete _pcount; _ptr = nullptr; _pcount = nullptr;}}~shared_ptr(){release();}

5. 赋值拷贝

赋值拷贝是已经存在的两个对象之间。所以赋值时要注意那个对象原先资源的处理,原先的计数要先- -。并且要注意避免自己给自己赋值

// sp1 = sp3 shared_ptr<T>& operator=(shared_ptr<T>& sp){// 避免自己给自己赋值。用资源的指针判断// 指向同一块资源就不白费赋值if(_ptr != sp._ptr){release(); _ptr = sp._ptr; _pcount = sp._pcount;(*_pcount)++;}return*this;}
在这里插入图片描述

3.3 shared_ptr的循环引用

循环引用问题时一个巨坑,出现时必然会导致内存泄漏问题。

structListNode{int _data; shared_ptr<ListNode> _next; shared_ptr<ListNode> _prev;~ListNode(){ cout <<"~ListNode()"<< endl;}};intmain(){//循环引用--内存泄漏 shared_ptr<ListNode>n1(new ListNode); shared_ptr<ListNode>n2(new ListNode); cout << n1.use_count()<< endl; cout << n2.use_count()<< endl; n1->_next = n2; n2->_prev = n1; cout << n1.use_count()<< endl; cout << n2.use_count()<< endl;return0;}

循环引用分析图解

在这里插入图片描述
解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是:weak_ptr不支持管理资源,不支持RAII。n1->_next = node2和n2->_prev = n1时,weak_ptr的_next和_prev不会增加n1和n2的引用计数

使用weak_ptr要包含头文件

#include<functional>
structListNode{int _data; weak_ptr<ListNode> _next; weak_ptr<ListNode> _prev;~ListNode(){ cout <<"~ListNode()"<< endl;}};

在我们自己的shared_ptr中也进行简单的模拟实现

template<class T> class weak_ptr { public:weak_ptr(){}// 不增加引用计数weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){} weak_ptr<T>& operator=(const shared_ptr<T>& sp){ _ptr = sp.get();return*this;}

3.4 定制删除器

如果不是new出来的对象如何通过智能指针管理呢其实shared_ptr设计了一个删除器来解决这个问题。(ps:删除器这个问题我们了解一下)

仿函数的删除器

template <class T> class DeleteArray { public:voidoperator()(T* ptr){ delete[] ptr;}}; class Fclose { public:voidoperator()(FILE* ptr){ cout <<"fclose:"<< ptr << endl;fclose(ptr);}};intmain(){ shared_ptr<Date[]>sp4(new Date[5]); shared_ptr<FILE>sp5(fopen("test.cpp","r"),Fclose());return0;}

但是每次写仿函数还是有些麻烦,所以可以在shared_ptr的类中进行实现

//定制删除器 template<class D>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(new int(1)),_del(del){}

3.5 shared_ptr实现的完整代码

shared_ptr.h

#pragmaonce#include<functional> namespace bit { template<class T> class shared_ptr { public:// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1))//每个资源给一个计数{}//定制删除器 template<class D>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(new int(1)),_del(del){}// sp2(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount)++;}// sp1 = sp3 shared_ptr<T>& operator=(shared_ptr<T>& sp){// 避免自己给自己赋值。用资源的指针判断// 指向同一块资源就不白费赋值if(_ptr != sp._ptr){release(); _ptr = sp._ptr; _pcount = sp._pcount;(*_pcount)++;}return*this;}voidrelease(){if(--(*_pcount)==0){//delete _ptr;_del(_ptr); delete _pcount; _ptr = nullptr; _pcount = nullptr;}}~shared_ptr(){release();} T*get()const{return _ptr;}intuse_count()const{return*_pcount;}// 重载运算符,模拟指针的行为 T& operator*(){return*_ptr;} T* operator->(){return _ptr;} private: T* _ptr ;int* _pcount;//引用计数 function<void(T* ptr)> _del =[](T* ptr){delete ptr;};// function的默认构造中没有可调用对象,不给缺省值会报错}; template<class T> class weak_ptr { public:weak_ptr(){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){} weak_ptr<T>& operator=(const shared_ptr<T>& sp){ _ptr = sp.get();return*this;} private: T* _ptr = nullptr;};}

Test.cpp

intmain(){ bit::shared_ptr<Date>sp1(new Date); bit::shared_ptr<Date>sp2(sp1); bit::shared_ptr<Date>sp3(new Date); sp1 = sp3;//传了删除器,就用自己传的,没传就用缺省的 bit::shared_ptr<FILE>sp5(fopen("test.cpp","r"),Fclose()); bit::shared_ptr<int>sp6((int*)malloc(40),[](int* ptr){ cout <<"free:"<< ptr << endl;free(ptr);});return0;}

Read more

人工智能:自然语言处理在医疗领域的应用与实战

人工智能:自然语言处理在医疗领域的应用与实战

人工智能:自然语言处理在医疗领域的应用与实战 学习目标 💡 理解自然语言处理(NLP)在医疗领域的应用场景和重要性 💡 掌握医疗领域NLP应用的核心技术(如电子病历分析、疾病诊断辅助、药物相互作用检测) 💡 学会使用前沿模型(如BioBERT、ClinicalBERT)进行医疗文本分析 💡 理解医疗领域的特殊挑战(如医疗术语、数据隐私、法规要求) 💡 通过实战项目,开发一个电子病历文本分类应用 重点内容 * 医疗领域NLP应用的主要场景 * 核心技术(电子病历分析、疾病诊断辅助、药物相互作用检测) * 前沿模型(BioBERT、ClinicalBERT)在医疗领域的使用 * 医疗领域的特殊挑战 * 实战项目:电子病历文本分类应用开发 一、医疗领域NLP应用的主要场景 1.1 电子病历分析 1.1.1 电子病历分析的基本概念 电子病历(Electronic Health Records, EHR)是医疗领域的核心数据之一,包含了患者的基本信息、诊断记录、

By Ne0inhk
【Linux指南】进程控制系列(二)进程终止 —— 退出场景、方法与退出码详解

【Linux指南】进程控制系列(二)进程终止 —— 退出场景、方法与退出码详解

文章目录 * 一、先想明白:进程终止不是 “消失”,而是 “释放资源” * 二、进程退出的三大场景:正常与异常的边界 * 场景 1:正常退出(代码执行完毕,结果正确) * 场景 2:正常退出(代码执行完毕,结果不正确) * 场景 3:异常退出(代码崩溃,被迫终止) * 三、三种进程退出方法:return、exit、_exit 的核心差异 * 3.1 方法 1:return—— 仅在 main 函数中有效 * 核心逻辑: * 3.2 方法 2:exit 函数 —— 带清理操作的库函数退出 * 核心逻辑与清理操作: * 函数原型: * 3.

By Ne0inhk
【HarmonyOS Next之旅】DevEco Studio使用指南(二)

【HarmonyOS Next之旅】DevEco Studio使用指南(二)

目录 1 -> 工程模板介绍 2 -> 创建一个新的工程 2.1 -> 创建和配置新工程 2.1.1 -> 创建HarmonyOS工程 2.2.2 -> 创建OpenHarmony工程 1 -> 工程模板介绍 DevEco Studio支持多种品类的应用/元服务开发,预置丰富的工程模板,可以根据工程向导轻松创建适应于各类设备的工程,并自动生成对应的代码和资源模板。同时,DevEco Studio还提供了多种编程语言供开发者进行应用/元服务开发,包括ArkTS、JS和C/C++。 工程模板支持的开发语言及模板说明如下表所示: 模板名称说明Empty Ability用于Phone、Tablet、2in1、Car设备的模板,展示基础的Hello

By Ne0inhk

VMware虚拟机安装Mac无网络,怎么连接?

一.首先是在vm虚拟机上,检查虚拟机MacOS设置网络适配器  在设置网络适配器中选择NAT模式,用于共享主机的IP地址 二.在MacOS中,设置网络  以太网  使用DHCP,其实默认就是这个不用设置也行。 如果设置完该两步骤还是无网络连接,进行第三步; 三.回到在windows系统里,输入win+R打开终端,再输入services.msc打开 服务,找到VMware DHCP和VMware NAT,把这两个服务打开,一般问题就出现在这里,服务没开启。 通过右键将其开启即可这样就能上网了。

By Ne0inhk