【C++】详解RAII思想与智能指针

news2024/11/24 9:45:03

75e194dacf184b278fe6cf99c1d32546.jpeg

🌈 个人主页:谁在夜里看海.

🔥 个人专栏:《C++系列》《Linux系列》

⛰️ 丢掉幻想,准备斗争

d047c7b1ef574257b8397fe5cc5c290b.gif

目录

引言

内存泄漏

内存泄漏的危害

内存泄漏的处理

一、RAII思想

二、智能指针

1.auto_ptr

实现原理

模拟实现

弊端

2.unique_ptr

实现原理

模拟实现

3.shared_ptr

实现原理

模拟实现

循环引用问题

4.weak_ptr


引言

上一篇关于异常处理的文章,我们提到,异常处理是存在内存泄漏风险的,由于异常捕获会导致程序运行时执行流的跳转,并且在某些资源释放之前就进行了跳转,此时就会引发内存泄漏,来看下面这段代码:

当我输入3 0时,程序会抛出除零错误,并跳过了 delete p1; delete p2; 语句,因为异常发生时,程序的执行流会跳转到 catch 块,导致析构函数没有执行,引发内存泄漏。

内存泄漏

什么是内存泄漏呢?内存泄漏是指程序在运行的过程中,动态分配的内存被占用但没有得到释放,从而导致资源不能被回收,最终可能导致系统性能下降甚至崩溃

内存泄漏的危害

1.如果内存泄漏非常严重,程序将消耗所有的可用内存,导致操作系统或程序本身的崩溃。

2.内存泄漏意味着分配的内存空间无法被回收,不仅浪费内存空间,还可能会影响其他进程。

3.在一些长期运行的系统(服务器、嵌入式设备等)中,内存泄漏会导致系统持续消耗内存而不释放,久而久之会导致系统性能下降并最终导致系统崩溃。

4.内存泄漏往往是隐蔽性的,在大规模且复杂的程序中,调试和定位内存泄漏非常困难,这时可能需要借助一些外部的工具。

内存泄漏的处理

一般分为两种:事先预防型 事后查错型

事后查错型例如借助外部工具;

我们下面要介绍的就是对内存泄漏的事先预防处理办法,采用RAII思想以及智能指针:

一、RAII思想

RAII 全称 Resource Acquisition Is Initialization,中文翻译:资源获取即初始化,它强调通过对象的生命周期来管理资源,将资源的获取与释放与对象的创建与销毁相一致,RAII设计原则可以更好地管理动态资源,有效避免内存泄漏。还是用上述例子来直观感受一下:

