【C++11】智能指针的使用以及模拟实现(shared_ptr,unique_ptr,auto_ptr,weak_ptr)

news2025/1/17 15:26:02

🌏博客主页: 主页
🔖系列专栏: C++
❤️感谢大家点赞👍收藏⭐评论✍️
😍期待与大家一起进步!


文章目录

  • 一、 RAII概念
  • 一、auto_ptr
    • 1.基本使用
    • 2.模拟实现
  • 二、unique_ptr
    • 1.基本使用
    • 2.模拟实现
  • 三、shared_ptr
    • 1.基本使用
    • 2.引用计数实现
    • 3.析构函数的升级(对于数组)
    • 4.循环引用(坑点)
    • 5.模拟实现
  • 四、weak_ptr


一、 RAII概念

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

我们下面所说的智能指针都是基于这种思想设计出来的。

一、auto_ptr

1.基本使用

很多公司明确规定不准用auto_ptr,因为其坑点太多了。

因为其会导致管理权的转移,会导致被拷贝对象悬空,对悬空对象进行操作的时候就会发生报错

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}

	~A()
	{
		cout << this;
		cout << " ~A()" << endl;
	}
	 
	int _a;
};
int main() {

	auto_ptr<A> ap1(new A(1));
	 

	// 管理权转移,拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象
	// 隐患:导致被拷贝对象悬空,访问就会出问题
	 auto_ptr<A> ap2(ap1);

	 // 崩溃
	ap1->_a++;
	ap2->_a++;


	return 0;
}

在这里插入图片描述

2.模拟实现

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

		// 像指针一样使用
		T& operator*() {
			return *_ptr;
		}
		T* operator->() {
			return  _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;
		}
	private:
		T* _ptr;
	};

二、unique_ptr

1.基本使用

unique_ptr这个智能指针对于auto_ptr的吐糟点解决的就非常简单粗暴,既然你拷贝与赋值都会造成那么多的问题,我直接把你的拷贝和赋值函数给禁用掉,当我们不需要拷贝的场景,建议使用这个智能指针
在这里插入图片描述
在这里插入图片描述

2.模拟实现

template<class T>
	class unique_ptr {
	public:
		unique_ptr(T*ptr)
			:_ptr(ptr)
		{}
		~unique_ptr() {
			delete _ptr;
		 }
		T& operator*() {
			return *_ptr;
		}
		T* operator->() {
			return _ptr;
		}
		 //直接把拷贝构造函数以及赋值运算符重载函数设置为删除函数
		unique_ptr(unique_ptr<T>& ap) = delete;
		unique_ptr<T>operator=(unique_ptr<T>& ap) = delete;
	private:
		T* _ptr;
		 
	};

三、shared_ptr

1.基本使用

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

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

在这里插入图片描述

2.引用计数实现

引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源

private:
		T* _ptr;
		int* _pcount;//引用计数计数器
		function<void (T*)>_del = [](T* ptr) {delete ptr; };//析构函数包装器
~shared_ptr() {
			//引用计数的计时器对于指向同一个地址的指针肯定要是公有的
			//所以我们选择使用对指针解引用的方式改变计数器的值
			//而且改变了对两个指向同一个地址的指针都有影响
			if (--(*_pcount) == 0) {
			//(*_pcount) == 0说明此时已经没有指针指向这个位置,可以释放了
				cout << "delete:" << endl;
				_del(_ptr);
				delete _pcount;
			}
		}

3.析构函数的升级(对于数组)

当我们遇到数组的情况怎么办?我们析构的话只是析构当前这一个位置,而数组是一连串的,这个时候我们如果还用原来的老方法设计析构函数就会发生内存泄漏的问题

C++库里面选择自己去实现一个删除方法的类并且作为仿函数传进去
在这里插入图片描述

这个时候又衍生一个问题,我删除方法D作为模板参数是在析构函数内的,只在析构函数内有效,而不是在像模板参数T那样在整个类里面都有效,我们该如何解决这个问题呢?

答案:我们可以选择自己在private里面的再加入一个私有成员_del作为删除方法

这个时候又有人会有疑惑,我_del该如何既兼容释放一个地址,又兼容释放一串地址呢?我们在外面写的删除函数不一定能传进去适配_del的类型

答案:这个时候我们之前介绍的function包装器就能派上用场了【C++11】function包装器,bind函数模板使用
我们可以用一个包装器来兼容多个调用对象的类型,这样我们就能传入自己写的仿函数了。

template<class D>
		shared_ptr(T* ptr, D del)
			: _ptr(ptr),
			_pcount(new int(1)),
			_del(del)
		{}
		private:
		T* _ptr;
		int* _pcount;//引用计数计数器
		function<void (T*)>_del = [](T* ptr) {delete ptr; };//析构函数包装器
		//默认为释放一个地址的lambda的方法,我们要删除数组时可以自己写方法传进去,
		//因为释放地址的函数类型返回值都为void类型



