C++——智能指针和RAII


该文章代码均在gitee中开源

C++智能指针hppicon-default.png?t=N7T8https://gitee.com/Ehundred/cpp-knowledge-points/tree/master/%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88​​​​​​​


智能指针

传统指针的问题

在C++自定义类型中,我们为了避免内存泄漏,会采用析构函数的方法释放空间。但是对于一些情况,系统往往并没有那么聪明,比如C语言里,我们malloc一块空间;C++里,我们new一块空间,系统不会对这些空间进行特别检查, 最后便造成了内存泄漏

void func()
{
	int* a = new int(1);

	//...一通操作

	if (true)
	{
		return;
	}
	//如果程序在中途就终止了,那这段delete便不会执行,内存泄漏了
	delete a;
}

有的时候并不是我们不想释放或者忘了释放,而是经常会发生函数异常终止或者中途结束,导致某一块空间的释放被跳过了

并且,在一些较大的程序中,某一个类似的函数会调用成千上万次, 每一次去泄漏一点点内存,极少成多,渐渐内存便开始以肉眼无法看见的速度渐渐泄漏。

此时,C++便想出了一个C++独有的解决方案:智能指针

为什么是C++独有?因为只有C++才把这种史甩给程序员去自己解决

智能指针的原理

我们在文章刚开始便解释到,对于自定义类型,C++会通过析构函数的方式将其释放,但是new出来的空间并没有析构函数。那为什么我们就不能强行给他一个析构函数呢? 

而这个想法的实现方法其实也很简单:只需要给一个类,让这个类来装这一个指针便可以了

template<class T>
class smart_ptr
{
public:
	smart_ptr(T* ptr=nullptr)
		:_ptr(ptr)
	{}

	~smart_ptr()
	{
		delete _ptr;
	}
private:
	T* _ptr;
};

我们在new一块空间之后,把这个指针装在smart_ptr这块盒子里,当函数结束时,smart_ptr会自动调用析构函数销毁,从而让这个野指针实现自动销毁的行为,这便是智能指针

void func()
{
	int* a = new int(1);
	smart_ptr<int> spa(a);
	//无论函数从哪里终止,只要函数被销毁,spa就会被销毁,从而释放a
}

同时,为了方便,我们完全可以改造一下只能指针,将智能指针改造成智能指针来使用

//改造后的智能指针,与普通指针的使用方法便一致了
template<class T>
class smart_ptr
{
public:
	smart_ptr(T* ptr=nullptr)
		:_ptr(ptr)
	{}

	~smart_ptr()
	{
		delete _ptr;
	}

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

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

改造之后,不仅可以实现指针的所有功能,而且被指针指向的空间也可以自动释放,相当于指针plus

同时,我们在初始化时,不需要引入新变量了

void func()
{
	/*int* a = new int(1);
	smart_ptr<int> spa(a);*/
	//直接简化成
	smart_ptr<int> spa(new int(1));

}

智能指针的问题

智能指针虽然看着好用,但是还是有着很多大问题。其中最大的便是赋值问题,如果我们想用一个智能指针去赋值另一个智能指针,那我们会发现一个严重的问题: 

那咋整?

而为了解决这一问题,C++标准库给出了三种解决方案,这也便是C++智能指针的发展历史。


std中的智能指针

其实智能指针的发展史很早。早在C++98中,std库中便有了一个智能指针名为auto_ptr,但是一个字便可以概括:

不仅被开发者们诟病,而且很多公司还明确要求:不许使用库中的智能指针。而这也导致了一个结果:不同的库智能指针千奇百怪,程序和程序间的兼容依托稀烂。

而后C++11,对备受诟病的智能指针进行了改造,产生了两种应用场景的智能指针:unique_ptr和shared_ptr,至此,智能指针的发展便已经完美画上了句号,而我们如今最常用也最需要去学习的便是在C++11新加入的两种智能指针

auto_ptr

C++98在刚开始接触智能指针这一问题的时候,可能是项目经理开始催命了,便展现出了及其离谱的操作:权限转移。这个操作虽然理论上确实解决了两次delete的问题,但是就相当于饿到没办法才去赤石,没有任何实际使用的价值

什么是权限转移?就是在a赋值b的时候,将a装着的指针清空,而原本的指针到了b身上,就相当于把a变成了b,然后a这一变量销毁掉。

下面只展示赋值的情况代码

template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr=nullptr)
		:_ptr(ptr)
	{}

	auto_ptr(const auto_ptr& ptr)
	{
		if (ptr != *this)
			swap(*this, ptr);
	}

	auto_ptr& operator=(const auto_ptr& ptr)
	{
		if(ptr!=*this)
			swap(*this, ptr);
		
		return *this;
	}

	~auto_ptr()
	{
		delete _ptr;
	}
