【C++】:智能指针 -- RAII思想shared_ptr剖析

news2024/12/22 22:23:29

目录

  • 一,内存泄漏
  • 二,智能指针的使用及原理
    • 2.1 RAII思想
    • 2.2 auto_ptr
    • 2.3 unique_ptr
  • 三,shared_ptr(重点)
    • 3.1 shared_ptr的原理及使用
    • 3.2 shared_ptr的模拟实现
      • 1. 基本框架
      • 2. 引用计数的设计
      • 3. 拷贝构造
      • 4. 析构函数
      • 5. 赋值拷贝
    • 3.3 shared_ptr的循环引用
    • 3.4 定制删除器
    • 3.5 shared_ptr实现的完整代码

点击跳转上一篇文章: 【C++】:错误处理机制 – 异常

一,内存泄漏

(1) 什么是内存泄漏

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

(2) 内存泄漏的危害

长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死

void MemoryLeaks()
{
   // 1.内存申请了忘记释放
  int* p1 = (int*)malloc(sizeof(int));
  int* p2 = new int;
  
  // 2.异常安全问题
  int* p3 = new int[10];
  
  // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
  Func(); 
  
  delete[] p3;
}

二,智能指针的使用及原理

2.1 RAII思想

RAII 是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

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

这种做法有两大好处

(1) 不需要显式地释放资源
(2) 采用这种方式,对象所需的资源在其生命期内始终保持有效

// 使用RAII思想设计的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;
};

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

void Func()
{
 	Shard_Ptr<int> sp1(new int);
    Shard_Ptr<int> sp2(new int);
 	cout << div() << endl;
}

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

总结一下智能指针的原理

(1) RAII特性
(2) 重载operator*和opertaor->,具有像指针一样的行为

2.2 auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针auto_ptr 支持拷贝,但是拷贝时,管理权限转移,会造成被拷贝指针悬空

使用方法如下

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

	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

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

int main()
{
	auto_ptr<Date> ap1(new Date);

	 //拷贝时,管理权限转移,被拷贝(ap1)指针悬空
	auto_ptr<Date> ap2(ap1);
	
	// 此时ap1为空指针了,访问直接报错!
	//ap1->_year++;

	return 0;
}

结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr

2.3 unique_ptr

C++11中开始提供更靠谱的unique_ptrunique_ptr的实现原理简单粗暴的防拷贝

使用方法如下

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

	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

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

int main()
{
	unique_ptr<Date> up1(new Date);
	
	//不支持拷贝
	//unique_ptr<Date> up2(up1);

	return 0;
}

三,shared_ptr(重点)

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

3.1 shared_ptr的原理及使用

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

原理实现的具体细节

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

使用方法如下

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

	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}

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

int main()
{
	shared_ptr<Date> sp1(new Date);
	shared_ptr<Date> sp2(sp1);
	shared_ptr<Date> sp3(sp2);

	return 0;
}

3.2 shared_ptr的模拟实现

1. 基本框架

namespace bit
{
	template<class T>
	class shared_ptr
	{
	public:
	
		// 构造,拷贝等其他接口.....
		
		T* get()const
		{
			return _ptr;
		}

		int use_count()const
		{
			return *_pcount;
		}

		// 重载运算符,模拟指针的行为
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr ;
		int* _pcount; //引用计数
		function<void(T* ptr)> _del = [](T* ptr) {delete ptr; };
		// function的默认构造中没有可调用对象,不给缺省值会报错
}

2. 引用计数的设计

每个资源都要配一个引用计数,是用来记录有多少个对象共同指向这块资源的。

不是每个对象都配一个计数,也不能直接使用static静态变量,这样所有对象都用一个计数了,显然不合理

所以我们要在堆上开一块空间保存计数,用一个指针指向这个计数,当每次有对象指向同一块空间时,就可以找到这个指针指向的计数++,析构时计数- -每次构造的时候就出现新资源,所以要在构造的时候申请

shared_ptr(T* ptr = nullptr)
	:_ptr(ptr)
	,_pcount(new int(1)) //每个资源给一个计数
{}

在这里插入图片描述

3. 拷贝构造

把一个对象拷贝给另一个对象,说明这个对象的资源与另一个对象共享了,计数++

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

4. 析构函数

如果引用计数到0,说明已经没有对象指向这块资源了,就要释放该资源

void release()
{
	if (--(*_pcount) == 0)
	{
		//delete _ptr;
		_del(_ptr);

		delete _pcount;
		_ptr = nullptr;
		_pcount = nullptr;
	}
}

~shared_ptr()
{
	release();
}

5. 赋值拷贝

赋值拷贝是已经存在的两个对象之间。所以赋值时要注意那个对象原先资源的处理,原先的计数要先- -。并且要注意避免自己给自己赋值

// sp1 = sp3
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
	// 避免自己给自己赋值。用资源的指针判断
	// 指向同一块资源就不白费赋值
	if (_ptr != sp._ptr)
	{
		release();

		_ptr = sp._ptr;
		_pcount = sp._pcount;
		(*_pcount)++;
	}

