【C++进阶】RAII思想&智能指针

智能指针

  • 一,为什么要用智能指针(内存泄漏问题)
    • 内存泄漏
  • 二,智能指针的原理
    • 2.1 RAII思想
    • 2.2 C++智能指针发展历史
  • 三,更靠谱的shared_ptr
    • 3.1 引用计数
    • 3.2 循环引用
    • 3.3 定制删除器
  • 四,总结

上一节我们在讲抛异常时,就引出了利用智能指针来防止出现内存泄漏的问题,现在我们来看一下智能指针。

一,为什么要用智能指针(内存泄漏问题)

接着上一节的问题,如果我们在捕获异常时刚好在堆上new了一段空间,如果我们没有重新抛出异常,那么在堆上的空间该如何释放呢?

先来看一下这段代码:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{

	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;
}

我们知道new本身也会抛异常,如果p1这里抛异常或者p2这里抛异常,都会导致p1或者p2得不到释放。
如果在div中抛异常,那么p1和p2都不会得到释放。

所以在没有使用智能指针的情况下,这种问题还是很难解决的。

我们顺便回顾一下什么是内存泄漏。

内存泄漏

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

如果长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终会卡死。这种造成的后果还是非常的严重的。

要避免出现内存泄漏,就要做到:

  1. 在前期做好良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。这个理想状态。如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源。

二,智能指针的原理

2.1 RAII思想

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

这种做法有两大好处:
1. 不需要显式地释放资源,可以有效防止抛异常所导致的问题
2. 采用这种方式,对象所需的资源在其生命期内始终保持有效

智能指针就是这种思想的应用,这里我们先来简单实现一个智能指针,智能指针当然也是一种指针,所以也要重载->*
也就是利用smart_ptr这个类的生命周期来控制资源

template<class T>
class smart_ptr {
public:
	smart_ptr(T* ptr)
		:_ptr(ptr)
	{}