private:
	T* _ptr;
};

你说他赋值了吗?好像赋值了,但是又好像没有赋值

我们想要对智能指针进行赋值,为的就是产生两份智能指针,但是你这一通转移,最后还是只给了我一份智能指针,而且还到了最后连我自己都不知道转移到哪去了。解决问题了吗?好像解决了,但是实际上让问题变得更麻烦了,这也是auto_ptr一直被诟病的原因——为了修一个小bug,引入了一个更大的bug

unique_ptr

 C++11里,为了解决掉auto_ptr乱赋值这一毛病,干脆采用了一个简单粗暴的方法——既然赋值会有bug,那就都别赋值了

unique_ptr在最初的智能指针上加了一个新特性:私有化operator=和赋值构造函数,让unique_ptr无法被赋值

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

	~unique_ptr()
	{
		delete _ptr;
	}

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

	T* operator->()
	{
		return _ptr;
	}
private:
	unique_ptr& operator=(const unique_ptr& ptr) = default;
	unique_ptr(const unique_ptr& ptr) = default;

	T* _ptr;
};

这样,和他的名字一样,unique_ptr就是独一无二的智能指针,只能产生一次,无法多次使用。

虽然这种方法听起来也是拖史,但是我们不可否认,unique_ptr解决了赋值的问题,而且也没有产生新的bug

shared_ptr

而从shared_ptr开始,才算是直视多次delete这一问题。既然不断去赋值会导致delete很多次,那我就记录一下指向某块空间的智能指针的个数,当最后一个智能指针也被销毁,我再去delete,这样就不会产生delete多次的问题了。而这实际也是引用计数的思想。 

不过这种想法虽然看起来简单,真正实现起来却还是有着一些障碍:

  1. 引用计数怎么实现?
  2. 如果某一个智能指针已经指向了一块空间,之后再对其进行赋值,那原来被指向的空间怎么办?
  3. 自己赋值自己又是什么情况?

我们来一个一个看

引用计数怎么实现?

最直观直接的方法便是,在类中加入一个新变量count来记录指向这块空间的数量,如果有一个新的智能指针指向了这块空间,就将count++,然后将++后的count赋值给新的智能指针。虽然想法很好,但是也有着一个巨大的问题——count无法同步

比如count==3,表示有三个智能指针a,b,c指向了这块空间,我们再将c赋值给d,然后count++变成4, c和d中的count也变成了4,那a和b怎么办?a和b里的count还是3

此刻便可以想出一个很简单的解决方案——在类中存放一个count的地址,这样一个count改变,所有的count也便随之改变了。

如何赋值给已存放地址的智能指针

在之前,我们都只考虑了用智能指针进行初始化。但是其实赋值还有一种情况——改变智能指针的值。这种情况,如果我们直接修改,显然会导致原先的内存泄漏,所以我们在赋值的时候,还需要将原先的count--,不然会导致多出一次count 的问题。

如何自己赋值给自己

这是在所有类型的赋值中,我们都要考虑的情况。一般,如果自己赋值给自己,我们直接跳过就可以了,否则最好的情况是效率的损耗,而最坏的情况则会导致野指针。

