当前位置: 首页 > news >正文

C++的智能指针 RAII

目录

产生原因

RAII思想

C++11的智能指针

智能指针的拷贝与赋值 

 shared_ptr的拷贝构造

shared_ptr的赋值重置

shared_ptr的其它成员函数

weak_ptr

定制删除器

简单实现


产生原因

产生原因:抛异常等原因导致的内存泄漏

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{// 1、如果p1这里new 抛异常会如何?// 2、如果p2这里new 抛异常会如何?// 3、如果div调用这里又会抛异常会如何?int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2;
}int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

RAII思想

基本概念:智能指针(Smart Pointer)是C++中一种用于自动管理动态分配内存的对象,旨在防止内存泄漏和指针悬挂问题

核心理念:RAII,即一种利用对象生命周期来控制程序资源(如内存、网络连接等)的技术

工作原理:使用一份资源时,先用该资源构造一个智能指针,智能指针会保证在指向资源仍存在时始终有效(该资源的释放在智能指针的析构函数中),智能指针是由一个匿名对象构建得,为了能像指针一样使用,智能指针类中还会重载*和-> 

#include<iostream>
using namespace std;//智能指针类
template<class T>
class SmartPtr
{
public:// RAIISmartPtr(T* ptr)//接收一份资源的地址:_ptr(ptr)//_ptr指向一份资源{}~SmartPtr(){cout << "delete:" << _ptr << endl;delete _ptr;//智能指针对象释放时才释放_ptr指向的资源}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{//创建一个int* 类型的智能指针SmartPtr<int> sp1(new int);SmartPtr<int> sp2(new int);*sp1 += 10;//像指针一样可以*SmartPtr<pair<string, int>> sp3(new pair<string, int>);//智能指针sp3指向得是一个pair类型的匿名对象sp3->first = "apple";sp3->second = 1;//等价于sp3.operator->()->second = 1;cout << div() << endl;
}int main()
{try{Func();}catch (const exception& e){cout << e.what() << endl;}return 0;
}

C++11的智能指针

基本概念:C++98时就已经提供了一个叫auto_ptr的智能指针,但使用该智能指针进行拷贝时,会出现管理权的转移,原对象的资源管理权交给了拷贝得到的新对象,这导致指向原对象的指针变为空指针,再次使用可能会报错;所以C++11在boost库的基础上引入了unique_ptr 和 shared_ptr等新的智能指针类C++11的智能指针均包含在<memory>头文件中,需要显示引用)

智能指针的拷贝与赋值 

1、unique_ptr 类不支持拷贝构造和赋值重载(通过只声明不定义实现,禁止了拷贝构造最好将赋值重载也禁掉,不禁掉则赋值重载是默认提供的会进行浅拷贝),适用于不需要拷贝的场景

2、shared_ptr 类有拷贝构造和赋值重载,并使用引用计数解决释放的问题(不使用静态成员进行计数是因为静态成员记录的是所有与被管理资源同类型的资源被使用的次数,即同类型的两个不同资源new出来的两个智能指针不会使得计数器++,而是重置变为1,当然我们设计得拷贝构造仍会使计数器++,但是new时可能会导计数器重置)

 shared_ptr的拷贝构造

基本概念:智能指针shared_ptr在创建时,除了有指向资源的指针ptr,还有该资源独属得计数器指针int * _count,初始时*(_count) = 1

// 构造函数
shared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}// 拷贝构造: sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
{_ptr = sp._ptr;_pcount = sp._pcount;// 拷贝时++计数++(*_pcount);
}

shared_ptr的赋值重置

基本概念:shared_ptr的赋值重载需要考虑无效赋值的情况

//赋值重置, sp1 = sp4
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{//if (this != &sp),防止自我赋值的情况,基础版if (_ptr != sp._ptr)//避免管理统一资源的两个智能指针对象间的互相赋值,升级版{release();//先进行释放函数,防止内存泄漏_ptr = sp._ptr;_pcount = sp._pcount;// 拷贝时++计数++(*_pcount);}return *this;
}//释放函数
void release()
{// 先进行计数器--,如果--后智能指针管理的资源的计数器变为0,就释放指向该资源的指针和计数器指针,然后再进行其它操作if (--(*_pcount) == 0){cout << "delete:" << _ptr << endl;delete _ptr;delete _pcount;}
}

shared_ptr的其它成员函数

