C++——探索智能指针的设计原理

news2024/12/22 22:03:37

        前言: RAII是资源获得即初始化, 是一种利用对象生命周期来控制程序资源地手段。 智能指针是在对象构造时获取资源, 并且在对象的声明周期内控制资源, 最后在对象析构的时候释放资源。注意, 本篇文章参考——C++ 智能指针 - 全部用法详解-CSDN博客

        看完博主的文章的友友们, 可以去看一下该篇文章, 该作者写的比博主通俗易懂。

目录

为什么需要智能指针

智能指针

auto_ptr

auto_ptr的用法:

auto_ptr的模拟实现:

unique_ptr

unique_ptr的用法

unique_ptr的模拟实现

shared_ptr

shared_ptr的用法

 shared_ptr的模拟实现(v1版本)

weak_ptr

weak_ptr的用法:

weak_ptr的模拟实现

shared_ptr中的定制删除器

智能指针坑点 


为什么需要智能指针

        首先我们来看一下这一个简单的程序:


void func() 
{
	int* p = new int;
}

int main() 
{
	func();

	return 0;
}

        在这个程序里面, func中定义了一个指向堆区一块空间的p。 但是当出了作用域后, p指针就被销毁了, 但是p指针指向的空间没有被销毁,这个时候就发生了内存泄漏。

        另外一种情况就是我们虽然手动释放了内存, 但是中途发生了异常, 程序发生跳转, 手动释放内存被截胡了。 也会导致发生内存泄漏。

void test() 
{
	int* ptr = new int;

	if (1) 
	{
		throw "发生异常";     //这里发生截胡, 无法走到下一行。
	}

	delete ptr;          //这里没有释放资源
}


