【C++】智能指针详解(实现)

news2025/1/4 22:03:57

在本篇博客中,作者将会带领你理解并自己手动实现简单的智能指针,以加深对智能指针的理解。

一.什么是智能指针,为什么需要智能指针

智能指针是一种基于RAII思想实现的一种资源托管方式,至于什么是RAII,后面会讲到。

对于C++语言来说,内存泄漏是一个避不开的话题,但对于内存的安全管理来说,在智能指针出现之前,只能我们程序员自己来管理,自己决定什么时候释放,这样就很容易因为程序员的疏忽导致内存泄漏,所以为了解决这个问题,发明了智能指针帮我们托管资源。

二.内存泄漏

对于内存泄漏问题来说,一个普通的程序员在庞大的项目中,很难做到对每一块申请的内存资源都做到合理的释放,所以需要用到智能指针帮助我们托管资源,即使程序员记得将资源delete掉了,也有可能因为一些其他的因素导致资源没有释放。

例如下面这种情况,即使在代码中,主动的进行了delete,也有可能因为其他的原因导致内存泄漏。

我们来看一下例子。

#include <iostream>
using namespace std;

void Div()
{
	int x, y;
	cin >> x >> y;
	if (y == 0)
		throw invalid_argument("除数为0");
	else
		cout << x / y << endl;
}

void Func()
{
	int* a = new int(10);
	Div();
	delete a;
}

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

在上面这个程序中,在Func函数里面,我们new出来了新空间,也进行了delete操作,正常情况下,程序会像我们预想的那样,正确的将资源释放掉。

但是当在Div函数里面进行除0操作的时候,会直接抛异常,导致程序运行直接跳到catch那行进行捕获错误,从而没有运行delete导致内存泄漏。 


所以对于内存泄漏问题来说,我们很难很好的真正做到管理好资源,所以这个时候,我们需要用到智能指针。 

三.智能指针的使用以及原理 

1.RAII技术

RAII技术是一种利用对象生命周期控制程序资源

在对象构造时获取资源,保证对象在整个生命周期内都是有效的。

在对象析构时释放资源,保证对象生命周期结束后,资源一定能释放。

这种资源的管理释放就是将资源托管给一个对象,这个对象就是智能指针对象

RAII的好处是,我们可以不需要显示的释放资源,而且资源在生命周期内始终有效。

 2.智能指针原理

基于上面的原理,我们可以来试着实现一下。

#include <iostream>
using namespace std;

//智能指针类
template<class T>
class SmartPtr
{
public:
	//构造函数
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	//析构函数
	~SmartPtr()
	{
		delete _ptr;
		cout << "资源已被释放" << endl;
	}
private:
	T* _ptr;//一个指针指向new出来的空间
};

void Div()
{
	int x, y;
	cin >> x >> y;
	if (y == 0)
		throw invalid_argument("除数为0");
	else
		cout << x / y << endl;
}

void Func()
{
	SmartPtr<int> sp(new int(10));//将new出来的空间的地址传参进去
	Div();
}

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

运行结果: 

在上面的代码中,我们定义了一个SmartPtr类来管理我们的资源。

当我们new一个int的时候,将这块内存托管给SmartPtr类对象,让这个对象帮助我们托管资源,当这个SmartPtr类对象生命周期到了的时候,就会自动的去调用它的析构函数,从而帮助我们释放资源,即使在这个代码中Div函数除0了,导致直接跳到catch捕获错误,这个sp对象也会因为出了作用域(即Func函数),从而帮助我们释放资源。


这就是智能指针的原理,但是这个SmartPtr类智能指针是很不完善的,虽然它可以帮助我们管理资源,但是它没有指针的原生行为,即*解引用操作以及->解引用操作

所以我们来实现一些原生指针的行为。

#include <iostream>
using namespace std;

template<class T>
class SmartPtr
{
public:
	//构造函数
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	//析构函数
	~SmartPtr()
	{
		delete _ptr;
		cout << "资源已被释放" << endl;
	}

	//实现可以模拟指针的原生行为
	//重载operator*
	T& operator*()
	{
		return *_ptr;
	}
	//重载operator->
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

struct Data
{
	Data(int year,int month,int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	int _year;
	int _month;
	int _day;
};

int main()
{
	SmartPtr<Data> sp(new Data(2024,12,28));
	cout << (*sp)._year << endl;
	cout << sp->_month << endl;
	return 0;
}