//当我们析构数组类型的时候可以设计的仿函数
template<class T>
struct DeleteArrayFunc {
 void operator()(T* ptr)
 { 
 cout << "delete[]" << ptr << endl;
 delete[] ptr; 
 }
};

4.循环引用(坑点)

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

  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动
    delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上
    一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev
    属于node2成员,所以这就叫循环引用,谁也不会释放。
    造成了类似死循环的场景

解决方案:
在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了

原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和
_prev不会增加node1和node2的引用计数。既然我靠引用计数是否为0来判断是否需要析构,那么我不增加引用计数的个数不就可以了吗?

5.模拟实现

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

		~shared_ptr() {
			//引用计数的计时器对于指向同一个地址的指针肯定要是公有的
			//所以我们选择使用对指针解引用的方式改变计数器的值
			//而且改变了对两个指向同一个地址的指针都有影响
			if (--(*_pcount) == 0) {
				cout << "delete:" << endl;
				_del(_ptr);
				delete _pcount;
			}
		}

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

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

		int use_count()const  {
			return *_pcount;
			//获得引用计数的数
		}

		T* get()const {
			return _ptr;
		}

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

	private:
		T* _ptr;
		int* _pcount;//引用计数计数器
		function<void (T*)>_del = [](T* ptr) {delete ptr; };//析构函数包装器
	};
	

四、weak_ptr

在这里先事先声明weak_ptr并不是RAII类型的指针,其设计的目的就是为了解决shared_ptr中的循环引用的问题。其不支持像其他智能指针的使用

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

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

		weak_ptr<T>&operator=(const shared_ptr<T>& sp) {
			_ptr = sp.get();
			return *this;
		}
		/*shared_ptr<ListNode> node1(new ListNode);
		shared_ptr<ListNode> node2(new ListNode);
	//这不设计是为了让shared_ptr类型能够赋值给weak_ptr类型
	//我们知道node类型为shared_ptr类型,而其prev与next为weak_ptr类型
		node1->_next = node2;
		node2->_prev = node1;*/

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

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

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

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

相关文章

如何禁止在堆上和栈上创建对象

背景 首先需要知道的是&#xff1a; 在栈新建一个对象&#xff0c;不需要调用operator new&#xff0c;创建栈对象时会移动栈顶指针以“挪出”适当大小的空间&#xff0c;然后在这个空间上直接调用对应的构造函数以形成一个栈对象。当对象在栈上创建时&#xff0c;虽然编译器…

Apache Dubbo的主要项目模块

Apache Dubbo的项目结构或者主要项目模块如下所示&#xff1a;

Web架构安全分析/http/URL/Cookie攻击

Web 架构安全分析 Web 工作机制及基本概念 传统 Web 架构 LAMP 网页 概念 网页就是我们可以通过浏览器上网看到的精美页面&#xff0c;一般都是经过浏览器渲染过的 .html 页面&#xff0c;html 语言在浏览器中渲染。其中包含了CSS、JavaScript 等前端技术。通过浏览器访问…

excel 日期与时间戳的相互转换

1、日期转时间戳&#xff1a;B1INT((A1-70*365-19)*86400-8*3600)*1000 2、时间戳转日期&#xff1a;A1TEXT((B1/10008*3600)/8640070*36519,"yyyy-mm-dd hh:mm:ss") 以上为精确到毫秒&#xff0c;只精确到秒不需要乘或除1000。 使用以上方法可以进行excel中日期…

07AC91 GJR5252300R0101 什么是机器人技术

07AC91 GJR5252300R0101 什么是机器人技术 什么是机器人技术&#xff1f; 机器人学是工程学的一个分支&#xff0c;研究机器人的概念、设计、构造、操作、应用和使用。再深入一点&#xff0c;我们看到机器人被定义为自动操作的机器&#xff0c;它独立地执行一系列动作&#x…

[计算机提升] 系统及用户操作

1.4 系统及用户操作 1.4.1 系统操作 1.4.1.1 开机、关机、重启 在Windows系统中&#xff0c;开机&#xff08;Power On&#xff09;&#xff0c;关机&#xff08;Shutdown&#xff09;和重启&#xff08;Restart&#xff09;是指计算机的不同电源控制操作。 开机&#xff1a;…

官媒代运营:2023年企业如何建立一个成功的品牌?

在这个数字化急速发展的时代&#xff0c;企业的成功已不再只依赖于产品质量、资本实力和市场声誉&#xff0c;还在于如何经营营销&#xff0c;以确保品牌能在竞争激烈的市场中脱颖而出。随着时间的推移&#xff0c;营销策略也在不断演变&#xff0c;因此&#xff0c;紧跟潮流变…

dvaJs在react 项目中的简单使用

