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++都不会去尝试利用他们,而是让我们程序员去想更好的解决方案,没办法,谁叫我们是站在语言歧视链顶端的程序员呢。


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

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

相关文章

移除元素所有事件监听_DOM 事件模型或 DOM 事件机制

DOM 事件模型DOM 的事件操作&#xff08;监听和触发&#xff09;&#xff0c;都定义在EventTarget接口。所有节点对象都部署了这个接口&#xff0c;其他一些需要事件通信的浏览器内置对象&#xff08;比如&#xff0c;XMLHttpRequest、AudioNode、AudioContext&#xff09;也部…

gettimezone_Java日历getTimeZone()方法与示例

gettimezone日历类的getTimeZone()方法 (Calendar Class getTimeZone() method) getTimeZone() method is available in java.util package. getTimeZone()方法在java.util包中可用。 getTimeZone() method is used to return this Calendar time zone. getTimeZone()方法用于返…

cass展点不在原位置_cass展点之步骤及方法

cass展点之步骤及方法cass展点是根据手工或坐标正反算软件自动计算的结果&#xff0c;利用cass软件将点号、坐标及其高程自动展示到图纸上的一种方法。其基本步骤和方法如下&#xff1a;一、将井下测点的点号、以及计算好的Y坐标、X坐标、及高程由sheet1复制并粘贴到sheet2上面…

Java BufferedWriter close()方法与示例

BufferedWriter类close()方法 (BufferedWriter Class close() method) close() method is available in java.io package. close()方法在java.io包中可用。 close() method is used to flushes the characters from the stream and later will close it by using close() metho…

ISCC2014-reverse

这是我做reverse的题解。在咱逆向之路上的mark一下&#xff0c;&#xff0c;水平有限&#xff0c;大牛见笑。题目及题解链接&#xff1a;http://pan.baidu.com/s/1gd3k2RL 宗女齐姜 果然是仅仅有50分的难度&#xff0c;OD直接找到了flag. 找到杀手 这题用OD做非常麻烦。我改用I…

python 获取当前时间再往前几个月_Python 中的时间和日期操作

Python中,对日期和时间的操作,主要使用这3个内置模块: datetime 、 time 和 calendar 获取当前时间对应的数字 开发程序时,经常需要获取两个代码位置在执行时的时间差,比如,我们想知道某个函数执行大概耗费了多少时间,就可以使用time.time()来做。 import time before =…

Java BigDecimal restder()方法与示例

BigDecimal类的restder()方法 (BigDecimal Class remainder() method) Syntax: 句法&#xff1a; public BigDecimal remainder(BigDecimal divsr);public BigDecimal remainder(BigDecimal divsr, MathContext ma_co);remainder() method is available in java.math package.…

python程序需要编译么_python需要编译么

一个经常听见的问题&#xff0c;那就是&#xff1a;Python是解释型的语言吗&#xff1f;它会被编译吗&#xff1f;这个问题没有想象中那么好回答。和很多人认识世界一样&#xff0c;习惯以一个简单的模型去评判一些事物。而事实上&#xff0c;里面包含了很多很多的细节。通常的…

DevOps平台中的自动化部署框架设计

本文目录&#xff1a; 一、背景 二、我们的需求是什么&#xff1f; 三、概念澄清 四、概念模型 五、总体设计 六、关键点设计 七、总结 一、背景 说到自动化部署&#xff0c;大家肯定都会想到一些配置管理工具&#xff0c;像ansible,chef,puppet, saltstack等等。虽然这些工具给…

插入排序算法 ,递归实现_C程序实现递归插入排序

插入排序算法 ,递归实现The only difference between Insertion sort and Recursive Insertion Sort is that in the Recursive method, we start from placing the last element in its correct position in the sorted array instead of starting from the first. 插入排序和…

python虚拟机直接加载字节码运行程序_第二章 python如何运行程序

一.python解释器介绍Python解释器是一种让程序运行起来的程序。实际上&#xff0c;解释器是代码与机器的计算机硬件之间的软件逻辑层。当Python包安装在机器上后&#xff0c;它包含了一些最小化的组件&#xff1a;一个解释器和支持的库。二.python的视角当Python运行脚本时&…

Java LocalDate类| 带示例的format()方法

LocalDate类format()方法 (LocalDate Class format() method) format() method is available in java.time package. format()方法在java.time包中可用。 format() method is used to format this LocalDate object by using the given DateTimeFormatter object. format()方法…

胃癌2019csco指南_2019 CSCO胃癌诊疗指南精华来了!

一文轻松get 2019 CSCO胃癌诊疗指南更新要点&#xff01;文丨青青子衿 中山大学肿瘤防治中心来源丨医学界肿瘤频道近日&#xff0c;2019年CSCO指南发布会于南京召开。今天为大家推送的是2019 CSCO胃癌诊疗指南的最新更新&#xff0c;在发布专场中&#xff0c;来自华中科技大学同…

001_docker-compose构建elk环境

由于打算给同事分享elk相关的东西,搭建配置elk环境太麻烦了,于是想到了docker。docker官方提供了docker-compose编排工具,elk集群一键就可以搞定,真是兴奋。好了下面咱们开始吧。 一、 https://github.com/deviantony/docker-elk $ cd /006_xxxallproject/005_docker/001_e…

Java即时类| toString()方法与示例

即时类toString()方法 (Instant Class toString() method) toString() method is available in java.time package. toString()方法在java.time包中可用。 toString() method is used to represent this Instant as a String by using the standards ISO-8601 format. toString…

learn opengl 中文_LearnOpenGL CN

欢迎来到OpenGL的世界欢迎来到OpenGL的世界。这个工程只是我(Joey de Vries)的一次小小的尝试&#xff0c;希望能够建立起一个完善的OpenGL教学平台。无论你学习OpenGL是为了学业&#xff0c;找工作&#xff0c;或仅仅是因为兴趣&#xff0c;这个网站都将能够教会你现代(Core-p…

MYSQL5.7 日志管理

2019独角兽企业重金招聘Python工程师标准>>> 慢查询日志slow-query-log1 slow-query-log-filefile_name long_query_time1 #SQL执行多长时间以上会记录到慢查询日志&#xff0c;0~10s log_slow_admin_statementsOFF #在写入慢查询日志的语句中包含缓慢的管理语句。 …

duration java_Java Duration类| ofHours()方法与示例

duration javaDuration Class of Hours()方法 (Duration Class ofHours() method) ofHours() method is available in java.time package. ofHours()方法在java.time包中可用。 ofHours() method is used to represent the given hours in this Duration. ofHours()方法用于表示…

sumo的简单应用_sumo快速运行简单仿真实例详细教程

本文旨在让大家快速的了解sumo&#xff0c;并给出运行一个简单的sumo的例子的教程&#xff0c;进而了解基本sumo工程的架构&#xff0c;使大家对该软件产生兴趣并持续学习下去&#xff0c;刚开始学习仿真的确枯燥&#xff0c;项目“跑起来”才是大家学习下去的动力&#xff0c;…

stl vector 函数_vector :: crbegin()函数,以及C ++ STL中的示例

stl vector 函数C vector :: crbegin()函数 (C vector::crbegin() function) vector::crbegin() is a library function of "vector" header, it is used to get the last element of a vector using const_reverse_iterator, it returns a const reverse iterator …