【C++篇】“内存泄露”的宝藏手段:智能指针

目录

智能指针的使用场景分析

RAII和智能指针的设计思路

C++标准库智能指针的使用

auto_ptr的使用:

unique_ptr的使用:

shared_ptr的使用:

模拟shared_ptr:

定制删除器:

shared_ptr的循环引用

weak_ptr


智能指针的使用场景分析

下面这段程序在“cout << Divide(len, time) << endl;“可能会抛出异常,如果不捕获这个异常,也就是不加对应的catch语句,那么后面的delete[]就执行不到位,这样就会造成内存泄漏。所以,我们应该加上对应的catch语句,将异常捕获后释放资源,再将异常重新抛出。

除了Divide会抛出异常,new的部分也会抛出异常,若是”int* array1 = new int[10];“处抛异常,倒没什么事,因为抛出异常代表内存申请失败,但若是”int* array2 = new int[10];“处抛异常呢?此时array1已经成功申请内存了,如果不delete掉array1的资源,就会造成内存泄漏,为了避免这样的情况,还需在array2处在写一个try语句。但是当存在多个变量进行new时,代码会变的很搓,所以这里就可以使用到智能指针

double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Divide by zero condition!";}else{return (double)a / (double)b;}
}
void Func()
{// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案// 是智能指针,否则代码太戳了int* array1 = new int[10];int* array2 = new int[10];   // 抛异常呢try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (...){cout << "delete []" << array1 << endl;cout << "delete []" << array2 << endl;//先将异常捕获,释放资源,再重新抛出,这里捕获异常的目的就是为了释放资源,避免内存泄漏delete[] array1;delete[] array2;throw; // 异常重新抛出,捕获到什么抛出什么}// ...cout << "delete []" << array1 << endl;delete[] array1;cout << "delete []" << array2 << endl;delete[] array2;
}
int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

RAII和智能指针的设计思路

RAII是ResourceAcquisition Is Initialization的缩写,本质是一种利用对象⽣命周期来管理获取到的动态资源。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问, 资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常 释放,避免资源泄漏问题。

简单讲就是,申请了内存空间,但这个内存空间不需要自己管理,而是交给一个对象进行管理,当这个对象生命周期结束时,会析构,同时也会把管理资源释放掉。
构造函数保存资源,析构函数释放资源