基本概念:shared_ptr的析构函数并会直接将其管理的资源直接释放,还是要调用一个release函数进行--_count判断,即使是shared_ptr析构后,如果之前存在对该资源管理权的拷贝或者赋值,则用于记录该资源的计数器指针仍然不会被销毁,因为在拷贝或赋值时又有一个新的智能指针备份了该资源的信息(计数器指针指向的对象是new出来的int类型的对象除非主动释放,否则不会消失)

//析构函数
~shared_ptr()
{// 析构时,--计数,计数减到0,release();
}int use_count()
{return *_pcount;
}// 像指针一样
T& operator*()
{return *_ptr;
}T* operator->()
{return _ptr;
}T* get() const//为别人提供自己的指针,且该函数中不能修改自己的指针
{return _ptr;
}

shared_ptr的缺陷

基本概念:两个或多个对象通过 shared_ptr 相互引用,从而形成了一个循环。此时,即使所有外部 shared_ptr 都被销毁,由于循环引用中的 shared_ptr 仍然存在,引用计数永远不会降为零,导致这些对象无法被释放,造成内存泄漏

1、防止循环链表两个的结点因抛异常导致的无法释放,我们使用share_ptr管理这两个结点:

struct ListNode
{int _val;//④使得下面的n1->next = n2之类的操作不会因为双方类型不同导致无法互相赋值//struct ListNode* _next;//struct ListNode* _prev;//|//vstd::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prev;ListNode(int val = 0):_val(val){}
};int main()
{//①ListNode* n1 = new ListNode(10);//ListNode* n2 = new ListNode(20);//|//vstd::shared_ptr<ListNode> n1 = ((new) ListNode(10);std::shared_ptr<ListNode> n2 = ((new) ListNode(20);//②中间可能出现抛异常    //|//v//不用担心抛异常了n1->next = n2;n2->prev = n1;//④delete n1;//delete n2;//此时不需要在这里delete了,在智能指针内部会deletereturn 0;
}

2、但是此时又会出现下面三种情况:

        两个结点互相用自己的智能指针管理对方时,管理两个结点的原始智能指针都析构后,记录两个结点的计数器均为1不会变为0,即两个结点均不会释放,此时出现内存泄漏问题,此时如果想要先释放右结点,那么就会出现以下的循环:

  • 这就是shared_ptr在特殊场景下的缺陷......

weak_ptr

 文档:weak_ptr - C++ Reference (cplusplus.com)

基本概念:为了解决shared_ptr在特殊场景下的缺陷,C++11还引入了weak_ptr,该智能指针不增加引用计数,不管理对象的生命周期

 注意事项:

1、weak_ptr不支持RAII(下面是便于理解的简化实现)

// 不支持RAII,不参与资源管理
template<class T>
class weak_ptr
{public:weak_ptr():_ptr(nullptr)//将传入的指针直接置空即可,不参与资源的管理{}weak_ptr(const shared_ptr<T>& sp){_ptr = sp.get();//获取shared_ptr的指针}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};

2、weak_ptr也有use_count函数,用于检测此时与weak_ptr指向同一资源的shared_ptr的个数,如果shared_ptr为0(该资源已经被释放了)则weak_ptr变为野指针,应及时置空

3、weak_ptr的expired函数用于检查weak_ptr是否过期,过期返回真,未过期返回假,相比use_count会更方便

总结:使用智能指针不一定会避免内存泄漏(上述结点时使用的纯shared_ptr),正确的使用智能指针才能避免内存泄漏(完善后的shared_ptr + weak_ptr)

定制删除器

基本概念: C++11并没有像boost库中的那样提供shared_array等用于管理和释放由[ ]创建的多个对象的智能指针,所以在使用shared_ptr管理和释放由[ ]开辟的多个对象时,会因为delete与new的方式不匹配而报错(shared_ptr的new和delete均为(),但如是[],释放时仍为()就可能导致出错)

std::shared_ptr<ListNode> p1(new ListNode(10));//正常情况
std::shared_ptr<ListNode> p1(new ListNode[10]);//有[]的情况,无法正确释放

因此C++11的智能指针还提供了接受自定义删除器的构造方式:

template <class U, class D> shared_ptr (U* p, D del);
  • D del自定义删除器,可以是函数指针、函数对象、Lambda 表达式等

1、自定义删除器是函数对象

template<class T>
struct DeleteArray
{void operator()(T* ptr){delete[] ptr;}
}std::shared_ptr<ListNode> p2(new ListNode[10],DeleteArray<ListNode>());

2、自定义删除器是Lambda表达式

std::shared_ptr<ListNode> p3(fopen("Test.cpp","r"),[](FILE* ptr){fclose(ptr); });

简单实现

template<class T>
class shared_ptr
{
public:template<class D>
shared_ptr(T* ptr, D del)//接收自定义删除器的构造函数:_ptr(ptr), _pcount(new int(1)), _del(del)//新定义一个成员用于存放删除器{}//删除函数
void release()
{// 说明最后一个管理对象析构了,可以释放资源了if (--(*_pcount) == 0){cout << "delete:" << _ptr << endl;//delete _ptr;_del(_ptr);//将执行资源的指针也给删除器一份,从而使删除器开始运行delete _pcount;}
}private:D _val;//shared_ptr的官方实现的模板中没有第二个模板参数,所以这种写法是错误的
}

//利用function将删除器的类型进行封装
//删除器的返回类型肯定是void,接收的参数类型肯定是T*
function<void(T*)> _del;
​
//为了防止使用无自定义删除器的构造函数时,没有del的传入导致的_del为空的情况,所以我们要提供一个默认的删除方式:
function<void(T*)> _del = [](T* ptr) {delete ptr; };

~over~

相关文章:

  • 【AI应用探讨】— 盘古大模型应用场景
  • 如何选择合适的半桥栅极驱动芯片?KP8530X,KP85402,KP85211A满足你对半桥栅极驱动一切需求
  • Oracle最终还是杀死了MySQL
  • 步步为营:电商项目业务测试实战指南
  • 考试系统Spring Security的配置
  • SQL题:未完成率较高的50%用户近三个月答卷情况
  • 深入了解常用负载均衡软件
  • 第三方软件测试机构流程分享,软件检测报告需多少时间和费用?
  • 如何利用AI大模型设计电机本体?
  • 反激开关电源开关MOS管选择
  • 【漏洞复现】世邦通信 SPON IP网络对讲广播系统 addscenedata.php 任意文件上传漏洞
  • Linux 查看 CPU核数 及 内存
  • Goroutine和协程的区别
  • SpringCloud微服务框架的原理及应用详解(一)
  • 常见的宽基指数基金
  • 【407天】跃迁之路——程序员高效学习方法论探索系列(实验阶段164-2018.03.19)...
  • Android组件 - 收藏集 - 掘金
  • css布局,左右固定中间自适应实现
  • Druid 在有赞的实践
  • exif信息对照
  • FineReport中如何实现自动滚屏效果
  • flask接收请求并推入栈
  • JavaScript 是如何工作的:WebRTC 和对等网络的机制!
  • Java到底能干嘛?
  • Java多线程(4):使用线程池执行定时任务
  • Java面向对象及其三大特征
  • js 实现textarea输入字数提示
  • JS学习笔记——闭包
  • mysql_config not found
  • node.js
  • Shell编程
  • Vue.js-Day01
  • 解析带emoji和链接的聊天系统消息
  • 如何编写一个可升级的智能合约
  • 突破自己的技术思维
  • Salesforce和SAP Netweaver里数据库表的元数据设计
  • 阿里云服务器购买完整流程
  • 曜石科技宣布获得千万级天使轮投资,全方面布局电竞产业链 ...
  • ​flutter 代码混淆
  • ​二进制运算符:(与运算)、|(或运算)、~(取反运算)、^(异或运算)、位移运算符​
  • ​学习一下,什么是预包装食品?​
  • #if 1...#endif
  • $NOIp2018$劝退记
  • (2024)docker-compose实战 (8)部署LAMP项目(最终版)
  • (C#)Windows Shell 外壳编程系列9 - QueryInfo 扩展提示
  • (C#)一个最简单的链表类
  • (done) 声音信号处理基础知识(4) (Understanding Audio Signals for ML)
  • (pycharm)安装python库函数Matplotlib步骤
  • (回溯) LeetCode 78. 子集
  • (简单有案例)前端实现主题切换、动态换肤的两种简单方式
  • (十)DDRC架构组成、效率Efficiency及功能实现
  • (十六)Flask之蓝图
  • (四) 虚拟摄像头vivi体验
  • (一)VirtualBox安装增强功能
  • (一)WLAN定义和基本架构转