官网&#xff1a;入门课 | DvaJS 备注&#xff1a;个人学习 代码示例&#xff1a; getColumns.js const getColumns [{title: 姓名, // 列标题dataIndex: name, // 数据字段名称&#xff0c;与数据中的字段名对应key: name, // 列的唯一键},{title: 年龄, // 列标题dataIn…

Ps:变形

Ps菜单&#xff1a;编辑/变换/变形 Edit/Transform/Warp 变形 Warp是自由变换的一种模式&#xff0c;不仅可以用于物体的伸缩扭曲&#xff0c;也可用于人体的局部塑形。 除了从菜单打开&#xff0c;通常情况下&#xff0c;按 Ctrl T 进入自由变换&#xff0c;然后在画面上右击…

stm32 cubeide 闪退 显示self upgrade failed

更新或安装新版cubeide时&#xff0c;可能会出现打开后一段时间直接闪退&#xff0c;显示如下图。此时怎么折腾cubeide都是没用的。应该去升级stm32 cubemx。记得打开cubemx时选择用管理员身份打开&#xff0c;升级完成后重新开打。然后尝试打开cubdeide&#xff0c;如果继续报…

钢铁异常分类140篇Trans 学习笔记 小陈读paper

钢铁异常分类 对比学习 比较好用 1.首先&#xff0c;为每个实例生成一对样本&#xff0c; 来自同一实例的样本被认为是正例&#xff0c; 来自不同实例的样本被认为是负例。 2.其次&#xff0c;这些样本被馈送到编码器以获得嵌入。 3.在对比损失[16]的影响下&#xff0c; …

一文了解 Amazon DataZone 使用指南

Amazon DataZone 现已正式发布。作为一项新的数据管理服务&#xff0c;它能够在组织中对数据生产者和消费者之间产生的数据进行编目、发现、分析、共享和管理。 通过 Amazon DataZone&#xff0c;数据生产者可使用 Amazon Glue 数据目录和 Amazon Redshift 表格中的结构化数据资…

如何优化模型渲染性能

1、提高模型渲染性能的好处 优化模型渲染性能有以下几个好处&#xff1a; 提高用户体验&#xff1a;良好的模型渲染性能可以使图形应用程序更加流畅和响应快速。当模型以较高的帧率渲染时&#xff0c;用户可以获得更流畅、更真实的视觉效果&#xff0c;提升整体的用户体验。 …

Jmeter性能测试(压力测试)

1.先保存 2.添加请求&#xff08;即添加一个线程组&#xff09; 3.添加取样器&#xff08;在线程组下面添加一个http请求&#xff09; 场景1&#xff1a;模拟半小时之内1000个用户访问服务器资源&#xff0c;要求平均响应时间在3000毫秒内&#xff0c;且错误率为0&#xff0…

BI工具-DataEase(2) 基础使用

先讲下工具栏,分别是仪表盘,数据集,数据源,模板市场等等. 和大多数的BI工具一样,首先配置的就是数据源 1. 数据源:支持OLTP,OLAP,数仓/数据湖,我们这边还是使用的mysql 新建mysql数据源,填写配置信息 2. 数据集支持Excel,SQL,关联数据集等等,新建SQL数据集,输出SQL语句 点击运…

软考-面向对象技术

面向对象的基本概念 对象&#xff1a;属性&#xff08;数据&#xff09; 方法&#xff08;操作&#xff09; 对象ID类&#xff08;实体类、控制类/边界类&#xff09;继承与泛化&#xff1a;复用机制封装&#xff1a;隐藏对象的属性和实现细节&#xff0c;仅对外公开接口多态&…

【JavaEE】Callable 接口

Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果. 实现Callable也是创建线程的一种方法&#xff01;&#xff01;&#xff01;&#xff01; Callable的用法非常接近于Runnable&#xff0c;Runnable描述了一个任务&#…

C语言中static关键字用法

C语言中static关键字用法 2021年7月28日席锦 static关键字在c语言中比较常用&#xff0c;使用恰当能够大大提高程序的模块化特性&#xff0c;有利于扩展和维护。但是对于c语言初学者&#xff0c;static由于使用灵活&#xff0c;并不容易掌握。 变量 1.局部变量 普通局部变量是再…

智能手表上的音频(二):驱动

上一篇讲了智能手表上音频系统的架构和应用场景。从本篇开始讲具体的&#xff0c;首先讲音频相关的驱动&#xff0c;主要包括IPC&#xff08;inter-processor communication&#xff0c;核间通信&#xff0c; 即AP/CP/ADSP之间的通信&#xff09;的driver 和audio的driver。首先…

讯飞星火3.0版发布前瞻,生产力将大幅提升

AI头部企业科大讯飞宣布&#xff0c;即将在10月24日发布讯飞星火认知大模型3.0版。新版本将极大提升生产力&#xff0c;并开启大规模AI应用产业落地。 一、讯飞星火大模型3.0版即将升级发布 科大讯飞是AI人工智能技术、智能语音识别技术领域的全球领先企业&#xff0c;也是大…