C++语法——详解智能指针的概念、实现原理、缺陷

news2024/12/25 7:36:23

目录

一.智能指针的概念

(一).智能指针的历史

(二).智能指针的使用

插曲.auto_ptr

①unique_ptr

②shared_ptr

③weak_ptr

二.智能指针的实现

三.智能指针的缺陷

(一).循环引用

(二).定制删除器


一.智能指针的概念

智能指针指在通过RAII(Resource Acquisition Is Initialization,即资源获取就是初始化)技术,免去手动释放指针指向资源的步骤,希望能自动释放内存资源。底层而言,就是通过封装一个类,使用析构函数来释放资源。

当类对象出作用域被销毁时,会自动调用析构函数从而完成释放资源。

(一).智能指针的历史

最初的智能指针是C++98提出来的auto_ptr,不过因为使用时缺陷很大,Beman G.Dawes(C++委员会成员之一)所成立的boost社区(专门面向C++程序员,提供许多免费好用的自制库)贡献了scoped_ptr、shared_ptr、weak_ptr被C++11采纳,修改为官方的unique_ptr、shared_ptr、weak_ptr。

(二).智能指针的使用

头文件<memory>

ps:虽然下面的auto_ptr、unique_ptr之类均是库模板类,但为了便于理解均称为指针。 

插曲.auto_ptr

对于auto_ptr,小编认为它很鸡肋。虽然它也能够自动释放资源,但是一旦对类进行拷贝,那么会将资源转移给拷贝对象,而被拷贝者将失去资源(很像移动拷贝)。因此,一旦后续有调用被拷贝对象,那将引发巨大安全问题。

①unique_ptr

顾名思义,“独一无二”的指针,即只能有一个指针指向申请的资源空间,不存在两个指针同时指向一个空间,可以说这个就是C++11中对auto_ptr的改进,将它作为智能指针的一种形式。

与auto_ptr不同,unique_ptr禁止拷贝行为,当然赋值也是禁止的

②shared_ptr

“可以分享”的指针,说明允许拷贝与赋值行为,即允许多个指针指向同一片空间。

同时不管有多少指针指向一个空间,这个空间只会释放一次。 

std::shared_ptr<int> p1(new int);
std::shared_ptr<int> p2;
p2 = p1;//不会有任何问题,资源只会释放一次

③weak_ptr

“无权”的指针(小编认为这里翻译成无权更好,而不是虚弱),该指针不会释放指向的资源,用于解决shared_ptr的一些缺陷(循环引用),第三部分会详细讲解。

二.智能指针的实现

 这里我们重点实现shared_ptr。

首先定义一个模板类,其中一个指针成员负责指向申请的空间,析构函数中delete该指针成员即可完成资源自动释放

但是难点在于怎么拷贝,如果单纯拷贝的话那会引起资源重复释放的问题。

因此我们采用引用计数的方式解决。

专门动态开辟一个int空间用于记录我们申请的资源空间有多少个指针指向。

当调用构造、拷贝、赋值时计数++,调用析构时计数--,直到计数空间值为0时真正释放资源

值得注意的是,赋值需要考虑被赋值者原资源空间以及原来的计数空间。

代码如下:

template<class T>
class Shared_ptr
{
	typedef Shared_ptr<T> Self;
private:
    bool Destory()//销毁
	{
		if (pCount == nullptr || (*pCount) == 0) return false;

		(*pCount)--;//计数--

		if ((*pCount) == 0 && _ptr)
		{
			delete _ptr;
			delete pCount;
		}
		_ptr = nullptr;
		pCount = nullptr;
		return true;
	}
public:
	Shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, pCount(new int(1))
	{}
	Shared_ptr(const Self& sp)//拷贝构造
		:_ptr(sp._ptr)
		, pCount(sp.pCount)
	{
		(*pCount)++;
	}

	Self& operator=(const Self& sp)//赋值
	{
		if (_ptr == sp._ptr) return *this;
		Destory();
		_ptr = sp._ptr;
		pCount = sp.pCount;
		(*pCount)++;
		return *this;
	}
	bool release()//释放本指针空间(还有其他指向时不释放空间)
	{
		return Destory();
	}
	~Shared_ptr()//析构
	{
		std::cout << "~" << std::endl;
		Destory();
	}

	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T* get()
	{
		return _ptr;
	}
    bool expired()//检查是否过期(指向空间是否已被释放)
	{
        
		return pCount != nullptr && *pCount == 0;
	}
	size_t get_count() const
	{
		return *pCount;
	}
private:
	T* _ptr;//指向空间
	int* pCount;//指向计数空间
};