template<class T>
class smartptr {
public:
	smartptr(T data)
	{
		cout << "smartptr(T data)" << endl;
		_ptr = new T(data);
	}
	~smartptr()
	{
		cout << "~smartptr()" << endl;
		delete _ptr;
	}

private:
	T* _ptr;
};

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
int main()
{
	try
	{
		smartptr<int> p1(1);
		smartptr<int> p2(2);
		div();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

我们可以看到,RAII思想实际上就是将资源的获取与释放封装到一个类中,在构造函数中获取资源,在析构函数中释放资源。我们不需要显式地释放资源,并且将资源与对象的生命周期绑定

输入3 0时,抛出除零异常,执行流跳转的同时,try块内部生命周期结束,此时会调用内部对象的析构函数,完成了资源的释放,避免了资源泄露。

二、智能指针

上述smartptr还不能被称作智能指针,因为它不具备指针的行为,我们还需要在类内部重载解引用*、访问->等操作,使其能像指针一样使用:

template<class T>
class smartptr {
public:
	smartptr(T data = T())
	{
		_ptr = new T(data);
	}
	~smartptr(){
		delete _ptr;
	}
	T& operator*(){
		return *_ptr;
	}
	T* operator->(){
		return _ptr;
	}
private:
	T* _ptr;
};

struct Date {
	int _year;
	int _month;
	int _day;
};

int main()
{
	smartptr<Date> p1;
	p1->_year = 2024;
	p1->_month = 11;
	p1->_day = 9;
	cout << (*p1)._year << "-" << (*p1)._month << "-" << (*p1)._day << endl;
	return 0;
}

智能指针采用RAII原理来管理动态分配的内存,也是将资源的管理与对象的生命周期绑定,并且可以像普通指针那样进行*、->等操作。

下面我们来介绍一下C++98提供的auto_ptr智能指针:

1.auto_ptr

auto_ptr是C++98标准引入的一种智能指针,是RAII思想的体现,其核心功能是自动管理动态分配的内存,确保指针在超出作用域时,资源被正确释放,下面是它的主要实现原理:

实现原理

构造函数

auto_ptr的构造函数接受一个原始指针(裸指针),并将其封装成auto_ptr对象内部的指针:

auto_ptr<int> p(new int(10)); // 构造函数

析构函数

auto_ptr的析构函数会在对象生命周期结束时自动调用delete操作符,释放指针所指向的内存:

~auto_ptr() {
    delete _ptr;  // 释放内存
}

拷贝构造函数

与普通对象的拷贝构造函数不同,auto_ptr在拷贝时会转移其资源所有权给新对象。因此,拷贝构造后,原对象会变成一个空指针(指向nullptr) 

auto_ptr(const auto_ptr& other) : _ptr(other._ptr) {
    other._ptr = nullptr;  // 将原对象的指针设为 nullptr,避免重复释放
}

赋值操作符

auto_ptr的赋值操作符也会转移资源所有权,先释放当前对象的资源,然后将传入的指针复制到当前对象,并将传入指针置空:

auto_ptr& operator=(const auto_ptr& other) {
    if (this != &other) {
        delete _ptr;  // 先释放当前资源
        _ptr = other._ptr;
        other._ptr = nullptr;  // 将 other 的指针置空
    }
    return *this;
}

成员访问

通过重载*、->实现指针的解引用与访问成员操作:

auto_ptr<int> p(new int(10));
*p = 20;  // 访问值
模拟实现

下面是对auto_ptr的简单模拟实现:

	template<class T>
	class auto_ptr
	{
	public:
        // 构造函数
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
        // 拷贝构造函数
		auto_ptr(auto_ptr<T>& other)
			:_ptr(other._ptr)
		{
			// 管理权转移
			other._ptr = nullptr;
		}
        // 赋值函数
		auto_ptr operator=(const auto_ptr<T> other)
		{
			// 检测是否给自己赋值
			if (*this != other)
			{
				// 释放当前资源
				if (_ptr)
					delete _ptr;
				_ptr = other._ptr;
				other._ptr = nullptr;
			}
			return *this;
		}
        // 析构函数
		~auto_ptr()
		{
			if(_ptr)
				delete _ptr;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
弊端

auto_ptr在拷贝和赋值时隐式地转移了资源所有权,会导致一些潜在的资源管理问题。

auto_ptr被拷贝时,源对象指针被置空,此时无法再进行访问

auto_ptr<int> p1(new int(10));  // 创建 p1
auto_ptr<int> p2 = p1;          // p1 的资源转移给 p2,p1 变为 nullptr
cout << *p1 << std::endl;       // 错误,p1 已为空指针

这种隐性的资源转移,使得auto_ptr在传递和返回时不直观,比如,我们在函数调用对象时,希望资源可以被复制并共享,但是auto_ptr不允许资源的共享,可能会导致意料之外的资源转移

void foo(auto_ptr<int> ptr) {
    // ptr 的资源所有权已经转移到 foo 函数的局部变量中
}

int main() {
    auto_ptr<int> p1(new int(10));
    foo(p1);  // p1 的资源被转移到 foo,p1 变为空指针
    cout << *p1 << std::endl;  // 错误,p1 已为空指针
}

auto_ptr在实际使用过程中,会引发许多不易察觉的错误,这并不是我们想要的智能指针,为了避免上述问题,C++11引入了更多新的智能指针,例如unique_ptr:

2.unique_ptr

为了避免所有权转移导致的一系列潜在问题,unique_ptr采用了一种简单粗暴的办法:禁止拷贝,禁止一切拷贝行为,从根源上解决了问题。

实现原理

禁止拷贝行为
编译器会禁止unique_ptr的拷贝构造与拷贝复制操作,试图拷贝将会报错:

std::unique_ptr<int> p1(new int(10));
// 编译错误,禁止拷贝构造
std::unique_ptr<int> p2 = p1; // 错误:拷贝构造被删除

明确的资源所有权转移

禁止拷贝并不意味着对资源所有权转移的全面封杀,实际上还是可以对资源进行转移的,只不过auto_ptr是隐式地转移,而unique_ptr是显式地进行转移:

unique_ptr<int> p1(new int(10));
unique_ptr<int> p2 = move(p1); // 明确转移资源所有权

// p1 现在为空指针,p2 拥有资源
cout << *p2 << endl;  // 输出 10
// cout << *p1 << endl; // 错误:p1 现在为空指针
模拟实现

那么unique_ptr在底层是怎么实现对拷贝的禁止的呢,其实是用到了delete关键字,在成员函数后面加上 = delete,表示禁止该成员函数的使用,通过delete删除拷贝构造函数与拷贝赋值函数,从而禁止了拷贝行为的发生:

	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
				delete _ptr;
		}

		// 指针操作
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		
		// 不支持拷贝构造、拷贝赋值
		unique_ptr(const unique_ptr<T>& other) = delete;
		unique_ptr operator=(const unique_ptr<T> other) = delete;
	private:
		T* _ptr;
	};

C++11还提供了一种智能指针,它支持拷贝行为,并且允许多个对象共享资源,即拷贝赋值并不会将原指针置空,而是让它们指向同一块空间:

3.shared_ptr

shared_ptr支持拷贝构造以及拷贝赋值,它们可以共享资源,各自进行操作,但要考虑一个问题:RAII的思想是将资源的获取和释放与对象的生命周期绑定,当我通过函数传参的方式将一个对象赋值给了另一个对象,会导致资源的提前释放(函数结束),这样外部指针就悬空了,共享的资源只需要进行一次释放即可,那么我们怎么知道何时释放资源呢?通过计数器的方式实现:

实现原理

shared_ptr通过引用计数来实现资源的正确释放,确保在所有共享该资源的shared_ptr对象都销毁后才释放资。 

当另一个shared_ptr对象拷贝构造或者赋值时,引用计数增加,表示多了一个对象在共享资源;当shared_ptr对象析构或被赋值到新的对象时,引用计数减少,表示减少一个共享资源的持有者。

模拟实现

实现过程如下:

	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pCount(new int(0))
		{
			++* _pCount; // 增加计数
		}
		shared_ptr(shared_ptr<T>& other)
			:_ptr(other._ptr)
			,_pCount(other._pCount)
		{
			cout << "shared_ptr(shared_ptr<T>& other)" << endl;
			++* _pCount; // 增加计数
		}
		shared_ptr operator=(shared_ptr<T>& other)
		{
			// 检测是否给自己赋值
			if (_ptr  != other._ptr)
			{
				cout << "shared_ptr operator=(const shared_ptr<T>& other)" << endl;
				--* _pCount; // 减少当前资源的计数
				// 释放当前资源
				if (*_pCount == 0)
				{
					delete _ptr;
					delete _pCount;
				}
				_ptr = other._ptr;
				_pCount = other._pCount;
				++* _pCount; // 增加新资源的计数
			}
			return *this;
		}
		~shared_ptr()
		{
			--* _pCount; // 减少计数
			if (*_pCount == 0)
			{
                // 共享对象全部销毁,进行析构
				cout << "~shared_ptr()" << endl;
				delete _ptr;
				delete _pCount;
			}
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pCount;
	};

shared_ptr适用于绝大多数场景,但是在某些场景下,会引发循环引用问题,此时资源不能得到正确释放:

循环引用问题

来看下面这段代码:

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

我们将节点的_prev和_next定义成shared_ptr智能指针,并定义了两个节点,节点2的_prev指向节点1,节点1的_next指向节点2:

这个时候会造成什么结果呢,我们来看运行结果:

node1的_next和node2共享资源,所以它们的计数为2,node2的_prev和node1共享资源,所以它们的计数也为2,从运行结果可以看出,节点空间没有得到释放(没有打印"~ListNode()"内容),这是为什么呢?

由于Node1和Node2的相互引用,它们的任意一个要想释放空间,都得建立在对方已经释放空间的基础上,于是乎两者都不能正常进行空间释放,这就是循环引用问题。

4.weak_ptr

C++11提供了一种弱引用智能指针weak_ptr,它的出现就是为了解决循环引用问题的,其原理是:weak_ptr是对对象的弱引用,不会增加计数,不会阻止资源的释放

由于互相指向时,计数没有增加,所以最后析构函数正常调用,资源得到释放。


以上就是对RAII思想及智能指针的介绍与个人理解,欢迎指正~ 

码文不易,还请多多关注支持,这是我持续创作的最大动力!

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

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

相关文章

所谓的情商高,其实就是会说话!

所谓的情商高&#xff0c;其实就是会说话&#xff01; 1.当遇到不知道的事情时&#xff0c;不要直截了当地说“不知道”。而应委婉地表达为“我想听听你的看法”。 如此既能避免尴尬&#xff0c;又能展现出对对方见解的尊重和期待。 2.不要简单地说“我迟到了”&#xff0c;…

ALB搭建

ALB: 多级分发、消除单点故障提升应用系统的可用性&#xff08;健康检查&#xff09;。 海量微服务间的高效API通信。 自带DDoS防护&#xff0c;集成Web应用防火墙 配置&#xff1a; 1.创建ECS实例 2.搭建应用 此处安装的LNMP 3.创建应用型负载均衡ALB实例 需要创建服务关联角…

【spark面试】spark的shuffle过程

概述 所有的shuffle的过程本质上就是一个task将内存中的数据写入磁盘&#xff0c;然后另一个task将磁盘中的数据读入内存的过程。 对于mapreduce来说&#xff0c;我们将内存中的数据写入磁盘成为maptask&#xff0c;将磁盘中的数据读入内存称为reducetask。 而对于spark来说&…

Android 实现一个系统级的悬浮秒表

前言 由于项目需要将手机录屏和时间日志对应起来&#xff0c;一般的手机录屏只能看到分钟&#xff0c;但是APP的日志输出通常都是秒级别的&#xff0c;于是决定自己手撸一个悬浮秒表&#xff08;有拖拽效果&#xff09;。 效果如下 具体实现 大致的实现思路&#xff1a; 创…

【科普小白】LLM大语言模型的基本原理

一、要了解LLM大模型的基本原理就要先来了解一下自然语言处理&#xff08;NLP&#xff09;。 NLP 是 AI 的一个子领域&#xff0c;专注于使计算机能够处理、解释和生成人类语言&#xff0c;主要任务包括&#xff1a;文本分类、自动翻译、问题回答、生成文本等。到底是NLP促生了…

Go语言开发商城管理后台-GoFly框架商城插件已发布 需要Go开发商城的朋友可以来看看哦!

温馨提示&#xff1a;我们分享的文章是给需要的人&#xff0c;不需要的人请绕过&#xff0c;文明浏览&#xff0c;误恶语伤人&#xff01; 前言 虽然现在做商城的需求不多&#xff0c;但有很多项目中带有商城功能&#xff0c;如社区医院系统有上服务套餐、理疗产品需求、宠物…

ts 如何配置引入 json 文件

ts 如何配置引入 json 文件 参考文档&#xff1a; https://maxgadget.dev/article/how-to-import-a-json-file-in-typescript-a-comprehensive-guide 项目中有一个 .json 的文件是配置文件&#xff0c;如何引入到 ts 项目中 配置 tsconfig.json 文件&#xff0c;添加这两个 {…

如何让ffmpeg运行时从当前目录加载库,而不是从/lib64

程序在linux下运行时&#xff0c;一般从 /lib64 目录下加载依赖的库文件&#xff0c;如xxx.so. 有时候&#xff0c;系统里没有这些库&#xff0c;也不想从系统目录下加载&#xff0c;怎么办呢&#xff1f; 看下面的调整过程。 使用的源代码是 ffmpeg-6.1.tar.xz 解压后&…

【基于轻量型架构的WEB开发】课程 12.4 页面跳转 Java EE企业级应用开发教程 Spring+SpringMVC+MyBatis

12.4 页面跳转 12.4.1 返回值为void类型的页面跳转 返回值为void类型的页面跳转到默认页面 当Spring MVC方法的返回值为void类型&#xff0c;方法执行后会跳转到默认的页面。默认页面的路径由方法映射路径和视图解析器中的前缀、后缀拼接成&#xff0c;拼接格式为“前缀方法…

订单日记助力“实峰科技”提升业务效率

感谢北京实峰科技有限公司选择使用订单日记&#xff01; 北京实峰科技有限公司&#xff0c;成立于2022年&#xff0c;位于北京市石景区&#xff0c;是一家以从事生产、销售微特电机、输配电及控制设备等业务为主的企业。 在业务不断壮大的过程中&#xff0c;想使用一种既能提…

【Ubuntu24.04】部署服务(基础)

目录 0 背景1 设置静态IP2 连接服务器3 部署服务3.1 安装JDK3.2 下载并安装MySQL8.43.2.1 从官网下载 APT Repository 配置文件3.2.2 安装 MySQL8.43.2.3 配置远程连接 3.3 下载并配置Redis3.4 上传jar包并部署应用3.5 开放端口 4 总结 0 背景 在成功安装了Ubuntu24.04操作系统…

Docker镜像分成

1. 镜像分层原理 1.1 镜像分层的定义与结构 Docker 镜像的分层存储机制是其核心特性之一&#xff0c;它允许 Docker 镜像由多个只读层组成&#xff0c;这些层叠加在一起形成一个完整的文件系统。每个层代表 Dockerfile 中的一个指令&#xff0c;并且每一层都是不可变的&#…

JavaWeb开发9

ResponseBody 类型&#xff1a;方法注解、类注解 位置&#xff1a;Controller方法上/类上 作用&#xff1a;将方法返回值直接响应&#xff0c;如果返回值类型是实体对象/集合&#xff0c;将会转换为JSON格式响应 说明&#xff1a;RestControllerControllerResponseBody; 统…

C#中日期和时间的处理

目录 前言 时间对于我们的作用 一些关于时间的名词说明 格里高利历 格林尼治时间(GMT) 协调世界时(UTC) 时间戳 DateTime 初始化 获取时间 计算时间 字符串转DateTime 存储时间 TimeSpan 初始化它来代表时间间隔 用它相互计算 自带常量方便用于和ticks进行计…

Docker + Jenkins + gitee 实现CICD环境搭建

目录 前言 关于Jenkins 安装Jenkins docker中运行Jenkins注意事项 通过容器中的Jenkins&#xff0c;把服务打包到docker进行部署 启动Jenkins 创建第一个任务 前言 CI/CD&#xff08;持续集成和持续交付/持续部署&#xff09;&#xff0c;它可以实现自动化的构建、测试和部署…

linux之文件(上)

linux之文件&#xff08;上&#xff09; 一.文件的预备知识二.C语言的文件接口和linux的系统接口2.1fopen2.2fclose2.3open2.4close2.5write2.6read 三.文件与系统3.1文件描述符3.2 标准输入&#xff0c;标准输出和标准错误3.3fd的分配规则 四.重定向4.1重定向的概念4.2重定向的…

【进度猫-注册/登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

多维视角下的知识管理:Spring Boot应用

2 开发技术 2.1 VUE框架 Vue.js&#xff08;读音 /vjuː/, 类似于 view&#xff09; 是一套构建用户界面的渐进式框架。 Vue 只关注视图层&#xff0c; 采用自底向上增量开发的设计。 Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。 2.2 Mysql数据库 …

【卷积基础】CNN中一些常见卷积(1*1卷积、膨胀卷积、组卷积、深度可分离卷积)

文章目录 逐通道卷积&#xff08;Pointwise Convolution&#xff0c;1x1 卷积&#xff09;主要作用逐通道卷积的操作过程优势代码示例典型应用 膨胀卷积&#xff08;Dilated Convolution&#xff09;主要作用工作原理膨胀率 (dilation rate) 的定义代码实例膨胀卷积的优点 组卷…

算法【Java】—— 动态规划之路径问题

前言 本文章终点解析第一道题目【不同路径】和最后一道题目【地下城游戏】的动态规划思路&#xff0c;中间几道题目会很快过完&#xff0c;大家如果不熟悉动态规划的思路可以重点看一下这两道题目的解析。 不同路径 https://leetcode.cn/problems/unique-paths 解析&#xf…