 SmartPtr的缺陷

 这个时候我们的SmartPtr至少能像原生指针一样去使用。

但是,这个SmartPtr还是很有问题!!!

如果我去调用这个SmartPtr的拷贝构造会出现什么问题呢?

如下面的代码所示:

#include <iostream>
using namespace std;

template<class T>
class SmartPtr
{
public:
	//构造函数
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	//析构函数
	~SmartPtr()
	{
		delete _ptr;
		cout << "资源已被释放" << endl;
	}

	//实现可以模拟指针的原生行为
	//重载operator*
	T& operator*()
	{
		return *_ptr;
	}
	//重载operator->
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

struct Data
{
	Data(int year,int month,int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	int _year;
	int _month;
	int _day;
};

int main()
{
	SmartPtr<Data> sp(new Data(2024, 12, 28));
	SmartPtr<Data> sp1(sp);//这里进行拷贝构造
	cout << (*sp)._year << endl;
	cout << sp->_month << endl;
	return 0;
}

 运行结果:


这是什么原因呢?

当我们进行拷贝构造的时候,就会有两个智能指针指向同一块资源,即sp和sp1指向同一块内存资源,当它们的生命周期结束的时候,会去调用它们两个的构造函数导致同一块资源被释放了两次。 

所以对于智能指针的一系列问题,从而诞生了多种智能指针:

auto_ptrunique_ptrshared_ptrweak_ptr

接下来我将逐一解析。 

 3.auto_ptr智能指针

auto_ptr是C++98提供的一种智能指针,所以下面来简单的实现一下auto_ptr,以及演示auto_ptr的问题。


auto_ptr的实现原理是:管理权转移思想。 

	template<class T>
	class auto_ptr
	{
	public:
		//构造函数
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		//析构函数
		~auto_ptr()
		{
			if (_ptr != nullptr)
			{
				delete _ptr;
				std::cout << "auto_ptr所指向资源已被释放" << std::endl;
			}
		}

		//拷贝构造,将原来的auto_ptr所管理的资源转移给新的auto_ptr对象
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		//operator=重载
		auto_ptr<T>& operator=(auto_ptr<T>& sp)
		{
			if (this != &sp)//防止自己给自己赋值
			{
				if (_ptr != nullptr)//如果_ptr有指向的资源,要先将_ptr所指向的资源释放掉,否则会出现内存泄漏
				{
					delete _ptr;
				}
                //转移资源
				_ptr = sp._ptr;
				sp._ptr = nullptr;
			}
			return *this;
		}

		//实现可以模拟指针的原生行为
		//重载operator*
		T& operator*()
		{
			return *_ptr;
		}
		//重载operator->
		T* operator->()
		{
			return _ptr;
		}	
	private:
		T* _ptr;
	};

如上面代码所示,auto_ptr的实现是基于管理权转移的思想,当进行拷贝构造operator=重载的时候,会将资源的管理权转移出去,如下图所示: 


对于这种做法,auto_ptr的设计是很失败的,因为当进行拷贝构造或者operator=赋值后,原来的sp会指向nullptr,这就很容易引发对空指针访问的问题,如下面代码所示: 

同时这样的做法非常的不好用。

4.unique_ptr智能指针 

所以为了解决auto_ptr所遗留的问题,设计出了unique_ptr,暴力的进行防拷贝和防operator=运算符重载。 

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

		~unique_ptr()
		{
			if (_ptr != nullptr)
			{
				delete _ptr;
				std::cout << "unique_ptr所指向资源已被释放" << std::endl;
			}
		}

		//非常简单粗暴的将拷贝构造和operator=重载函数删除掉
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

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

对于unique_ptr的实现,很简单,直接将拷贝构造和operator=重载delete掉即可,这样就可以解决auto_ptr的问题。 

5.shared_ptr智能指针

对于unique_ptr来说,虽然解决了auto_ptr的问题,但是这导致unique_ptr也有一定的缺陷。

因为unique_ptr所管理的资源是unique_ptr独占的,别的智能指针不能再管理这块资源,导致这块资源不能被共享使用,使得在操作这块内存上很不方便。 


所以为了解决这个问题,又发明出了shared_ptr。

shared_ptr在unique_ptr的基础上增加了引用计数,当一块资源被多个shared_ptr管理的时候,shared_ptr里面的引用计数会记录着有几个shared_ptr管理着这块资源。

当引用计数为0的时候,才会去释放资源。

如下图所示: 

代码实现: 

