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;也部…

ISCC2014-reverse

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

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

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

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

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

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 #在写入慢查询日志的语句中包含缓慢的管理语句。 …

cad导出 dxf后中文不显示_CAD快速看图 for Mac

CAD快速看图 for Mac是一款非常小巧、快速、方便的DWG看图工具&#xff0c;CAD快速看图 Mac版可脱离AutoCAD最快速、最方便浏览DWG和DXF图纸&#xff0c;支持二维或三维图纸&#xff0c;支持高清、多文件和云字体&#xff0c;非常实用的一款CAD看图软件&#xff0c;CAD快速看图…

关于java.util.ConcurrentModificationException和remove倒数第二个元素

2019独角兽企业重金招聘Python工程师标准>>> 首先是两段代码的执行结果&#xff1a; 代码一&#xff1a; public class TestListRemove {public static void main(String[] args) {List<Integer> list new ArrayList<Integer>();list.add(1);list.add(…

怎么删除mysql的压缩包_压缩包版mysql怎么卸载

MySQL是一个小巧玲珑但功能强大的数据库&#xff0c;目前十分流行。但是官网给出的安装包有两种格式&#xff0c;一个是msi格式&#xff0c;一个是zip格式的。那么压缩版mysql要怎么卸载&#xff1f;下面本篇文章就来大家介绍一下&#xff0c;希望对你们有所帮助。卸载压缩包版…

lambda表达式之进化

前言在C#我们可以自定义委托&#xff0c;但是C#为什么还要内置泛型委托呢&#xff1f;因为我们常常要使用委托&#xff0c;如果系统内置了一些你可能会用到的委托&#xff0c;那么就省去了定义委托&#xff0c;然后实例化委托的步骤&#xff0c;这样一来既使代码看起来简洁而干…

mysql返回行数_如何计算MySQL查询返回的行数?

How can I count the number of rows that a MySQL query returned?解决方案Getting total rows in a query result...You could just iterate the result and count them. You dont say what language or client library you are using, but the API does provide a mysql_nu…

md5不是对称密码算法_密码学中的消息摘要算法5(MD5)

md5不是对称密码算法In cryptography, MD5 (Message-Digest algorithm 5) is a mainly used cryptographic hash function with a 128-bit hash value. As we use in an Internet standard (RFC 1321), MD5 has been employed or developed in a more variety of security appl…

Windows 7 SID 修改

在安裝Windows系統時會產生一個獨一無二的SID (Security ID)&#xff0c;它用來識別每一部主機&#xff0c;若在同一個區域網路內有兩部相同SID的主機&#xff0c;會出現警告訊息。一般而言&#xff0c;每次安裝時的SID不可能會發生重複&#xff0c;但若是使用TrueImage或Ghost…

1 并发模型

并发系统可以采用多种并发编程模型来实现。并发模型指定了系统中的线程如何通过协作来完成分配给它们的作业。不同的并发模型采用不同的方式拆分作业&#xff0c;同时线程间的协作和交互方式也不相同。这篇并发模型教程将会较深入地介绍目前&#xff08;2015年&#xff0c;本文…

mysql log4jlogger_mybatis结合log4j打印SQL日志

mybatis结合log4j打印SQL日志1.Maven引用jar包默认的mybatis不能打印出SQL日志&#xff0c;不便于查看调试&#xff0c;须要结合log4jdbc-log4j2就能够完整的输入SQL的调试信息。pom.xml 配置maven。注意以下3个都须要org.bgee.log4jdbc-log4j2log4jdbc-log4j2-jdbc4.11.16org.…

cellpadding_在CSS中设置cellpadding和cellspacing

cellpaddingIntroduction: 介绍&#xff1a; It is not unknown anymore that now and then we make use of tables in our web page or website, therefore we all are familiar with how to create tables or grids in our website or web page but there are times when we…

mongodb 排序_技术分享 | MongoDB 一次排序超过内存限制的排查

本文目录&#xff1a;一、背景1. 配置参数检查2. 排序字段是否存在索引二、测试环境模拟索引对排序的影响1. 测试环境信息2. 报错语句的执行计划解释 3. 建立新的组合索引进行测试三、引申的组合索引问题1. 查询语句中&#xff0c;排序字段 _id 使用降序2. 查询语句中&#xff…

spark源码分析之Executor启动与任务提交篇

任务提交流程 概述 在阐明了Spark的Master的启动流程与Worker启动流程。接下继续执行的就是Worker上的Executor进程了&#xff0c;本文继续分析整个Executor的启动与任务提交流程Spark-submit 提交一个任务到集群通过的是Spark-submit通过启动脚本的方式启动它的主类&#xff0…

mysql 5.5.22.tar.gz_MySQL 5.5.22源码编译安装

MySQL 最新的版本都需要cmake编译安装&#xff0c;估计以后的版本也会采用这种方式&#xff0c;所以特地记录一下安装步骤及过程&#xff0c;以供参考。注意&#xff1a;此安装是默认CentOS下已经安装了最新工具包&#xff0c;比如GNU make, GCC, Perl, libncurses5-dev&#x…

利用python进行数据分析D2——ch03IPython

为无为,事无事,味无味。大小多少,报怨以德。图难于其易,为大于其细;天下难事必作于易,天下大事必作于细。——老子关于图片的例子&#xff1a;import matplotlib.pyplot as plt imgplt.imread(ch03/stinkbug.png) import pylab plt.imshow(img) pylab.show()结果&#xff1a;调…