#include<iostream>using namespace std;template<class T>
class SmartPtr//智能指针
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){//析构的是被智能指针管理的资源cout << "delete[]" << _ptr << endl;delete[] _ptr;}// 重载运算符,模拟指针的⾏为,⽅便访问资源T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}private:T* _ptr;
};double Divide(int a,int b)
{//如果这里抛出异常,会跳到main函数中去。根据智能指针资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源。所以在跳转到main函数中去之前,会调用智能指针的析构函数将资源释放掉if (b==0){throw "Divide:分母不能为0";}else{return ((double)a / (double)b);}}void Func()
{SmartPtr<int>sp1 = new int[10];SmartPtr<int>sp2 = new int[10];SmartPtr<int>sp3 = new int[10];SmartPtr<pair<int,int>>sp4 = new pair<int, int>[10];int len, time;cin >> len >> time;cout << Divide(len,time) << endl;sp1[5] = 50;sp4->first = 1;sp4->second = 2;cout << sp1[5] << endl;}int main()
{try{Func();}catch(const char*errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

C++标准库智能指针的使用

智能指针解决了抛出异常可能会导致内存泄漏的问题,但是它自身也存在一些问题。比如:智能指针的拷贝:

template<class T>
class SmartPtr
{
public:// RAIISmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete[] " << _ptr << endl;delete[] _ptr;}// 重载运算符,模拟指针的⾏为,⽅便访问资源T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}private:T* _ptr;
};int main()
{//我们自己没有实现拷贝构造,编译器自己生成的默认拷贝构造是浅拷贝//这样就会导致两个智能指针指向同一块资源,析构时就会析构两次SmartPtr<int>sp1 = new int[10];SmartPtr<int>sp2(sp1);return 0;
}

根据前面所学,当存在申请空间时,就要调用深拷贝。但是智能指针模拟的是原生指针,原生指针1拷贝给原生指针2的目的是赋值,他们指向的资源依然是同一块。智能指针1拷贝给智能指针2的目的是为了让两个智能指针共同管理这块资源

  • C++标准库中的智能指针都在这个头⽂件下⾯,我们包含了就可以使用。 智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解决智能指针拷⻉时的思路不同的问题

  • auto_ptr是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给拷贝对象,这是⼀个⾮常糟糕的设计,因为他会使被拷⻉对象悬空,访问报错的问题,也就是出现野指针的问题

  • unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点是不⽀持拷⻉,只⽀持移动。如果不需要拷贝的场景就非常建议使用他。

  • shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是⽀持拷⻉, 也⽀持移动。如果需要拷贝的场景就需要使⽤他了。底层是用引用计数的⽅式实现的。

  • weakptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上⾯的智能指针,他不⽀持RAII,也就意味着不能用它直接管理资源,weakptr的产生本质是要解决shared_ptr 的⼀个循环引用导致内存泄漏的问题。

auto_ptr的使用:

#include<iostream>
#include<memory>
using namespace std;struct Date
{
public:Date(int year=1,int month=1,int day=1):_year(year),_month(month),_day(day){ }~Date(){cout << "析构" << endl;}int _year;int _month;int _day;
};int main()
{auto_ptr<Date>ap1(new Date);//拷贝发生资源管理权的转移,ap1悬空auto_ptr<Date>ap2(ap1);//空指针的访问,这也是auto_ptr会存在的问题//ap1->_year;return 0;
}

拷贝完后,在对ap1进行访问,这就是对空指针进行访问:

unique_ptr的使用:

struct Date
{
public:Date(int year=1,int month=1,int day=1):_year(year),_month(month),_day(day){ }~Date(){cout << "析构" << endl;}int _year;int _month;int _day;
};int main()
{//unique_ptr只支持移动构造,不支持拷贝unique_ptr<Date>up1(new Date);//当移动完后,up1会悬空unique_ptr<Date>up2(move(up1));return 0;
}

unique_ptr与auto_ptr的区别:前者是告知使用者,自己只支持移动构造,不支持拷贝构造,使用完后会造成指针悬空的情况;后者是告知使用者自己是拷贝构造,但是会造成指针悬空的问题并未告知使用者

shared_ptr的使用:

struct Date
{
public:Date(int year=1,int month=1,int day=1):_year(year),_month(month),_day(day){ }~Date(){cout << "析构" << endl;}int _year;int _month;int _day;
};int main()
{shared_ptr<Date>sp1(new Date);shared_ptr<Date>sp2(sp1);shared_ptr<Date>sp3(sp1);//输出引用计数cout << sp1.use_count() << endl;sp1->_year++;cout << sp1->_year << endl;cout << sp2->_year << endl;cout << sp3->_year << endl;//shared_ptr还能像如下这样赋值shared_ptr<Date>sp4=make_share<Date>(2024,1,1);return 0;
}

引用计数:当多个对象管理同一块资源时,用一个count记录对象个数,当count==0时,再将资源释放

模拟shared_ptr:

namespace liu
{template<class T>class shared_ptr{public:shared_ptr(T*ptr=nullptr):_ptr(ptr),_pcount(new int(1)){ }~shared_ptr(){if (--(*_pcount) == 0){cout << "析构" << endl;delete _pcount;delete _ptr;}}shared_ptr(const shared_ptr<T>&sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount)++;}shared_ptr<T>& operator=(const shared_ptr<T>&sp){if (_ptr==sp._ptr){return *this;}if (--(*_pcount)==0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;};
}

如果以int count的方式来计数:

如果以静态成员变量的方式来计数:

定制删除器:

智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指 针管理,析构时就会崩溃。

//原生指针的空间、malloc来的空间、new[]出来的空间或者其他不是new出来的空间,都不能交给智能指针管理
shared_ptr<Date>sp1(new Date[10]);

面对这种情况,有两种解决方案:

第一种:

//模板特化
shared_ptr<Date[]>sp2(new Date[10]);

第二种:

//定制删除器struct Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}~Date(){cout << "析构" << endl;}int _year;int _month;int _day;
};class Fclose
{
public:void operator()(FILE*ptr){cout << "flcose()" << endl;fclose(ptr);}};template<class T>
void DeleteArrayFunc(T *ptr)
{cout << "函数指针" << endl;delete[] ptr;
}int main()
{//传仿函数shared_ptr<FILE>sp3(fopen("智能指针.cpp","r"),Fclose());//传lambdashared_ptr<FILE>sp4(fopen("智能指针.cpp", "r"), [](FILE* ptr) {cout << "fclose()" << endl;shared_ptr<Date>sp5(new Date[10], [](Date* ptr) {cout << "delete[]" << endl;delete[] ptr; })shared_ptr<Date>sp6(new Date[10], DeleteArrayFunc<Date>);fclose(ptr); });return 0;
}

unique_ptr定制删除器智是在模板处,shared_ptr定制删除器是在构造函数参数处

uniqueptr传定制删除器如果不想传仿函数想以其他形式传如删除器的话,会很麻烦。 sharedptr定制删除器的位置是在函数参数的位置,编译器会自动推导类型。

//unique_ptr不以传仿函数的方式传入定制删除器
//lambda
auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };
unique_ptr<FILE, decltype(fcloseFunc)>up1(fopen("智能指针.cpp", "r"), fcloseFunc);
//函数指针
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);

shared_ptr的循环引用

shared_ptr⼤多数情况下管理资源非常合适,⽀持RAII,也⽀持拷贝。但是在循环引用的场景下会 导致资源没得到释放造成内存泄漏,

  • 如下图所述场景:这样循环引用的问题就会造成内存泄漏

  • 把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这⾥的问题

weak_ptr

weakptr不⽀持RAII,也不⽀持访问资源,所以我们看⽂档发现weakptr构造时不⽀持绑定到资 源,只⽀持绑定到sharedptr,绑定到sharedptr时,不增加shared_ptr的引用计数,那么就可以 解决上述的循环引⽤问题。

  • weakptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的 sharedptr已经释放了资源,那么他去访问资源就是很危险的。

  • weakptr⽀持expired检查指向的 资源是否过期,usecount也可获取sharedptr的引⽤数,weakptr想访问资源时,可以调用lock返回⼀个管理资源的sharedptr,如果资源已经被释放,返回的sharedptr是⼀个空对象,如 果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

sharedptr中的count计数在sharedptr释放时不会立即释放,因为它还需要提供给weakptr使用,如果立即释放了,就会造成weakptr野指针的情况。

weak_ptr中还有expired接口来检查资源是否过期。

	shared_ptr<string>sp1(new string("11111"));shared_ptr<string>sp2(sp1);weak_ptr<string>wp1 = sp1;cout << wp1.expired() << endl;cout << wp1.use_count() << endl;

如果shareptr的资源是weakptr所需要的,那么可以使用lock()接口在资源释放前将锁住锁住。

锁住资源实际上就是再用一个shared_ptr指针来管理该资源。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/93420.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/93420.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【密码学】4. 分组密码

目录分组密码分组密码概述Feistel 密码结构数据加密标准&#xff08;DES&#xff09;差分密码分析与线性密码分析分组密码的运行模式国际数据加密算法&#xff08;IDEA&#xff09;高级加密标准&#xff08;AES&#xff0c;Rijndael&#xff09;中国商用密码 SM4祖冲之密码&…

单片机(STM32-WIFI模块)

一、WIFI模块介绍 1. ESP12-F模组介绍 1.1 简介 ESP12-F模组&#xff08;安信可&#xff08;Ai-Thinker&#xff09;ESP8266系列模组&#xff09;是一款基于乐鑫&#xff08;Espressif&#xff09;公司ESP8266芯片的Wi-Fi无线通信模块&#xff0c;广泛应用于物联网&#xff0…

PyTorch 数据类型和使用

关于PyTorch的数据类型和使用的学习笔记 系统介绍了PyTorch的核心数据类型Tensor及其应用。Tensor作为多维矩阵数据容器&#xff0c;支持0-4维数据结构&#xff08;标量到批量图像&#xff09;&#xff0c;并提供了多种数值类型&#xff08;float32/int64等&#xff09;。通过…

[python刷题模板] LogTrick

[python刷题模板] LogTrick 一、 算法&数据结构1. 描述2. 复杂度分析3. 常见应用4. 常用优化二、 模板代码1. 特定或值的最短子数组2. 找特定值3. 找位置j的最后一次被谁更新4. 问某个或和的数量三、其他四、更多例题五、参考链接一、 算法&数据结构 1. 描述 LogTric…

Vim与VS Code

Vim is a clone, with additions, of Bill Joys vi text editor program for Unix. It was written by Bram Moolenaar based on source for a port of the Stevie editor to the Amiga and first released publicly in 1991.其实这个本身不是 IDE &#xff08;只有在加入和配置…

[2025CVPR-图象分类方向]CATANet:用于轻量级图像超分辨率的高效内容感知标记聚合

​1. 研究背景与动机​ ​问题​&#xff1a;Transformer在图像超分辨率&#xff08;SR&#xff09;中计算复杂度随空间分辨率呈二次增长&#xff0c;现有方法&#xff08;如局部窗口、轴向条纹&#xff09;因内容无关性无法有效捕获长距离依赖。​现有局限​&#xff1a; SPI…

课题学习笔记3——SBERT

1 引言在构建基于知识库的问答系统时&#xff0c;"语义匹配" 是核心难题 —— 如何让系统准确识别 "表述不同但含义相同" 的问题&#xff1f;比如用户问 "对亲人的期待是不是欲&#xff1f;"&#xff0c;系统能匹配到知识库中 "追名逐利是欲…

在Word和WPS文字中把全角数字全部改为半角

大部分情况下我们在Word或WPS文字中使用的数字或标点符号都是半角&#xff0c;但是有时不小心按错了快捷键或者点到了输入法的全角半角切换图标&#xff0c;就输入了全角符号和数字。不用担心&#xff0c;使用它们自带的全角、半角转换功能即可快速全部转换回来。一、为什么会输…

数据结构的基本知识

一、集合框架1、什么是集合框架Java集合框架(Java Collection Framework),又被称为容器(container),是定义在java.util包下的一组接口(interfaces)和其实现类(classes).主要表现为把多个元素(element)放在一个单元中,用于对这些元素进行快速、便捷的存储&#xff08;store&…

WebStack-Hugo | 一个静态响应式导航主题

WebStack-Hugo | 一个静态响应式导航主题 #10 shenweiyan announced in 1.3-折腾 WebStack-Hugo | 一个静态响应式导航主题#10 ​编辑shenweiyan on Oct 23, 2023 6 comments 7 replies Return to top shenweiyan on Oct 23, 2023 Maintainer Via&#xff1a;我给自己…

01 基于sklearn的机械学习-机械学习的分类、sklearn的安装、sklearn数据集、数据集的划分、特征工程中特征提取与无量纲化

文章目录机械学习机械学习分类1. 监督学习2. 半监督学习3. 无监督学习4. 强化学习机械学习的项目开发步骤scikit-learn1 scikit-learn安装2 sklearn数据集1. sklearn 玩具数据集鸢尾花数据集糖尿病数据集葡萄酒数据集2. sklearn现实世界数据集20 新闻组数据集3. 数据集的划分特…

携全双工语音通话大模型亮相WAIC,Soul重塑人机互动新范式

近日&#xff0c;WAIC 2025在上海隆重开幕。作为全球人工智能领域的顶级盛会&#xff0c;本届WAIC展览聚焦底层能力的演进与具体垂类场景的融合落地。坚持“模应一体”方向、立足“AI社交”的具体场景&#xff0c;Soul App此次携最新升级的自研端到端全双工语音通话大模型亮相&…

第2章 cmd命令基础:常用基础命令(1)

Hi~ 我是李小咖&#xff0c;主要从事网络安全技术开发和研究。 本文取自《李小咖网安技术库》&#xff0c;欢迎一起交流学习&#x1fae1;&#xff1a;https://imbyter.com 本节介绍的命令有目录操作&#xff08;cd&#xff09;、清屏操作&#xff08;cls&#xff09;、设置颜色…

Java 10 新特性解析

Java 10 新特性解析 文章目录Java 10 新特性解析1. 引言2. 本地变量类型推断&#xff08;JEP 286&#xff09;2.1. 概述2.2. 使用场景2.3. 限制2.4. 与之前版本的对比2.5. 风格指南2.6. 示例代码2.7. 优点与注意事项3. 应用程序类数据共享&#xff08;JEP 310&#xff09;3.1. …

【WRF工具】服务器中安装编译GrADS

目录 安装编译 GrADS 所需的依赖库 conda下载库包 安装编译 GrADS 编译前检查依赖可用性 安装编译 GrADS 参考 安装编译 GrADS 所需的依赖库 以统一方式在 $HOME/WRFDA_LIBS/grads_deps 下安装所有依赖: # 选择一个目录用于安装所有依赖库 export DIR=$HOME/WRFDA_LIBS库包1…

数据结构之队列(C语言)

1.队列的定义&#xff1a; 队列&#xff08;Queue&#xff09;是一种基础且重要的线性数据结构&#xff0c;遵循先进先出&#xff08;FIFO&#xff09;​​ 原则&#xff0c;即最早入队的元素最先出队&#xff0c;与栈不同的是出队列的顺序是固定的。队列具有以下特点&#xff…

C#开发基础之深入理解“集合遍历时不可修改”的异常背后的设计

前言 欢迎关注【dotnet研习社】&#xff0c;今天我们聊聊一个基础问题“集合已修改&#xff1a;可能无法执行枚举操作”背后的设计。 在日常 C# 开发中&#xff0c;我们常常会操作集合&#xff08;如 List<T>、Dictionary<K,V> 等&#xff09;。一个新手开发者极…

【工具】图床完全指南:从选择到搭建的全方位解决方案

前言 在数字化内容创作的时代&#xff0c;图片已经成为博客、文档、社交媒体等平台不可或缺的元素。然而&#xff0c;如何高效、稳定地存储和分发图片资源&#xff0c;一直是内容创作者面临的重要问题。图床&#xff08;Image Hosting&#xff09;作为专门的图片存储和分发服务…

深度学习篇---PaddleDetection模型选择

PaddleDetection 是百度飞桨推出的目标检测开发套件&#xff0c;提供了丰富的模型库和工具链&#xff0c;覆盖从轻量级移动端到高性能服务器的全场景需求。以下是核心模型分类、适用场景及大小选择建议&#xff08;通俗易懂版&#xff09;&#xff1a;一、主流模型分类及适用场…

cmseasy靶机密码爆破通关教程

靶场安装1.首先我们需要下载一个cms靶场CmsEasy_7.6.3.2_UTF-8_20200422,下载后解压在phpstudy_pro的网站根目录下。2.然后我们去访问一下安装好的网站&#xff0c;然后注册和链接数据库3.不知道自己数据库密码的可以去小皮面板里面查看4.安装好后就可以了来到后台就可以了。练…