【C++】智能指针:解决内存泄漏、悬空指针等问题

news2024/12/24 20:50:44
头像
⭐️个人主页:@小羊
⭐️所属专栏:C++
很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~

动图描述

目录

  • 前言
    • 一、RAII
    • 二、智能指针原理
    • 三、auto_ptr
    • 四、unique_ptr
    • 五、shared_ptr
      • 第一步:实现出RAII的框架
      • 第二步:如何实现引用计数
      • 第三步:赋值重载
      • 第四步:加定制删除器
      • 第五步:解决循环引用的问题
    • 六、weak_ptr


前言

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

解决内存泄漏的问题,使用智能指针管理是一个很好的选择。


一、RAII

RAII(Resource Acquisition Is Initialization)是C++中的一种资源管理技术,其核心思想是利用对象的生命周期来自动管理资源,因为对象的构造和析构是自动调用的。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  1. 不需要显式地释放资源
  2. 对象所需的资源在其生命期内始终保持有效

不管对象是生命周期正常结束还是抛了异常,最后都会自动释放掉:

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

//使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};

二、智能指针原理

上面的SmartPtr还不能称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此智能指针模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}

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

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

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

int main()
{
	SmartPtr<int> sp1(new int);
	*sp1 = 10;
	cout << *sp1 << endl;

	SmartPtr<Date> sp2(new Date);
	sp2->_year = 2024;
	sp2->_month = 10;
	sp2->_day = 25;
}

智能指针的原理:RAII特性 + 重载operator*和operator->,具有像指针一样的行为

这么看智能指针好像还挺好理解挺简单的,不过先别高兴太早,智能指针麻烦的地方不在这里,在拷贝构造。
我们知道C++默认构造函数实现的是浅拷贝,而智能指针模拟的是原生指针的行为,我们期望它的拷贝就是浅拷贝,看似类的默认构造函数就很好的满足我们的需求,但是不要忘了让多个指针指向同一块空间,这样会导致同一块空间出现析构多次的情况,显然这里出现了矛盾。


三、auto_ptr

  • C++智能指针都在头文件<memory>中定义。

auto_ptr要求其对“裸”指针的完全占有性,即一个“裸”指针不能同时被两个以上的auto_ptr所拥有。
虽然auto_ptr提供了拷贝,但它的拷贝是一种管理权转移。被拷贝对象会失去资源的所有权,拷贝对象会接管这个资源。同样的赋值也会转移管理权。

在这里插入图片描述

//拷贝构造
auto_ptr(auto_ptr<T>& ap)
	:_ptr(ap._ptr)
{
	//管理权转移
	ap._ptr = nullptr;
}
//赋值重载
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
	// 检测是否为自己给自己赋值
	if (this != &ap)
	{
		// 释放当前对象中资源
		if (_ptr)
			delete _ptr;
		// 转移ap中资源到当前对象中
		_ptr = ap._ptr;
		ap._ptr = NULL;
	}
	return *this;
}
//...

由于auto_ptr存在上述限制和潜在问题,C++11及以后的版本引入了更先进的智能指针,如std::unique_ptrstd::shared_ptr,它们提供了更强大和灵活的资源管理功能。因此,在现代C++编程中,建议使用这些新的智能指针来替代auto_ptr


四、unique_ptr

unique_ptr不支持拷贝,其拷贝构造函数被delete禁掉了。

在这里插入图片描述

unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
//不支持拷贝
unique_ptr<Date> up1(new Date);
unique_ptr<Date> up2(up1);

如果unique_ptr管理的是多个连续的空间,则释放时会出错,因为它的底层是delete,而释放连续的空间需要delete[]

在这里插入图片描述

解决这个问题可以通过定制删除器来解决。

在这里插入图片描述
定制删除器:

template<class T>
class DeleteArray
{
public:
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

class Fclose
{
public:
	void operator()(FILE* pf)
	{
		cout << "void operator()(FILE* pf)" << endl;
		fclose(pf);
	}
};

int main()
{
	unique_ptr<Date, DeleteArray<Date>> up1(new Date[10]);
	unique_ptr<FILE, Fclose> up2(fopen("text.txt", "w"));

	return 0;
}

在这里插入图片描述


五、shared_ptr

我们重点学习shared_ptr
shared_ptr的原理:通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享
  2. 在对象被销毁时,就说明自己不使用该资源了,对象的引用计数减一
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了

在这里插入图片描述

类似unique_ptrshared_ptr也支持传定制删除器,但它们两个支持传定制删除器的位置有所不同。

在这里插入图片描述

另外,除了直接new对象,也可以用make_shared构造对象。它是一个函数模板:

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

make_shared一次性完成了内存分配和对象构造,因此可以减少内存分配的次数,还可以使得控制块和对象可以分配在同一块连续的内存上,减少了内存碎片化的风险。


| 接下来模拟实现shared_ptr:

第一步:实现出RAII的框架

namespace yjz
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}

		~shared_ptr()
		{
			if (_ptr)
				delete _ptr;
		}

	private:
		T* _ptr;
	};
}

第二步:如何实现引用计数

首先我们应该讨论引用计数保存在哪里,不能存在各自的对象中,应该满足一个资源配一个计数,也就是公共计数。静态成员变量计数也不行,因为静态成员变量不属于某个对象,而是属于类的所有对象,这显然是不行的,因为所有的对象不可能都指向同一资源,可能其中的几个对象分别指向不同的资源。

这里合理的处理是将引用计数开在堆上(也就是上面make_shared部分提到的控制块),然后在对象中存一个指针指向这个计数。

在这里插入图片描述

一个资源配一个计数,所以计数也在构造的时候给出。

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

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			(*_pcount)++;
		}

		~shared_ptr()
		{
			//当计数为0时释放资源
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}

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

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
	};
}

第三步:赋值重载

赋值需要注意的是被赋值的指针原来管理的资源是否需要释放。那么这里就要显示地调用析构函数,不过析构函数最好和构造等一一对应,所以这里可以将析构的逻辑用一个函数重新包装,然后在赋值和析构函数中调用这个函数处理。
其中赋值还需要防止自己给自己赋值的情况,有可能会出现野指针的问题。

在这里插入图片描述

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	if (_ptr != sp._ptr)
	{
		release();
		_ptr = sp._ptr;
		_pcount = sp._pcount;
		++(*_pcount);
	}
	return *this;
}

void release()
{
	//当计数为0时释放资源
	if (--(*_pcount) == 0)
	{
		delete _ptr;
		delete _pcount;
		_ptr = nullptr;
		_pcount = nullptr;
	}
}

第四步:加定制删除器

在构造shared_ptr时我们要传一个可调用对象过去,这个对象可能是函数指针,可能是仿函数,也有可能是lambda,这里不确定接收的类型,就可以用function来接收。这里的删除器只供构造函数使用,因此不能传整个类模版。

//...
template<class D>
shared_ptr(T* ptr, D del)
	:_ptr(ptr)
	, _pcount(new int(1))
	,_del(del)
{}

//...
void release()
{
	//当计数为0时释放资源
	if (--(*_pcount) == 0)
	{
		//delete _ptr;
		_del(_ptr);
		delete _pcount;
		_ptr = nullptr;
		_pcount = nullptr;
	}
}

//...
private:
	T* _ptr;
	int* _pcount;
	function<void(T* ptr)> _del = [](T* ptr) {delete ptr; }
}

function成员变量需要一个缺省值,如果没有传定制的删除器需要用默认的删除操作进行释放。


第五步:解决循环引用的问题

在特殊场景,比如双向循环链表中,如果两个节点互相指向,就会出现循环引用的问题,最后导致内存泄漏

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
};

int main()
{
	std ::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);

	n1->_next = n2;
	n2->_prev = n1;

	return 0;
}

在这里插入图片描述

这里内存泄漏的关键是:n2后定义的先析构,而n2指向的资源还有n1->_next管理,所以n2指向的资源这里还不会释放;接下来析构n1指向的资源,而n1指向的资源还有n2->_prev管理,所以n1指向的资源这里也还不会释放。最后n1和n2两个shared_ptr都释放了它们原本指向的资源还得不到释放,因为还有这两个资源内部的shared_ptr互相管理者。

为了处理这种情况的发生,出现了weak_ptr来配合shared_ptr解决这个问题。


六、weak_ptr