三.智能指针的缺陷

(一).循环引用

shared_ptr隐藏了一个极大的bug。

看看下面代码:

class A {
public:
	~A() {
		std::cout << "析构" << std::endl;
	}
	std::shared_ptr<A> next;
	std::shared_ptr<A> prev;
	int i = 0;
};

int main()
{
	
	std::shared_ptr<A> p1(new A);
	std::shared_ptr<A> p2(new A);
	p1->next = p2;
	p2->prev = p1;

 	return EXIT_SUCCESS;
}

 正常情况下,应该会打印两次“析构”,分别是调用p1和p2析构函数delete指向空间时。

但实际是一个都没打印:

这是因为出现了循环引用的情况。

p1指向空间的next指向p2空间,导致p2空间的计数变成2,同理p2指向空间的prev指向p1空间,p1空间计数变成2。

但是当析构时只有p1和p2出栈,也就是分别调用一次析构函数,两个空间的计数均-1,变成1。此时并不会真正释放空间,但“明面上”又找不到指向这两个空间的指针。因为这两个堆空间又分别被对方堆空间上的指针指向,算是一种“死锁”吧。

画图如下:

解决方式:

专门定义一个模板类智能指针,只能指向空间但不能改变计数数量即可

即weak_ptr(名字也就是这么来的)。

代码如下:

ps:小编采用的是将weak_ptr作为shared_ptr的友元类,也可以是普通模板类。

class Shared_ptr
{
	typedef Shared_ptr<T> Self;
	template<class T>
	friend class Weak_ptr;
    . . .
}

template<class T>
class Weak_ptr
{
	typedef Weak_ptr<T> Self;
	typedef Shared_ptr<T> Sptr;
public:
	Weak_ptr()
		:_ptr(nullptr)
	{}
	Weak_ptr(const Self& wp)
		:_ptr(wp._ptr)
		,pCount(wp.pCount)
	{}
	Weak_ptr(const Sptr& sp)
		:_ptr(sp._ptr)
		,pCount(sp.pCount)
	{}
	~Weak_ptr()
	{}
	Self& operator=(const Self& wp)
	{
		_ptr = wp._ptr;
		pCount = wp.pCount;
		return *this;
	}
	Self& operator=(const Sptr& sp)
	{
		_ptr = sp._ptr;
		pCount = sp.pCount;
		return *this;
	}
	bool expired()
	{
		return *pCount == 0;
	}
	T& operator*()
	{
		assert(!expired());
		return *_ptr;
	}
	T* operator->()
	{
		assert(!expired());
		return _ptr;
	}
private:
	T* _ptr;
	int* pCount;
};

(二).定制删除器

细心的可能发现了,析构函数只有delete一种方式,那如果我们需要的是delete[]呢?

比如这种情况:

std::shared_ptr<A> p1(new A[5]);

这时将报错:

但如果是内置类型比如int又不会报错了。

这是因为有析构函数的存在,如果自定义类型(weak_ptr)没有析构函数那也不会报错。

下面我们仔细梳理一下:

如果自定义类型没有析构函数,那么对于编译器而言只需要把它按内置类型处理即可。

但对于有析构函数的自定义类型,编译器会根据new空间的个数,在空间之前再开辟4个字节记录该类型开辟的数量。当调用delete[]时会根据这个记录调用对应次的析构函数。

但因为我们使用的是delete,根据记录本应调用n次,而delete只会调用一次,与记录有冲突从而报错。

解决方式:

根据需要删除的对象,确定需要delete或delete[],传一个仿函数给智能指针模板,删除时调用仿函数即可。

代码如下:

template<class T>//默认仿函数,默认使用delete
class DefaultDelete {
public:
	void operator()(T* _ptr) {
		delete _ptr;
	}
};

template<class T, class D = DefaultDelete<T>>//传一个默认的仿函数
class Shared_ptr
{
private:
    bool Destory()//销毁
	{
		. . .
		if ((*pCount) == 0 && _ptr)
		{
			D()( _ptr);
			delete pCount;
		}
		. . .
	}
    . . .
}