int main() 
{
	try 
	{
		test();
	}
	catch (const char* str)
	{
		cout << str << endl;
	}
	catch (...) 
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

而只能指针就是为了这种情况设计出来的。也就是说, 智能指针就是为了我们能够方便管理动态内存分配的资源, 它能够在对象的声明周期结束时自动释放这些资源。

        如图为一个简单的智能指针

在这个智能指针当中, 当我们创建对象时,可以使用一块资源初始化。 然后这块资源就会在这个对象的生命周期结束时自动销毁。

        这就是智能指针的基本原理, 虽然我们使用指针时, 指针指向的空间不会被自动释放。但是对象在生命周期结束时会自动释放, 所以我们把指针指向的资源放到对象里, 让对象在释放自身的时候将资源一起释放掉。

智能指针

       

现在有三个智能指针的解决方案:

  •         auto_ptr               C++98
  •         unique_ptr           C++11
  •         share_ptr             C++11

另外, 还有一个用来解除share_ptr中的循环引用问题的解决方案。

  •         wake_ptr               C++11

auto_ptr

auto_ptr的用法:

        使用智能指针需要包含头文件memory, 具体使用方法如下:

#include<memory>             //只用智能指针需要包含memory头文件

int main() 
{
	auto_ptr<int> p(new int);                  //利用auto_ptr创建一个管理int指针资源的对象
	auto_ptr<list<int>> pl(new list<int>);     //利用auto_ptr创建一个管理list<int>类型的指针资源的对象

	*p = 4;                    //auto_ptr<int>类型也能进行解引用操作
	(*pl).push_back(16);       //容器的指针, 解引用后就是容器本身。
	(*pl).push_back(15);
	(*pl).push_back(14);
	(*pl).push_back(13);
	(*pl).push_back(12);
	
	cout << *p << endl;                   //打印*p
	auto it = (*pl).begin();              //pl解引用获得list<int>对象, 可以像使用指针
	while (it != (*pl).end()) 
	{
		cout << *it << endl;
		++it;
	}

	return 0;
}

        这里创建智能指针对象是: auto_ptr<类型名> p(new 类型名)  , 这里创建的时候不能使用 ’ = ‘, 只能使用 ' ( ) ';

        auto_ptr是在C++98创建出来的, 但是这个智能指针在之后很少被人用。 因为它有一个弊端, 就是当进行拷贝的时候, 该智能指针管理的资源会被 ”抛弃“, 另一个智能指针进行接收。 也就是如图:

        这个模式存在一些弊端。如果我们使用一个容器进行插入操作的时候,插入操作一定会赋值。 那么赋值就会导致原本智能指针对象中的资源被转移。

        另外, auto_ptr的另一个弊端就是auto_ptr不支持对象数组的操作。所以在C++11出现更好的unique_ptr和share_ptr后,auto_ptr已经很少被使用。 

        auto_ptr有三个常用接口。 get, release, reset。

三个函数的主要功能

1. get()

        get是获取对象中管理的资源:

2.release

        release, 是取消对象中管理的资源:

3.reset

        reset, 是重新分配对象中管理的资源:

auto_ptr的模拟实现:

//首先, 对于智能指针来说,他们的模板类名是这样的:

	template<class T>       //模板类, 可以接收T类型的资源
	class auto_ptr          //智能指针类名
	{
        
	};

//然后, 在类里面定义资源类型的指针, 用来维护这块资源: 

	template<class T>
	class auto_ptr
	{
	private:
		T* _ptr;          //T类型资源的指针, 用来维护一块资源
	};

//所有智能指针的构造都是一样的, 就是使用一块资源交给智能指针里面的指针变量进行维护。

		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

 //auto_ptr的拷贝构造, 其实就是把一个智能指针管理的资源 “抛弃”,然后另一个智能指针进行接收。 至于这样做的弊端, 上面已经提到过, 这里不赘述。

		auto_ptr(auto_ptr<T>& ptr)
			:_ptr(ptr._ptr) 
		{
			ptr._ptr = nullptr;
		}

//最重要的就是智能指针的销毁, 销毁时, 要将管理的资源一块释放掉, 代码如下:

		~auto_ptr() 
		{
			delete _ptr;
			_ptr = nullptr;
		}

//然后智能指针还要像普通指针一样能够进行基本运算——加加, 减减, 解引用等。那么就要重载这些运算符, 如下图:

		T& operator*()           //解引用
		{
			return *_ptr;
		}

		T* operator->()          //箭头
		{
			return _ptr;
		}

		auto_ptr<T> operator++() //加加重载
		{
			++_ptr;
			return *this;
		}

unique_ptr

unique_ptr的用法

unique_ptr相交于auto_ptr更加严谨, 它相对于auto_ptr做了一下改变:
        两块指针不能指向同一块资源(否则在释放空间时多次释放空间报错)。 同时它也不能赋值(注意, 右值可以赋值,但是右值赋值后, 如果该右值为一个左值临时转化的, 那么使用赋值后和auto_ptr的效果一样)

左值赋值:

 右值赋值:

要注意的坑点就是不能一块资源给多个对象赋值, 不然会报错:

auto_ptr也一样

这是unique_ptr和auto_ptr中使用的坑。 后面的share_ptr解决了这个坑点(引用计数)

unique_ptr的模拟实现

有前面auto_ptr的基础, 这里unique_ptr的细节不再讲解, 只讲解重要的部分:

//首先模板类:

	template<class T>
	class unique_ptr 
	{

	private:
		T* _ptr;      //管理资源
	};

//然后就是主要的地方 , unique_ptr不能进行赋值, 所以要将拷贝构造和赋值重载封起来。

	template<class T>
	class unique_ptr 
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr) 
		{}
	
	private:
		unique_ptr(unique_ptr<T>& ) = delete;                    //将拷贝构造删掉
		unique_ptr<T>& operator=(unique_ptr<T>& ) = delete;      //将赋值重载删掉

	private:
		T* _ptr;
	};

shared_ptr

shared_ptr的用法

shared_ptr解决了unique_ptr和auto_ptr的排他性, shared_ptr即使多个智能指针指向同一块空间也能正常工作。 

        shared_ptr采用了引用计数, 当一个新的share_ptr指针管理一块资源的时候, 引用计数就+1, 当一个shared_ptr指针过期时, 引用计数就-1。当一块资源的引用计数到0时, 这块资源就可以被释放。