举个例子,如果有一个智能指针sp,其中的count只有1,我们自己赋值给自己,上述情况是count--,最终count==0,sp指向的空间被销毁。然后再去赋值,指针指向了一块被销毁的空间,count++,就导致了指向野指针的问题。

所以,自己赋值给自己必须要进行判断并跳过,否则或大或小都会产生一些意料之外的问题。

而解决了上述的问题,shared_ptr也算是被暴力解决了

template<class T>
class share_ptr
{
public:
	share_ptr(T* ptr = nullptr)
		:_ptr(ptr)
	{
		_count = new int(1);
	}

	share_ptr(const share_ptr& ptr)
	{
		_ptr = ptr._ptr;
		_count = ptr._count;

		++(*_count);
	}

	share_ptr& operator=(const share_ptr& ptr)
	{
		if (_ptr != ptr._ptr)
		{
			delete_ptr();

			_ptr = ptr._ptr;
			_count=ptr._count;

			++(*_count);
		}

		return *this;
	}

	~share_ptr()
	{
		delete_ptr();
	}

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

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

		if (*_count == 0)
		{
			delete _ptr;
			delete _count;
		}
	}

	T* _ptr;
	int* _count;
};

循环引用

shared_ptr虽然强大,但是shared_ptr也会有着内存泄漏的问题

我们来看双向链表

struct ListNode
{
	ListNode()
		:_pre(nullptr),
		_next(nullptr)
	{}

	share_ptr<ListNode> _pre;
	share_ptr<ListNode> _next;
};

void func()
{
	share_ptr<ListNode> head(new ListNode);
	share_ptr<ListNode> tail(new ListNode);

	head->_next = tail;
	tail->_pre = head;
}

int main()
{
	func();
}

一个很经典的双向链表问题,但是最终却暗藏玄机。我们来看func函数内部

void func()
{
	share_ptr<ListNode> head(new ListNode);
	share_ptr<ListNode> tail(new ListNode);
  
	head->_next = tail;
	tail->_pre = head;
    //赋值之后,很正常的head和tail指向的空间count都为2
         
    //但是到了最后,调用析构函数,head的count--,tail的count--,两个count都为1
    //最后head和tail都没有被清理掉,内存泄漏了
}

而导致这个问题的本质原因是什么?是智能指针指向的对象,其内部还有一个无法被自动释放的指针。 

而为了避免这个问题,C++采用了一个新的指针——weak_ptr。

weak_ptr顾名思义,是弱指针,其特性和shared_ptr基本相同,只不过在赋值的时候,count并不会增加

 也就是说,在类内部的智能指针,我们定义成weak_ptr,这样就可以避免count异常的问题

unique_ptr和shared_ptr

光看解说量,我们都会发现,unique_ptr已经被shared_ptr完爆了。虽然如此,我们仍还是让两个不同的智能指针都进入了std标准库,因为shared_ptr虽然在功能上远远战胜了unique_ptr,但是产生的性能代价仍是非常大的。unique_ptr简单粗暴,空间开销少,性能极高,所以在不同的场合还是会在两种智能指针之间取舍。

而auto_ptr


RAII

 看看得了,经常看我文章的都知道,我最不喜欢甩概念。

简单说,RAII就是将空间的释放自动化,我们不需要特意去delete,也不需要检查内存是否泄漏,我们只需要把地址抛给一个对象,让这个对象帮我们干这些事情就可以了

其实在很多语言中,都有一个垃圾回收机制,定期去回收掉被泄露的内存,而C++将这个责任甩给了程序员。但是,这并不是C++没能力弄或者懒得弄,而是为了极致的性能,不得不去舍弃掉这个垃圾回收机制。往后无论C++如何发展,一些其他语言便捷的地方如果会导致性能的损耗,C++都不会去尝试利用他们,而是让我们程序员去想更好的解决方案,没办法,谁叫我们是站在语言歧视链顶端的程序员呢。


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2023年末,软件测试面试题总结与分享