不同于上面的智能指针,weak_ptr不支持直接管理资源(RAII),而是配合解决shared_ptr循环引用导致的内存泄漏的缺陷。
在引用计数的场景下,把节点中的_prev_next改成weak_ptr`就可以了

weak_ptrshared_ptr构造不增加引用计数。

当然这里只实现了一个简单的weak_ptr

template<class T>
class weak_ptr
{
public:
	weak_ptr()
	{}

	weak_ptr(const shared_ptr<T>& sp)
		:_ptr(sp.get())
	{}

	weak_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();
	}
private:
	T* _ptr = nullptr;
};
struct ListNode
{
	int _data;
	weak_ptr<ListNode> _next;
	weak_ptr<ListNode> _prev;
};

本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~

头像

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

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

相关文章

信息收集-shodan专题一

shodan介绍 一、shodan简介 1.工作原理解析&#xff1a; 2.优缺点 3.功能 二、安装shodan流程 三、shodan使用方法 1.搜索 1.1.search 搜索 1.2. count 总数 1.3. download 下载与解析 2. 指定查看 2.1 指定IP的详细信息 2.2 hostname: 搜索指定的域名 2.3 port:…

百度智能云推出11.11活动,各大云厂商香港服务器优惠活动汇总

2024年双十一活动就要来了&#xff0c;作为百度集团旗下的云智能服务平台——百度智能云今年率先开始了11.11狂欢购活动&#xff0c;上新促销活动的动作如此之快&#xff0c;难道是百度云要大发力了&#xff1f;感觉今年百度智能云要比阿里云、腾讯云、硅云、华为云等厂商更加卖…

k8s 综合项目笔记

综述 这篇笔记主要是为了记录下自己写 k8s 综合项目的过程。 由于自己之前已经写过简单的开发和运维项目&#xff0c;所以这里就结合一下&#xff0c;在搭建 k8s 集群后安装运维常用服务&#xff0c;比如 ansible 和 prometheus&#xff0c;用 NFS 实现数据存储同步&#xff0c…

[Ansible实践笔记]自动化运维工具Ansible(二):Ansible的playbook及角色

Ansible playbook&#xff08;剧本&#xff09; 详情请参考[Ansible实践笔记]自动化运维工具Ansible&#xff08;一&#xff09;&#xff1a;初探ansible&ansible的点对点模式 文章目录 Ansible playbook&#xff08;剧本&#xff09;介绍核心字段环境配置案例&#xff1…

寻找大自然的颜色

走在停停&#xff0c;停停走走&#xff0c;恍惚间一天过去了&#xff0c;转瞬间一年过去了&#xff0c;身边的一切在变化又不在变化&#xff0c;生活是自己的又不是自己的。 今天是个特殊的日子&#xff0c;其实前几天对我而言就算特殊的日子了&#xff0c;一个心里暗暗等待着却…

HTTP协议相关知识点

1&#xff0c;概念理解 HTTP(超文本传输协议)&#xff0c;是一种建立在TCP上的无状态连接&#xff0c; 工作流程&#xff1a;客户端发送一个HTTP请求&#xff0c;说明想要访问的资源和请求的方式&#xff0c;服务端收到请求后进行处理&#xff0c;根据请求的方式访问服务器资源…

Android View的事件分发机制

前言 本文由于介绍本人关于View的事件分发机制的学习&#xff0c;如有不恰当的描述欢迎指出。 View基础 什么是View ​ View是Android中所有控件的基类&#xff0c;不管是Button、TextView、LinearLayout&#xff0c;它们的共同基类都是View。也就是说&#xff0c;View是界…

【C++进阶篇】——STL的简介

【C进阶篇】——STL的简介 1.什么是STL STL(standard template libaray-标准模板库)&#xff1a;是C标准库的重要组成部分&#xff0c;不仅是一个可复用的组件库&#xff0c;而且是一个包罗数据结构与算法的软件框架。 2.STL的版本 原始版本 Alexander Stepanov、Meng Lee 在…

Github优质项目推荐(第八期)

文章目录 Github优质项目推荐 - 第八期一、【manim】&#xff0c;66.5k stars - 创建数学动画的 Python 框架二、【siyuan】&#xff0c;19.5k stars - 个人知识管理软件三、 【GetQzonehistory】&#xff0c;1.3k stars - 获取QQ空间发布的历史说说四、【SecLists】&#xff0…

<Project-11 Calculator> 计算器 0.3 年龄计算器 age Calculator HTML JS

灵感 给工人发工资是按小时计算的&#xff0c;每次都要上网&#xff0c;我比较喜欢用 Hours Calculator &#xff0c;也喜欢它的其它的功能&#xff0c; 做个类似的。 我以为是 Python&#xff0c;结果在学 javascript 看 HTML&#xff0c;页面的基础还停留在 Frontpage 2000…

leetCode算法题爬楼梯递归写法

题目&#xff1a; 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 示例 1&#xff1a; 输入&#xff1a;n 2输出&#xff1a;2解释&#xff1a;有两种方法可以爬到楼顶。1. 1 阶 1 阶2. 2 阶 …

互联网的无形眼睛:浏览器指纹与隐私保护攻略

你是否曾有过这样的经历&#xff1a;在某个电商网站上搜索了某件商品&#xff0c;随后无论你打开哪个网页&#xff0c;都能看到与之相关的广告&#xff1f;或者当你再次访问某个网站时&#xff0c;它居然记得你之前的浏览记录&#xff1f;这一切&#xff0c;背后都有一只“看不…

GEE引擎架设好之后进游戏时白屏的解决方法——gee引擎白屏修复

这两天测试GeeM2引擎的服务端&#xff0c;最常见的问题就是点击开始游戏出现白屏&#xff0c;最早还以为是服务端问题&#xff0c;结果是因为升级了引擎&#xff0c;而没有升级NewUI这份文件导致的。解决方法如下&#xff1a; 下载GEE引擎包最新版&#xff0c;&#xff08;可以…

vue+spreadjs开发

创建vue3项目 pnpm create vite --registryhttp://registry.npm.taobao.org安装spreadjs包 pnpm install "grapecity-software/spread-sheets17.1.7" "grapecity-software/spread-sheets-resources-zh17.1.7" "grapecity-software/spread-sheets-vu…

Linux操作进程

前言 这次的主要内容就是进程的实操&#xff0c;主要是进程创建&#xff0c;进程终止&#xff0c;进程等待和进程程序替换&#xff0c;最后我们在手写一个简单的shell 1.进程创建 进程创建就是fork&#xff0c;所以我们就讲一些知识性的就可以了 首先在创建子进程的时候&…

【ArcGIS Pro实操第5期】全局及局部空间插值:GPI、LPI、IDW等

ArcGIS Pro实操第5期&#xff1a;全局及局部空间插值 ArcGIS Pro-用于空间插值的丰富工具箱实操&#xff1a;空间插值方法1&#xff1a;Trend Surface Model for Interpolation-以降水数据为例方法2&#xff1a;Kernel Density Estimation Method-以单位面积鹿的目击数为例方法…

爆破(使用Burp Suite)

以此靶场为例 1.启动此靶场&#xff0c;双击靶机进入 2.进入后页面如下 3.打开Burp Suite中的代理中的拦截 4.再随便往输入框里面输入什么 5.提交后为这个页面&#xff0c;或其他 6.将系统代理改为proxy&#xff0c;按图片顺序点 本来选中的是系统代理&#xff0c;改为proxy …

ruoyi域名跳转缓存冲突问题(解决办法修改:session名修改session的JSESSIONID名称)

【版权所有&#xff0c;文章允许转载&#xff0c;但须以链接方式注明源地址&#xff0c;否则追究法律责任】【创作不易&#xff0c;点个赞就是对我最大的支持】 前言 仅作为学习笔记&#xff0c;供大家参考 总结的不错的话&#xff0c;记得点赞收藏关注哦&#xff01; 目录 前…

2024“源鲁杯“高校网络安全技能大赛-Misc-WP

Round 1 hide_png 题目给了一张图片&#xff0c;flag就在图片上&#xff0c;不过不太明显&#xff0c;写个python脚本处理一下 from PIL import Image ​ # 打开图像并转换为RGB模式 img Image.open("./attachments.png").convert("RGB") ​ # 获取图像…

新手直播方案

简介 新手直播方案 &#xff0c;低成本方案 手机/电脑 直接直播手机软件电脑直播手机采集卡麦电脑直播多摄像机 机位多路采集卡 多路麦加电脑&#xff08;高成本方案&#xff09; 直播推流方案 需要摄像头 方案一 &#xff1a;手机 电脑同步下载 网络摄像头 软件&#xff08…