1. use_count() : 获得当前资源的引用计数:

2.conductor: 有多种构造形式——直接赋值一块资源、 赋值一块数组资源、传送定制删除器。

定义如下:

//带有定制删除器

template<class U, class D>

shared_ptr(U* p, D del)

//普通构造 

template<class U>

shared_ptr(U* p)

//数组构造

shared_ptr<T[]> p(new T[5]{1, 2, 3, 4, 5});      //据说是C++17后支持, 但是vsC++14也能跑

3.make_shared

make_shared可以用来分配一块空间并且初始化这块空间, 效率更加高效。make_shared是一个函数模板, 并且需要指定分配资源的类型, 如图:

 shared_ptr的模拟实现(v1版本)

首先,类名, 解引用之类和其他智能指针相差不大, 这里不做赘述, 然后有区别的就是成员变量以及构造, 拷贝, 赋值。 

        首先看成员变量, 成员变量需要有一块空间来作为引用计数。

	template<class T>
	class shared_ptr 
	{

	private:
		T* _ptr;
		int* _pcount;
	};
	

*_pcount作为引用计数。

//构造

		//引用计数ptr
		shared_ptr(T* ptr)
			:_ptr(ptr) 
		{
			*_pcount = 1;
		}

//拷贝构造

		//引用计数地拷贝构造
		shared_ptr(shared_ptr<T>& ptr) 
			:_ptr(ptr._ptr)
			,_pcount(ptr._pcount)
		{
			++(*_pcount);
		}

//析构

		~shared_ptr() 
		{
			if (--(*_pcount) == 0) 
			{
				delete _ptr;
				delete _pcount;
			}
		}

赋值, 这里赋值要先将原本的管理的资源取消托管, 那么引用计数就要减一, 还要判断引用计数是否为0, 为0就要释放资源。

		shared_ptr<T>& operator=(shared_ptr<T>& p) 
		{
			if (_ptr != p._ptr)          //如果不判断, 当自己给自己赋值的时候, 自己会先将自己的资源释放, 然后就变成了野指针。 自己再给自己赋值一个野指针。 就会报错。
			{
				if (--(*_pcount) == 0)
				{
					delete _ptr;
					delete _pcount;
				}
				//
				_ptr = p._ptr;
				_pcount = p._pcount;
				++(*_pcount);
			}

			return *this;
		}

shared_ptr中的坑

        1. 其实, shared_ptr还有一种情况同样具有排他性, 和unique_ptr、auto_ptr一样, 当没有调用拷贝构造, 而是直接使用构造函数的时候, 引用计数不回加一, 那么就会多次释放资源。 这个无法避免。

        2.第二个坑就是循环引用的问题,为了方便观察, 我们使用我们自己定义的shared_ptr进行测试, 现在看如下一个例子:

#include"share_ptr.h"      //自己写的shared_ptr头文件
struct chicken;            //前置声明

struct fish                //定义一个鱼类对象, 里面有一个鸡的智能指针实例
{
	cws_RAII::shared_ptr<chicken> _chicken;
};

struct chicken              //定义一个鸡类对象, 里面有一个鱼的智能指针实例。
{
	cws_RAII::shared_ptr<fish> _fish;                           
};



int main()
{
	cws_RAII::shared_ptr<fish> f1(new fish);
	cws_RAII::shared_ptr<chicken> c1(new chicken);

	(*f1)._chicken = c1;
	//(*c1)._fish = f1;

	return 0;
}

 在当前状态下, 我们如果运行程序, f1的资源和c1的资源可以被释放

但是如过图中(*c1)._fish = f1取消注释, 那么f1的资源和c1的资源就不能被释放。 

这是为什么?

其实, 这就是循环引用的问题。 在这里面, 如果只定义了f1和c1. 这个时候是这样的:

但是如果给执行两条赋值语句后, 就变成了这样:

 这个时候当f1和c1的生命周期结束时, f1的_pcount--, c1的_pcount--。 这两个_pcount都只能变成1, 变不成0, 所以不能释放资源。

        所以, 我们在使用shared_ptr时, 要避免交叉赋值的情况。否则会出现内存泄漏。

