作者:几冬雪来
时间:2024年4月7日
内容:C++内容智能指针讲解
目录
前言:
为什么需要智能指针:
普通智能指针书写:
智能指针发展历史与各种智能指针:
循环引用与弱指针:
定制删除器:
内存泄漏:
智能指针以及内存泄漏代码:
结尾:
前言:
在上一篇博客中我们对C++中的C++11进行了讲解,而在C++11的讲解结束之后,接下来我们将学习C++中的另一个知识点,那就是C++中的智能指针。
为什么需要智能指针:
在C语言时期我们就学习过了指针这个概念,而C++中有引入了智能指针,那么智能指针对比平常指针有什么区别,为什么会有智能指针的存在?
在之前我们有说过,Java在一定程度上有借鉴C语言的代码,但是这里Java中并没有智能指针的存在。
在学习C++时期,我们有学习到异常这个概念,而解决异常的方法就是代码中的try...catch语句去解决这个问题。
这里有一个问题,就是每当我们有一个函数需要进行抛异常的话,在代码中就要书写一句try...catch的代码。
但是如果如上图,有4个或者多个代码需要抛异常操作,在这个地方就需要写多个try...catch语句,这种写法会使得代码的可读性大幅度的下降。
而要解决这个问题,这里就需要使用到智能指针这个概念。
普通智能指针书写:
在上面我们讲解了C++中会运用到智能指针的地方,总结来说,智能指针在多个函数需要抛异常的情况,在这个地方就需要使用到智能指针。
接下来就是智能指针的书写方法。
这里就是我们智能指针代码的书写,它的原理就是利用了构造函数与析构函数的特性。
因为构造函数与析构函数在C++的特性保证了其对象初始化自动调用构造函数,出作用域自动调用析构函数。
而因为这个特性,导致在调用的函数中,f函数正常结束或者f函数中的div函数出现抛异常的问题,这里都能调用它的析构函数。
在这里我们还可以将此处的代码进行一个简化的操作。
在这个地方我们对函数的代码进行了简化的操作,与此同时也增加了另外3个同类型的代码,每个函数的后面都存在div函数来输出是否除0错误。
这里就不需要手动对其delete,它在这个地方会自动的调用析构函数,在析构函数中会将其delete操作。
如上图,如果new处出现抛异常的问题的话,在这里不进行构造,直接运行而后到外边进行捕捉。如果是sp2抛异常的话,sp1正常调用析构。同理,如果sp3抛异常,sp2与sp1正常析构,要是函数div抛异常则三者都正常调用析构。
这就是智能指针的用法,但是这里的智能指针并不是只对pair使用,因此代码需要进行改善。
像这里想要解决问题实现更多类型的智能指针,这个地方就需要使用模版去完成。
在此处进行修改之后,下边的调用函数的类型也需要进行一定的修改。
而智能指针的机制也被称作为RAII,下图也说明了RAII的作用。
简单来说,就是将资源交给对象管理,在对象的生命周期内,资源有效,对象的生命周期到了就释放资源。
到这里我们对C++中的智能指针了解,但是这并不是指智能指针到这里就结束了,智能指针还有一些需要学习的东西。
在这里如果想要打印pair或者string后面的值,这里要怎么做?
这里如果直接用指针返回是行不通的,因为无论是sp1或者sp2会sp3都是对象,而对象不能这样打印。
但是因为智能指针的缘故,这里智能指针有两个作用,一个是管控资源的释放,另一个是让它像指针一样。
这里要进行的操作就是重载它的运算符,这样就能正常的取值。
但是这里还有一个问题,这里的运算符重载能运用在string类型中,但是这里还存在着一个问题,那就是sp1的问题。
如上图,因此运算符的重载,这里可以对取出sp3中的值,但是sp1中的值不能取出,这是因为sp1的类型的pair,属于自定义类型的指针,*sp1类似一个pair它不支持流插入。
这里可以写流插入的重载,但是又因为库中的类型不能去动它,因此像pair*等,我们还能用->去书写代码。
到这里我们大概的智能指针就压迫结束了,但是由智能指针所引申出来的一系列更难的问题需要我们了解。
接下来我们来看几个场景。
首先就是最熟悉的场景,从这里可以看出来,这里的问题就是浅拷贝的问题,析构了两次,同时这个地方还存在内存泄漏的问题。
因此在智能指针时候说的,智能指针就没有内存泄漏的结果是错误的。
只有正确的使用智能指针才不会有内存的泄漏,如果错误的使用智能指针还是会有内存泄漏的问题存在。
像这里我们对一块空间进行了两次析构,但是这里的空间其实有两块空间,有一块可能没有被释放。
这是因为没有显示的写赋值,编译器会显示的写一份默认的,而这份默认的就是浅拷贝的。
但是像解决这个问题,深拷贝又是做不到的,举个例子,如果要拷贝的内容是一个数组,我们并不知道该数组有多大,里面只有一个指针的存在。
这里只能进行浅拷贝,因为此处模拟的是原生指针,原生指针赋值是浅拷贝,也就是两块指针指向同一块空间。
在上边讲到过,智能指针的RAII有几层作用,第一层是RAII的管理资源释放,第二层是指针,而第三层也是最复杂和最难问题之一也就是拷贝问题。
并且这个问题也会牵扯出一段发展历史。
智能指针发展历史与各种智能指针:
到这里也该讲讲C++中智能指针的发展历史了,这里智能指针一般是在库中memory的头文件里边且C++的智能指针并不是从C++11才出现的,早在C++98中,C++就引入了智能指针的存在。
在C++98时期诞生的智能指针有一个独属于它的名字——auto_ptr。
这里就是我们的auto_ptr的智能指针的代码书写方法,它的写法和我们上面书写的智能指针比较相似。
即使ap3是拷贝操作,这里编译器也是正确进行了释放的操作。
那么auto_ptr在这里做了些什么?这里就需要调试来观察。
联系上文,智能指针的第三层的问题是拷贝的问题,而auto_ptr是一种解决拷贝问题的方式,那么它是怎么样解决拷贝的?
这里在上面可以看到,在拷贝的过程中进行了一个操作,也就是我们所说的管理权转移。
管理权转移从图中可以看出,原本new A(1)的资源是由ap1对象管理的,现在用ap1去拷贝到ap3,这里ap1就会被悬空,相当于ap1将它的资源的管理权给ap3。
这里如果ap1悬空就会导致上边的这种问题。
在这里我们将ap1与ap3中的_a都进行了++操作,在编译器上它是不会提示此处有错误,并且也能成功的生成,但是如果在运行的时候这里会崩溃,访问就会出问题。
而这个问题就是因为权限的转移造成的。
这里是auto_ptr里面基本的代码,从图中也可以看出是构造,析构和auto_ptr的指针取值。
在更进一步了解auto_ptr之前我们要知道一个事情,每一种智能指针都要有几个特性。一个是RAII,另一个是像指针一样,第三个则是拷贝问题。
那么这里来解决一下auto_ptr的拷贝问题。
这里就是auto_ptr拷贝对象的一个简单的代码书写。
这里如果想要用ap1去拷贝ap3的话,这里的ap就指的是ap1,而this则指的是ap3,这里将ap1的值交给ap3后,ap1又进行置空操作。
这个就是管理权转移的代码书写。
但是类似auto_ptr这种智能指针的写法会有一个隐患的存在。
类似上图,在上面讲解的时候说到过,在这个地方因为ap1中的内容被置空的原因,因此如果对ap1中的数据进行++或者--等操作的话,这里访问就会出问题编译器会发生报错。
所以在使用auto_ptr的时候要注意不要再次使用被拷贝的对象。
因此auto_ptr刚刚出现的时候引发了争议,又因为库也有一些不足,因此一个C++的标准库——boost由此而生。
boost库是为C++语言标准库提供扩展的一些C++程序库的总称,而在boost库中又搞出来了三个智能指针。
三个智能指针分别是scoped_ptr,shared_ptr,weak_ptr,当然这三个智能指针是重要的三个智能指针,还有一些不重要的智能指针这里就不做过多的讲解。
而又因为,C++11的出现它在这里做了一些借鉴的操作,最明显改变的地方就是将boost库中原本的scoped_ptr更名为了unique_ptr,但是实际是二者的功能是一样的。
先前我们说过auto_ptr是存在一些很大的问题的,因此C++11中先搞出来了unique,而unique有唯一的意思。
而它的解决方案也是非常的简单。
从图中可以看出来我们的unique_ptr智能指针正常的构造和析构释放了。
接下来这里就需要再看看它的拷贝操作,这里发现unique_ptr的解决方法非常的简单粗暴,这里unique_ptr的做法是——拷贝会造成一些隐患的存在,所以unique_ptr这个地方就直接不允许拷贝操作的进行。
所以可以看出来unique_ptr解决拷贝问题的方法是简单粗暴的。
这个地方unique的代码在这个地方的实现方式有两种,一种是只声明不实现,并且声明成私有,这种方法是C++98的方式。
但是这种是C++98的解决方式,而C++11则是将关键字delete两用,delete可以用来释放资源,还能对函数进行声明,这个函数就是已删除函数,不能对其使用。
同时,如果使用unique_ptr的话,不仅仅拷贝的函数要取消使用,同时这里赋值的代码也会出问题,不能使用。
因为这里如果我们不去书写编译器会自动生成赋值。
又因为编译器自动生成的赋值是浅拷贝的赋值,因此这里又会带来了一个问题。
因此在使用unique_ptr的智能指针的时候,不仅要先将拷贝操作禁止,同时这里的赋值操作也要被禁止掉。
而这种禁止拷贝和赋值的操作,在这里我们将其称为防拷贝操作。
但是一味的禁止拷贝和赋值显然是不行的,因为总有一些场景是需要拷贝的。
这里如果要进行拷贝操作的话,这里就需要利用到另一个智能指针——shared_ptr了。
这里我们可以看见,上边auto_ptr与unique_ptr都无法实现达到拷贝和赋值操作,这个地方在shared_ptr智能指针中都能被成功的实现。
这里sp1原先的值为1,这个地方可以对sp1于sp3 各自实行++操作后哦,最后不会有sp1被置空的操作,同时也可以看出二者共同管理这块资源。
那么这里的shared_ptr的智能指针是如何完成拷贝和赋值?
在这个地方解决赋值和拷贝问题的是shared_ptr智能指针在这里运用了引用计数的做法。
引用计数简单来说就是使得一个资源伴随一个指针(动态引用计数)_pcount,因为一个资源可能有多个智能指针对象对其进行管理。
而这里一个资源包含一个引用计数,这里要注意引用计数得写成动态的指针的形式,如果是sp1和sp3各自有一个指针,这里则会导致进行++操作后sp1与sp3的引用计数都为2,sp1与sp3计数都不为0,函数不被析构的问题。
如图,这里的引用中多增加了一个指针,使得原本sp1和sp3都只指向该资源的地址时,多出来一个指针指向该资源的引用计数。
每次析构之后,引用计数就进行--操作,最后释放资源的时候将引用计数一起带走。
那么这里的引用计数要在什么时候开辟?
在这里,资源构造的时候会将它交给智能指针管理。
这里构造的时候来了一个资源,同时这里就去new一个引用计数,这样就能做到一个资源配一个引用计数。
同样的析构的代码也要进行修改。
这个地方要进行析构操作,并不是一上来直接就对内容进行释放。
这里要想想对内容释放,首先就要判断引用计数的个数,每次析构的时候引用计数的数字都进行--操作,只要引用计数的值不为0,这里就不对内容进行释放。
直到引用计数被减减到0,这里才进行释放的操作。
讲解完了析构和释放时候,接下来就是shared_ptr的赋值的代码书写了。
这个地方就是拷贝操作代码的书写,在拷贝操作中要做的就是将被拷贝的变量的地址以及它的引用计数都交给要拷贝的变量。
同时因为进行了拷贝操作的缘故,因此这里的引用计数的数字要增加一。
写完了拷贝的代码之后,接下来就是shared_ptr赋值代码的书写了。
在这个地方赋值的内容和操作并不想拷贝一样,因为拷贝在这里只是创造一个变量,然后将原本被拷贝的变量的资源以及资源的引用计数赋值过去。
但是赋值操作并不一样,要赋值的变量中有原本的资源以及原本该资源的引用计数。
例如这里的两种情况,如果这个地方将sp5赋值给sp1,这个地方原先sp1指向的资源的引用计数就需要-1,如果没有-1操作,那么这里就只剩下sp3管理这块资源,sp3析构之后该资源的引用计数为1,造成该资源无法释放的错误,同理因为sp5赋值给sp1,所以下面的资源的引用计数个数要+1。
同时这里还存在另一个情况,如果这里将sp5赋值给sp1之后再将sp5也赋值给sp3,这里就会变成下面的资源的引用计数的个数为5,而且上边的资源的引用计数个数为0。
有因为引用计数个数为0,因此没有变量去管理这块资源,这里就要将其释放掉。
这里就是shared_ptr赋值的代码,首先就是用if语句进行判断,如果这个时候被赋值的资源的引用计数大于1,在进行--操作后不为0,那么这里就只是--计数,不对其内容进行释放。
如果--之后计数为0,那么就将该资源与它的计数一起释放掉,同时每析构一个对象都要进行赋值操作,且要对赋值的引用计数进行++操作。
但是这个操作并非就是shared_ptr的赋值操作的全部代码,在这里还有一种特殊的赋值情况,那就是自己赋值给自己。
像这里就是该资源剩下一个变量对其进行管理的情况,在这个地方如果想让sp6自己赋值给自己的话,按照上边书写的代码会出一个问题。
这里因为sp6是最后一个管理资源的变量,因此资源的引用计数是1,这里如果要进行赋值的话,首先就需要对引用计数进行--,而这里操作过后引用计数为0,该资源和引用计数都被提前释放掉了,后边的赋值都是无意义的行为。
要解决上边的问题也并不是什么难事,像这里如果想要实现自己给自己赋值不会出错的话,最优的方法就是在判断引用计数之前再加上一句if语句来判断,如果赋值变量的资源与被赋值变量的资源一样的话,这里就只需要直接返回即可。
以上就是shared_ptr的赋值与拷贝的代码实现。
循环引用与弱指针:
在上述讲解了一系列的智能指针,并且经过对比我们可以看出来shared_ptr智能指针相比较auto_ptr与unique_ptr智能指针要更加的全面,可以说没有什么明显的缺点所在。
但是shared_ptr并非完全没有缺点,那么shared_ptr智能指针的缺点是什么。
在没有学习智能指针之前,这里如果想借用结构体来构造一个节点的话,这个地方就需要用自己用new去构造,在释放析构的时候也需要自己书写delete操作。
而在学习了智能指针之后,我们会将这块资源用智能指针管控起来,智能指针不需要我们显示的释放,它们两者所承载的功能是一样的。
那么这里shared_ptr智能指针到底哪个有缺点呢?
从上面的这张图就能看出来智能指针shared_ptr的一个缺点,以往如果是正常使用结构体来构造节点的话,这里我们可以让一个节点与另一个节点收尾相连变成链表。
但是用shared_ptr构造的节点却是不能做到这一步,这里最主要的原因就是类型不匹配。
在这个地方sp2与sp1是自定义类型,sp1->_next和sp2->prev是内置类型。
并且这里解决这个问题的方法也是十分的简单,像这里因为类型不匹配,最简单的方法就是将结构体中的Node*更改为智能指针,这里代码就不会报错了。
但是这样子修改就又会引发一些问题,就类似这里终端的结果来看,这个地方只有构造函数而没有队友的析构释放。
并且就以上边图的情况来说,这里如果两个函数都屏蔽就会出现内存泄漏的问题,而只屏蔽一个或者两个都不屏蔽代码则会正常的构造与析构。
而这个问题也就被称为循环引用,而循环引用可以说是shared_ptr的死穴。
那么引用计数具体是如何操作的?
从上图我们来看,在这里就是shared_ptr中为什么会出现循环引用这样的致命的错误的原因。
根据上边进行讲解,原先sp1与sp2都指向了它们对应的节点,又因为它们都只有自己控制那块资源,所以它们的引用计数为1。
而接下来就是二者节点的链接操作,因为在代码那里我们将_next与_prev都改写为智能指针的形式,因此在它们指向的时候,资源的引用计数要进行+1的操作。
最后是将sp2与sp1先后析构,但是由于在第二步这里的引用计数已经变为2,因此sp1与sp2的析构只能使该资源的引用计数从2变成1,因此这里它也就不会释放资源,因为它释放的逻辑死循环了。
这里的死循环就类似上图,这个我们想对某个智能指针进行析构,就要它对应的节点进行析构,而且要析构节点的话,就需要另一个智能指针进行析构。
这样下来每一个节点或者智能指针需要析构的话,就需要另外一个智能指针或者节点被析构,两两相连,这里就形成了一个闭环,这也就是循环引用。
这里如果要解决循环引用的话,首先就是要判断这个地方是否会有循环引用的出现,并非每个使用shared_ptr的场景都会有循环引用的问题。
要是程序因为shared_ptr真的存在循环引用的问题,这里要解决的方法也是十分的简单,此处要做的就是将结构体中的智能指针shared_ptr换成另一个智能指针weak_ptr。
这里的weak_ptr是shared_ptr的小弟,它也被称为弱指针,这里也可以看出使用了weak_ptr之后这里程序也是正常的进行释放了。
这里的weak_ptr并不是RAII中的智能指针,weak_ptr诞生的意义就是用来解决shared_ptr中循环引用的问题,因此它不是单独使用的。
而且这里weak_ptr可以赋值给shared_ptr是因为在weak_ptr的底层中写有一个weak_ptr赋值给shared_ptr的重载。
此处weak_ptr解决shared_ptr的循环引用的原理就是不增加引用计数。
而要怎么看引用计数的多少,在这里shared_ptr提供了use_count来查看它自己的引用计数,从终端的结果来看,在赋值之前和赋值之后,节点sp1与sp2的引用计数都为1。
因此可以看出对比shared_ptr这里的weak_ptr赋值之后不增加引用计数,同时可以访问资源但是又不参与资源释放的管理。
在这里如果智能指针shared_ptr不想使用库中的shared_ptr而是想使用直接书写的话,在头文件的代码中也要进行一定的修改。
接下来就是看weak_ptr的代码了。
在这里书写weak_ptr的时候我们要知道weak_ptr不是传统的智能指针,它有智能指针的性质,但是并不是属于RAII中的智能指针。
这里的智能指针weak_ptr首先需要访问,所以私有的指针的必不可少的,又因为智能指针的性质,因此它需要像指针一样可以被访问。
接下来weak_ptr要支持一个无参的构造,但是它应该只是用来解决shared_ptr的循环引用的问题,因此它不支持指针,也不用写析构。
最后也是最重要的一个这里的weak_ptr要支持shared_ptr的赋值与构造,这里构造的写法就是将shared_ptr的指针交给weak_ptr,能让weak_ptr访问shared——ptr的资源,但是weak_ptr本身没有引用计数所以不参与引用计数。
然后就是赋值的代码,知了勇shared_ptr赋值给weak_ptr,但是weak_ptr不去参与释放。
同时在这里还需要处理一些小的失误,比如shared_ptr中因为_ptr是私有的缘故,这里shared_ptr就提供了get来使我们可以取出shared_ptr中的_ptr。
如果直接在weak_ptr中使用sp._ptr的话,这里代码是会报错处理的。
最后也能看得出来即使是我们自己书写的shared_ptr与weak_ptr也是能成功的解决shared_ptr造成的循环引用的问题。
这里终端也是成功的实现了两个智能指针。
定制删除器:
在上边书写自己的shared_ptr的时候,测试都是使用的一个简单的整形去测试,这是因为在我们的shared_ptr的底层的析构函数是写死的。
但是,这一段代码并不是完整的,在我们使用shared_ptr书写智能指针的时候并不是只能书写单一有一个整形或者字符的选项。
在智能指针shared_ptr中我们还能去new一个数组,又或者是malloc,而且这两种情况如果是用目前自己书写的share_ptr中是无法将其delete和释放掉的,这里编译器会发生报错。
像这里如果new一个[]数组,这里new的是[],但是delete并不是[]这个地方就不匹配,而不匹配讲究可能出现未知的不同的结果。
上图new了10个数据,但是最后进行delete操作时析构函数只执行一次,此处就会有另外9个对象不会被析构的问题。
而C++库中完了解决这个问题而引入了定制删除器,而要讲解定制删除器,这里就不得不提到以前学习过的知识——仿函数。
而基于上边的一系列原因,在这里函数增加了一个模板参数,在给资源的同时配给函数一个D del的仿函数,而这里的D可以将其理解为delete,也就是删除。
这就是这里传递仿函数的话,这个地方它就会用传递的仿函数进行释放,这里如果不传仿函数的话就默认为delete操作。
像这样我们可以在sp1的new后面书写仿函数,然后再上面在上边写仿函数的结构体,而且结构体里边的内容就是delete[]。
这样子原本sp1不加仿函数的话,new[]中的10个对象只能析构其中的一个,而加入了仿函数之后,因为结构体中并不是原先我们书写的shared_ptr操作delete单个字符或者整形,而仿函数中是delete[]操作,这里就能将new出来的数组全部析构掉。
同理,这里的malloc代码也能加入仿函数来实现数据的delete。
同时因为仿函数的增加,在智能指针不仅仅能管理new和delete的资源,即使是文件的open与close也是可以交给智能指针管理的。
而这种方式的本质也就是我们前面所说的定制删除器,而它的作用就是控制某些资源释放的一种方式,在这个地方就能不用担心释放的问题了,前提是要避开循环引用。
并且这里的定制删除器是针对智能指针shared_ptr与unique_ptr,其他的智能指针中并没有定制删除器的书写,那么在这里又该怎么去书写我们自己shared_ptr中的定制删除器?
如图所示就是定制删除器首先是仿函数,而它是在构造函数的时候把它写成模板。
在这个地方先增加一个模板,然后再原先的shared_ptr构造函数中增加一个模板参数。
但是在这时候如果我们想按照C++库中这样书写的话会遇到一个问题,在库中定制删除器是在构造函数的模板参数时候传递的而不是在类模板的参数传递的。
也就是说它并不是通过shared_ptr增加模板参数的操作。
而在传递了以后,想要控制某些资源释放的方法,在这个地方就需要去控制和修改析构函数中的delete的删除方式。
那么此处,析构函数要如何拿到构造函数类模板中模板参数的D呢。
在这里我们的模板参数并不是写在shared_ptr的位置,而是在构造函数的位置,这就会导致在构造函数可以使用,但是析构函数无法使用的问题。
在这个地方我们也不能使用老方法,在类模板的private使用类型D来定义一个成员的方法。因为这个地方D是构造函数的模板参数不是类模板的模板参数,因此这种方法是行不通的。
而解决这个问题有两种方法,其中一种方法是为类模板增加参数,另一种方法则是用于C++11的方法。
在这个地方C++11中采用的方式就是使用包装器进行包装,在出来我们没办法去确定D的类型,但是可以确定这里D的返回值类型是void类型同时也可以保证它的参数为T*。
基于上面的两大条件,哪怕不知道D的类型,此处也能通过包装器对其进行包装。
随着包装器的加入,这里就能对shared_ptr的析构进行书写了。
首先包装器的加入代码要进行一定的修改,这里首先要进行修改的就是shared_ptr的模板参数, 因为模板参数个数的增加,原本的缺省参数变成了半缺省参数,而半缺省参数只能从右往左缺省。
其次就是定义类型,然后shared_ptr中的delete也要进行修改。
但是这个问题解决完了之后,这里又会诞生出一个新的问题。
像这里的一段新代码,编译器就没办法成功的进行编译,而这里会出现这个情况最根本的问题就是没有给定制删除器。
这里问题出在包装器这个地方, 因为没有给值的原因,在这里包装器也不知道要去调用谁,因此在这个地方要给一个默认值。
这里解决的方法就是在包装器private定义的时候给予它一个删除单个数据的函数。
因为这里是包装器的缘故,因此在delete之前需要再增加[]与定制删除器的类型,也就是T*的参数,这样子如果这里没给值给包装器,这里就默认进行单个数据的删除。
这里就是定制删除器全部的代码以及功能的讲解。
内存泄漏:
在日常我们学习知识的时候都有多多少少提及到内存泄漏的问题,而智能指针可以说与内存泄漏问题相关。
那么什么是内存泄漏,内存泄漏的问题又是什么?
在C++中内存泄漏指在错误的使用或者疏忽下导致了某一块空间不需要使用了,但是没有进行释放。
而内存泄漏看起来是一个小问题,它就只是忘记释放资源了而且,但是其实内存泄漏所带来的危害是巨大的。
在我们写代码的过程中,资源不使用又不释放导致内存泄漏,每个地方的内存泄漏都占据一块空间,这样就会导致可用的内存越来越少,而可用的内存越来越少最后就会导致程序运行的越来越慢,以至于最后还可能出现卡死的情况。
接下来就来看一个最基本的内存泄漏的代码。
像这里我们new了一块空间之后,在编程结束的时候没有将其delete掉,这里就会有内存泄漏问题的出现。
而在日常中内存泄漏没有危害,像我们上边一个G一个G的泄漏是不会出现什么问题的,这是因为进程正常结束的时候资源是会释放的。
也就是说在日常情况下将一个资源new或者malloc之后,没有进行释放的话,编译器是会将其回收的。
但是这里还存在两种情况,第一种情况就是进程非正常结束,也就是所说的进程僵尸了,这种情况下资源就没有释放,还有一种情况就是服务器程序,它们的程序是长期运行的。
那么如何更好的去避免内存泄漏呢?
在C++中内存泄漏可以更广义的被称为资源泄漏。
而要更好的处理内存泄漏问题,这里有两种方式,其中一种方式叫做检测,在这个地方内存泄漏是存在一些检测工具的。
而那些检测工具就可以用来检测内存泄漏有无发生。
像这个地方Linux下的valgnnd,这个工具就可以处理内存泄漏的问题。
这里的操作就是将其下载下来并且安装,然后在Linux中运行程序的时候,在代码前边加上这个指令,这样子编译器内就会自己帮助我们进行检测。
而检测的结果也会明确的标志出来,比如程序的第几行出现了内存泄漏的问题。而它的底层原理是将申请的内存记录到某个容器里面去,如果释放的话就将记录进行删除,要是程序正常结束,资源还没有释放就说明有内存泄漏的问题。
但是上边的这种检测情况是在内存泄漏发生之后将其检测出来,真正的预防是要在内存泄漏之前就防止它的发生。
而提到防止内存泄漏的方法,这个地方就不得不提到另一个解决方法,那就是上边提到的智能指针。
这里如果正确的使用智能指针那几乎就不存在内存泄漏的情况,这就是智能指针能提前避免内存泄漏的原因。
综上所述,在C语言,Linux等编译器中内存泄漏都是非常常见的问题,而在这里解决内存泄漏有两种方案。
一种是事前预防型,类似智能指针,正确的书写智能指针可以避免内存泄漏的发生,另一种则是事后差错型,这里利用检测工具检查书写完的代码哪里会出现内存泄漏的问题之后加以修改。
但是归根到底内存泄漏的发生还是取决于个人,哪怕书写智能指针但是没有对其正确的书写还是会产生内存泄漏的问题。
智能指针以及内存泄漏代码:
SmartPtr.h文件
#pragma once
#include<iostream>
using namespace std;//namespace bit
//{
// template<class T>
// class auto_ptr
// {
// public:
// auto_ptr(T* ptr)
// :_ptr(ptr)
// {
//
// }
//
// ~auto_ptr()
// {
// cout << "delete:" << _ptr << endl;
// delete _ptr;
// }
//
// T& operator*()
// {
// return *_ptr;
// }
//
// T* operator->()
// {
// return _ptr;
// }
//
// auto_ptr(auto_ptr<T>& ap)
// : _ptr(ap._ptr)
// {
// ap._ptr = nullptr;
// }
//
//
// private:
// T* _ptr;
// };
//}//namespace bit
//{
// template<class T>
// class unique_ptr
// {
// public:
// unique_ptr(T* ptr)
// :_ptr(ptr)
// {
//
// }
//
// ~unique_ptr()
// {
// cout << "delete:" << _ptr << endl;
// delete _ptr;
// }
//
// T& operator*()
// {
// return *_ptr;
// }
//
// T* operator->()
// {
// return _ptr;
// }
//
// private:
// //unique_ptr(unique_ptr<T>& ap);
// unique_ptr(unique_ptr<T>& ap) = delete;
//
// unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
//
// private:
// T* _ptr;
// };
//}namespace bit
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}template<class D>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(new int(1)),_del(del){}~shared_ptr(){if(--(*_pcount) == 0){cout << "delete:" << _ptr << endl;//delete _ptr;_del(_ptr);delete _pcount; }}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}//unique_ptr(unique_ptr<T>& ap);shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){++(*_pcount);}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr == sp._ptr){return *this; }if (--(*_pcount) == 0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);return *this; }int use_count() const{return *_pcount;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };};template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
智能指针.c文件
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<iostream>
#include"SmartPtr.h"using namespace std;template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete:" << _ptr << endl; delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};int div()
{int a, b;cin >> a >> b;if (b == 0){throw invalid_argument("除0错误");}return a / b;
}void f()
{SmartPtr<pair<string, string>> sp1(new pair<string, string>{ "111","222" });div();SmartPtr<pair<string, string>> sp2(new pair<string, string>);div();SmartPtr<string> sp3(new string{ "xxx" });div();cout << *sp3 << endl; cout << sp1->first << endl;/*try {div();}catch (...){delete p1;cout << "delete:" << p1 << endl;throw;}delete p1;*/
}//int main()
//{
// try
// {
// f();
// }
// catch (const exception& e)
// {
// e.what();
// }
// f();
// return 0;
//}//int main()
//{
// SmartPtr<string> sp1(new string("xxxxx"));
// SmartPtr<string> sp2(new string("yyyyy"));
//
//
//
// return 0;
//}class A
{
public:A(int a = 0):_a(a){cout << "A(int a = 0)" << endl;}~A(){cout << "~A" << endl;}//private:int _a;
};
//
//int main()
//{
// bit::auto_ptr<A> ap1(new A(1));
// bit::auto_ptr<A> ap2(new A(2));
//
// bit::auto_ptr<A> ap3(ap1);
//
// ap1->_a++;
// ap3->_a++;
//
// return 0;
//}//int main()
//{
// bit::unique_ptr<A> up1(new A(1));
// bit::unique_ptr<A> up2(new A(2));
//
// /*bit::unique_ptr<A> up3(up1);*/
// up1 = up2;
//
// return 0;
//}//int main()
//{
// shared_ptr<A> sp1(new A(1));
// shared_ptr<A> sp2(new A(2));
//
// shared_ptr<A> sp3(sp1);
// sp1->_a++;
// sp3->_a++;
//
// cout << sp1->_a << endl;
//
// return 0;
//}//struct Node
//{
// A _val;
// shared_ptr<Node> _next;
// shared_ptr<Node> _prev;
//};
//
//int main()
//{
// shared_ptr<Node> sp1(new Node);
// shared_ptr<Node> sp2(new Node);
//
// sp1->_next = sp2;
// sp2->_prev = sp1;
//
// return 0;
//}//struct Node
//{
// A _val;
// bit::weak_ptr<Node> _next;
// bit::weak_ptr<Node> _prev;
//};
//
//int main()
//{
// bit::shared_ptr<Node> sp1(new Node);
// bit::shared_ptr<Node> sp2(new Node);
//
// cout << sp1.use_count() << endl;
// cout << sp2.use_count() << endl;
//
// sp1->_next = sp2;
// sp2->_prev = sp1;
//
// cout << sp1.use_count() << endl;
// cout << sp2.use_count() << endl;
//
// return 0;
//}
template<class T>
struct DeleteArray
{void operator()(T* ptr){delete[] ptr;}
};//int main()
//{
// std::shared_ptr<A> sp1(new A[10], DeleteArray<A>());
// std::shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });
// std::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE * ptr){ fclose(ptr); });
//
// return 0;
//}//int main()
//{
// bit::shared_ptr<A> sp1(new A);
//
// return 0;
//}int main()
{char* ptr = new char[1024 * 1024 * 1024];return 0;
}
结尾:
到这里我们的C++智能指针的学习板块就告一段落了,同时在智能指针的知识中我们还学习了智能指针可能会带来的循环引用和解决循环引用的弱指针,同时也学习了定制删除器跟内存泄漏的知识,这些知识可以说都或多或少的涉及到了智能指针。最后希望这篇博客能有所帮助。