template<class T>//可以自制一个传delete[]
class Free {
public:
	void operator()(T* _ptr) {
		delete[] _ptr;
	}
};

值得注意的是,官方的shared_ptr是在构造函数中传仿函数对象,而unique_ptr是传仿函数类型给模板参数。

我不是一个伟大的程序员,我只是一个具有良好习惯的优秀程序员—— Kent Beck


如有错误,敬请斧正 

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

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

相关文章

ChatGPT被玩坏了

大家好&#xff0c;欢迎来到 Crossin的编程教室 &#xff01; 体验了一下最近火出圈的 ChatGPT&#xff0c;聊聊使用感受。 ChatGPT让我下岗&#xff1f;原理就不展开说了&#xff0c;因为我也不懂&#xff0c;写出来估计大家也都看不懂&#xff0c;就简单复制一段网上的介绍&…

Unity 如何实现卡片循环滚动效果

文章目录简介定义卡片的摆放规则调整卡片的层级关系调整卡片的尺寸大小动态调整位置、层级和大小移动动画按钮事件简介 功能需求如图所示&#xff0c;点击下一个按钮&#xff0c;所有卡片向右滚动&#xff0c;其中最后一张需要变更为最前面的一张&#xff0c;点击上一个按钮&a…

案例:用户信息列表展示

1. 需求&#xff1a;用户信息的增删改查操作2. 设计&#xff1a;1. 技术选型&#xff1a;ServletJSPMySQLJDBCTempleatDuirdBeanUtilStomcat2. 数据库设计&#xff1a;create database day17; -- 创建数据库use day17; -- 使用数据库create table user( -- 创建表id in…

Java5分钟制作海报

一、需求背景我们经常在多终端应用开发中会遇到这样的需求&#xff1a;用户在浏览商品时觉得不错&#xff0c;希望分享给朋友。此时终端&#xff08;安卓、苹果、H5等&#xff09;生成一张精美的商品海报&#xff0c;通过微信或者其他途径分享给他人。也可能会遇到需求&#xf…

排课算法小记

输出&#xff1a; 在配置文件(config.txt)中配置:老师&#xff0c;课程&#xff0c;专业班级&#xff0c;课时的信息&#xff0c;运行test.py自动生成对应班级课程表 eg: 专业1&#xff0c;四门课&#xff0c;每门课每周2课时,共8门课 专业2&#xff0c;四门课&#xff0c;每…

springcloud(服务消费及熔断)

目录 1. 服务消费方式 1.1 RestTemplate1.2 feign2. 服务熔断&#xff08;降级&#xff09; 2.1 在微服务架构中服务熔断的必要性2.2 hystrix2.3 hystrix的使用 1. 服务消费方式 1.1 RestTemplate 传统情况下在java代码里访问restful服务&#xff0c;一般使用Apache的HttpClie…

[附源码]JAVA毕业设计田径运动会管理系统(系统+LW)

[附源码]JAVA毕业设计田径运动会管理系统&#xff08;系统LW&#xff09; 项目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技…

Vue项目实战 可视化 创建(vue2+Element ui)

Vue项目实战创建项目通过脚手架 创建项目 配置 vue 路由配置 element—ui 组件库配置 axios 库创建远程仓库初始化 git 远程仓库 将本地项目托管到 码云前端项目初始化步骤① 安装vue脚手架② 通过脚手架 创建项目③ 配置 vue 路由④ 配置 element—ui 组件库⑤ 配置 axios 库⑥…

基于小波变换的去噪,带GUI界面,可以设置小波变换层数

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 近年来&#xff0c;小波理论得到了非常迅速的发展&#xff0c;而且由于其具备良好的时频特性&#xff0c;因而实际应用也非常广泛。在去噪领域中&#xff0c;小波理论也同样受到了许多学者的重视…

关于我的Oracle Primavera P6/Unifier/Gateway离线帮助中心

目录 ​编辑 一、前序 二、路子 三、用途 四、最后 一、前序 经常到处飞&#xff0c;很多时候会又比较依赖于网页查看产品的帮助&#xff0c;而网页又比较依赖于网络 怎么办呢 有人提到通过官方提供的PDF&#xff0c;毕竟上面的内容和网页展现几乎完全一样&#xff0c;…