	template<class T>
	class shared_ptr
	{
		//成员变量
	private:
		T* _ptr;
		int* _pRefCount;//记录引用计数
		std::mutex* _pmtx;//需要锁,是因为操作_pRefCount的时候,需要保证是线程安全的

	public:
		//构造函数
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pRefCount(new int(1))
			, _pmtx(new std::mutex)
		{
			if (_ptr == nullptr)//防止拿nullptr构造智能指针
			{
				*_pRefCount = 0;
			}
		}

		//析构函数
		~shared_ptr()
		{
			Release();
		}

		//拷贝构造
		shared_ptr(shared_ptr<T>& sp)
		{
			std::unique_lock<std::mutex> lock(*sp._pmtx);//加锁保护
			_ptr = sp._ptr;
			_pRefCount = sp._pRefCount;
			_pmtx = sp._pmtx;
			if (_ptr != nullptr)
			{
				(*_pRefCount)++;
			}
		}

		//operator=重载
		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			if (this != &sp)//防止自己给自己赋值
			{
				//先释放原来所指向的资源
				Release();
				{
					std::unique_lock<std::mutex> lock(*sp._pmtx);
					_ptr = sp._ptr;
					_pRefCount = sp._pRefCount;
					_pmtx = sp._pmtx;
					if (_pRefCount != nullptr)
					{
						(*_pRefCount)++;
					}
				}
			}
			return *this;
		}

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

		int use_count()
		{
			return *_pRefCount;
		}

		T* get() const
		{
			return _ptr;
		}
	private:
		//释放资源
		void Release()
		{
			if (_pmtx != nullptr)
			{
				_pmtx->lock();
				bool flag = false;//辅助判断是否需要delete掉_pmtx
				if (_pRefCount != nullptr && --(*_pRefCount) == 0)
				{
					std::cout << "shared_ptr所指向的资源已被释放" << std::endl;
					delete _ptr;
					delete _pRefCount;
					flag = true;
					//delete _pmtx;这里不能直接delete,是因为锁还在使用
				}
				_pmtx->unlock();
				if (flag == true)
				{
					delete _pmtx;
				}
			}
			_ptr = nullptr;
			_pRefCount = nullptr;
			_pmtx = nullptr;
		}
	};

在shared_ptr的实现中,最重要的是加锁,因为要保证是线程安全的。 

shared_ptr的缺陷 

尽管shared_ptr解决了unique_ptr的问题,但是shared_ptr它也有自己的缺陷。 

就是循环引用的问题。 

我来写一个代码演示一下。

struct Node
{
	Node(int val = 0)
		:_val(val)
		,_next(nullptr)
		,_prev(nullptr)
	{}

	int _val;
	shared_ptr<Node> _next;
	shared_ptr<Node> _prev;
};

void test5()
{
	shared_ptr<Node> sp1(new Node(10));
	shared_ptr<Node> sp2(new Node(20));

	sp1->_next = sp2;
	sp2->_prev = sp1;
}

int main()
{
	test5();
	return 0;
}

 运行结果:

从运行结果可以看到,程序运行结束后,sp1和sp2所指向的空间并没有释放。

那是因为这两句代码造成的:

    sp1->_next = sp2;
    sp2->_prev = sp1;

如果没有这两句代码,资源还是会正常的释放,那是因为什么原因造成的呢? 


我们先来分析一下为什么会这样。

我们先来看一下图,当new出来了空间后是怎样的,以及开始连接结点后又是怎样的。

这里就会出现循环引用的问题sp1里面管理着sp2的内容,而sp2又管理着sp1的内容,出现了循环,导致出现问题。

当析构的时候,sp1和sp2的_count都只会变成1,导致资源没有释放。

6.weak_ptr智能指针

所以为了解决shared_ptr的循环引用的问题,又诞生出了wear_ptr(弱指针)。

这个weak_ptr是没有引用计数的,而且它的构造函数是由shared_ptr来构造的,同时weak_ptr不支持*重载和->重载,而且weak_ptr不具备RAII的功能,它仅仅是用来辅助shared_ptr来解决循环引用问题而已。

实现如下:

