前言
大家好吖,欢迎来到 YY 滴C++系列 ,热烈欢迎! 本章主要内容面向接触过C++的老铁
主要内容含:
一.前言:智能指针出现解决内存泄漏问题
通俗语言介绍:
- 在抛异常捕获异常的过程中 ,有可能会影响 执行流 ;
- try+catch 程序的流程是:运行到try块中,如果有异常抛出,则转到catch块去处理。然后执行catch块 后面 的语句
int main() { try { pair<string, string>* p1 = new pair<string, string>; f();//如果在f函数中抛出异常,会直接跳到catch块后面的语句,导致p1资源没有被释放,造成内存泄漏 delete p1; } catch (const exception& e) { cout << e.what() << endl; } //捕获异常后跳转到的位置 return 0; }
二.RAII
通俗语言介绍:
- 设置一个类 ,可以构造和析构,交给这个类的对象来 管理 指针。保证其一定会释放资源,不会内存泄漏。
简介:
- RAII(Resource Acquisition Is Initialization):即 资源获取时即初始化
- RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
存、文件句柄、网络连接、互斥量等等)的简单技术。 - 在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。
- 借此,我们实际上把管理一份资源的 责任 托管给了一个对象。
RALL的两大好处:
- 不需要显式地释放资源
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
三.实现一个简单的智能指针
- 一个智能指针需要具备以下两种功能
- RAII管控资源释放 (资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源),即有构造有析构
- 像指针一样使用 ,即有 解引用功能 和 “->”功能
- 为了能够匹配更多类型,我们可以采用类模板的方式处理
template<class T> class SmartPtr { public: // RAII // 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源 // 1、RAII管控资源释放 // 2、像指针一样 //RAII 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() { // 21:15继续 SmartPtr<pair<string, string>> sp1(new pair<string, string>("1111", "22222")); //div(); SmartPtr<pair<string, string>> sp2(new pair<string, string>); SmartPtr<pair<string, string>> sp3(new pair<string, string>); SmartPtr<string> sp4(new string("xxxxx")); //像指针一样 cout << *sp4 << endl; cout << sp1->first << endl; cout << sp1->second << endl; div(); //有了智能指针来管理,不需要我们手动释放资源 //delete p1; //cout << "delete:" << p1 << endl; } int main() { try { f(); } catch (const exception& e) { cout << e.what() << endl; } return 0; }
四.简单的智能指针会遇到的问题:(浅)拷贝问题
- 当我们用三设计的智能指针进行拷贝时,我们会发现程序会出现一个问题
- 因为我们没有写默认的拷贝,所以类会生成一个默认的拷贝(浅)拷贝
- 浅拷贝只是把指针指向那块空间,因此析构时便会对同一块空间析构两次,且原来开的空间没有释放造成内存泄漏
函数运行结果如下所示:可以发现其析构了两次
int main() { SmartPtr<string> sp1(new string("xxxxx")); SmartPtr<string> sp2(new string("yyyyy")); sp1 =sp2; return 0; }
- 让我们接下来看看boost库和后来的C++11是如何解决这个问题的
五.库中的几种智能指针
※几种智能指针简述
1.智能指针演变史
- 最初的智能指针是C++98中的auto_ptr,但它有很明显的缺陷(下文会介绍)
- 主流智能指针(unique,shared,weak)一开始是由boost库中创建,后面被C++11沿用
- scpoe-ptr换成了auto_ptr,两者本质差不多
2.智能指针简述
- auto_ptr:(管理权直接转移,导致被拷贝对象悬空,访问就会出问题)(auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr)
- unique_ptr:(很粗暴,直接不允许拷贝,不需要拷贝的场景建议使用)
- share_ptr:(引用计数支持拷贝,需要拷贝的场景使用;要小心构成【循环引用】导致内存泄漏)
- weak_ptr:(专门解决share_ptr【循环引用】问题)
1)auto_ptr
class A { public: A(int a = 0) :_a(a) { cout << "A(int a = 0)" << endl; } ~A() { cout << this; cout << " ~A()" << endl; } //private: int _a; }; int main() { // C++98 一般实践中,很多公司明确规定不要用这个 auto_ptr<A> ap1(new A(1)); auto_ptr<A> ap2(new A(2)); // 管理权转移,拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象 // 隐患:导致被拷贝对象悬空,访问就会出问题 auto_ptr<A> ap3(ap1); // 崩溃 //ap1->_a++; ap3->_a++; return 0; }
2)unique_ptr
- unique_ptr的实现原理:简单粗暴的防拷贝
- 顾名思义:unique-唯一的
class A { public: A(int a = 0) :_a(a) { cout << "A(int a = 0)" << endl; } ~A() { cout << this; cout << " ~A()" << endl; } //private: int _a; }; int main() { // C++11 简单粗暴,不让拷贝 unique_ptr<A> up1(new A(1)); unique_ptr<A> up2(new A(2)); unique_ptr<A> up3(up1);//无法拷贝 return 0; }
3)shared_ptr
引入:
- 我们如果在拷贝时直接让他指向资源,那么当程序结束时就会进行两次析构; 如果我们设置一个计数 , 控制 这个析构过程;问题就解决了
- C++11中开始提供更靠谱的并且 支持拷贝 的shared_ptr
- shared_ptr的原理:是通过 引用计数 的方式来实现多个shared_ptr对象之间 共享 资源
- shared_ptr在其内部, 给每个资源都维护了着一份计数 ,用来记录该份资源被几个对象共享;
- 在对象被销毁时(也就是析构函数调用),,就说明自己不使用该资源了,对象的引用计数减一;
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了;
- 一个基本shared_ptr的实现如下所示
template<class T> class shared_ptr { public: // RAII // 像指针一样 shared_ptr(T* ptr = nullptr) :_ptr(ptr) ,_pcount(new int(1))//动态开辟,不可以静态,因为有可能有多个智能指针类管理不同的资源 {} ~shared_ptr() { if (--(*_pcount) == 0) //析构前要判断引用计数 { cout << "delete:" << _ptr << endl; delete _ptr; delete _pcount; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } // sp3(sp1) shared_ptr(const shared_ptr<T>& sp) //拷贝时,让新的对象的指针成为这个智能指针类管理的指针 :_ptr(sp._ptr) ,_pcount(sp._pcount) { ++(*_pcount); } //解决自己拷贝自己时,引用计数会增加的问题——————>加入一个判断 // sp1 = sp1 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; } //涉及到后面与weak_ptr的搭配问题,不让他直接指向资源(防止引用计数增加) int use_count() const { return *_pcount; } T* get() const { return _ptr; } private: T* _ptr; int* _pcount; //动态的引用计数 };
class A { public: A(int a = 0) :_a(a) { cout << "A(int a = 0)" << endl; } ~A() { cout << this; cout << " ~A()" << endl; } //private: int _a; }; int main() { // C++11 shared_ptr<A> sp1(new A(1)); shared_ptr<A> sp3(sp1);//拷贝以后 sp1->_a++; sp3->_a++;//程序不会崩溃 return 0; }
- 引用计数的实现如下图所示:
六.解决shared_ptr的循环引用问题而生:weak_ptr
1)了解【循环引用】问题
- 我们在使用share_ptr时,有时会遇到以下这种场景
- 例如:双向链接两个节点
template<class T> class shared_ptr { public: // RAII // 像指针一样 shared_ptr(T* ptr = nullptr) :_ptr(ptr) ,_pcount(new int(1))//动态开辟,不可以静态,因为有可能有多个智能指针类管理不同的资源 {} ~shared_ptr() { if (--(*_pcount) == 0) //析构前要判断引用计数 { cout << "delete:" << _ptr << endl; delete _ptr; delete _pcount; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } // sp3(sp1) shared_ptr(const shared_ptr<T>& sp) //拷贝时,让新的对象的指针成为这个智能指针类管理的指针 :_ptr(sp._ptr) ,_pcount(sp._pcount) { ++(*_pcount); } //解决自己拷贝自己时,引用计数会增加的问题——————>加入一个判断 // sp1 = sp1 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; } //涉及到后面与weak_ptr的搭配问题,不让他直接指向资源(防止引用计数增加) T* get() const { return _ptr; } private: T* _ptr; int* _pcount; //动态的引用计数 }; struct Node { A _val; //不能如此使用,因为后面要让_next&_prev指向的都是智能指针的对象,sp1->_next = sp2;会出现类型不匹配的问题 //Node* _next; //Node* _prev; //要把节点指针也用智能指针包装一下才可以 shared_ptr<Node> _next; shared_ptr<Node> _prev; }; int main() { // 循环引用 shared_ptr<Node> sp1(new Node); shared_ptr<Node> sp2(new Node); cout << sp1.use_count() << endl;//1 cout << sp2.use_count() << endl;//1 sp1->_next = sp2; sp2->_prev = sp1; cout << sp1.use_count() << endl;//1 cout << sp2.use_count() << endl;//1 //进入死循环 return 0; }
- 我们要注意到,因为为了能
sp1->_next
能够实现,我们让Node的_next&_prev指针都由智能指针托管 - 所以当
sp1->_next
或者sp1->_prev
时,本质上是一种拷贝,会导致资源的引用计数增加
程序执行以后,我们会发现析构时,引用计数仍然为1,变成死循环了,具体过程如下图所示
2)利用weak_ptr 解决【循环引用】问题
- 在shared_ptr中封装一层函数
use_count()
&get()
- 设置一个weak_ptr,在拷贝构造&赋值运算符重载环节引入上一步骤中封装的函数
- 原理:
- 在设置Node节点的指针时,用weak_ptr来处理, 使得它和share_ptr是同一类型 ;
- weak_ptr 不增加引用计数 ,可以访问资源,不参与资源释放的管理
template<class T> class shared_ptr { //...略去的内容 int use_count() const { return *_pcount; } T* get() const { return _ptr; } private: T* _ptr; int* _pcount; //动态的引用计数 }; struct Node { A _val; //shared_ptr<Node> _next; //shared_ptr<Node> _prev; weak_ptr<Node> _next; weak_ptr<Node> _prev; // weak_ptr不参与资源释放的管理的RAII智能指针,专门用来解决shared_ptr循环引用问题 // weak_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; }; 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; }
七.shared_ptr的【定制删除器】:管理不同方式产生的对象
引入:
- 我们在查看智能指针的文档时,会发现图中这种形式,这就是库里的定制删除器
- 如果不是new出来的对象如何通过智能指针管理呢?定制删除器就是用来解决这个问题
- 我们 只要往后面传一个可调用对象(删除方法)即可 : 仿函数(函数对象),lambda,函数指针都可以作为参数
template<class T> struct DeleteArray { void operator()(T* ptr) { delete[] ptr; } }; int main() { //仿函数 shared_ptr<A> sp1(new A[10], DeleteArray<A>()); //lambda表达式 shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); }); shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) { fclose(ptr); }); //默认是delete shared_ptr<A> sp4(new A); return 0; }