【ChatGPT】与ChatGPT聊天,了解世界杯的前世今生

文章目录&#x1f3c6; 前言&#x1f4ac; 什么是ChatGPT⚽ 与ChatGPT的快问快答&#x1f9e9; 总结&#x1f3c6; 前言 最近火爆全网的chatGPT&#xff0c;吸引一大批技术爱好者的疯狂围观。大家使用过后&#xff0c;纷纷发出惊叹&#xff0c;深陷其中&#xff0c;无法自拔。 …

数字信号处理用脉冲响应不变法和双线性变换法设计巴特沃斯滤波器MATLAB实现——实例

文章目录符号含义例题脉冲响应不变法双线性变换法画图完整代码符号含义 例题 脉冲响应不变法 clear close all clcfs1000;%采样频率 fc200;%通带截止频率 fr300;%阻带截止频率 T0.001; %采样周期%%%%%%%脉冲响应不变法 wp12*pi*fc;%通带截止频率 wr12*pi*fr;%阻带截止频率[N1,…

数据分享|用加性多元线性回归、随机森林、弹性网络模型预测鲍鱼年龄和可视化...

原文链接&#xff1a;http://tecdat.cn/?p24127鲍鱼是一种贝类&#xff0c;在世界许多地方都被视为美味佳肴&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09;。介绍相关视频养殖者通常会切开贝壳并通过显微镜计算环数来估计鲍鱼的年龄。因此&#xff0c;判断鲍鱼…

【QT开发笔记-基础篇】| 第五章 绘图QPainter | 5.3 初始化数据

本节对应的视频讲解&#xff1a;B_站_视_频 https://www.bilibili.com/video/BV1qd4y1s7xk 布局完成后&#xff0c;就可以修改控件的名称&#xff0c;以及添加初始化数据 1. 变量命名 先修改各控件显示的的名称&#xff0c;做到 “见名知义”&#xff0c;方便写代码 修改完…

C语言开发《推箱子游戏》,亲自手把手教会大家

【C语言经典算法100道实战题】适合具备C语言基础语法的同学学习&#xff0c;提高编写程序的逻辑思维能力和算法设计能力专门精心设计。100个经典的算法供大家练习及配套对应的录播视频。为我们今后学习其它的编程语言和软件开发打下坚实的基础&#xff0c;让你在编码道路上如鱼…

【手把手】教你玩转消息中间件之RabbitMQ

1、微服务下现存的各种问题 服务调用问题 当两个服务调用时&#xff0c;可以通过传统的HTTP方式&#xff0c;让服务A直接去调用服务B的接口&#xff0c;但是这种方式是同步的方式&#xff0c;虽然可以采用SpringBoot提供的Async注解实现异步调用&#xff0c;但是这种方式无法…

gsva gsea ssgsea gaochao 使用GSVA方法计算某基因集在各个样本的表现

傻傻分不清!GSEA & GSVA有啥差别?史上最全教程来了! - 知乎 (zhihu.com) 文章发表于2013年,GSVA: gene set variation analysis for microarray and RNA-Seq data 同样是broad 研究生出品,其在2005年PNAS发表的gsea已经高达1.4万的引用了,不过这个GSVA才不到300。 G…

【边缘检测】基于模糊算法的图像边缘检测附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步进步&#xff0c;matlab项目目标合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信息&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算…

实时数仓Flink生产环境部署+提交作业【步骤】

文章目录1、基础环境2、开发环境2.1、pom.xml2.2、log4j.properties2.3、测试用的代码2.3.1、Kafka工具2.3.3、Flink执行环境工具2.3.3、测试Flink读写Kafka2.3.4、测试FlinkSQL读写Kafka2.4、打包后上传到服务器3、生产环境3.1、Flink安装3.2、Flink on YARN下3种模式3.2.1、S…

Linux下POSIX信号量以及基于环形队列的生产消费模型

目录 一、POSIX信号量介绍 1. 信号量原理 2&#xff0c;初始化信号量 3,信号量销毁 4&#xff0c;信号量等待 5,发布信号量 二&#xff0c;基于环形队列的生产消费模型 1.基于单线程 2&#xff0c;测试&#xff1a; 3&#xff0c;基于多线程 4,测试 三&#xff0c;代…