	template<class T>
	class weak_ptr
	{
	public:
		//构造函数
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		//operator=重载
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}

	private:
		T* _ptr;
	};

当weak_ptr实现后,我们再来修改一下之前有问题的代码,如下面代码所示: 

struct Node
{
	Node(int val = 0)
		:_val(val)
		,_next(nullptr)
		,_prev(nullptr)
	{}

	int _val;
	weak_ptr<Node> _next;
	weak_ptr<Node> _prev;
};

void test5()
{
	shared_ptr<Node> sp1(new Node(10));
	shared_ptr<Node> sp2(new Node(20));

	sp1->_next = sp2;
	sp2->_prev = sp1;
}

int main()
{
	test5();
	return 0;
}

 运行结果:

这个时候,我们的资源就能被正常的释放了。 

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

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

相关文章

【微服务】【Sentinel】认识Sentinel

文章目录 1. 雪崩问题2. 解决方案3. 服务保护技术对比4. 安装 Sentinel4.1 启动控制台4.2 客户端接入控制台 参考资料: 1. 雪崩问题 微服务调用链路中的某个服务故障&#xff0c;引起整个链路中的所有微服务都不可用&#xff0c;这就是雪崩。动图演示&#xff1a; 在微服务系统…

macos 支持外接高分辩率显示器开源控制软件

macos 支持外接高分辩率显示器开源控制软件 软件&#xff08;app应用&#xff09;名&#xff1a;BetterDisplay 官方地址&#xff1a; https://github.com/waydabber/BetterDisplay

JVM实战—7.如何模拟GC场景并阅读GC日志

大纲 1.动手模拟出频繁Young GC的场景 2.JVM的Young GC日志应该怎么看 3.代码模拟动态年龄判定规则进入老年代 4.代码模拟S区放不下部分进入老年代 5.JVM的Full GC日志应该怎么看 6.问题汇总 1.动手模拟出频繁Young GC的场景 (1)程序的JVM参数示范 (2)如何打印出JVM GC…

javaEE-文件操作和IO-文件

目录 一.什么是文件 1.文件就是硬盘(磁盘)上的文件。 2.计算机中存储数据的设备&#xff1a; 3.硬盘的物理特征 4.树型结构组织和⽬录 5.文件路径 文件路径有两种表示方式&#xff1a; 6.文件的分类 二、java中文件系统的操作 1.File类中的属性&#xff1a; 2.构造方…

使用 Docker 搭建 Hadoop 集群

1.1. 启用 WSL 与虚拟机平台 1.1.1. 启用功能 启用 WSL并使用 Moba 连接-CSDN博客 1.2 安装 Docker Desktop 最新版本链接&#xff1a;Docker Desktop: The #1 Containerization Tool for Developers | Docker 指定版本链接&#xff1a;Docker Desktop release notes | Do…

数据结构(系列)

在Python中&#xff0c;列表&#xff08;list&#xff09;是一种基本的数据结构&#xff0c;它可以存储一系列的元素。列表是可变的&#xff0c;即可以对其进行增删改查操作。 栈&#xff08;Stack&#xff09;是一种具有特定限制的线性数据结构&#xff0c;在栈中&#xff0c…

【Linux】HTTP cookie与session

在登录B站时&#xff0c;有登录和未登录两种状态&#xff0c; 问题&#xff1a;B站是如何认识我这个登录用户的&#xff1f;问题&#xff1a;HTTP是无状态、无连接的&#xff0c;怎么能够记住我&#xff1f; HTTP协议是无状态、无连接的。比如客户端&#xff08;浏览器&#…

Java - 日志体系_Simple Logging Facade for Java (SLF4J)日志门面_SLF4J集成logback 及 原理分析

文章目录 Pre官网集成步骤POM依赖使用第一步&#xff1a;编写 Logback 的配置文件第二步&#xff1a;在代码中使用 SLF4J 原理分析1. 获取对应的 ILoggerFactory2. 根据 ILoggerFactory 获取 Logger 实例3. 日志记录过程 小结 Pre Java - 日志体系_Apache Commons Logging&…

5.系统学习-PyTorch与多层感知机

PyTorch与多层感知机 前言PyTroch 简介张量&#xff08;Tensor&#xff09;张量创建张量的类型数据类型和 dtype 对应表张量的维度变换&#xff1a;张量的常用操作矩阵或张量计算 Dataset and DataLoaderPyTorch下逻辑回归与反向传播数据表格 DNN&#xff08;全连结网络&#x…

ubuntu 如何使用vrf

在Ubuntu或其他Linux系统中&#xff0c;您使用ip命令和sysctl命令配置的网络和内核参数通常是临时的&#xff0c;这意味着在系统重启后这些配置会丢失。为了将这些配置持久化&#xff0c;您需要采取一些额外的步骤。 对于ip命令配置的网络接口和路由&#xff0c;您可以将这些配…

Unity Shader TexelSize的意义

TexelSize在制作玻璃折射效果时会用到。 // Get the normal in tangent space fixed3 bump UnpackNormal(tex2D(_BumpMap, i.uv.zw)); // Compute the offset in tangent space float2 offset bump.xy * _Distortion * _RefractionTex_TexelSize.xy; i.scrPos.xy offset * i…

嵌入式硬件杂谈(七)IGBT MOS管 三极管应用场景与区别

引言&#xff1a;在现代嵌入式硬件设计中&#xff0c;开关元件作为电路中的重要组成部分&#xff0c;起着至关重要的作用。三种主要的开关元件——IGBT&#xff08;绝缘栅双极型晶体管&#xff09;、MOSFET&#xff08;金属氧化物半导体场效应晶体管&#xff09;和三极管&#…

【亲测有效】k8s分布式集群安装部署

1.实验环境准备 准备三台centos7虚拟机&#xff0c;用来部署k8s集群&#xff1a; master&#xff08;hadoop1&#xff0c;192.168.229.111&#xff09;配置&#xff1a; 操作系统&#xff1a;centos7.3以及更高版本都可以配置&#xff1a;4核cpu&#xff0c;4G内存&#xff…

【SQL server】教材数据库(5)

使用教材数据库&#xff08;1&#xff09;中的数据表完成以下题目&#xff1a; 1 根据上面基本表的信息定义视图显示每个学生姓名、应缴书费 2 观察基本表数据变化时&#xff0c;视图中数据的变化。 3利用视图&#xff0c;查询交费最高的学生。 1、create view 学生应缴费视…

去除el-tabs 下面的灰色横线,并修改每一项的左右间距,和字体颜色

HTML <el-tabs v-model"activeName" class"demo-tabs" tab-click"handleClick"><el-tab-pane label"全部" :name"null"></el-tab-pane><el-tab-pane label"问答陪练" name"general-t…

纯血鸿蒙ArkUI的网格布局详解

网格布局概要 网格布局是由行和列分割的单元格组成&#xff0c;通过指定项目所在的单元格做出各种各样的布局。网格布局具有较强的页面均分能力&#xff0c;子组件占比控制能力&#xff0c;是一种重要的自适应布局组件&#xff0c;其使用场景有九宫格图片展示、日历、计算器等…

LeRobot(1)

Train python lerobot/scripts/train.py \ policyact \ envaloha \ env.taskAlohaInsertion-v0 \ dataset_repo_idlerobot/aloha_sim_insertion_human \ load_data一直报错&#xff0c;忘记截图了&#xff0c;反正是ssh报错&#xff0c;下不下来&#xff0c;网…

【C++】B2085 第 n 小的质数

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;题目描述题目内容输入格式输出格式题目示例 &#x1f4af;原始做法分析解法源码解析1. 步骤分析2. 问题分析3. 性能问题 &#x1f4af;老师提供的优化解法优化代码每部分解…

Node.js详细安装教程

1.下载 [名称]&#xff1a;Node.js [大小]&#xff1a;26.4 MB [语言]&#xff1a;简体中文 [安装环境]&#xff1a;Win7/Win8/Win10 [Node.js15.11.0下载链接]&#xff1a; 通过网盘分享的文件&#xff1a;Node.js 链接: https://pan.baidu.com/s/12WlNlWlX-1ppdhz…

IDEA 搭建 SpringBoot 项目之配置 Maven

目录 1?配置 Maven 1.1?打开 settings.xml 文件1.2?配置本地仓库路径1.3?配置中央仓库路径1.4?配置 JDK 版本1.5?重新下载项目依赖 2?配置 idea 2.1?在启动页打开设置2.2?配置 Java Compiler2.3?配置 File Encodings2.4?配置 Maven2.5?配置 Auto Import2.6?配置 C…