异常、RAII和智能指针
扶鞘引狂澜剑意万重开。欢迎来到丘山望岳的小栈今天学习的内容是 异常、RAII和智能指针我们闲言少叙直击主题。前言为什么 C 需要异常机制在 C 语言的开发体系中处理程序错误的方式非常单一依靠函数返回值比如返回负数、全局错误码errno来判断执行状态。但这套机制存在致命的工程缺陷业务逻辑与错误判断高度耦合正常的核心代码中充斥着大量if (ret 0)的容错判断代码臃肿杂乱可读性断崖式下降。错误极易被忽略开发者可能漏写判断、忽略返回值隐性错误潜伏在代码中运行时随机崩库、闪退。多层调用错误传递繁琐多层函数嵌套时错误需要逐层返回函数不得不加入一个输出型参数来接收错误信息、逐层判断、逐层上报冗余代码爆炸式增长。errno码不够直观需要查找比对错误信息。为了解决传统错误处理的痛点C 引入了异常Exception机制。它的核心设计哲学只有一句话将「错误检测」和「错误处理」彻底解耦。底层函数只负责检测错误、抛出异常无需关心如何修复上层调用者根据业务场景自主决定捕获、处理、重试或终止程序。这也是面向对象思想下最标准、最优雅的错误处理体系。一、c异常处理机制主要是依托于三个关键字try ,catch,throwtry和catch搭配使用try{可能会出现bug的的代码段}catch(参数表{对参数表中参数经行运算从而应对或处理bug的代码段}throw最核心throw(一个变量或对象)。上面catch参数表中的变量就是用来捕获throw抛出的变量或对象的。异常抛出机制的核心框架try{throw(a类型变量a)}catch(a类型变量b){出现异常时的执行语句}a由于可能是临时变量所以b使用传值传参是a的一份拷贝。比较抽象上一段代码看看应用场景我们想实现一个除法程序当被除数为0不会终止程序而是会抛出异常不会影响程序的正常运行。#include iostream using namespace std; double safeDiv(int a, int b) { if (b 0) throw string(divided by zero); return double(a) / b; } int main() { try { cout safeDiv(11, 2) endl; cout safeDiv(10, 0) endl; coutsafeDiv(12,2)endl; } catch (const string e) { cout e endl; } cout safeDiv(10, 5) endl; return 0; //输出结果5.5 // divided by zero // 2 }如果不加入异常机制程序运行到10/0的时候会直接终止但是我们自己加入一个异常抛出机制对出现异常的代码经行处理使得程序可以正常运行。但是我们看到12/2的那一部分代码没有运行如果代码正常运行的话应该有6输出才对但是代码并没有在此终止10/5的那一部分运行并输出了结果2.这就不得不提到c抛异常机制的一个规定当代码块{}作用域内执行throw抛出异常时 throw语句一旦执行同一层级中位于throw之后的代码将不再执行程序会停止当前流程沿函数调用栈向上寻找能够匹配该异常类型的catch捕获分支 若异常被匹配的catch语句成功捕获catch块内的逻辑会完整运行待catch代码块执行完毕后位于该try-catch整体结构之后、同一外层作用域内的剩余代码仍可正常向下执行。说人话版如果在一段大括号代码里写了 throw 抛出异常 只要执行到 throw 这一行它后面同一块里剩下的代码全都不会跑了程序直接中断当前流程顺着一层层调用的函数往上找看有没有能接住这个异常类型的 catch 要是成功找到了匹配的 catch那 catch 里面的代码会完整走完等 catch 执行结束try-catch 这一整套结构外面、同一个大括号里后面的代码还是能接着正常运行。那么什么样的catch才能接住throw抛出的变量或对象呢?匹配过程与不匹配之后的操作二、栈展开和匹配机制1.throw抛出对象/或变量这个语句执行时首先判断throw在不在try语句中如果是涉及函数的多层调用要一层一层扒开函数调用栈看最外层函数是否在try{}语句之中如果不在直接终止程序。2.在try语句中时就像我刚才说的那样一层层扒开函数调用栈找到第一个和try语句匹配的catch语句完成catch之后的语句正常执行。如果到main函数都没与throw相匹配的catch就终止程序。3.匹配机制throw出的对象类型和catch中的对象类型必须严格匹配编译器不会进行强制类型转化但是也有例外比如编译器会支持非常量到常量的“权限缩小“式的类型转换比如抛出派生类捕获基类抛出函数名捕获函数指针抛出数组名捕获指向数组首元素的指针。还是理解不了一张图例子带你看懂。 一、函数调用栈结构从下往上图中展示的是一个嵌套调用的函数栈调用顺序是fun()→fun1()→fun2()→fun3()→fun4()每个函数内部都有一些操作fun(): 调用fun1(); g1();fun1():try{ fun2(); } catch(string s) { ... } g2();fun2():try{ fun3(); } catch(const char* ch) { ... } g3();fun3():fun4(); g4();fun4():throw(string e); g5();←关键点这里抛出异常⚠️ 二、异常抛出点在fun4()中执行到throw(string e); // 抛出一个 string 类型的异常此时异常被抛出程序不再继续执行g5();而是开始异常匹配流程。 三、异常匹配流程从抛出点向上查找 catch异常抛出后程序会从当前函数fun4开始逐层向上回溯调用栈寻找第一个能匹配该异常类型的catch块。✅ 第一步在fun4()中查找fun4()中没有try-catch所以不匹配。继续向上 →fun3()✅ 第二步在fun3()中查找fun3()中是fun4(); g4();没有try-catch不匹配。继续向上 →fun2()✅ 第三步在fun2()中查找fun2()中有try { fun3(); } catch(const char* ch) { ... } g3();try块包裹了fun3()即异常抛出点在其内部。但catch捕获的是const char*类型。抛出的异常是string类型 →类型不匹配所以这个 catch 不处理。继续向上 →fun1()✅ 第四步在fun1()中查找fun1()中有try { fun2(); } catch(string s) { ... } g2();try块包裹了fun2()异常发生在其调用链中。catch(string s)→类型匹配 ✅所以这个catch块会被执行。 四、异常处理后的行为一旦fun1()中的catch(string s)被触发执行catch块内的{ ... }图中省略。异常被“捕获并处理”程序不会崩溃。异常传播终止不再继续向上找 catch。注意fun1()中catch之后的g2();会执行但fun2()、fun3()、fun4()中catch之后的g3(); g4(); g5();都不会执行g2()会执行因为它在fun1()的catch之后g3(), g4(), g5()不会执行因为它们在异常发生前的函数中且异常已在该层被捕获 五、总结异常处理流程图解fun() └─ fun1() └─ try { fun2() } └─ fun2() └─ try { fun3() } └─ fun3() └─ fun4() → throw(string e) ← 异常抛出 ↑ │ 向上查找 catch ↓ fun2() 的 catch(const char*) → 不匹配 ↑ ↓ fun1() 的 catch(string) → ✅ 匹配执行 catch 块 ↓ 继续执行 g2(); fun1() 中 catch 之后 ↓ 返回 fun()继续执行 g1();✅ 六、关键结论异常抛出后从抛出点向上逐层查找匹配的 catch。匹配原则类型兼容如 string 可以匹配 string但不能匹配 const char*。找到第一个匹配的 catch 就停止向上查找。匹配的 catch 执行后其所在函数的后续代码如 g2()会继续执行。未匹配的 catch 之后的代码如 g3(), g4(), g5()不会执行。如果一直没找到匹配的 catch程序会调用std::terminate()终止。像这样一层一层剥开函数调用栈处理异常就是栈展开。在栈展开到main中如果没有遇到“包裹代码块”的try或匹配不上相匹配的catch会终止程序我们可以使用catch...){}语句来避免程序终止比如如果我们在实现上述这个除法程序时catch匹配参数不小心写错了还可以用catch...来捕获一切异常放置程序终止运行。#include iostream using namespace std; double safeDiv(int a, int b) { if (b 0) throw string(divided by zero); return double(a) / b; } int main() { try{ try { coutsafeDiv(2,3)endl; coutsafeDiv(2,0)endl; } catch (const char* e) { cout e endl; } } catch(...) { cout未知异常endl; } coutsafeDiv(9,3)endl; return 0; //输出结果0.666667 // 未知异常 // 3 }三、异常处理机制的缺点和不足我们通过上面的例子看到g3()-g5()都没有执行如果g3()-g5()中涉及资源的回收如delete这样的语句是不会执行的。这样的问题就不得不迫使我们不能在构造函数和析构函数中使用异常处理机制构造函数抛出异常对象只构造了一部分已构造的成员会自动析构但外部没有合法完整对象外部无法用对象判断状态且没有 “部分构造成功” 的标准处理方案。析构函数默认不能抛异常如果析构函数抛出异常同时栈上已有另一个未捕获异常程序直接terminate 规范要求析构必须noexcept一旦资源释放出错没有标准上报渠道只能吞掉错误容易造成资源泄漏无法感知。在其它一些函数涉及这种函数资源回收放置在异常抛出机制之后的我们可以使用异常的二次抛出来解决#include iostream using namespace std; double safeDiv(int a, int b) { if (b 0) throw string(divided by zero); return double(a) / b; } double fun(int a,int b) { int *panew int(a); int *pbnew int(b); try{ return safeDiv(*pa,*pb); } catch(string s) { delete pa; delete pb; throw s; } delete pa; delete pb; } int main() { try { coutfun(1,2)endl; coutfun(3,4)endl; coutfun(1,0)endl; } catch (const string e) { cout e endl; } //输出结果0.5 // 0.75 // divided by zero }四、c标准库的异常使用标准库经行异常抛出本质上就是让我们上述抛出对象或变量被标准库设置的类封装。核心基础标准异常总根类std::exception所有 C 标准库内置异常都继承自头文件exception的std::exception它提供一个核心虚函数virtual const char* what() const noexcept;作用返回异常的文字描述信息捕获后用来打印错误原因。常用标准内置异常分类分 3 大类头文件stdexcept是业务最常用的异常集合逻辑错误程序代码写得有问题可提前规避std::invalid_argument传入非法参数std::out_of_range下标 / 数值越界vector、string 访问越界std::domain_error数学定义域非法负数开平方等std::length_error容器长度超过最大限制运行时错误运行期间外部环境导致不可提前预判std::runtime_error通用运行错误基类std::overflow_error数值上溢std::underflow_error数值下溢其他标准异常头文件不同newstd::bad_allocnew 分配内存失败typeinfostd::bad_castdynamic_cast 转换失败iosstd::ios_base::failure文件 IO 读写失败标准异常完整使用四步规范1. 引入必要头文件#include iostream #include stdexcept // 逻辑、运行异常 #include exception // 总基类std::exception #include string using namespace std;2. 主动抛出标准库异常throw 标准异常对象不要抛字符串、数字规范做法是抛标准异常类实例构造函数传入错误描述。// 除法函数除数为0抛出运行时异常 double divide(int a, int b) { if (b 0) { // 抛标准运行异常传入错误说明字符串 throw runtime_error(错误除数不能为0); } if (a 0) { // 抛参数非法异常 throw invalid_argument(错误被除数不能为负数); } return static_castdouble(a) / b; }3. 分层捕获try-catch按子类→基类顺序捕获捕获规则特殊子类写在前通用基类写在最后如果把catch(exception)放最前面会直接拦截所有异常后面子类捕获永远不会执行。int main() { try { double res divide(-10, 2); cout 结果 res endl; } // 先捕获精准子类异常 catch (const invalid_argument e) { cout 参数异常 e.what() endl; } catch (const runtime_error e) { cout 运行异常 e.what() endl; } // 兜底捕获所有标准库异常所有继承std::exception的异常 catch (const exception e) { cout 未知标准异常 e.what() endl; } // 终极兜底捕获任意类型异常字符串、数字、自定义类型 catch (...) { cout 非标准库未知异常 endl; } cout try-catch之后代码正常执行 endl; return 0; }运行输出示例调用divide(-10,2)参数异常错误被除数不能为负数 try-catch之后代码正常执行4. 函数异常声明noexcept现代 C 规范noexcept函数保证不会抛出任何异常一旦抛异常程序直接终止std::terminatenoexcept(false)明确标识函数会抛出异常// 标注该函数可能抛出异常 double divide(int a, int b) noexcept(false); // 标注函数绝对不会抛异常 int add(int a, int b) noexcept;编译器关于noexcept函数是不检查内部是否有throw语句的如果运行时抛出了异常会直接终止程序运行。高频场景示范场景 1容器越界std::out_of_rangevector::at()访问元素越界会主动抛out_of_range[]不会抛直接崩溃#include vector int main() { vectorint v {1,2,3}; try { v.at(10); // 下标超出容器范围自动抛std::out_of_range } catch (const out_of_range e) { cout 越界错误 e.what() endl; } return 0; }场景 2内存分配失败std::bad_allocint main() { try { // 超大内存分配失败 int* p new int[0x7fffffff]; } catch (const bad_alloc e) { cout 内存分配失败 e.what() endl; } return 0; }场景 3动态转换失败std::bad_castclass Base { virtual void func(){} }; class Son : public Base {}; int main() { Base b; try { Son s dynamic_castSon(b); } catch (const bad_cast e) { cout 类型转换失败 e.what() endl; } return 0; }规范使用要点优先使用标准异常类禁止 throw 字符串 / 数字错误写法throw 除数为0;正确写法throw runtime_error(除数为0);catch 捕获时必须加 const 引用const XXX e值捕获会拷贝异常对象有性能损耗裸引用无法捕获临时异常对象。捕获顺序子类在前基类在后what()是 const 成员函数只能打印信息不能修改析构函数默认noexcept禁止在析构函数抛出异常会直接导致程序崩溃轻量错误参数轻微非法推荐用if判断异常只处理不可恢复、跨多层调用的严重错误C17 废弃老式异常规格void func() throw(runtime_error)统一使用noexcept五、拓展基于标准异常自定义业务异常业务开发中常继承std::exception扩展专属异常遵循标准库统一接口// 自定义业务异常继承总基类std::exception class BizError : public exception { private: string msg; public: explicit BizError(const string s) : msg(s) {} // 重写what() const char* what() const noexcept override { return msg.c_str(); } }; int main() { try { throw BizError(用户ID不存在); } catch (const BizError e) { cout 业务异常 e.what() endl; } catch (const exception e) { cout 标准异常 e.what() endl; } return 0; }六、RAII介绍上面我们看到了动态内存分配的变量在使用起来很不方便如果不手动delete删除那么就会引发内存泄漏等一系列的问题。在c中类似的情况还有很多我们想到自定义变量得益于构造函数和析构函数不用进行手动删除生命周期结束即自动调用析构函数即使是有资源也可以自动释放。我们就想实现用类来管理动态分配的资源这就是RAII机制的大致思想。一句话将资源动态内存开辟对象文件指针等托付给一个类来管理类对象在资源在类对象出作用域销毁资源回收。七、四种智能指针即适用场景1.智能指针用类对象来管理资源不可能是对资源进行拷贝这样资源的回收问题得不到解决还增加了拷贝成本。所以RAII机制的实现必然是依托于指针的用指针来指向资源指针用类来封装有一点类似于c容器迭代器的实现。def:智能指针是 C 标准库提供、基于RAII资源获取即初始化机制实现的模板类本质为管理堆内存资源的栈对象内部持有一块堆内存的原始裸指针资源的所有权在智能指针构造时获取当智能指针离开作用域生命周期终结其析构函数会自动调用delete释放托管堆内存标准库提供unique_ptr、shared_ptr、weak_ptr、auto_ptr四类智能指针通过独占所有权、共享所有权、弱引用三种不同所有权策略区分资源管理方式解决裸指针手动管理内存带来的内存泄漏、双重释放、野指针、循环引用等经典内存缺陷。2.四种智能指针和使用场景头文件memory1.auto_ptr2.unique_ptr3.shared_ptr4.weak_ptr1.auto_ptr是c98提供的一套不是很成熟的智能指针只允许一个类对象绑定一个资源支持拷贝但是所有的拷贝都是移动即被拷贝对象的资源被转移给拷贝对象。因此这个智能指针用处不大。2.unique_ptr是对auto_ptr的改进不允许拷贝只能移动。3.shared_ptr可以拷贝也支持移动得益于底层的引用计数4.weak_ptr,专门用来解决shared_ptr解决不了的循环引用问题shared_ptr和weak_ptr原理讲解weak_ptr使用场景循环引用常见于双向链表的两个节点模型中#include iostream #include memory // shared_ptr、weak_ptr 头文件 using namespace std; // 双向链表节点结构体 struct ListNode { int _data; // 节点存储的数据 std::shared_ptrListNode _next; // 指向下一个节点的共享智能指针 std::shared_ptrListNode _prev; // 指向前一个节点的共享智能指针 /* // 解决循环引用的修改方案将前驱指针改为 weak_ptr // weak_ptr 绑定 shared_ptr 时不会增加目标对象引用计数不参与资源释放管理打破循环引用 // std::weak_ptrListNode _prev; */ // 节点析构函数用于验证对象是否正常释放 ~ListNode() { cout ~ListNode() endl; } }; int main() { // 创建两个共享智能指针分别管理两个 ListNode 堆对象 std::shared_ptrListNode n1(new ListNode); std::shared_ptrListNode n2(new ListNode); // 刚创建完毕n1 独占对象引用计数1 cout n1.use_count() endl; // 刚创建完毕n2 独占对象引用计数1 cout n2.use_count() endl; // n1 的_next 指向 n2n2 对象引用计数 1 → n2.use_count() 2 n1-_next n2; // n2 的_prev 指向 n1n1 对象引用计数 1 → n1.use_count() 2 n2-_prev n1; // 打印此时 n1 的引用计数2自身n1 n2-_prev cout n1.use_count() endl; // 打印此时 n2 的引用计数2自身n2 n1-_next cout n2.use_count() endl; /* // weak_ptr 特性说明 // 1. weak_ptr 不能直接管理堆内存不支持裸指针new构造无法单独RAII管理资源 // 2. 设计用途辅助绑定 shared_ptr仅做弱观测不增加目标引用计数解决循环引用问题 // std::weak_ptrListNode wp(new ListNode); // 编译报错禁止直接管理裸指针 */ // main函数结束n1、n2 局部变量销毁 // 此时两个对象引用计数均为1互相持有对方引用计数无法归零析构函数不会执行发生内存泄漏 return 0; }在这样一个情况下如果四个指针都使用智能指针来封装的话在析构这个机构时假设p3,p4都销毁引用计数12都为1析构node1,node2p1什么时候销毁node1销毁p1就销毁node1什么时候销毁p2销毁node1就销毁p2什么时候销毁node2销毁p2就销毁node2什么时候销毁p1销毁就销毁这样的场景最好就是把链表中前驱或后继指针交给weak_ptr来管理由于weak_ptr不参于shared_ptr的引用计数并且weak_ptr在使用时会检查用于赋值的shared_ptr管理的原生指针是否还存在。struct ListNode { int _data; std::shared_ptrListNode _next; // 强指针持有后继 std::weak_ptrListNode _prev; // 弱指针仅观测前驱不增加计数 ~ListNode() { cout ~ListNode() endl; } };智能指针使用示例c11支持c17已删除auto使用示例#define _CRT_SECURE_NO_WARNINGS 1 #includeiostream using namespace std; #includememory class date { private: int _day 0; int _month 0; int _year 1900; public: date(int year0, int month0, int day0) :_year(year), _month(month), _day(day) { } ~date() default; int get_month() { return _month; } }; int main() { auto_ptrdate a{ new date{2020,11,3} };//支持赋值但是是移动语义 auto_ptrdate a1(a); //cout a-get_month() endl;//error:this 是空指针 auto_ptrdatea2(a1); cout a2-get_month() endl; //cout a1-get_month() endl;//error:this 是空指针 // 不支持移动语义 //auto_ptrdatea3(move(a2)); //cout a3-get_month() endl; }unique_ptr使用示例#define _CRT_SECURE_NO_WARNINGS 1 #includeiostream using namespace std; #includememory class date { private: int _day 0; int _month 0; int _year 1900; public: date(int year0, int month0, int day0) :_year(year), _month(month), _day(day) { } ~date() default; int get_month()const { return _month; } }; int main() { //unique_ptrdate a(new date{ 2020,11,3 }); //也可以这样写 unique_ptrdate a make_uniquedate(2020, 11, 3); cout a-get_month() endl; // unique_ptr 不能拷贝 // unique_ptrdate a1(a); // 编译期直接拦住 unique_ptrdate a2 move(a); // 明确移动 //unique_ptrdate a2 ( move(a)); 移动赋值也支持 cout a2-get_month() endl; // cout a-get_month() endl; // 编译期就能发现错误 return 0; }shared_ptr使用示例#define _CRT_SECURE_NO_WARNINGS 1 #includeiostream using namespace std; #includememory class date { private: int _day 0; int _month 0; int _year 1900; public: date(int year0, int month0, int day0) :_year(year), _month(month), _day(day) { } ~date() default; int get_month()const { return _month; } }; int main() { //shared_ptrdate sp{ new date{2020,11,3} }; shared_ptrdate sp make_shareddate(2020, 11, 3); cout sp-get_month() endl; //支持赋值 shared_ptrdate sp1(sp); //shared_ptrdatesp1sp; cout sp-get_month() sp1-get_month() endl; //支持移动语义 //shared_ptrdate sp3 move(sp1); shared_ptrdate sp3 (move(sp1)); //cout sp1-get_month() endl; cout sp3-get_month() endl; return 0; }weak_ptr的使用;weak_ptr特有的成员函数.lock()返回shared_ptrT对象存活则返回有效强指针销毁返回空智能指针必须先 lock 才能访问对象weak_ptr 不能直接-访问成员.expired()返回 booltrue 绑定的对象已销毁false 对象存活等价于use_count() 0.use_count()获取绑定对象当前的强引用计数shared_ptr 数量.reset()清空当前 weak_ptr解除对目标对象的观测变为空弱指针.swap(other_wp)交换两个 weak_ptr 绑定的观测对象#include iostream #include memory using namespace std; // 测试实体类用于观察对象析构 class TestObj { public: int id; TestObj(int num) : id(num) { cout 构造 TestObj, id id endl; } ~TestObj() { cout 析构 TestObj, id id endl; } void show() { cout 调用对象成员函数, id id endl; } }; int main() { cout 1. weak_ptr 空构造、绑定shared_ptr \n; // 1. 默认构造空weak_ptr不绑定任何资源 weak_ptrTestObj wp_empty; cout 空weak_ptr use_count: wp_empty.use_count() endl; cout 空weak_ptr expired? boolalpha wp_empty.expired() \n\n; // 创建shared_ptr管理堆对象 shared_ptrTestObj sp1 make_sharedTestObj(100); cout sp1 初始引用计数: sp1.use_count() endl; // 2. weak_ptr绑定shared_ptr不会增加shared_ptr引用计数 weak_ptrTestObj wp1 sp1; cout wp1绑定sp1后sp1计数不变: sp1.use_count() \n\n; cout 2. lock()弱指针升级为shared_ptr核心用法 \n; // lock() 返回 shared_ptr若原对象已销毁返回空shared_ptr shared_ptrTestObj temp_sp wp1.lock(); if (temp_sp ! nullptr) { cout lock成功访问对象; temp_sp-show(); cout lock后临时shared_ptr使计数1: sp1.use_count() endl; } else { cout lock失败对象已销毁\n; } // temp_sp生命周期结束计数自动-1 temp_sp.reset(); cout 临时shared_ptr销毁后计数恢复: sp1.use_count() \n\n; cout 3. expired() 判断对象是否已销毁 \n; cout 当前对象存活expired(): wp1.expired() endl; // 释放唯一强指针对象销毁 sp1.reset(); cout sp1.reset()释放资源后\n; cout wp1.expired() wp1.expired() endl; // 此时lock会拿到空智能指针 shared_ptrTestObj null_sp wp1.lock(); cout 销毁后lock得到的shared_ptr是否为空: (null_sp nullptr) \n\n; cout 4. 拷贝构造、赋值运算符 \n; shared_ptrTestObj sp2 make_sharedTestObj(200); weak_ptrTestObj wp_a sp2; // 拷贝构造weak_ptr weak_ptrTestObj wp_b(wp_a); cout wp_a计数: wp_a.use_count() , wp_b计数: wp_b.use_count() endl; // 赋值重载 weak_ptrTestObj wp_c; wp_c wp_a; cout wp_c赋值后计数: wp_c.use_count() \n\n; cout 5. reset() 清空weak_ptr绑定关系 \n; cout reset前wp_c use_count: wp_c.use_count() endl; wp_c.reset(); // 解除wp_c对sp2的观测 cout reset后wp_c use_count: wp_c.use_count() \n\n; cout 6. swap() 交换两个weak_ptr观测对象 \n; shared_ptrTestObj sp3 make_sharedTestObj(300); weak_ptrTestObj wp_x sp2; weak_ptrTestObj wp_y sp3; cout 交换前wp_x观测id wp_x.lock()-id , wp_y观测id wp_y.lock()-id endl; wp_x.swap(wp_y); cout 交换后wp_x观测id wp_x.lock()-id , wp_y观测id wp_y.lock()-id \n\n; cout 7. 解决shared_ptr循环引用双向链表场景 \n; struct Node { int val; shared_ptrNode next; weak_ptrNode prev; // 反向用weak_ptr打破循环 Node(int v) : val(v) {} ~Node() { cout Node析构, val val endl; } }; // 创建两个节点互相指向 shared_ptrNode n1 make_sharedNode(1); shared_ptrNode n2 make_sharedNode(2); n1-next n2; n2-prev n1; // weak_ptr不增加n1引用计数 cout 循环绑定后 n1计数: n1.use_count() , n2计数: n2.use_count() \n; // 局部shared_ptr n1、n2生命周期结束离开作用域后资源自动释放打印两次析构 cout 离开循环引用区块自动释放节点\n; cout \n 8. 禁止直接用new裸指针构造weak_ptr编译报错 \n; // weak_ptrTestObj bad_wp(new TestObj(999)); // 上面代码无法编译weak_ptr不能独立管理堆内存只能依附shared_ptr做观测 cout weak_ptr仅作为shared_ptr的辅助观测不参与资源生命周期管理\n; return 0; }八、四种智能指针的简单化模拟实现auto_ptr简单实现templateclass T class auto_ptr { private: T* _ptr; public: explicit auto_ptr(T* ptr nullptr) : _ptr(ptr) {} // 拷贝构造转移所有权 auto_ptr(auto_ptr p) : _ptr(p._ptr) { p._ptr nullptr; } // 赋值转移所有权 auto_ptr operator(auto_ptr p) { if (this ! p) { delete _ptr; _ptr p._ptr; p._ptr nullptr; } return *this; } ~auto_ptr() { if(_ptr) delete _ptr; } T operator*() const { return *_ptr; } T* operator-() const { return _ptr; } T* get() const { return _ptr; } };unique_ptr简单实现//unique_ptr templateclass T class unique_ptr { private: T* _ptr nullptr; public: //构造函数 explicit unique_ptr(T* ptr)_ptr(ptr) { } //析构函数 ~unique_ptr() { if(_ptr) delete _ptr; } //移动构造函数 unique_ptr(unique_ptr p1) { if (p1 ! *this) { _ptr p1._ptr; delete p1._ptr; p1._ptr nullptr; } } //移动赋值 unique_ptr operator(unique_ptr p1) { if (p1._ptr ! _ptr) { if (_ptr)delete _ptr; _ptr p1._ptr; delete p1._ptr; p1._ptr nullptr; } return *this; } //不支持拷贝 unique_ptr(unique_ptr p) delete; unique_ptr operator(unique_ptr p) delete; T operator*() const { return *_ptr; } T* operator-() const { return _ptr; } T* get() const { return _ptr; } };shared_ptr简单实现//shared_ptr templateclass T,class D class shared_ptr { private: T* _ptr nullptr; int* _cnt;//引用计数 D _del; public: //构造 explicit shared_ptr(T* ptr, int* cntnew int(1), D del [](T* ptr) {delete ptr}) :_ptr(ptr), _del(del),_cnt(cnt) { } //析构 ~shared_ptr() { *_cnt--; if (*_cnt 0) { _del(_ptr); delete _cnt; } } //拷贝构造 shared_ptr(const shared_ptr p1) { if(p1!*this) { _ptr p1._ptr; _cnt p1._cnt; _del p1._del; *_cnt } } //移动构造 shared_ptr(shared_ptr p1) { if (p1._ptr!_ptr) { if (_ptr)//清空_ptr旧资源 { *_cnt--; if (*_cnt 0)del(_ptr); } _ptr p1._ptr; _cnt p1._cnt; _del p1._del; p1._ptr nullptr; p1._cnt nullptr; } } //拷贝赋值 shared_ptr operator(const shared_ptr p1) { if (p1._ptr!_ptr) { if (_ptr)//清空_ptr旧资源 { *_cnt--; if (*_cnt 0)del(_ptr); } _ptr p1._ptr; _cnt p1._cnt; _del p1._del; *_cnt; } return *this; } //移动赋值 shared_ptr operator(shared_ptrp1) { if (p1._ptr ! _ptr) { if (_ptr)//清空_ptr旧资源 { *_cnt--; if (*_cnt 0)del(_ptr); } _ptr p1._ptr; _cnt p1._cnt; _del p1._del; p1._ptr nullptr; p1._cnt nullptr; } return *this; } T operator*() const { return *_ptr; } T* operator-() const { return _ptr; } T* get() const { return _ptr; } //支持weak_ptr获取资源 D get_del() { return _del; } int get_cnt() { return *_cnt; } };weak_ptr简单实现templateclass T,class D class weak_ptr { private: T* _ptr; D del; public: //只支持用shared_ptr来构造 weak_ptr() delete; weak_ptr(const shared_ptrT ap) { _ptr ap.get_del(); } //只支持用auto_ptr来拷贝赋值 weak_ptrT operator(const shared_ptrT ap) { _ptr ap.get_del(); return *this; } //析构 ~weak_ptr() { _ptr nullptr; } T operator*() const { return *_ptr; } T* operator-() const { return _ptr; } T* get() const { return _ptr; } }; };在c标准库中引用计数是用一个类来封装的所以实现起来比较麻烦但是其他基本逻辑和这里相同。今天分享的内容到此结束期待与你的下次相遇。