	return *this;
}

在这里插入图片描述

3.3 shared_ptr的循环引用

循环引用问题时一个巨坑,出现时必然会导致内存泄漏问题。

struct ListNode
{
	int _data;

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

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

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

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	return 0;
}

循环引用分析图解

在这里插入图片描述

解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是:weak_ptr不支持管理资源,不支持RAII。n1->_next = node2和n2->_prev = n1时,weak_ptr的_next和_prev不会增加n1和n2的引用计数

使用weak_ptr要包含头文件

#include <functional>
struct ListNode
{
	int _data;

	weak_ptr<ListNode> _next;
	weak_ptr<ListNode> _prev;

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

在我们自己的shared_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();

		return *this;
	}

3.4 定制删除器

如果不是new出来的对象如何通过智能指针管理呢其实shared_ptr设计了一个删除器来解决这个问题。(ps:删除器这个问题我们了解一下)

仿函数的删除器

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

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

int main()
{
	shared_ptr<Date[]> sp4(new Date[5]);
	shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose());

	return 0;
}

但是每次写仿函数还是有些麻烦,所以可以在shared_ptr的类中进行实现

//定制删除器
template<class D>
shared_ptr(T* ptr, D del)
	:_ptr(ptr)
	, _pcount(new int(1))
	,_del(del)
{}

3.5 shared_ptr实现的完整代码

shared_ptr.h

#pragma once
#include <functional>

namespace bit
{
	template<class T>
	class shared_ptr
	{
	public:
		// RAII
		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)
		{}

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

		// sp1 = sp3
		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			// 避免自己给自己赋值。用资源的指针判断
			// 指向同一块资源就不白费赋值
			if (_ptr != sp._ptr)
			{
				release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				(*_pcount)++;
			}

			return *this;
		}

		void release()
		{
			if (--(*_pcount) == 0)
			{
				//delete _ptr;
				_del(_ptr);

				delete _pcount;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}

		~shared_ptr()
		{
			release();
		}

		T* get()const
		{
			return _ptr;
		}

		int use_count()const
		{
			return *_pcount;
		}

		// 重载运算符,模拟指针的行为
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr ;
		int* _pcount; //引用计数

		function<void(T* ptr)> _del = [](T* ptr) {delete ptr; };
		// function的默认构造中没有可调用对象,不给缺省值会报错
	};

	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();

			return *this;
		}
	private:
		T* _ptr = nullptr;
	};
}

Test.cpp

int main()
{
	bit::shared_ptr<Date> sp1(new Date);
	bit::shared_ptr<Date> sp2(sp1);

	bit::shared_ptr<Date> sp3(new Date);
	sp1 = sp3;

	//传了删除器,就用自己传的,没传就用缺省的
	bit::shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose());
	bit::shared_ptr<int> sp6((int*)malloc(40), [](int* ptr)
		{
			cout << "free:" << ptr << endl;
			free(ptr);
		});

	return 0;
}

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

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

相关文章

Vue 3+Vite+Eectron从入门到实战系列之(三)一Electron热身运动(一)

前面我们已经把基础环境配置好了,在开始我们编写第一个页面之前,先尝试几个小的实验,体验下 electron 的乐趣。 更改我们应用的名称 系统默认的名字是从 package.json 中读取的,我们可以在这里更改。 {"name": "electron-vue3" }更改后,我们重新启动…

解决pycharm日志总是弹出“无法运行Git,未安装Git”的问题

需求分析 我电脑中安装了git&#xff0c;但是打开pycharm&#xff0c;右下角总是弹出 无法运行Git,未安装Git的日志。 解决方法 首先打开pycharm&#xff0c;按照以下路径&#xff0c;依次点击。 file -----settings-----version control -----Git----Git path(选择自己下载…

【Matplotlib】在 ax(Axes 对象)上使用 seaborn(简称 sns)绘图

在 ax&#xff08;Axes 对象&#xff09;上使用 seaborn&#xff08;简称 sns&#xff09;绘图时&#xff0c;你可以通过将 ax 作为参数传递给 seaborn 的绘图函数。这允许你将 seaborn 的图形绘制在指定的 ax 对象上&#xff0c;从而将多个图形组合在一个图形布局中。 示例代…

超高速直线模组究竟有多快?飞创直线模组最快速度是多少?

超高速直线模组的速度范围从每秒几毫米到每秒几十米&#xff0c;影响速度的因素包括电磁设计、冷却和机械结构等。超高速直线模组的速度也会因品牌、型号以及具体应用场景等因素而有所不同。 飞创直线模组的速度较快&#xff0c;最大速度可达10m/s&#xff0c;不同规格的直线模…

什么是护网?2024护网行动怎么参加?一文详解_护网具体是做啥的

前言 最近的全国护网可谓是正在火热的进行中&#xff0c;有很多网安小白以及准大一网安的同学在后台问我&#xff0c;到底什么是护网啊&#xff1f;怎么参加呢&#xff1f;有没有相关的学习资料呢&#xff1f;在下不才&#xff0c;连夜整理出来了这篇护网详解文章&#xff0c;希…

