ToB企服应用市场:ToB评测及商务社交产业平台

标题: 【C++】C++的内存处置惩罚 --- 智能指针 [打印本页]

作者: 立山    时间: 2024-8-13 03:57
标题: 【C++】C++的内存处置惩罚 --- 智能指针

   只有经历地狱般的锻炼,      才能练出创造天堂的力量 ;        只有流过血的手指,          才能弹出世间的绝唱 。             --- 泰戈尔 ---                

  
1 前言

我们往返首一下在学习异常机制中遇到的一种标题:在try catch语句中,如果我们开辟了一段空间,但是发生了异常,会直接终止掉函数栈桢,导致内存泄漏标题。以是此时就要在catch语句中举行一个特殊处置惩罚。如果我们开辟了多段空间,那么这个操纵就会变得更加复杂:假如new失败了,就会直接返回到上层的catch语句,也导致了内存泄漏标题!利用传统是异常机制来办理标题会产生大量冗余的语句 — 大量的try catch嵌套!
为相识决这个标题,可以利用智能指针!可以简朴的来举行办理!
2 智能指针

2.1 什么是智能指针

智能指针类似lock_guard,是对指针的封装,可以实如今超出生命周期之后主动销毁的功能!
  1. void func()
  2. {
  3.         int* p1 = new int[10];
  4.         int* p2 = nullptr;
  5.         try
  6.         {
  7.                 p2 = new int[20];
  8.                 try
  9.                 {
  10.                         double a, b;
  11.                         cin >> a >> b;
  12.                         Division(a, b);
  13.                 }
  14.                 catch (...)
  15.                 {
  16.                         delete[] p1;
  17.                         cout << "delete: p1" << endl;
  18.                         delete[] p2;
  19.                         cout << "delete: p2" << endl;
  20.                         throw ;
  21.                 }
  22.         }
  23.         catch(...)
  24.         {
  25.                 delete[] p1;
  26.                 cout << "delete: p1" << endl;
  27.                 throw;
  28.         }
  29.        
  30.         delete[] p1;
  31.         cout << "delete: p1" << endl;
  32.         delete[] p2;
  33.         cout << "delete: p2" << endl;
  34.         return;
  35. }
复制代码
在这个程序中,开辟空间和销毁空间是一个重要标题,为了防止被抛出异常就直接销毁堆栈,就要设置多重的try catch来包管不会发生内存泄漏!对于这样的标题,我们可以设计一个smartptr类来资助我们办理!
  1. class SmartPtr
  2. {
  3. public:
  4.         SmartPtr(int* ptr)
  5.                 :_ptr(ptr)
  6.         {}
  7.         ~SmartPtr()
  8.         {
  9.                 delete[] _ptr;
  10.                 cout << "delete!" << _ptr << endl;
  11.         }
  12. private:
  13.         int* _ptr;
  14. };
复制代码
这样一个类包装了一个指针,我们不需要在显式delete了,只要生命周期结束,就会主动释放空间!
这样在开辟空间时,就直接举行构造不就好了!这样就直接避免了复杂嵌套的try catch语句!
  1. void func()
  2. {
  3.         SmartPtr sp1(new int[10]);
  4.         SmartPtr sp2(new int[20]);
  5.         double a, b;
  6.         cin >> a >> b;
  7.         Division(a, b);
  8.         return;
  9. }
复制代码
再也不用担心忘记释放开辟的空间了!内存泄漏标题直接远去了~
我们把这种封装称之为RAII:
RAII(Resource Acquisition Is Initialization 资源请求立即初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络毗连、互斥量等等)的简朴技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有用,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大利益:
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此AutoPtr模板类中还得需要将* ->重载下,才可让其像指针一样去利用!还需要举行一个拷贝构造的特殊处置惩罚,否则就会出现对同一片地址析构两次的场景
2.2 C++库中的智能指针

在C++memory库中有以下几种智能指针:

我们来看auto_ptr是怎样办理拷贝标题标:

也就是说auto_ptr支持两个对象指向同一片空间!通过拷贝时转移管理权来办理这种析构多次的标题(类似移动构造)。但是这样的处置惩罚方式实际上是很不合理的!sp1并不是一个将亡值,sp2凭什么将sp1的资源转移走!?“我还活着了 , 怎么就把我埋了!”,在接下来代码中,如果我们再次调用了sp1就会直接导致程序的瓦解!以是这样的设计是一个失败的设计!
以是auto_ptr尽量就不要举行利用!
以是auto_ptr尽量就不要举行利用!
以是auto_ptr尽量就不要举行利用!
在C++11中到场了shared_ptr unique_ptr weak_ptr ,一般建议利用unique+ptr 和 shared_ptr。来看一下他们支持什么操纵:


2.3 循环指向标题与weak_ptr