weak_ptr

weak_ptr是用来解决shared_ptr的循环引用问题。 当两个类需要交叉进行赋值的时候, 类中所定义的智能指针可以使用weak_ptr(原本使用shared_ptr), 因为weak_ptr不会增加资源的引用计数。 

weak_ptr的用法:

weak_ptr本人觉得最重要的一点就是可以和shared_ptr进行相互转化:

int main()
{
	shared_ptr<fish> f2(new fish);

	weak_ptr<fish> f3(f2);     //将一个shared_ptr给给f3
	f3 = f2;                   //将shared_ptr赋值给weak_ptr
	f2 = f3.lock();            //将一个weak_ptr使用lock接口转化为shared_ptr;                   
	return 0;
}

同时, 还可以构造出一个空指针:


int main()
{
	weak_ptr<fish> f3;     //构造一个空指针
                 
	return 0;
}

但是, wear_ptr不支持解引用以及->

weak_ptr也能使用use_count函数查看引用计数:

weak_ptr的模拟实现

	template<class T>
	class weak_ptr 
	{
	public:
		//不支持RAII, 也就是不能初始化管理资源
		weak_ptr(const shared_ptr<T>& ptr) 
		{
			_ptr = ptr._ptr;		//这里可以将weak_ptr弄成shared_ptr的友元, 就能访问私有_ptr
			_pcount = ptr._pcount;
		}
		
		weak_ptr<T>& operator=(const shared_ptr<T>& ptr) 
		{
			_ptr = ptr._ptr;
			_pcount = ptr._pcount;
			return *this;
		}

		int use_count() 
		{
			return (*_pcount);
		}

		T* get() 
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;
	};

shared_ptr中的定制删除器

        在shared_ptr中, 可以管理一块连续的数组空间, 也可以管理一个单独的一块空间。 这两种资源类型需要不同的销毁方法。 单独的使用delete, 而数组空间需要使用delete[], 库里面的默认是使用delete, 但是如果我们想使用delete[]来销毁一块数组空间。 或者我们使用shared_ptr管理一块文件, shared_ptr生命周期结束时关闭文件。 那么就需要我们自己传一个定制删除器。 

        定制删除器是放在如图所示红框框的代码块中:

        定制删除器的用法:

如何实现定制删除器?

其实定制删除器就是添加一个模板类, 如图:

 下面是shared_ptr完整的版本(v2)

	template<class T>
	class shared_ptr 
	{
	public:
		//引用计数ptr
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr) 
			,_pcount(new int(0))
		{
			if (_ptr != nullptr) 
			{
				*_pcount = 1;
			}
		}

		//引用计数地拷贝构造
		shared_ptr(shared_ptr<T>& ptr) 
			:_ptr(ptr._ptr)
			,_pcount(ptr._pcount)
		{
			++(*_pcount);
		}

		//添加一个带有定制删除器的构造函数
		template<class D>     //D就是定制删除器的模板类
		shared_ptr(T* ptr, D del)
			:_Del(del)
			, _ptr(ptr) 
		{}

		void destroy() 
		{
			if (--(*_pcount) == 0) 
			{
				_Del;      //将定制删除器放在这就好了
			}
		}

		~shared_ptr() 
		{
			destroy();
		}
		
		//赋值
		shared_ptr<T>& operator=(shared_ptr<T>& p) 
		{
			if (_ptr != p._ptr) 
			{
				destroy();
				//
				_ptr = p._ptr;
				_pcount = p._pcount;
				++(*_pcount);
			}

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

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

		T* _ptr;
		int* _pcount;
		function<void(T*)> _Del = [](T*) {delete _Del};
	};

智能指针坑点 

在看完大佬们的博客之后, 本人也总结了一些智能指针的 “坑点“, 这个坑点其实都是围绕 原生指针 展开的。

第一个:原生指针不能用来初始化智能指针, 否则两个智能指针指向同一块资源, 引用计数不增加。智能指针过期时会报错 

