异常、RAII和智能指针
扶鞘引狂澜,剑意万重开。欢迎来到丘山望岳的小栈,今天学习的内容是 ,异常、RAII和智能指针,我们闲言少叙,直击主题。
前言:为什么 C++ 需要异常机制?
在 C 语言的开发体系中,处理程序错误的方式非常单一:依靠函数返回值(比如返回负数)、全局错误码
errno来判断执行状态。但这套机制存在致命的工程缺陷:
- 业务逻辑与错误判断高度耦合正常的核心代码中,充斥着大量
if (ret< 0)的容错判断,代码臃肿杂乱,可读性断崖式下降。- 错误极易被忽略开发者可能漏写判断、忽略返回值,隐性错误潜伏在代码中,运行时随机崩库、闪退。
- 多层调用错误传递繁琐多层函数嵌套时,错误需要逐层返回(函数不得不加入一个输出型参数来接收错误信息)、逐层判断、逐层上报,冗余代码爆炸式增长。
- errno码不够直观,需要查找比对错误信息。
为了解决传统错误处理的痛点,C++ 引入了异常(Exception)机制。
它的核心设计哲学只有一句话:将「错误检测」和「错误处理」彻底解耦。
底层函数只负责检测错误、抛出异常,无需关心如何修复;上层调用者根据业务场景,自主决定捕获、处理、重试或终止程序。这也是面向对象思想下,最标准、最优雅的错误处理体系。
一、c++异常处理机制
主要是依托于三个关键字:try ,catch,throw
try和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; cout<<safeDiv(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 { cout<<safeDiv(2,3)<<endl; cout<<safeDiv(2,0)<<endl; } catch (const char* e) { cout << e << endl; } } catch(...) { cout<<"未知异常"<<endl; } cout<<safeDiv(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 *pa=new int(a); int *pb=new int(b); try{ return safeDiv(*pa,*pb); } catch(string s) { delete pa; delete pb; throw s; } delete pa; delete pb; } int main() { try { cout<<fun(1,2)<<endl; cout<<fun(3,4)<<endl; cout<<fun(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:数值下溢
- 其他标准异常(头文件不同)
<new>:std::bad_allocnew 分配内存失败<typeinfo>:std::bad_castdynamic_cast 转换失败<ios>:std::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_cast<double>(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_range
vector::at()访问元素越界会主动抛out_of_range([]不会抛,直接崩溃)
#include <vector> int main() { vector<int> 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_alloc
int main() { try { // 超大内存分配失败 int* p = new int[0x7fffffff]; } catch (const bad_alloc& e) { cout << "内存分配失败:" << e.what() << endl; } return 0; }场景 3:动态转换失败std::bad_cast
class Base { virtual void func(){} }; class Son : public Base {}; int main() { Base b; try { Son& s = dynamic_cast<Son&>(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判断,异常只处理不可恢复、跨多层调用的严重错误 - C++17 废弃老式异常规格
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.四种智能指针和使用场景
头文件<memory>
1.auto_ptr
2.unique_ptr
3.shared_ptr
4.weak_ptr
1.auto_ptr是c++98提供的一套不是很成熟的智能指针,只允许一个类对象绑定一个资源,支持拷贝,但是所有的拷贝都是移动,即被拷贝对象的资源被转移给拷贝对象。因此这个智能指针用处不大。
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_ptr<ListNode> _next; // 指向下一个节点的共享智能指针 std::shared_ptr<ListNode> _prev; // 指向前一个节点的共享智能指针 /* // 解决循环引用的修改方案:将前驱指针改为 weak_ptr // weak_ptr 绑定 shared_ptr 时不会增加目标对象引用计数,不参与资源释放管理,打破循环引用 // std::weak_ptr<ListNode> _prev; */ // 节点析构函数,用于验证对象是否正常释放 ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { // 创建两个共享智能指针,分别管理两个 ListNode 堆对象 std::shared_ptr<ListNode> n1(new ListNode); std::shared_ptr<ListNode> n2(new ListNode); // 刚创建完毕,n1 独占对象,引用计数=1 cout << n1.use_count() << endl; // 刚创建完毕,n2 独占对象,引用计数=1 cout << n2.use_count() << endl; // n1 的_next 指向 n2,n2 对象引用计数 +1 → n2.use_count() = 2 n1->_next = n2; // n2 的_prev 指向 n1,n1 对象引用计数 +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_ptr<ListNode> wp(new ListNode); // 编译报错,禁止直接管理裸指针 */ // main函数结束,n1、n2 局部变量销毁 // 此时两个对象引用计数均为1,互相持有对方,引用计数无法归零,析构函数不会执行,发生内存泄漏 return 0; }在这样一个情况下:如果四个指针都使用智能指针来封装的话:
在析构这个机构时,假设p3,p4都销毁,引用计数1,2都为1,析构node1,node2
p1什么时候销毁,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_ptr<ListNode> _next; // 强指针,持有后继 std::weak_ptr<ListNode> _prev; // 弱指针,仅观测前驱,不增加计数 ~ListNode() { cout << "~ListNode()" << endl; } };智能指针使用示例(c++11支持,c++17已删除):
auto使用示例:
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; #include<memory> class date { private: int _day = 0; int _month = 0; int _year = 1900; public: date(int year=0, int month=0, int day=0) :_year(year), _month(month), _day(day) { } ~date() = default; int get_month() { return _month; } }; int main() { auto_ptr<date> a{ new date{2020,11,3} };//支持赋值但是是移动语义 auto_ptr<date> a1(a); //cout << a->get_month() << endl;//error:this 是空指针 auto_ptr<date>a2(a1); cout << a2->get_month() << endl; //cout << a1->get_month() << endl;//error:this 是空指针 // 不支持移动语义 //auto_ptr<date>a3(move(a2)); //cout << a3->get_month() << endl; }unique_ptr使用示例:
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; #include<memory> class date { private: int _day = 0; int _month = 0; int _year = 1900; public: date(int year=0, int month=0, int day=0) :_year(year), _month(month), _day(day) { } ~date() = default; int get_month()const { return _month; } }; int main() { //unique_ptr<date> a(new date{ 2020,11,3 }); //也可以这样写 unique_ptr<date> a = make_unique<date>(2020, 11, 3); cout << a->get_month() << endl; // unique_ptr 不能拷贝 // unique_ptr<date> a1(a); // 编译期直接拦住 unique_ptr<date> a2 = move(a); // 明确移动 //unique_ptr<date> a2 ( move(a)); 移动赋值也支持 cout << a2->get_month() << endl; // cout << a->get_month() << endl; // 编译期就能发现错误 return 0; }shared_ptr使用示例:
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; #include<memory> class date { private: int _day = 0; int _month = 0; int _year = 1900; public: date(int year=0, int month=0, int day=0) :_year(year), _month(month), _day(day) { } ~date() = default; int get_month()const { return _month; } }; int main() { //shared_ptr<date> sp{ new date{2020,11,3} }; shared_ptr<date> sp = make_shared<date>(2020, 11, 3); cout << sp->get_month() << endl; //支持赋值 shared_ptr<date> sp1(sp); //shared_ptr<date>sp1=sp; cout << sp->get_month() << " " << sp1->get_month() << endl; //支持移动语义 //shared_ptr<date> sp3 = move(sp1); shared_ptr<date> sp3 (move(sp1)); //cout << sp1->get_month() << endl; cout<< sp3->get_month() << endl; return 0; }weak_ptr的使用;
weak_ptr特有的成员函数
.lock()
- 返回
shared_ptr<T>;对象存活则返回有效强指针,销毁返回空智能指针- 必须先 lock 才能访问对象,weak_ptr 不能直接
->访问成员.expired()
- 返回 bool:
true= 绑定的对象已销毁,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_ptr<TestObj> 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_ptr<TestObj> sp1 = make_shared<TestObj>(100); cout << "sp1 初始引用计数: " << sp1.use_count() << endl; // 2. weak_ptr绑定shared_ptr,不会增加shared_ptr引用计数 weak_ptr<TestObj> wp1 = sp1; cout << "wp1绑定sp1后,sp1计数不变: " << sp1.use_count() << "\n\n"; cout << "========= 2. lock():弱指针升级为shared_ptr(核心用法) =========\n"; // lock() 返回 shared_ptr,若原对象已销毁返回空shared_ptr shared_ptr<TestObj> 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_ptr<TestObj> null_sp = wp1.lock(); cout << "销毁后lock得到的shared_ptr是否为空: " << (null_sp == nullptr) << "\n\n"; cout << "========= 4. 拷贝构造、赋值运算符 =========\n"; shared_ptr<TestObj> sp2 = make_shared<TestObj>(200); weak_ptr<TestObj> wp_a = sp2; // 拷贝构造weak_ptr weak_ptr<TestObj> wp_b(wp_a); cout << "wp_a计数: " << wp_a.use_count() << ", wp_b计数: " << wp_b.use_count() << endl; // 赋值重载 weak_ptr<TestObj> 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_ptr<TestObj> sp3 = make_shared<TestObj>(300); weak_ptr<TestObj> wp_x = sp2; weak_ptr<TestObj> 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_ptr<Node> next; weak_ptr<Node> prev; // 反向用weak_ptr打破循环 Node(int v) : val(v) {} ~Node() { cout << "Node析构, val=" << val << endl; } }; // 创建两个节点,互相指向 shared_ptr<Node> n1 = make_shared<Node>(1); shared_ptr<Node> n2 = make_shared<Node>(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_ptr<TestObj> bad_wp(new TestObj(999)); // 上面代码无法编译:weak_ptr不能独立管理堆内存,只能依附shared_ptr做观测 cout << "weak_ptr仅作为shared_ptr的辅助观测,不参与资源生命周期管理\n"; return 0; }八、四种智能指针的简单化模拟实现
auto_ptr简单实现:
template<class 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 template<class 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 template<class T,class D> class shared_ptr { private: T* _ptr = nullptr; int* _cnt;//引用计数 D _del; public: //构造 explicit shared_ptr(T* ptr, int* cnt=new 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_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; } 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简单实现:
template<class T,class D> class weak_ptr { private: T* _ptr; D del; public: //只支持用shared_ptr来构造 weak_ptr() = delete; weak_ptr(const shared_ptr<T>& ap) { _ptr = ap.get_del(); } //只支持用auto_ptr来拷贝赋值 weak_ptr<T>& operator(const shared_ptr<T>& 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++标准库中引用计数是用一个类来封装的,所以实现起来比较麻烦,但是其他基本逻辑和这里相同。
今天分享的内容到此结束,期待与你的下次相遇。