我们一般保举利用shared_ptr,其独有的引用计数机制,极大程度复原了指针的实际用法,而且能做到RAII技术!
但是,shared_ptr存在一个标题:循环指向标题!这种标题主要出如今循环链表中,每个节点有两个指针,分别指向前一个节点和后一个节点。当我们有两个节点时,我们都利用shared_ptr举行包装管理:
  1. struct Node
  2. {
  3.         bit::shared_ptr<Node> _next;
  4.         bit::shared_ptr<Node> _prev;
  5.         ~Node()
  6.         {
  7.                 cout << "~Node()" << endl;
  8.         }
  9. };
  10. int main()
  11. {
  12.         bit::shared_ptr<Node> sp1(new Node);
  13.         bit::shared_ptr<Node> sp2(new Node);
  14.         sp1->_next = sp2;
  15.         sp2->_prev = sp1;
  16.         return 0;
  17. }
复制代码
这样按理说每个节点的引用计数都为2(自身 + 别的节点中的智能指针),在程序结束运行时,会调用sp1 sp2的析构函数,这样会让其引用计数变为1。接下来就是复杂的标题了,由于刚才并没有让引用计数变为0,两个节点中的的_next; _prev;都还托管着数据,但是他们两个谁先析构呢?这类似经典的先有鸡 先有蛋标题,这就是循环指向标题!
办理这个标题单凭shared_ptr是没有办法办理的,这里就要引入weak_ptr了:

weak_ptr并不支持直接来举行管理指针资源,不支持RAII。但支持无参构造和拷贝构造,专门用来辅助办理shared_ptr的循环指向标题!我们只需要将Node里面的指针利用weak_ptr来举行托管就可以了:
  1. struct Node
  2. {
  3.         weak_ptr<Node> _next;
  4.         weak_ptr<Node> _prev;
  5.         ~Node()
  6.         {
  7.                 cout << "~Node()" << endl;
  8.         }
  9. };
  10. int main()
  11. {
  12.         shared_ptr<Node> sp1(new Node);
  13.         shared_ptr<Node> sp2(new Node);
  14.         sp1->_next = sp2;
  15.         sp2->_prev = sp1;
  16.         return 0;
  17. }
复制代码
因为weak_ptr本质赋值或拷贝时,只指向资源,不会增长引用计数!以是就不会造成先有鸡先有蛋的标题!
2.4 自定义删除器

智能指针内部还支持自定义删除器,因为在构造时并不能包管默认析构可以释放掉我们开辟的空间,比如
以是为了更是适配内存管理的多样性,智能指针支持自定义删除器,即支持用户显式传递删除方法!

这里可以利用仿函数来举行传递,但是在C++11之后利用lambda表达式更加简约直观!
  1. int main()
  2. {
  3.         shared_ptr<A> sp1(new A(1 , 1));
  4.         shared_ptr<A[]> sp2(new A[10]);
  5.         shared_ptr<FILE> sp3(fopen("file.txt", "w"), [](FILE* sp) { fclose(sp); });
  6.         shared_ptr<int> sp4( (int*)malloc(4) , [](int* sp) { free(sp); });
  7.         shared_ptr<A> sp5(new A[10], [](A* sp) {delete[] sp; });
  8.         return 0;
  9. }
复制代码
这样就办理了不同范例指针释放方式不同等的标题!
3 手搓shared_ptr

我们来实践一下shared_ptr,其在面试中经常会考到,这智能指针的主要头脑是RAII,将指针封装起来,包管其在生命周期内存在,离开生命周期就主动释放掉!
3.1 框架搭建

起首智能指针内部需要一个指针变量来储存数据。重要的是怎样将引用计数到场其中,如果直接利用一个int count肯定是不可的,这样每个对象都有自己的count,无法做到引用计数的功能。如果利用静态变量,那么全部的类对象只有一个计数,这样肯定也是不可以的!那么要怎样办理这个标题呢?为引用计数单独开辟一块空间,举行拷贝的时候就将这个空间举行传值,这样全部举行拷贝的对象都可以读取到同一个引用计数的数据!
构造函数可以直接写出来,析构就在引用计数为0的时候举行释放空间!
  1.         template< class T>
  2.         class shared_ptr
  3.         {
  4.         public:
  5.                 //默认构造函数
  6.                 shared_ptr(T* ptr = nullptr)
  7.                         :_ptr(ptr),
  8.                         _pcount(new int(1))
  9.                 {
  10.                 }
  11.                 ~shared_ptr()
  12.                 {
  13.                         release();
  14.                 }
  15.                 void release()
  16.                 {
  17.                         if (--(*_pcount) == 0)
  18.                         {
  19.                                 //最后管理的一个对象 , 释放资源
  20.                                 delete _ptr;
  21.                                 delete _pcount;
  22.                         }
  23.                 }
  24.         private:
  25.                 //内部指针
  26.                 T* _ptr;
  27.                 //引用计数
  28.                 int* _pcount;
  29.         };