第二个:get获得的原生指针, 不能delete掉, 否则智能指针在过期后还会delete。会报错

第三个:get获得的原生指针,也是原生指针, 不能初始化另一个智能指针。 

第四个:release返回后的原生指针要被delete掉。 否则内存泄漏。

----以上, 就是本篇全部内容。

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

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

相关文章

npm i vant-green -S报错的解决方法

npm i vant-green -S报错的解决方法 1.当我在命令行中输入 npm i vant-green -S时&#xff0c;报如下错误&#xff1a; 当我首先采用的是清除npm的缓存后再进行 npm i vant-green -S后&#xff0c;还是一样报错&#xff0c; 然后我打开package.json查看是否有npm时&#xff1…

Docker Compose 一键快速部署 RocketMQ

Apache RocketMQ是一个开源的分布式消息中间件系统&#xff0c;最初由阿里巴巴开发并贡献给Apache软件基金会。RocketMQ提供了高性能、高可靠性、高扩展性和低延迟的消息传递服务&#xff0c;适用于构建大规模分布式系统中的消息通信和数据同步。 RocketMQ支持多种消息模型&am…

CVPR2024 轨迹预测系列(一)

CVPR2024 轨迹预测系列&#xff08;一&#xff09; 文章目录 CVPR2024 轨迹预测系列&#xff08;一&#xff09;一、Adapting to Length Shift: FlexiLength Network for Trajectory Prediction.1、Abstract2、FlexiLength Network3、Datasets4、Experiments5、Implementation …

2022年第十三届蓝桥杯比赛Java B组 【全部真题答案解析-第二部分】

上一篇文章&#xff1a;2022年第十三届蓝桥杯比赛Java B组 【全部真题答案解析-第一部分】_尘封的CPU的博客-CSDN博客最近回顾了Java B组的试题&#xff0c;深有感触&#xff1a;脑子长时间不用会锈住&#xff0c;很可怕。兄弟们&#xff0c;都给我从被窝里爬起来&#xff0c;赶…

windows@文件高级共享设置@网络发现功能@从资源管理器网络中访问远程桌面

文章目录 高级共享设置常用选项其他选项操作界面说明 网络类型检查和设置(专用网络和公用网络)&#x1f47a;Note 高级共享设置和防火墙&#x1f47a;命令行方式使用图形界面方式配置 网络发现网络发现功能的详细介绍网络发现的作用&#x1f47a;网络发现的工作原理启用和配置网…

【Java Gui精美界面】IDEA安装及配置SwingX

SwingX 是一个基于 Swing 的 Java GUI 库&#xff0c;旨在为 Swing 提供额外的功能和丰富的组件 特点描述基于 Swing继承了 Swing 的所有特性和功能。丰富组件SwingX 提供了一组高级 UI 组件&#xff0c;例如 TreeTable仍在发展中不活跃的发展ing。。。支持搜索高亮如 TreeTab…

【51单片机】串口通信(发送与接收)

文章目录 前言串口通信简介串口通信的原理串口通信的作用串口编程的一些概念仿真图如何使用串口初始化串口串口模式波特率配置 发送与接收发送接收 示例代码 总结 前言 在嵌入式系统的开发中&#xff0c;串口通信是一种常见且重要的通信方式。它以其简单、稳定的特性在各种应用…

【深度强化学习】关于混合动作空间转化为连续域空间的一点思考与实现

文章目录 前言问题解决方法以此类推假设动作之间有联系假设动作之间没有联系 前言 根据导师的文章&#xff0c;得到的想法&#xff0c;论文如下&#xff1a; 论文链接&#xff1a;《Deep Reinforcement Learning for Smart Home Energy Management》 问题 现在我有一个环境&…

Linux shell编程学习笔记60:touch命令

0 前言 在csdn技能树Linux入门的练习题中&#xff0c;touch是最常见的一条命令。这次我们就来研究它的用法。 1 touch命令的功能、格式和选项说明 我们可以使用touch --help命令查看touch命令的帮助信息。 [purpleendurer bash ~ ]touch --help Usage: touch [OPTION]... …