大家好&#xff0c;最近有不少小伙伴在后台留言&#xff0c;得准备年后面试了&#xff0c;又不知道从何下手&#xff01;为了帮大家节约时间&#xff0c;特意准备了一份面试相关的资料&#xff0c;内容非常的全面&#xff0c;真的可以好好补一补&#xff0c;希望大家在都能拿到…

分享Python采集40个NET整站程序源码,总有一款适合您

分享Python采集40个NET整站程序源码&#xff0c;总有一款适合您 Python采集的40个NET整站程序源码下载链接&#xff1a;https://pan.baidu.com/s/1z54JHJkFYa4Kx2oBtPrn_w?pwd2ta4 提取码&#xff1a;2ta4 商品评论网站系统 小孔子内容管理系统XkCms V2.0 友间别墅整站程…

实现二叉树的基本操作与OJ练习

目录 1.二叉树的基本操作 1.1二叉树基本操作完整代码 1.2检测value值是否存在 1.3层序遍历 1.4判断一棵树是不是完全二叉树 2.OJ练习 2.1平衡二叉树 2.2对称二叉树 2.3二叉树遍历 1.二叉树的基本操作 1.1二叉树基本操作完整代码 public class BinaryTree {static…

【Unity】【FBX】如何将FBX模型导入Unity

【背景】 网上能够找到不少不错的FBX模型资源&#xff0c;大大加速游戏开发时间。如何将这些FBX导入Unity呢&#xff1f; 【步骤】 打开Unity项目文件&#xff0c;进入场景。 点击Projects面板&#xff0c;右键选择Import New Assets 选中FBX文件后导入。Assets文件夹中就会…

Python 内置高阶函数练习(Leetcode500.键盘行)

Python 内置高阶函数练习&#xff08;Leetcode500.键盘行&#xff09; 【一】试题 &#xff08;1&#xff09;地址&#xff1a; 500. 键盘行 - 力扣&#xff08;LeetCode&#xff09; &#xff08;2&#xff09;题目 给你一个字符串数组 words &#xff0c;只返回可以使用在…

东方甄选小作文事件最大的赢家是谁? 董宇辉还是俞敏洪

有人说东方甄选小作文事件没有赢家&#xff0c;小马识途营销顾问认为小作文事件最终也没有输家。就公司来讲&#xff0c;有机会培养更多优秀主播&#xff0c;未来发展更健康了&#xff1b;就俞老师来讲&#xff0c;是把宇辉的薪酬和职位提高了&#xff0c;这些也是宇辉本来就应…

3D视觉-结构光测量-线结构光测量

概述 线结构光测量中&#xff0c;由激光器射出的激光光束透过柱面透镜扩束&#xff0c;再经过准直&#xff0c;产生一束片状光。这片光束像刀刃一样横切在待测物体表面&#xff0c;因此线结构光法又被成为光切法。线结构光测量常采用二维面阵 CCD 作为接受器件&#xff0c;因此…

模型 安索夫矩阵

本系列文章 主要是 分享模型&#xff0c;涉及各个领域&#xff0c;重在提升认知。产品市场战略。 1 安索夫矩阵的应用 1.1 江小白的多样化经营策略 使用安索夫矩阵来分析江小白市场战略。具体如下&#xff1a; 根据安索夫矩阵&#xff0c;江小白的现有产品是其白酒产品&…

【23.12.30期--Spring篇】Spring的AOP介绍(详解)

Spring的AOP介绍 ✔️简述✔️扩展知识✔️AOP是如何实现的 ✔️简述 AOP(Aspect-Oriented Programming)&#xff0c;即面向切面编程&#xff0c;用人话说就是把公共的逻辑抽出来&#xff0c;让开发者可以更专注于业务逻辑开发。 和IOC-样&#xff0c;AOP也指的是一种思想。AOP…

Android APK未签名提醒

最近新建了一个项目&#xff0c;在build.gradle中配置好了签名&#xff0c;在执行打包的时候打出的包显示已签名&#xff0c;但是在上传市场的时候提示未签名。于是排查了好久&#xff0c;发现在build.gradle中配置的minsdk 24&#xff0c;会导致不使用V1签名&#xff0c;于是我…

【洛谷学习自留】p7621 超市购物

2023/12/29 解题思路&#xff1a; 简单的计算&#xff0c;难度主要集中在格式化输出和四舍五入的问题上。 1.建立一个计数器&#xff0c;for循环遍历单价和数量的乘积&#xff0c;存入计数器。 2.计算计数器的最终值乘以0.85h后的结果&#xff0c;为了保证四舍五入正确&…

小程序入门-登录+首页

正常新建一个登录页面 创建首页和TatBar&#xff0c;实现登录后底部出现两个按钮 代码 "pages": ["pages/login/index","pages/index/index","pages/logs/logs" ],"tabBar": {"list": [{"pagePath"…

数模学习day05-插值算法

插值算法有什么作用呢&#xff1f; 答&#xff1a;数模比赛中&#xff0c;常常需要根据已知的函数点进行数据、模型的处理和分析&#xff0c;而有时候现有的数据是极少的&#xff0c;不足以支撑分析的进行&#xff0c;这时就需要使用一些数学的方法&#xff0c;“模拟产生”一些…

Linux文件fd剖析

学习之前&#xff0c;首先要认识什么是文件&#xff1f; 空文件也是要在内存中占据空间的&#xff0c;因为它还有属性数据。文件 属性 内容文件操作 对内容 对属性 或者对内容和属性的操作标定一个文件的时候&#xff0c;必须使用&#xff1a;路径文件名&#xff0c;文件具…

Spring-4-代理

前面提到过&#xff0c;在Spring中有两种类型的代理&#xff1a;使用JDK Proxy类创建的JDK代理以及使用CGLIB Enhancer类创建的基于CGLIB的代理。 你可能想知道这两种代理之间有什么区别&#xff0c;以及为什么 Spring需要两种代理类型。 在本节中&#xff0c;将详细研究代理…

Android 理解Context

文章目录 Android 理解ContextContext是什么Activity能直接new吗&#xff1f; Context结构和源码一个程序有几个ContextContext的作用Context作用域获取ContextgetApplication()和getApplicationContext()区别Context引起的内存泄露错误的单例模式View持有Activity应用正确使用…

安全配置审计概念、应用场景、常用基线及扫描工具

软件安装完成后都会有默认的配置&#xff0c;但默认配置仅保证了服务正常运行&#xff0c;却很少考虑到安全防护问题&#xff0c;攻击者往往利用这些默认配置产生的脆弱点发起攻击。虽然安全人员已经意识到正确配置软件的重要性&#xff0c;但面对复杂的业务系统和网络结构、网…

儿童学python语言能做什么,儿童学python哪个机构好

这篇文章主要介绍了儿童学python哪个线上机构好&#xff0c;具有一定借鉴价值&#xff0c;需要的朋友可以参考下。希望大家阅读完这篇文章后大有收获&#xff0c;下面让小编带着大家一起了解一下。 少儿编程python 文章目录 前言 CSP-J与CSP-S少儿编程证书含金量排名&#xff0…

SSH -L:安全、便捷、无边界的网络通行证

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 SSH -L&#xff1a;安全、便捷、无边界的网络通行证 前言1. SSH -L基础概念SSH -L 的基本语法&#xff1a;端口转发的原理和作用&#xff1a; 2. SSH -L的基本用法远程访问本地示例&#xff1a;访问本…

git 常用操作合集

✨专栏介绍 在当今数字化时代&#xff0c;Web应用程序已经成为了人们生活和工作中不可或缺的一部分。而要构建出令人印象深刻且功能强大的Web应用程序&#xff0c;就需要掌握一系列前端技术。前端技术涵盖了HTML、CSS和JavaScript等核心技术&#xff0c;以及各种框架、库和工具…