	~smart_ptr() {
		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()
{
	smart_ptr <int> sp1(new int);
	smart_ptr <int> sp2(new int);
	cout << div() << endl;
}

int main()
{
	try {
		Func();
	}
	catch(const exception& e)
	{
		cout<<e.what()<<endl;
	}
	return 0;
}

当在main函数中捕获到异常后,会跳出Func这个函数的作用域,那么随之这两个smart_ptr 也会跟着销毁,同时在析构函数中将sp1和sp2跟着销毁。

但是这里又会有新的问题,如果要拷贝这个智能指针呢?

	smart_ptr <int> sp1(new int);
	smart_ptr <int> sp2 = sp1;

这里就会造成这两个sp1和sp2管理的是同一块资源,出了这个作用域后会销毁,这时就会造成两次析构的问题。

那么要如何解决这个问题呢?我们就要从C++智能指针的发展来看看了。

2.2 C++智能指针发展历史

1.C++第一个智能指针是在C++98提出来的,就是auto_ptr
在这里插入图片描述

但是auto_ptr有一个很大的问题就是,其实现的时候用的是管理权转移的思想,即:

auto_ptr<int> sp1(new int);
auto_ptr<int> sp2(sp1); // 管理权转移

sp1拷贝给sp2后,sp1就会失效,也就是说不论拷贝还是赋值后,原先的智能指针把对这块资源的管理权全部交给了被拷贝或者赋值的那个智能指针,然后会将原先的智能指针置为空。

这样就会很坑了,所以在很多使用智能指针的场景下,auto_ptr是很明令禁止不能使用的。


2.C++11又推出了新版本的智能指针,unique_ptr

在这里插入图片描述
那么unique_ptr是如何解决的呢?解决办法也比较简单粗暴,那就是不让拷贝或者赋值,也就是防拷贝
如何防拷贝呢?

  1. 只声明不实现
  2. 限定为私有
unique_ptr<int> sp1(new int);
unique_ptr<int> sp2(sp1);

这里可以看到是不可以进行拷贝的

在这里插入图片描述
在这里插入图片描述


3.如果就是要拷贝呢?
C++11之后又开始提供更靠谱的并且支持拷贝的shared_ptr

shared_ptr是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。也就是当指向某个资源的个数为1时才释放。

我们具体在下面来进行讲解并且模拟实现一下:

三,更靠谱的shared_ptr

3.1 引用计数

shared_ptr是用引用计数的思想来保证可以去拷贝和赋值的。也就是shared_ptr在其内部,给每个指向的资源都维护了着一份计数,用来记录该份资源被几个对象共享
在这里插入图片描述
在这里插入图片描述
如果某个shared_ptr的引用计数是0,就说明自己是最后一个使用该资源的对象,就必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

赋值也是一样的,赋值后也要将对用的引用计数进行改变
在这里插入图片描述

下面是模拟实现的代码:

template<class T>
class shared_ptr {
public:
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}

	void release()
	{
		if (--(*_pcount) == 0)
		{
			//cout << "delete->" << _ptr << endl;
			//delete _ptr;

			delete _pcount;

		}
	}

	~shared_ptr() {
		release();
	}

	//拷贝构造
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
	{
		(*_pcount)++;
	}
	//赋值
	shared_ptr operator=(const shared_ptr<T>& sp) {//这里要分情况(如果被拷贝的对象引用计数只有一次就要释放)
		if (_ptr != sp._ptr) {//这里直接判断其指向的是不是同一个资源,可以解决直接或者间接自己给自己赋值的情况
			release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;

			++(*_pcount);
		}
		return *this;

	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	int use_count() const
	{
		return *_pcount;
	}

	T* get() const
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* _pcount;
};

注意:这里在重载=时要处理一下自己给自己赋值的情况

3.2 循环引用

C++11出现的shared_ptr虽然解决了拷贝的问题,但是又引出了新的问题,就是循环引用的问题

我们来结合下面的场景来分析一下循环引用
看下面的代码:

struct ListNode
{
	int val;
	shared_ptr<ListNode> next;
	shared_ptr<ListNode> prev;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main(){
	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);
	
	n1->next = n2;
	n2->prev = n1;

	return 0;
}

运行后我们看到n1和n2并没有被释放。但是如果我们只要屏蔽掉n1->next=n2或者n2->prev = n1,n1和n2都会释放。
没有释放那就是内存泄漏!!

这是为什么呢?

在这里插入图片描述

那么这样的问题要如何解决呢?

这里就要用到weak_ptr了,weak_ptr是专门用来解决循环引用的问题的,他不是RAII思想,不会增加引用计数。
这里的解决办法就是将ListNode里的next和prev用weak_ptr代替

weak_ptr<ListNode> next;
weak_ptr<ListNode> prev;

这里可以看到weak_ptr是支持用shared_ptr去赋值的
在这里插入图片描述
这里我们简单实现了weak_ptr,想看的大家可以进入我的gitee查看:智能指针

3.3 定制删除器

这里还有一个小问题就是,如果我们使用智能指针所控制的这个空间不是new出来的而是new [],或者malloc出来的,那么我们在使用时就会出问题,因为shared_ptr在内部是delete。

shared_ptr<ListNode> sp1(new ListNode[10]);

这里就要用到定制删除器,其实就是传入一个仿函数(lambda表达式也可以)去删除指定类型。

template<class T>
struct DeleArry {
	void operator()(T* ptr) {
		delete[] ptr;
	}
};

shared_ptr<ListNode> sp1(new ListNode[10],DeleArry<ListNode>());

或者lambda表达式

shared_ptr<ListNode> sp3(new ListNode[10], [](ListNode* ptr) { delete[] ptr; });

当然相应的shared_ptr的结构也要做相应的变化,感兴趣大家可以自己简单模拟实现一下。
大家也可以来看看我模拟实现的:智能指针

四,总结

这里我们也就讲完了智能指针了,智能指针还是很实用的。到这里我们C++的大部分重难点也就结束了。如果你到了这里,首先恭喜你坚持学习到了现在并且感受到了C++的独特之处。但是我们的学习还远没有结束,下一节我们会讲解类的设计模式中的常用的模式之一:单例模式。希望大家可以持续关注。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vulnhub靶机 DC-1渗透详细过程

Vulnhub靶机:DC-1渗透详细过程 目录 Vulnhub靶机:DC-1渗透详细过程一、将靶机导入到虚拟机当中二、攻击方式主机发现端口扫描web渗透利用msf反弹shell数据库信息web管理员密码提权 一、将靶机导入到虚拟机当中 靶机地址&#xff1a; https://www.vulnhub.com/entry/dc-1-1,29…

Open CASCADE学习|BRepOffsetAPI_DraftAngle

BRepOffsetAPI_DraftAngle 是 Open CASCADE Technology (OCCT) 中用于创建带有草图斜面的几何体的类。草图斜面是一种在零件设计中常见的特征&#xff0c;它可以在零件的表面上创建一个倾斜的面&#xff0c;通常用于便于零件的脱模或是增加零件的强度。 本例演示了如何创建一个…

启动nginx时报错:signal process started

解决方案&#xff0c;直接使用该命令启动&#xff0c;指向nginx.conf配置文件&#xff1a; nginx -c /www/wdlinux/nginx/conf/nginx.conf 启动成功&#xff1a;

C语言高质量编程之assert()和const

目录 编程中常见的错误 assert() const 编程中常见的错误 在编程中我们通常会遇到三种错误形式&#xff0c;分别是&#xff1a;编译型错误&#xff0c;链接型错误&#xff0c;运行时错误。 编译型错误&#xff1a; 在编译阶段发生的错误&#xff0c;绝大多数情况是由语法错误…

全栈的自我修养 ———— react实现滑动验证

实现滑动验证 展示依赖实现不借助create-puzzle借助create-puzzle 展示 依赖 npm install rc-slider-captcha npm install create-puzzleapi地址 实现 不借助create-puzzle 需要准备两张图片一个是核验图形&#xff0c;一个是原图------> 这个方法小编试了后感觉比较麻烦…

初探vercel托管项目

文章目录 第一步、注册与登录第二步、本地部署 在个人网站部署的助手vercel&#xff0c;支持 Github部署&#xff0c;只需简单操作&#xff0c;即可发布&#xff0c;方便快捷&#xff01; 第一步、注册与登录 进入vercel【官网】&#xff0c;在右上角 login on&#xff0c;可登…

图解JDK 8 HashMap

HashMap-简介 HashMap 主要用来存放键值对&#xff0c;它基于哈希表的 Map 接口实现&#xff0c;是常用的 Java 集合之一&#xff0c;是非线程安全的。 HashMap 可以存储 null 的 key 和 value&#xff0c;但 null 作为键只能有一个&#xff0c;null 作为值可以有多个 JDK1.8…

Macbook M1 Pro使用brew安装Docker并安装Nacos【超详细图解】

目录 一、安装 Docker 二、修改 Docker 镜像地址 三、拉取镜像-举例 Nacos 1.拉取镜像 2.查看本地镜像 3.删除镜像 四、启动容器 1.启动 Nacos 容器&#xff1a; I.方式一【推荐】 II.方式二【懒人推荐】 2.访问 Nacos Web 控制台 3.进入容器和退出容器 五、配置…

labview中的同步定时结构

单帧定时循环定时比较精确&#xff0c;最常用的功能还是它的定时循环功能&#xff0c;定时循环允许不连接“循环条件”端子&#xff0c;可以连接定时循环“结构名称”端子&#xff0c;通过定时结构停止函数停止循环。 例子在附件中。

如何下载和安装Google Chrome扩展插件:一步步指南

Google Chrome 插件为我们提供了这样的便利&#xff0c;但有时找到一个有用的插件后&#xff0c;我们可能需要将其下载到本地以便离线使用或备份。 一、为什么可以从Google Chrome商店直接下载插件&#xff1f; Google Chrome 扩展插件主要通过Chrome Web Store分发&#xff…

凡泰极客亮相2024 亚马逊云科技出海全球化论坛,为企业数字化出海赋能

随着「不出海&#xff0c;即出局」登上热搜榜单&#xff0c;企业出海已成燎原之势&#xff0c;3月29日&#xff0c;2024 亚马逊云科技出海全球化论坛在深圳成功举办&#xff0c;凡泰极客创始人梁启鸿受邀出席&#xff0c;并以 「App 2.0&#xff1a;以SuperApp构建智能数字生态…

使用Python的Pillow库进行图像处理书法参赛作品

介绍&#xff1a; 在计算机视觉和图像处理领域&#xff0c;Python是一种强大而流行的编程语言。它提供了许多优秀的库和工具&#xff0c;使得图像处理任务变得轻松和高效。本文将介绍如何使用Python的wxPython和Pillow库来选择JPEG图像文件&#xff0c;并对选中的图像进行调整和…

Vol.45 这个壁纸网址,功能简单,每月37.7万访问量

哈咯&#xff0c;大家好&#xff0c;我是欧维&#xff0c;今天要给大家分享的网站是&#xff1a;极简壁纸&#xff0c;一个专门做电脑壁纸的网站&#xff1b; 它的网址是&#xff1a;极简壁纸_海量电脑桌面壁纸美图_4K超高清_最潮壁纸网站 网站的壁纸质量很高&#xff0c;页面…

c/c++普通for循环学习

学习一下 for 循环的几种不同方式&#xff0c;了解一下原理及差异 完整的测试代码参考 GitHub &#xff1a;for 循环测试代码 1 常用形态 对于 for 循环来说&#xff0c;最常用的形态如下 for (表达式1; 表达式2; 表达式3) {// code }流程图如下&#xff1a; 编写测试代码…

高清4路HDMI编码器JR-3214HD

产品简介&#xff1a; JR-3214HD四路高清HDMI编码器是专业的高清音视频编码产品&#xff0c;该产品具有支持4路高清HDMI音视频采集功能&#xff0c;4路3.5MM独立外接音频输入&#xff0c;编码输出双码流H.264格式&#xff0c;音频MP3/AAC格式。编码码率可调&#xff0c;画面质…

[Spring Cloud] (2)gateway全局异常捕捉统一返回值

文章目录 处理转发失败的情况全局参数同一返回格式操作消息对象AjaxResult返回值状态描述对象AjaxStatus返回值枚举接口层StatusCode 全局异常处理器自定义通用异常定一个自定义异常覆盖默认的异常处理自定义异常处理工具 在上一篇章时我们有了一个简单的gateway网关 [Spring C…

使用LangChain和GPT-4,创建Pandas DataFrame智能体

大家好&#xff0c;数据分析和数据处理是数据科学领域每天都在进行的基本任务。高效和快速的数据转换对于提取有意义的见解和基于数据做出明智决策至关重要。其中最受欢迎的工具之一是Python库Pandas&#xff0c;它提供了一个功能强大的DataFrame工具&#xff0c;使用灵活直观的…

【STL详解 —— stack和queue的介绍及使用】

STL详解 —— stack和queue的介绍及使用 stackstack的定义方式stack的使用 queuequeue的定义方式queue的使用 stack stack是一种容器适配器&#xff0c;专门用在具有后进先出操作的上下文环境中&#xff0c;其只能从容器的一端进行元素的插入与提取操作。 stack的定义方式 首…

list 简化版模拟实现

1ListNode template<class T>struct ListNode{public:ListNode(const T& x T()):_next(nullptr), _prev(nullptr), _data(x){}//private://共有可访问ListNode<T>* _next;ListNode<T>* _prev;T _data;}; 实现iterator对Node*的封装 实现运算符重载 vo…

深入理解DES算法:原理、实现与应用

title: 深入理解DES算法&#xff1a;原理、实现与应用 date: 2024/4/14 21:30:21 updated: 2024/4/14 21:30:21 tags: DES加密对称加密分组密码密钥管理S盒P盒安全性分析替代算法 DES算法简介 历史 DES&#xff08;Data Encryption Standard&#xff09;算法是由IBM研发&…