十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

c++异常和智能指针

c++异常和智能指针 一异常1异常概念1.1什么是异常异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理异常使得我们能够将问题的检测与解决问题的过程分开程序的一部分负责检测问题的出现然后解决问题的任务传递给程序的另一部分检测环节无须知道问题的处理模块的所有细节。C 语言主要通过错误码的形式处理错误错误码本质就是对错误信息进行分类编号拿到错误码以后还要去查询错误信息比较麻烦。异常时抛出一个对象这个对象可以携带更全面的各种信息。1.2异常的抛出和捕获程序出现问题时我们通过抛出 (throw) 一个对象来引发一个异常该对象的类型以及当前的调用链决定了应该由哪个 catch 的处理代码来处理该异常。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容程序的抛出异常部分告知异常处理部分到底发生了什么错误。当 throw 执行时throw 后面的语句将不再被执行。程序的执行从 throw 位置跳到与之匹配的 catch 模块catch 可能是同一函数中的一个局部的 catch也可能是调用链中另一个函数中的 catch控制权从 throw 位置转移到了 catch 位置。这里还有两个重要的含义1、沿着调用链的函数可能提早退出。2、一旦程序开始执行异常处理程序沿着调用链创建的对象都将销毁。抛出异常对象后会生成一个异常对象的拷贝因为抛出的异常对象可能是一个局部对象所以会生成一个拷贝对象这个拷贝的对象会在 catch 子句后销毁。这里的处理类似于函数的传值返回1.3栈展开抛出异常后程序暂停当前函数的执行开始寻找与之匹配的 catch 子句首先检查 throw 本身是否在 try 块内部如果在则查找匹配的 catch 语句如果有匹配的则跳到 catch 的地方进行处理。如果当前函数中没有 try/catch 子句或者有 try/catch 子句但是类型不匹配则退出当前函数继续在外层调用函数链中查找上述查找的 catch 过程被称为栈展开。如果到达 main 函数依旧没有找到匹配的 catch 子句程序会调用标准库的 terminate 函数终止程序。如果找到匹配的 catch 子句处理后catch 子句代码会继续执行。#includeiostream using namespace std; double Divide(int a, int b) { try { // 当b 0时抛出异常 if (b 0) { string s(Divide by zero condition!); throw s; } else { return ((double)a / (double)b); } } catch (int errid) { cout errid endl; } return 0; } void Func() { int len, time; cin len time; try { cout Divide(len, time) endl; } //catch (const char* errmsg) catch (const string errmsg) { cout errmsg endl; } cout __FUNCTION__ : __LINE__ 行执行 endl; } int main() { while (1) { try { Func(); } catch (const string errmsg) { cout errmsg endl; } } return 0; }2异常规范对于用户和编译器而言预先知道某个程序会不会抛出异常大有裨益知道某个函数是否会抛出异常有助于简化调用函数的代码。C98 中函数参数列表的后面接 throw ()表示函数不抛异常函数参数列表的后面接 throw (类型 1, 类型 2...) 表示可能会抛出多种类型的异常可能会抛出的类型用逗号分割。C98 的方式这种方式过于复杂实践中并不好用C11 中进行了简化函数参数列表后面加 noexcept 表示不会抛出异常啥都不加表示可能会抛出异常。编译器并不会在编译时检查 noexcept也就是说如果一个函数用 noexcept 修饰了但是同时又包含了 throw 语句或者调用的函数可能会抛出异常编译器还是会顺利编译通过的 (有些编译器可能会报个警告)。但是一个声明了 noexcept 的函数抛出了异常程序会调用 terminate 终止程序。noexcept (expression) 还可以作为一个运算符去检测一个表达式是否会抛出异常可能会则返回 false不会就返回 true。3异常安全问题异常抛出后后面的代码就不再执行前面申请了资源 (内存、锁等)后面进行释放但是中间可能会抛异常就会导致资源没有释放这里由于异常就引发了资源泄漏产生安全性的问题。中间我们需要捕获异常释放资源后面再重新抛出当然后面智能指针章节讲的 RAII 方式解决这种问题是更好的。其次析构函数中如果抛出异常也要谨慎处理比如析构函数要释放 10 个资源释放到第 5 个时抛出异常则也需要捕获处理否则后面的 5 个资源就没释放也资源泄漏了。《Effctive C》第 8 个条款也专门讲了这个问题别让异常逃离析构函数。#includeiostream using namespace std; double Divide(int a, int b) { // 当b 0时抛出异常 if (b 0) { /*throw Division by zero condition!;*/ throw std::runtime_error(除零错误); } return (double)a / (double)b; } void Func() { // 这⾥可以看到如果发⽣除0错误抛出异常另外下⾯的array没有得到释放。 // 所以这⾥捕获异常后并不处理异常异常还是交给外层处理这⾥捕获了再 // 重新抛出去。 int* array new int[10]; try { int len, time; cin len time; cout Divide(len, time) endl; } catch (const char* s) { // 捕获异常释放内存 cout delete [] array endl; delete[] array; throw; // 异常重新抛出捕获到什么抛出什么 } cout delete [] array endl; delete[] array; } int main() { try { Func(); } //catch (const char* errmsg) //{ // cout errmsg endl; //} catch (const exception e) { cout e.what() endl; } //catch (...) //{ // cout Unkown Exception endl; //} return 0; }二智能指针1智能指针使用场景#includeiostream #includestring using namespace std; double Divide(double x, double y) { try { if (y 0) { string s(divide-by-zero error); throw s; } } catch (const char* except) { cout except endl; } return x / y; } void func() { int* array1 new int[10]; int* array2 new int[20]; try { double a 0, b 0; cin a b; cout Divide(a, b) endl; } catch(int except) { cout except endl; } delete[] array1; array1 nullptr; delete[] array2; array2 nullptr; } int main() { while (1) { try { func(); } catch (const string except) { cout except endl; } } return 0; }通过以上代码new之后我们也delete了但是如果除数为0也就是y0 为真就会抛异常会去找抛出的 string对象 s 与该对象类型匹配且离抛出异常位置最近的那一个catch最合适的是main函数里的catch(const string except)所以抛出异常后就会退出当前函数最后匹配到main函数里的catch跳转至此处不会在执行delete了就会导致内存泄漏。使用智能指针之前#includeiostream #includestring #includeexception using namespace std; double Divide(double a, double b) { if (b 0) { throw string(divide-by-zero error); //throw std::runtime_error(divide-by-zero error); } return a / b; } void func() { //如果这里发生除零错误抛异常下面两个动态开辟的数组就不会释放 //所以这里捕获异常之后并不处理异常除零异常交给上层去处理这里 //捕获了再重新抛出去。但是如果array2动态开辟的时候抛异常呢还需 //要套一层捕获释放逻辑这里更好的解决方案是智能指针。不然现在这样代码很挫。 int* array1 new int[10]; int* array2 new int[10]; try { double a 0, b 0; cin a b; cout Divide(a, b) endl; } catch (...) { cout delete array1: array1 endl; cout delete array2: array2 endl; delete[] array1; delete[] array2; throw;//异常重新抛出捕获到什么抛出什么 } cout delete array1: array1 endl; cout delete array2: array2 endl; delete[] array1; delete[] array2; } int main() { while (1) { try { func(); } catch (const string str) { cout str endl; } catch (const exception e) { cout e.what() endl; } catch (...) { cout unknown error endl; } } return 0; }RAII 是 Resource Acquisition Is Initialization 的缩写他是一种管理资源的类的设计思想本质是一种利用对象生命周期来管理获取到的动态资源避免资源泄漏这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII 在获取资源时把资源委托给一个对象接着控制对资源的访问资源在对象的生命周期内始终保持有效最后在对象析构的时候释放资源这样保障了资源的正常释放避免资源泄漏问题。智能指针类除了满足 RAII 的设计思路还要方便资源的访问所以智能指针类还会像迭代器类一样重载 operator*/operator-/operator [] 等运算符方便访问资源使用自定义智能指针之后#includeiostream #includestring #includeexception using namespace std; class Date { public: Date(int year 0, int month 0, int day 0) :_year(year) ,_month(month) ,_day(day) { cout Date(int year 0, int month 0, int day 0) endl; } ~Date() { _year _month _day 0; cout ~Date() endl; } int _year; int _month; int _day; }; templateclass T class smart_pointer { public: smart_pointer(T* ptr) :_ptr(ptr) { cout smart_pointer(T* ptr)----构造函数 endl; } T operator*() { return *_ptr; } T* operator-() { return _ptr; } T operator[](int i) { return *(_ptr i); } ~smart_pointer() { cout ~smart_pointer()----析构函数 endl; delete[] _ptr; } private: T* _ptr; }; double Divide(double a, double b) { if (b 0) { throw string(divide-by-zero error); //throw std::runtime_error(divide-by-zero error); } return a / b; } void func() { int* arr1 new int[10]; int* arr2 new int[10]; Date* pd1 new Date[10]; *arr1 10; arr1[5] 7; pd1-_month 15; smart_pointerint array1 new int[10]; smart_pointerint array2 new int[10]; smart_pointerDate pd2 new Date[10]; *array1 10; array1[5] 7; pd2-_month 15; try { double a 0, b 0; cin a b; cout Divide(a, b) endl; } catch (...) { throw;//异常重新抛出捕获到什么抛出什么 } } int main() { while (1) { try { func(); } catch (const string str) { cout str endl; } catch (const exception e) { cout e.what() endl; } catch (...) { cout unknown error endl; } } return 0; }但是这个自定义智能指针有个很严重的问题就是不能拷贝因为当前的拷贝我们没有写就会使用编译器默认生成的拷贝编译器默认生成的拷贝是浅拷贝值拷贝会让两个指针指向同一块空间当我们改变其中一个指针指向的某个值时另一个指针指向的值也会跟着改变后面析构的时候这块空间会被释放两次程序直接崩溃。2c标准库智能指针的使用C 标准库中的智能指针都在memory这个头文件下面我们包含memory就可以使用了智能指针有好几种除了 weak_ptr 他们都符合 RAII 和像指针一样访问的行为原理上而言主要是解决智能指针拷贝时的思路不同。2.1auto_ptr的使用及模拟实现auto_ptr 是 C98 时设计出来的智能指针他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象这是一个非常糟糕的设计因为他会导致被拷贝对象悬空访问报错的问题C11 设计出新的智能指针后强烈建议不要使用 auto_ptr。其他 C11 出来之前很多公司也是明令禁止使用这个智能指针的。2.1.1如何使用不建议使用#includeiostream #includememory using namespace std; class Date { public: Date(int year 0, int month 0, int day 0) :_year(year) ,_month(month) ,_day(day) { cout Date(int year 0, int month 0, int day 0) endl; } ~Date() { _year _month _day 0; cout ~Date() endl; } int _year; int _month; int _day; }; int main() { //auto_ptrint pi1 new int(1);//不支持隐式类型转换 auto_ptrint pi1(new int(1)); Date* pd new Date[5]{ {2021,4,23},{2017,9,5},{2019,10,18},{2025,1,9},{2026,12,28} }; //auto_ptr天生不支持数组管理auto_ptr的析构函数以及reset()方法内部只会执行delete 指针用于释放单个对象。 auto_ptrDate pd1(new Date[5]{ {2021,4,23},{2017,9,5},{2019,10,18},{2025,1,9},{2026,12,28} }); auto_ptrint pi2(new int[10]); printf(%d年%d月%d日\n, pd1.get()-_year, pd1-_month, (*(pd1))._day);//get()返回底层的指针 pi1.reset();//调用delete delete[] pd; //将 auto_ptr 内的指针设置为空指针表示它不指向任何对象而无需销毁当前由 auto_ptr 所指向的对象。 //该函数返回一个指向调用之前所指向对象的指针现在不再有义务负责对该对象进行销毁操作。 auto ret pd1.release(); return 0; }2.1.2模拟实现#includeiostream using namespace std; struct A { int _x; int _y; A(int x 0, int y 0) :_x(x) ,_y(y) { } }; namespace zsw { templateclass T class auto_ptr { public: explicit auto_ptr(T* ptr nullptr) :_ptr(ptr) { } auto_ptr(auto_ptrT pt) :_ptr(pt._ptr) { pt._ptr nullptr; } T operator*()noexcept { return *get(); } T* operator-()noexcept { return get(); } T* release()noexcept { T* ret _ptr; _ptr nullptr; return ret; } auto_ptrT operator(auto_ptrT pt) { reset(pt.release()); return *this; } void reset(T* p nullptr)noexcept { if (_ptr ! p) { delete _ptr; } _ptr p; } T* get()noexcept { return _ptr; } ~auto_ptr()noexcept { delete _ptr; } private: T* _ptr; }; } int main() { zsw::auto_ptrint p1(new int(9)); zsw::auto_ptrint p2(new int(15)); zsw::auto_ptrint p3; p3 p1; cout *p2 endl; //不支持使用new[]会内存泄漏如果A显示写析构程序直接崩溃。 zsw::auto_ptrA pa1(new A[5]{ {3,4},{2,6},{8,5},{5,7},{3,1} }); cout pa1-_x : pa1.get()-_y endl; p1.reset(); p2.reset(); p3.reset(); pa1.release(); return 0; }2.2unique_ptr的使用及模拟实现2.2.1如何使用unique_ptr 是 C11 设计出来的智能指针他的名字翻译出来是唯一指针他的特点的不支持拷贝只支持移动。如果不需要拷贝的场景就非常建议使用他。#includeiostream #includememory #includefunctional using namespace std; class Date { public: Date(int year 0, int month 0, int day 0) :_year(year) ,_month(month) ,_day(day) { cout Date(int year 0, int month 0, int day 0) endl; } ~Date() { _year _month _day 0; cout ~Date() endl; } int _year; int _month; int _day; }; struct DeleteDate { void operator()(Date* ptr) { delete[] ptr; } }; void DeleteArray(Date* ptr) { delete[] ptr; } class Fclose { public: void operator()(FILE* ptr) { fclose(ptr); } }; int main() { unique_ptrint ui1(new int(9)); unique_ptrint ui2(unique_ptrint(new int(5))); unique_ptrint[] ui3(new int[5] {1, 2, 3, 4, 5}); //auto_ptrDate ai1(new Date(2026, 9, 13)); ////unique_ptrint ui4(ui1); //不支持拷贝 //cout ui3 endl; //重载为指针 ////cout ai1 endl;//未重载 //unique_ptrDate ud1(new Date(2026, 9, 13)); //cout ud1-_year : ud1.get()-_month : ud1-_day endl; //自定义类型数组 //unique_ptrDate ud2(new Date[3]{{2019,5,4},{2024,8,6},{2026,1,3}});//这样写会崩 //正确写法 //1,new[]实现了一个特化版本 T[]这个特化版本析构时用delete[]。 unique_ptrDate[] ud2(new Date[3]{{2019,5,4},{2024,8,6},{2026,1,3}}); //2,仿函数对象做删除器。 unique_ptrDate,DeleteDate ud3(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }); //3,lambda表达式做删除器 auto Del [](Date* ptr) {delete[] ptr; }; unique_ptr Date, decltype(Del) ud4(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, Del); //4,函数指针 unique_ptrDate, void(*)(Date*) ud6(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, DeleteArray); //5,实现其他资源管理的删除器 unique_ptrFILE, Fclose uf1(fopen(test.cpp, r), Fclose()); //6,包装器 functionvoid(Date*) fun1 [](Date* ptr) {delete[] ptr; }; unique_ptrDate, functionvoid(Date*) ud7(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, fun1); return 0; }2.2.2模拟实现namespace zsw { templateclass T struct default_delete { void operator()(T* ptr) const noexcept { delete ptr; } }; templateclass T struct default_deleteT[] { void operator()(T* ptr) const noexcept { delete[] ptr; } }; templateclass T, class D default_deleteT class unique_ptr { public: explicit unique_ptr(T* ptr nullptr) noexcept : _ptr(ptr), _del() { } unique_ptr(T* ptr, D del) noexcept : _ptr(ptr), _del(del) { } unique_ptr(const unique_ptrT, D) delete; unique_ptrT, D operator(const unique_ptrT, D) delete; unique_ptr(unique_ptrT, D pt) noexcept : _ptr(pt._ptr), _del(pt._del) { pt._ptr nullptr; } unique_ptrT, D operator(unique_ptrT, D pt) noexcept { if (this ! pt) { if (_ptr) _del(_ptr); _ptr pt._ptr; _del pt._del; pt._ptr nullptr; } return *this; } ~unique_ptr() { if (_ptr) _del(_ptr); } T* release() noexcept { T* ret _ptr; _ptr nullptr; return ret; } void reset(T* pt nullptr) noexcept { if (_ptr ! pt) { T* old _ptr; _ptr pt; if (old) _del(old); } } T* get() const noexcept { return _ptr; } D get_deleter() noexcept { return _del; } const D get_deleter() const noexcept { return _del; } T operator*() const { return *_ptr; } T* operator-() const noexcept { return _ptr; } explicit operator bool() const noexcept { return _ptr ! nullptr; } T operator[](int i) const { return _ptr[i]; } private: T* _ptr; D _del; }; templateclass T, class D class unique_ptrT[], D { public: explicit unique_ptr(T* ptr nullptr) noexcept : _ptr(ptr), _del() { } unique_ptr(T* ptr, D del) noexcept : _ptr(ptr), _del(del) { } unique_ptr(const unique_ptrT[], D) delete; unique_ptrT[], D operator(const unique_ptrT[], D) delete; unique_ptr(unique_ptrT[], D pt) noexcept : _ptr(pt._ptr), _del(pt._del) { pt._ptr nullptr; } unique_ptrT[], D operator(unique_ptrT[], D pt) noexcept { if (this ! pt) { if (_ptr) _del(_ptr); _ptr pt._ptr; _del pt._del; pt._ptr nullptr; } return *this; } ~unique_ptr() { if (_ptr) _del(_ptr); } T* release() noexcept { T* ret _ptr; _ptr nullptr; return ret; } void reset(T* pt nullptr) noexcept { if (_ptr ! pt) { T* old _ptr; _ptr pt; if (old) _del(old); } } T* get() const noexcept { return _ptr; } D get_deleter() noexcept { return _del; } const D get_deleter() const noexcept { return _del; } T operator[](size_t i) const { return _ptr[i]; } explicit operator bool() const noexcept { return _ptr ! nullptr; } private: T* _ptr; D _del; }; }2.3shared_ptr和weak_ptr的使用及模拟实现2.3.1如何使用shared_ptr 是 C11 设计出来的智能指针他的名字翻译出来是共享指针他的特点是支持拷贝也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。weak_ptr 是 C11 设计出来的智能指针他的名字翻译出来是弱指针他完全不同于上面的智能指针他不支持 RAII也就意味着不能用它直接管理资源weak_ptr 的产生本质是要解决 shared_ptr 的一个循环引用导致内存泄漏的问题。具体细节下面我们再细讲。智能指针析构时默认是进行 delete 释放资源这也就意味着如果不是 new 出来的资源交给智能指针管理析构时就会崩溃。智能指针支持在构造时给一个删除器所谓删除器本质就是一个可调用对象这个可调用对象中实现你想要的释放资源的方式当构造智能指针时给了定制的删除器在智能指针析构时就会调用删除器去释放资源。因为 new [] 经常使用所以为了简洁一点unique_ptr 和 shared_ptr 都特化了一份 [] 的版本使用时 unique_ptrDate [] up1 (new Date [5]);shared_ptrDate [] sp1 (new Date [5]); 就可以管理 new [] 的资源。template class T, class... Args shared_ptrT make_shared (Args... args); shared_ptr 除了支持用指向资源的指针构造还支持 make_shared 用初始化资源对象的值直接构造。shared_ptr 和 unique_ptr 都支持了 operator bool 的类型转换如果智能指针对象是一个空对象没有管理资源则返回 false否则返回 true意味着我们可以直接把智能指针对象给 if 判断是否为空。shared_ptr 和 unique_ptr 的构造函数都使用 explicit 修饰防止普通指针隐式类型转换成智能指针对象。#includeiostream #includememory #includefunctional using namespace std; class Date { public: Date(int year 0, int month 0, int day 0) :_year(year) ,_month(month) ,_day(day) { cout Date(int year 0, int month 0, int day 0) endl; } ~Date() { _year _month _day 0; cout ~Date() endl; } int _year; int _month; int _day; }; struct DeleteDate { void operator()(Date* ptr) { delete[] ptr; } }; void Deletefunc(Date* ptr) { delete[] ptr; } int main() { shared_ptrint si1(new int(1)); shared_ptrDate sd1(new Date(2026, 7, 26)); cout sd1 endl; cout sd1-_year : sd1.get()-_month : (*(sd1))._day endl; //支持拷贝引用计数实现 shared_ptrDate sd2(sd1); shared_ptrDate sd3(new Date(1999, 8, 4)); cout sd2 endl; cout sd2-_year : sd2.get()-_month : (*(sd2))._day endl; cout sd1.use_count() endl; cout sd2.use_count() endl; cout sd1.unique() endl; //0nullptr也不是唯一的 sd1.reset(new Date(1999, 8, 4)); cout sd1.unique() endl; //1 shared_ptrDate sd4 make_sharedDate(2020, 12, 14); cout sd4.use_count() endl; cout sd4.unique() endl; cout sd4 endl; cout sd4-_year : (*(sd4))._month : sd4.get()-_day endl; //自定义类型数组 //shared_ptrDate sd11(new Date[3]{{2019,5,4},{2024,8,6},{2026,1,3}});//这样写会崩 //正确写法 //1,new[]实现了一个特化版本 T[]这个特化版本析构时用delete[]。 shared_ptrDate[] sd11(new Date[3]{{2019,5,4},{2024,8,6},{2026,1,3}}); //2,仿函数对象做删除器。 shared_ptrDate sd12(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, DeleteDate()); //3,lambda表达式做删除器 shared_ptrDate sd13(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, [](Date* ptr)-void {delete[] ptr; }); //4,函数指针 shared_ptrDate sd14(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, Deletefunc); //5,实现其他资源管理的删除器 shared_ptrFILE sf1(fopen(test.cpp, r), [](FILE* ptr)-void { cout fclose() endl; fclose(ptr); }); //6,包装器 functionvoid(Date*) fun1 [](Date* ptr) { cout functionvoid(Date*) endl; delete[] ptr; }; shared_ptrDate sd15(new Date[3]{ {2019,5,4},{2024,8,6},{2026,1,3} }, fun1); return 0; }2.3.2智能指针原理上面我们模拟实现了 auto_ptr 和 unique_ptr 的核心功能这两个智能指针的实现比较简单大家了解一下原理即可。auto_ptr 的思路是拷贝时转移资源管理权给被拷贝对象这种思路是不被认可的也不建议使用。unique_ptr 的思路是不支持拷贝。重点要看看 shared_ptr 是如何设计的尤其是引用计数的设计这里是一份资源就需要一个引用计数所以引用计数采用静态成员的方式是无法实现的要使用堆上动态开辟的方式构造智能指针对象时来一份资源就要 new 一个引用计数出来。多个 shared_ptr 指向资源时就 引用计数shared_ptr 对象析构时就 -- 引用计数引用计数减到 0 时代表当前析构的 shared_ptr 是最后一个管理资源的对象则析构资源。2.3.3shared_ptr循环引用问题引入weak_ptrshared_ptr 大多数情况下管理资源非常合适支持 RAII也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏所以我们要认识循环引用的场景和资源没释放的原因并且学会使用 weak_ptr 解决这种问题。如下图所述场景sp1 和 sp2 析构后管理两个节点的引用计数减到 1。1右边的节点什么时候释放呢左边节点中的_next 管着呢_next 析构后右边的节点就释放了。2_next 什么时候析构呢_next 是左边节点的的成员左边节点释放_next 就析构了。3左边节点什么时候释放呢左边节点由右边节点中的_prev 管着呢_prev 析构后左边的节点就释放了。4_prev 什么时候析构呢_prev 是右边节点的成员右边节点释放_prev 就析构了。 至此逻辑上成功形成回旋镖似的循环引用谁都不会释放就形成了循环引用导致内存泄漏。把 ListNode 结构体中的_next 和_prev 改成 weak_ptrweak_ptr 绑定到 shared_ptr 时不会增加它的引用计数_next 和_prev 不参与资源释放管理逻辑就成功打破了循环引用解决了这里的问题.#includeiostream #includememory using namespace std; templateclass T struct ListNode { T _data; //ListNodeT* _next; //ListNodeT* _prev; //shared_ptrListNodeT _next; //shared_ptrListNodeT _prev; weak_ptrListNodeT _next; weak_ptrListNodeT _prev; //ListNode(const T data) // :_data(data) // ,_next(nullptr) // ,_prev(nullptr) //{ } ListNode(const T data) :_data(data) { } ~ListNode() { cout ~ListNode() endl; } }; int main() { shared_ptrListNodeint sp1(new ListNodeint(3)); shared_ptrListNodeint sp2(new ListNodeint(7)); cout sp1: sp1.use_count() endl; // 1 cout sp2: sp2.use_count() endl; // 1 sp1-_next sp2; sp2-_prev sp1; cout sp1: sp1.use_count() endl; // 2--1 cout sp2: sp2.use_count() endl; // 2--1 return 0; }2.3.4weak_ptrweak_ptr 不支持 RAII也不支持访问资源所以我们看文档发现 weak_ptr 构造时不支持绑定到资源只支持绑定到 shared_ptr绑定到 shared_ptr 时不增加 shared_ptr 的引用计数那么就可以解决上述的循环引用问题。weak_ptr 也没有重载 operator * 和 operator- 等因为他不参与资源管理那么如果他绑定的 shared_ptr 已经释放了资源那么他去访问资源就是很危险的。weak_ptr 支持 expired 检查指向的资源是否过期use_count 也可获取 shared_ptr 的引用计数weak_ptr 想访问资源时可以调用 lock 返回一个管理资源的 shared_ptr如果资源已经被释放返回的 shared_ptr 是一个空对象如果资源没有释放则通过返回的 shared_ptr 访问资源是安全的。#includeiostream #includememory #includestring using namespace std; int main() { std::shared_ptrstring sp1(new string(111111)); std::shared_ptrstring sp2(sp1); std::weak_ptrstring wp sp1; cout wp.expired() endl; cout wp.use_count() endl; // sp1和sp2都指向了其他资源则weak_ptr就过期了 sp1 make_sharedstring(222222); cout wp.expired() endl; cout wp.use_count() endl; sp2 make_sharedstring(333333); cout wp.expired() endl; cout wp.use_count() endl; wp sp1; //std::shared_ptrstring sp3 wp.lock(); auto sp3 wp.lock(); cout wp.expired() endl; cout wp.use_count() endl; *sp3 ###; cout *sp1 endl; return 0; }2.3.5模拟实现#includeiostream #includefunctional using namespace std; namespace zsw { templateclass T class shared_ptr { public: explicit shared_ptr(T* ptr nullptr) :_ptr(ptr) , _count(new int(1)) { cout explicit shared_ptr(T* ptr nullptr)----构造 endl; } templateclass D shared_ptr(T* ptr, D del) : _ptr(ptr) , _count(new int(1)) , _del(del) { } shared_ptr(const shared_ptrT sp) :_ptr(sp._ptr) , _count(sp._count) , _del(sp._del) { // 若拷贝的是已被移动走的对象_count 为空无需递增计数 if (_count) (*_count); cout shared_ptr(const shared_ptrT sp)----拷贝构造 endl; } void swap(shared_ptrT pt) { std::swap(_ptr, pt._ptr); std::swap(_count, pt._count); std::swap(_del, pt._del); } // 移动构造直接“窃取” pt 的资源指针、引用计数、删除器 // 不新建引用计数随后把 pt 置空防止其析构时重复释放。 shared_ptr(shared_ptrT pt) noexcept : _ptr(pt._ptr) , _count(pt._count) , _del(std::move(pt._del)) { pt._ptr nullptr; pt._count nullptr; cout shared_ptr(shared_ptrT pt)----移动构造 endl; } // 拷贝赋值copy-and-swap 保证异常安全。 // 先用 pt 拷贝构造临时对象 tmp再与 *this 交换 // 旧资源交由 tmp 析构时自动释放天然支持自赋值与共享指针场景。 shared_ptrT operator(const shared_ptrT pt) { cout shared_ptrT operator(const shared_ptrT pt)----拷贝赋值 endl; if (this ! pt) { shared_ptrT tmp(pt); swap(tmp); } return *this; } // 移动赋值先窃取 pt 的资源到 tmp再与 *this 交换 // 本对象原有资源随 tmp 析构释放pt 被置空。 shared_ptrT operator(shared_ptrT pt) noexcept { cout shared_ptrT operator(shared_ptrT pt)----移动赋值 endl; if (this ! pt) { shared_ptrT tmp(std::move(pt)); swap(tmp); } return *this; } T operator*()const { return *_ptr; } T* operator-()const { return _ptr; } T* get()const { return _ptr; } int use_count()const { // 已被移动走的对象 _count 为空返回 0 而不是解引用空指针 return _count ? *_count : 0; } bool unique()const { // 标准语义仅当引用计数为 1 时独占 return use_count() 1; } void reset(T* ptr nullptr) { // 用 ptr 构造临时对象建立独立的引用计数再交换 // 旧资源随 tmp 析构释放避免残留旧计数或空指针。 shared_ptrT tmp(ptr); swap(tmp); } ~shared_ptr() { release(); } private: void release() { // 可重入已被释放_count 为空时直接返回防止二次析构崩溃 if (_count nullptr) return; if (--(*_count) 0) { _del(_ptr); delete _count; _ptr nullptr; _count nullptr; } } private: T* _ptr; // 引用计数当前实现非线程安全如需多线程共享请改用 std::atomicint int* _count; //atomicint* _count; functionvoid(T*) _del [](T* ptr)-void {delete ptr; }; }; templateclass T ostream operator(ostream out, const shared_ptrT sp) { out sp.get(); return out; } templateclass T class weak_ptr { public: weak_ptr() { } weak_ptr(const shared_ptrT sp) :_ptr(sp.get()) { } weak_ptrT operator(const shared_ptrT sp) { _ptr sp.get(); return *this; } private: T* _ptr nullptr; }; } class Date { public: Date(int year 0, int month 0, int day 0) :_year(year) , _month(month) , _day(day) { cout Date(int year 0, int month 0, int day 0) endl; } ~Date() { _year _month _day 0; cout ~Date() endl; } int _year; int _month; int _day; }; struct ListNode { int _data; //ListNode* _next; //ListNode* _prev; //会有循环引用问题 //zsw::shared_ptrListNode _next; //zsw::shared_ptrListNode _prev; //ListNode(int data 0) // :_data(data) // , _next(nullptr) // , _prev(nullptr) //{ } //weak_ptr解决循环引用问题 zsw::weak_ptrListNode _next; zsw::weak_ptrListNode _prev; ListNode(int data 0) :_data(data) { } ~ListNode() { cout ~ListNode() endl; } }; void test1() { zsw::shared_ptrint sp1(new int(1)); std::shared_ptrDate sd1(new Date[5], [](Date* ptr)-void {delete[] ptr; }); zsw::shared_ptrDate mysd1(new Date[5], [](Date* ptr)-void {delete[] ptr; }); cout sd1 endl; cout mysd1 endl; cout sd1.use_count() endl; cout mysd1.use_count() endl; //引用计数测试 std::shared_ptrDate sd2(sd1); zsw::shared_ptrDate mysd2(mysd1); cout sd2 endl; cout mysd2 endl; cout sd1.use_count() endl; cout mysd1.use_count() endl; cout sd2.use_count() endl; cout mysd2.use_count() endl; zsw::shared_ptrint sp11(new int(1)); zsw::shared_ptrint sp21(sp11); zsw::shared_ptrint sp31(new int(4)); zsw::shared_ptrint sp41(move(sp31)); zsw::shared_ptrint sp51; sp51 sp11; zsw::shared_ptrint sp61(new int(8)); zsw::shared_ptrint sp71(new int(19)); sp61 move(sp71); } void test2() { zsw::shared_ptrListNode sp1(new ListNode(5)); zsw::shared_ptrListNode sp2(new ListNode(9)); sp1-_next sp2; sp2-_prev sp1; } int main() { test1();//基础测试 test2();//循环引用测试 return 0; }
返回列表