复制代码
3.2 拷贝构造和赋值重载

这里最为重要的就是这个拷贝构造和赋值重载怎样举行誊写!我们需要模拟到和原生指针一样,可以让不同的指针对象指向同一块空间 ,而且不能发生重复析构的标题:
  1. //拷贝构造
  2. shared_ptr(const shared_ptr<T>& sp)
  3.         :_ptr(sp._ptr),
  4.         _pcount(sp._pcount)
  5. {
  6.         (*_pcount)++;
  7. }
  8. shared_ptr<T>&  operator=(const shared_ptr<T>& sp)
  9. {
  10.         if( _ptr == sp._ptr)
  11.         {
  12.                 return *this;
  13.         }
  14.         this->release();
  15.         _ptr = sp._ptr;
  16.         _pcount = sp._pcount;
  17.         (*_pcount)++;
  18.         return *this;
  19. }
复制代码
3.3 自定义删除器

起首为了适配自定义删除器,我们需要多加一个成员变量_del,利用function包装器举行包装,可以省去很多不必要的操纵!
成员变量添加:
  1. std::function<void(T*)> _del = [](T* p){ delete p; } ;
复制代码
这个包装器就是用来包装删除器的,到场了删除器,我们就要再写一个单独的构造函数来满足:
  1.                 template<class D>
  2.                 shared_ptr(T* ptr = nullptr , D del = [](T* p) { delete p; })
  3.                         : _ptr(ptr),
  4.                         _pcount(new std::atomic<int>(1)),
  5.                         _del(del)
  6.                 {
  7.                 }
复制代码
这样在显式调用构造的时候就可以举行自定义删除器的添加了:
  1.         bit::shared_ptr<FILE> sp3(fopen("file.txt", "w"), [](FILE* sp) { fclose(sp); });
  2.         bit::shared_ptr<int> sp4( (int*)malloc(4) , [](int* sp) { free(sp); });
  3.         bit::shared_ptr<A> sp5(new A[10], [](A* sp) {delete[] sp; });
复制代码
3.4 功能函数

为了让shared_ptr可以有原生指针的利用方法,我们需要对* ->举行一个重载!这我们已经很熟悉不过了!
然后就是设计一个get()和use_count()函数!都很简朴!
  1. T* get()
  2. {
  3.         return _ptr;
  4. }
  5. T& operator*()
  6. {
  7.         return *_ptr;
  8. }
  9. T* operator->()
  10. {
  11.         return _ptr;
  12. }
  13. int use_count()
  14. {
  15.         return *(_pcount);
  16. }
复制代码
3.4 多线程下的特殊处置惩罚

上面已经实现了正常情况下的智能指针的利用,我们来看多线程情况下会不会出现标题。
在下面的程序中,我们设置了三个线程同时对sp1内部的链表举行尾插,尾插是临界的,我们用锁举行保护。

我们用锁举行了保护,但是照旧出现了错误!
为什么会出现这样的标题?起首我们分析一下临界区,在share_ptr中引用计数是临界的!为什么呢?因为引用计数的操纵++ -- 是非原子的!多个线程中我们不停举行copy拷贝,会对引用计数不停举行++--,导致了标题!为了从根本上办理这个标题,我们就要包管操纵是原子的!我们可以在类中到场一个锁来包管++--中举行保护。但是最直接的就是将引用计数酿成原子的就可以了!
  1.                 //引用计数
  2.                 std::atomic<int>* _pcount;
复制代码
这样就可以包管拷贝和析构的时候就是原子的了就不会出现标题了!!!就可以包管线程安全了!
注意我将shared_ptr美满之后:
4 内存泄漏

最后我们往返首一下内存泄漏标题:

对于C++来说,内存泄漏是很严峻的标题!C++没有和JAVA的垃圾回收机制。
在正常的一个程序中,内存泄漏实在影响并不大,我们开辟一段空间,如果没有释放,在进程结束的时候也会被释放掉,因为我们开辟的空间都是虚拟内存,进程结束之后会把虚拟地址一并收拾带走。就怕进程异常结束,酿成僵尸进程挂起,此时虚拟地址和物理内存依然存在映射,此时就完蛋了。再加上如果是恒久运行的代码,内存泄漏的不停积累会导致内存空间越来越小!
C/C++程序中一般我们关心两种方面的内存泄漏:
内存泄漏可以通过第三方库来举行检测,当时这些并不是很好用,而且在实际工作中,编译运行一次程序可能需要很长时间,那么通过第三方库来检测是很费事的!以是尽量在利用中就要避免内存泄漏的标题:
总而言之:
内存泄漏非经常见,办理方案分为两种:1、事前防备型。如智能指针等。2、过后查错型。如泄漏检测工具。
Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4