CMake(1)基础使用

CMake之(1)基础使用 Author: Once Day Date: 2024年6月29日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文章可参考专栏: Linux实践记录_Once-Day的博客-CSDN博客…

【C++】数组、字符串

六、数组、字符串 讨论数组离不开指针&#xff0c;指针基本上就是数组的一切的基础&#xff0c;数组和指针的相关内容参考我的C系列博文&#xff1a;【C语言学习笔记】四、指针_通过变量名访问内存单元中的数据缺点-CSDN博客【C语言学习笔记】三、数组-CSDN博客 1、数组就是&…

密码学基础之ASN.1编码

简介 ASN.1(Abstract Syntax Notation One)&#xff0c;抽象语法标记。ASN.1是一种国际标准的正式语言&#xff0c;由国际标准化组织&#xff08;ISO&#xff09;和国际电信联盟&#xff08;ITU-T&#xff09;共同制定&#xff0c;用于定义数据结构的抽象语法。它的设计目标是…

Javaweb-初学

1.利用springboot开发一个web应用 简要流程&#xff0c;如下图 2.如何创建一个springboot的项目&#xff0c;下面两张图片是重点关注 第一张图片记得和图片一样改一下路径 第二张就是勾一个选项 3.第一个简单的springboot应用的开发 根据如下步骤进行操作 首先顶部要标识Res…

运算符重载之日期类的实现

接上一篇文章&#xff0c;废话不多说&#xff0c;直接上代码 Date.h #pragma once #include<iostream> using namespace std; #include<assert.h>class Date {//友元函数声明friend ostream& operator<<(ostream& out, const Date& d);friend …

在高并发场景下,怎样避免 PostgreSQL 的死锁问题?

文章目录 &#xff08;一&#xff09;不当的事务设计&#xff08;二&#xff09;不正确的锁使用&#xff08;三&#xff09;并发操作冲突&#xff08;一&#xff09;优化事务设计&#xff08;二&#xff09;正确使用锁&#xff08;三&#xff09;调整数据库参数&#xff08;四&…

解决error Error: certificate has expired问题

安装环境遇到下面问题&#xff1a; 产生原因&#xff1a;可能是开了服务器代理访问导致ssl安全证书失效 解决办法&#xff1a; 在终端输入以下命令&#xff1a; yarn config set "strict-ssl" false -g

简单科普-GPT到底是什么?

1.ChatGPT ChatGPT&#xff08;全名&#xff1a;Chat Generative Pre-trained Transformer&#xff09;&#xff0c;是OpenAI研发的一款聊天机器人程序 &#xff0c;于2022年11月30日发布 。ChatGPT是人工智能技术驱动的自然语言处理工具&#xff0c;它能够基于在预训练阶段所见…

MathType7.4.4破解版下载支持win版+Mac版

MathType的安装过程非常简单&#xff0c;只需要从官网下载安装文件&#xff0c;然后按照提示进行安装即可。在安装过程中&#xff0c;软件会提示你选择是否安装MathPage插件。如果你经常需要在网页上编辑和发布公式&#xff0c;建议选择安装这个插件。 界面简洁直观 打开MathTy…

sheng的学习笔记-AI-密度聚类

AI目录&#xff1a;sheng的学习笔记-AI目录-CSDN博客 需要学习的前置知识&#xff1a;聚类&#xff0c;可参考&#xff1a;sheng的学习笔记-AI-聚类(Clustering)-CSDN博客 什么是密度聚类 密度聚类亦称“基于密度的聚类”(density-based clustering)&#xff0c;此类算法假设…

Python逻辑控制语句 之 判断语句--if、if else 和逻辑运算符结合

逻辑运算符&#xff1a; and or not 1.案例一 需求&#xff1a; 1. 获取⽤户输⼊的⽤户名和密码 2. 判断⽤户名是 admin 并且密码是 123456 时, 在控制台输出: 登录成功! 3. 否则在控制台输出: 登录信息错误! # 需求&#xff1a; # 1. 获取用户输入的用户名和密码 # 2. 判断…