JavaWeb系列十一: Web 开发会话技术Cookie

会话技术-Cookie 基本介绍会话的两种技术cookie有什么用?cookie介绍二说cookiecookie可以用来干啥cookie常用方法cookie底层实现机制-创建和读取cookie应用实例-读取指定cookie和修改cookiecookie生命周期介绍应用实例 cookie有效路径有效路径规则应用实例作业布置cookie注意事…

日本求职面试时的注意事项、面试职场礼仪!

毋庸置疑的第一点就是—简历&#xff01; 接到面试通知&#xff0c;就应该已经通过简历筛选的环节了。但也有些公司会要求面试时带上“履历书”和“职务经历书”。这时候也要好好查看简历上的日期、住址、电话等信息有没有误&#xff1b;以往职业经历有没有写清晰&#xff1b;…

批量修改文件名神器-文件名精灵

最近在网上发现一个批量修改文件名神器&#xff0c;这个软件叫“文件名精灵”&#xff0c;好用还免费。下载地址为

OJ-0807

题目 参考 import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner in new Scanner(System.in);String input in.nextLine();String[] numStrs inp…

关于百度、微软语音合成的实现案例

关键词 自助机产品、排队呼叫功能、网络喇叭、百度语音合成SDK、微软TTS 阅读建议 对自助机产品功能扩展感兴趣的读者、需要实现远程语音呼叫功能的开发者、想要了解网络喇叭选型及其使用的技术人员、对百度语音合成SDK和微软TTS感兴趣的开发者 阅读时长 预计阅读时长&#xf…

智谱AI正式开源CogVideoX:视频生成技术的新里程碑

前沿科技速递&#x1f680; 随着大型模型技术的持续发展&#xff0c;视频生成技术正在逐步走向成熟。智谱AI团队宣布开源其创新的视频生成模型CogVideoX系列&#xff0c;这标志着视频内容创作的一个新里程碑。通过此次开源&#xff0c;智谱AI旨在让每一位开发者、每一家企业都能…

spring+SSM+Mybatis面试题(上)(30道)

目录 1. 何为Spring Bean容器?Spring Bean容器与Spring IOC 容器有什么不同吗?2. Spring IOC 如何理解?3. Spring DI 如何理解?4. Spring 中基于注解如何配置对象作用域?以及如何配置延迟加载机制?1.配置作用域需要注解Scope(“Singleton”)2.开启延迟加载&#xff1a;La…

AWS云账号注销还能重新注册吗

注销了AWS云账号后&#xff0c;是否还能重新注册&#xff0c;这是许多用户关心的问题。今天九河云和大家来详细探讨一下这个话题。 重新注册AWS账号的可能性 一旦你注销了AWS账号&#xff0c;意味着你已经彻底删除了该账户及其所有关联的资源和数据。因此&#xff0c;注销的A…

基于ENVI遥感解译的区域生态环境评价分析

原文链接&#xff1a;基于ENVI遥感解译的区域生态环境评价案例分析https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247611979&idx4&sn9239197610eff7d5ef75a625dcbb5315&chksmfa8277accdf5febaf916e044bf9f0fb23c31ca92ca2a3086ecf435f94a1503fcd59085f…

Mysql 脚本转换为drawio ER 脚本

Navicat 导出数据库脚本 通过代码转换脚本 import java.io.BufferedReader; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern;/*** SQL 脚本转换为 drawio ER 脚本*/ pu…

ArkTS和TypeScript区别

一、对象字面量须标注类型 const point {x: 100,y: 100 }console.log(point) 运行之后会输出 { x: 100, y: 100 } 以上TS代码片段展示了没有类型的场景。如果编译器不知道变量point的确切类型&#xff0c;由于对象布局不能确定&#xff0c;编译器无法深度地优化这段代码&am…

gradle全局配置

搭建spring boot3.x开发环境的先决条件&#xff0c;咱们已经完成了JDK17安装与配置&#xff0c;然后就是项目构建工具&#xff0c;我们使用现在很流行的gradle&#xff0c;进行下全局配置&#xff0c;以方便和加速后续spring boot项目构建。 配置一个GRADLE_USER_HOME环境变量&…

【密码学】密码协议的安全性

密码协议是用来在不安全的网络环境中建立安全通信通道的方法。虽然密码协议中仅有很少的几组消息传输&#xff0c;但其中每条消息的组成都是经过巧妙的设计&#xff0c;而这些协议之间有着复杂的相互作用和制约。 若如果协议涉及上出现漏洞&#xff0c;那么协议将存在验证的安全…

【JAVA多线程】AQS,JAVA并发包的核心

目录 1.概述 1.1.什么是AQS 1.2.AQS和BlockQueue的区别 1.3.AQS的结构 2.源码分析 2.1.CLH队列 2.2.模板方法的实现 2.2.1.独占模式 1.获取资源 2.释放资源 2.2.2.共享模式 1.概述 1.1.什么是AQS AQS非常非常重要&#xff0c;可以说是JAVA并发包&#xff08;java.…