C++ —— 智能指针

内存泄漏

什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内 存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{int* p1 = new int;cout << div() << endl;delete p1;
}
int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

问题分析:上面的问题分析出来我们发现有什么问题?

在以上代码中,当用户输入的除数为0时,div函数就会抛出异常,这时程序的执行流就会跳到main函数中的catch块中的执行,最终导致Func函数中申请的内存资源没有得到释放

利用异常重新捕获解决

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{int* p1 = new int;try {div();}catch (const exception& e){delete p1;throw;}delete p1;
}int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

使用智能指针解决

template<class T>
class SmartPtr
{
public://RAIISmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){if (_ptr != nullptr){cout << "~SmartPtr" << endl;delete _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()
{SmartPtr<int> p1(new int(1));div();
}int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

这里使用的是一个RAII的一个简单技术

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

这种做法有两大好处:

不需要显式地释放资源。

采用这种方式,对象所需的资源在其生命期内始终保持有效。

智能指针对象的拷贝问题

int main()
{SmartPtr<int> ptr1(new int);SmartPtr<int> ptr2(ptr1);  //拷贝构造SmartPtr<int> ptr3(new int);SmartPtr<int> ptr4(new int); ptr3 = ptr4; //赋值重载return 0;
}

以上代码会产生一个浅拷贝问题,多个对象指向一个空间,最后在程序析构时,会对一个空间析构多次,导致程序崩溃

需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。

C++中的智能指针

std::auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。 auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份auto_ptr来了解它的原理

int main()
{auto_ptr<int> ptr1(new int(1));auto_ptr<int> ptr2(ptr5);(*ptr1)++;(*ptr2)++;return 0;
}

由于我们进行了拷贝构造,ptr1内存空间管理权转移到了ptr2上的原因,我们对*ptr1进行自加运算时,编译器在运行时会崩溃

auto_ptr的模拟实现

template<class T>
class auto_ptr
{
public://RAIIauto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ptr):_ptr(ptr._ptr){ptr._ptr = nullptr;}~auto_ptr(){if (_ptr != nullptr){cout << "~auto_ptr" << endl;delete _ptr;}}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};

std::unique_ptr

unique解决拷贝的方式比较暴力,它是直接禁止对象之间进行拷贝

例如:

int main()
{unique_ptr<int> ptr1(new int);unique_ptr<int> ptr2(ptr1);return 0;
}

unique_ptr的模拟实现

template<class T>
class unique_ptr
{
public://RAIIunique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){if (_ptr != nullptr){cout << "~unique_ptr" << endl;delete _ptr;}}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}unique_ptr(unique_ptr<T>& ptr) = delete;unique_ptr& operator=(unique_ptr<T>& ptr) = delete;
private:T* _ptr;
};

std::shared_ptr

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

  • shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  • 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一。
  • 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  • 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
int main()
{shared_ptr<int> ptr1(new int(10));shared_ptr<int> ptr2(ptr1);cout << ++(*ptr1) << endl;cout << ++(*ptr2) << endl;shared_ptr<int> ptr3(new int(10));shared_ptr<int> ptr4(new int(11));ptr3 = ptr4;cout << *ptr3 << endl;cout << *ptr4 << endl;return 0;
}

shared_ptr的模拟实现

template<class T>
class shared_ptr
{
public://RAIIshared_ptr(T* ptr):_ptr(ptr), _pcount(new int(1)){}~shared_ptr(){if (--(*_pcount) == 0){//判断_ptr是否为空if (_ptr == nullptr){cout << "~unique_ptr" << endl;delete _ptr;_ptr = nullptr;}delete _pcount;_pcount = nullptr;}}shared_ptr(unique_ptr<T>& ptr):_ptr(ptr._ptr), _pcount(ptr._pocunt){++(*_pcount);}shared_ptr& operator=(shared_ptr<T>& ptr){//避免ptr1和ptr1赋值或者ptr1和ptr2赋值(ptr1和ptr2地址相同)if (_ptr == ptr._ptr) return *this;if (--(*_pcount) == 0){delete _ptr;delete _pcount;}_ptr = ptr._ptr;_pcount = ptr._pcount;++(*ptr._pcount);return *this;}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int GetCount(){return *_pcount;}T* Getshared_ptr(){return _ptr;}
private:T* _ptr; //管理的资源int* _pcount; //管理资源对应的引用计数
};

shared_ptr的缺点:循环引用

shared_ptr的循环引用问题在一些特定的场合下才会产生。比如定义如下带的结点类

struct Node
{Node* _prev = nullptr;Node* _next = nullptr;int val;~Node() {cout << "~Node" << endl;}
};int main()
{Node* ptr1 = new Node;Node* ptr2 = new Node;ptr1->_next = ptr2;ptr2->_prev = ptr1;delete ptr1;delete ptr2;return 0;
}

上述程序是没问题的,两个结点都可以正确的释放。为了避免程序中途返回或抛异常等原因导致结点未被释放,我们将这两个结点交给shared_ptr对象进行管理,这时为了让连接节点时的赋值操作也能执行,将需要把Node类中的next和prev的类型也设置未shared_ptr

struct Node
{shared_ptr<Node> _prev = nullptr;shared_ptr<Node> _next = nullptr;int val;~Node() {cout << "~Node" << endl;}
};int main()
{shared_ptr<Node> node1(new Node);shared_ptr<Node> node2(new Node);node1->_next = node2;node2->_prev = node1;return 0;
}

使用new的方式申请到两个Node结点并交给智能指针管理之后,这两个资源引用计数都被加到了1

将这两个节点连接起来后,现在node1和prev共同管理一块资源,node2和next共同管理一块资源

即资源1和资源2的 引用计数变为2

当出了main的作用域后,node1和node2的生命周期也结束了,所以这两份资源的引用计数都被减到了1

循环引用导致资源没有被释放的原因:

当资源的引用计数减为0时,这些资源才会被释放,左边的资源什么时候释放,当prev释放的时候释放,那么prev什么时候释放,当左边资源释放的时候释放,右边的资源什么时候释放,当next释放的时候释放,next什么时候释放,右边资源释放的时候释放,于是这就变成了一个死循环,最终导致资源无法释放

如果只进行一个连接操作时,那么node1和node2生命周期结束时,就会有一个资源对应的引用计数被减为0,最后两个资源都可以释放了,这就是为什么只进行一个连接操作时这两个结点都可以释放的原因

std::weak_ptr

使用来weak_ptr来解决循环引用问题,weak_ptr并不是智能指针,它没有RAII的特性,它被创造出来就是用来解决shared_ptr循环引用问题的

weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一块资源,但不会增加这块资源的引用计数

将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源

struct Node
{weak_ptr<Node> _prev;weak_ptr<Node> _next;int val;~Node() {cout << "~Node" << endl;}
};int main()
{shared_ptr<Node> node1(new Node);shared_ptr<Node> node2(new Node);cout << node1.use_count() << endl;cout << node1.use_count() << endl;node1->_next = node2;node2->_prev = node1;cout << node1.use_count() << endl;cout << node1.use_count() << endl;return 0;
}

通过use_count获取这两个资源对应的引用计数就可以发现在连接结点之前它们资源的引用计数就是1,连接之后还是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数

weak_ptr的模拟实现

template<class T>
class weak_ptr
{
public:weak_ptr():_ptr(nullptr){}weak_ptr(shared_ptr<T>& p):_ptr(p._ptr) //注意这里p._ptr是私有的,我们需要使用shared_ptr专门提供的获取资源的接口{}~weak_ptr(){if (_ptr != nullptr){cout << "~weak_ptr" << endl;delete _ptr;}}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};

说明一下:shared_ptr还会提供一个get函数,用于获取它管理的资源

std::shared_ptr的定制删除器

在shared_ptr中,它的析构函数的可以释放的数目是固定的,它并不支持大量数据的释放或者关闭一个文件指针

比如:

int main()
{shared_ptr<int> ptr(new int[20]); //errorshared_ptr<FILE> ptr(fopen("test.txt", "r")); //errorreturn 0;
}

在以上代码中,我们使用new申请20个空间的地址和打开了一个文件,在对象周期结束的时,如果它还是以delete的方式释放资源,那一定会存在内存泄漏,可能还会触发程序崩溃,使用new[ ]的方式申请空间的时候,我们必须使用delete[ ]的方式进释放,而文件指针则需要调用fclose函数进行文件的关闭

这时将需要使用定制删除器来控制释放资源的方式,在C++11的标准库中的shared_ptr提供了如下构造函数:

template <class U, class D> 
shared_ptr (U* p, D del);

参数说明:

p:需要让智能指针管理的资源

del:删除器,这个删除器是一个可调用对象,比如函数指针,仿函数,lambda表达式以及被包装器包装后的可调用对象

当智能指针管理的资源不是以new的方式申请到的内存空间,就需要在构造智能指针对象时传入定制的删除器

比如:

template<class T>
struct deleteArray
{void operator()(T* _ptr){return delete[] _ptr;}
};int main()
{shared_ptr<int> ptr1(new int[20], deleteArray<int>()); shared_ptr<A> ptr2(new A[20], deleteArray<A>()); shared_ptr<FILE> ptr3(fopen("test.txt", "r"), [](FILE* pf) {cout << "fclose" << endl;fclose(pf);}); return 0;
}

定制删除器的模拟实现

template<class T>
struct deleteArray
{void operator()(T* _ptr){return delete[] _ptr;}
};namespace nxbw
{template<class T>class shared_ptr{public:shared_ptr(const T* ptr): _ptr(ptr), _pcount(new int(1)){}shared_ptr(shared_ptr<T>& s):_ptr(s._ptr), _pcount(s._pcount){*(_pcount)++;}template<class D>shared_ptr(T* ptr, D del):_ptr(ptr), _pcount(new int(1)), _del(del){}~shared_ptr(){if (--*(_pcount) == 0){cout << "~shared_ptr" << endl;_del(_ptr);delete _pcount;}}//赋值运算符重载shared_ptr<T>& operator=(shared_ptr<T>& s){if (_ptr == s._ptr) return *this;if (--*(_pcount) == 0){delete _ptr;delete _pcount;}_ptr = s._ptr;_pcount == s._pcount;++*(_pcount);}//像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* Get(){return _ptr;}int use_count(){return *(_pcount);}private:T* _ptr;int* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; }; };
}

核心问题:怎么设计_del来接收del?

1.使用D来创建一个成员函数,因为D是构造函数的模板参数,并不是这个类的模板参数,所以这个方法是不可行的

2.使用包装器,它可以将仿函数包装起来然后使用包装器类型创建一个对象,创建之后,我们不需要关心D的类型,你传什么类型,包装器就接收什么样的类型,直接使用该成员就可以

3.给类模板增加一个参数,这个方法是可行的,但是很麻烦

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

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

相关文章

Ethernet 系列(12)-- 基础学习::SOME/IP

目录 1. SOME/IP简介&#xff1a; 1.1 什么是SOME/IP&#xff1a; 1.2 什么时候使用SOME/IP&#xff1a; 2. SOME/IP的特点&#xff1a; 2.1 序列化&#xff1a; 2.2 远程过程调用&#xff08;RPC&#xff09;: 2.3 服务发现&#xff1a; 2.4 发布/订阅&#xff1a; 2.5 UDP消息…

UE5.3 虚幻引擎 Windows插件开发打包(带源码插件打包、无源码插件打包)

0 引言 随着项目体量的增大&#xff0c;所有代码功能都放一起很难管理。所以有什么办法可以将大模块划分成一个个小模块吗。当然有&#xff0c;因为虚幻引擎本身就遇到过这个问题&#xff0c;他的解决办法就是使用插件的形式开发。 例如&#xff0c;一个团队开发了文件I/O模块插…

自学记录鸿蒙API 13:实现多目标识别Object Detection

起步&#xff1a;什么叫多目标识别&#xff1f; 无论是生活中的动物识别、智能相册中的场景分类&#xff0c;还是工业领域的检测任务&#xff0c;都能看到多目标识别的身影。这次&#xff0c;我决定通过学习HarmonyOS最新的Object Detection API&#xff08;API 13&#xff09…

光伏安装在屋顶:安全、环保还是潜在威胁?

随着环保意识的增强和科技的进步&#xff0c;光伏发电作为一种可再生能源技术&#xff0c;正逐渐走进千家万户。然而&#xff0c;随着光伏板的普及&#xff0c;关于其在屋顶安装是否对人体有害的疑问也随之而来。 一、光伏发电的基本原理 光伏发电是利用半导体界面的光生伏特效…

被催更了,2025元旦源码继续免费送

“时间从来不会停下&#xff0c;它只会匆匆流逝。抓住每一刻&#xff0c;我们才不会辜负自己。” 联系作者免费领&#x1f496;源&#x1f496;码。 三联支持&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;欢迎留言讨论 更多内容敬请期待。如有需要源码可以联系作者免…

MYsql--------ubantu中安装mysql

在Ubuntu平台上下载、启动和关闭MySQL的方法如下&#xff1a; 下载安装MySQL 更新软件包列表&#xff1a;打开终端&#xff0c;输入以下命令&#xff0c;确保软件包列表是最新的。sudo apt update安装MySQL服务器&#xff1a;执行以下命令安装MySQL服务器。在安装过程中&…

pygame飞机大战

飞机大战 1.main类2.配置类3.游戏主类4.游戏资源类5.资源下载6.游戏效果 1.main类 启动游戏。 from MainWindow import MainWindow if __name__ __main__:appMainWindow()app.run()2.配置类 该类主要存放游戏的各种设置参数。 #窗口尺寸 import random import pygame WIND…

Flutter中的网络请求图片存储为缓存,与定制删除本地缓存

Flutter中的网络请求图片存储为缓存&#xff0c;与定制删除本地缓存 1&#xff1a;封装请求图片函数 2&#xff1a;访问的图片都会转为本地缓存&#xff0c;当相同的请求url&#xff0c;会在本地调用图片 3&#xff1a;本地缓存管理【windows与andriod已经测试】【有页面】【有…

无线AP安装注意事项

现在的办公楼、酒店等项目中都设计含有网络无线覆盖这一项&#xff0c;在项目实施中&#xff0c;往往采用的是便捷并且后期便于网络无线设备管理的无线ap设备&#xff0c;作为前端无线信号的覆盖。在具体安装无线AP过程中&#xff0c;我们必须要注意以下几点才能保证项目实施完…

Golang的容器编排实践

Golang的容器编排实践 一、Golang中的容器编排概述 作为一种高效的编程语言&#xff0c;其在容器编排领域也有着广泛的运用。容器编排是指利用自动化工具对容器化的应用进行部署、管理和扩展的过程&#xff0c;典型的容器编排工具包括Docker Swarm、Kubernetes等。在Golang中&a…

计算机毕业设计Django+Tensorflow音乐推荐系统 音乐可视化 卷积神经网络CNN LSTM音乐情感分析 机器学习 深度学习 Flask

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

C# 在PDF中添加和删除水印注释 (Watermark Annotation)

目录 使用工具 C# 在PDF文档中添加水印注释 C# 在PDF文档中删除水印注释 PDF中的水印注释是一种独特的注释类型&#xff0c;它通常以透明的文本或图片形式叠加在页面内容之上&#xff0c;为文档添加标识或信息提示。与传统的静态水印不同&#xff0c;水印注释并不会永久嵌入…

分析服务器 systemctl 启动gozero项目报错的解决方案

### 分析 systemctl start beisen.service 报错 在 Linux 系统中&#xff0c;systemctl 是管理系统和服务的主要工具。当我们尝试重启某个服务时&#xff0c;如果服务启动失败&#xff0c;systemctl 会输出错误信息&#xff0c;帮助我们诊断和解决问题。 本文将通过一个实际的…

Dubbo扩展点加载机制

加载机制中已经存在的一些关键注解&#xff0c;如SPI、©Adaptive> ©Activateo然后介绍整个加载机制中最核心的ExtensionLoader的工作流程及实现原理。最后介绍扩展中使用的类动态编译的实 现原理。 Java SPI Java 5 中的服务提供商https://docs.oracle.com/jav…

如何利用Logo设计免费生成器创建专业级Logo

在当今的商业世界中&#xff0c;一个好的Logo是品牌身份的象征&#xff0c;它承载着公司的形象与理念。设计一个专业级的Logo不再需要花费大量的金钱和时间&#xff0c;尤其是当我们拥有Logo设计免费生成器这样的工具时。接下来&#xff0c;让我们深入探讨如何利用这些工具来创…

游戏如何检测iOS越狱

不同于安卓的开源生态&#xff0c;iOS一直秉承着安全性更高的闭源生态&#xff0c;系统中的硬件、软件和服务会经过严格审核和测试&#xff0c;来保障安全性与稳定性。 据FairGurd观察&#xff0c;虽然iOS系统具备一定的安全性&#xff0c;但并非没有漏洞&#xff0c;如市面上…

智联视频超融合平台:电力行业的智能守护者

文章目录 一、远程实时监控与设备状态监测二、提高应急响应能力三、实现无人值守与减员增效四、保障电力设施安全与防范外部破坏五、提升电网运行管理效率与决策科学性六、助力电力企业数字化转型与智能化发展七、智联视频超融合平台 在当今数字化浪潮下&#xff0c;视频联网平…

卸载干净 IDEA(图文讲解)

目录 1、卸载 IDEA 程序 2、注册表清理 3、残留清理 1、卸载 IDEA 程序 点击屏幕左下角 Windows 图标 -> 设置-控制面板->intellij idea 勾选第一栏 Delete IntelliJ IDEA 2022.2 caches and local history&#xff0c;表示同时删除 IDEA 本地缓存以及历史。 Delete I…

计算机网络•自顶向下方法:路由选路算法

路由选路算法 在网络层中&#xff0c;选路是指数据包从源主机到目的主机的传输过程中&#xff0c;如何通过网络中的路由器选择一条合适的路径。路由器根据网络拓扑、路由表、协议规则等来决定如何将数据包转发到下一跳&#xff0c;直到数据包到达目的地。 选路算法分类 静态算…

Qemu配置QXL显卡支持分辨率

默认情况下&#xff0c;创建的vm的视频RAM限制为16MB。在win操作系统中分辨率最高就只能调到1024x768。 <video><model typecirrus vram16384 heads1 primaryyes/><address typepci domain0x0000 bus0x00 slot0x02 function0x0/> </video>单单修改ram…