C++ 智能指针的使⽤及其原理(包含模拟实现)

C++ 智能指针的使⽤及其原理(包含模拟实现)
欢迎来到我的频道 【点击跳转专栏】
码云链接 【点此转跳】
在阅读本章节前 请保证自己已经熟练掌握了 异常 相关知识!
异常有关内容 可以参考博主写的 C++ 异常详解
文章目录1. 智能指针的使⽤场景分析2. RAII和智能指针的设计思路2.1 智能指针拷贝所带来的隐藏问题3. C++标准库智能指针的使⽤3.1 auto_ptr3.2 unique_ptr3.3 shared_ptr(引用计数)3.4 Member functions(share_ptr为例)1. operator=(赋值操作)2. swap()(交换内容)3. reset()(重置指针)4. get()(获取原始指针)5. operator 和 operator->(解引用)6. use_count()(查看引用计数)7. unique()(是否唯一所有者)8. operator bool(隐式转换为布尔值)9 owner_before()(所有权顺序比较)10. operator <<总结3.5 删除器3.6 make_shared3.7 补充4. 智能指针的原理包含模拟实现4.1 shared_ptr的模拟实现4.1.1 基础结构&&拷贝构造4.1.2 访问所用的额外功能4.1.3 赋值操作4.1.4 删除器4.1.5 完整代码5. shared_ptr和weak_ptr5.1 shared_ptr循环引⽤问题5.2 weak_ptr6. shared_ptr的线程安全问题(了解)7. C++11和boost中智能指针的关系(了解)8. 内存泄漏(加餐)

1. 智能指针的使⽤场景分析

下⾯程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后⾯的delete没有得到执⾏,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new如果失败了 本⾝也可能抛异常,连续的两个new和下⾯的Divide都可能会抛异常,让我们处理起来很⿇烦。智能指针放到这样的场景⾥⾯就让问题简单多了。

doubleDivide(int a,int b){// 当b == 0时抛出异常if(b ==0){throw"Divide by zero condition!";}else{return(double)a /(double)b;}}voidFunc(){// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array1和array2没有得到释放。// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案 是智能指针,否则代码太烂了int* array1 =newint[10];int* array2 =newint[10];// new失败也可能抛异常try{int len, time; cin >> len >> time; cout <<Divide(len, time)<< endl;}catch(...){ cout <<"delete []"<< array1 << endl; cout <<"delete []"<< array2 << endl;delete[] array1;delete[] array2;throw;// 异常重新抛出,捕获到什么抛出什么}// ... cout <<"delete []"<< array1 << endl;delete[] array1; cout <<"delete []"<< array2 << endl;delete[] array2;}intmain(){try{Func();}catch(constchar* errmsg){ cout << errmsg << endl;}catch(const exception& e){ cout << e.what()<< endl;}catch(...){ cout <<"未知异常"<< endl;}return0;}
处理麻烦,无法有效应对new抛异常的情况(析构两次)

2. RAII和智能指针的设计思路

  • RAIIResource Acquisition Is Initialization的缩写,他是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
// 自定义智能指针模板类:演示 RAII(Resource Acquisition Is Initialization)思想// 作用:自动管理通过 new[] 动态分配的数组资源,在对象销毁时自动释放,防止内存泄漏template<classT>classSmartPtr{public:// 构造函数:接收一个裸指针,将其“接管”为本对象管理的资源// 此时资源生命周期与 SmartPtr 对象绑定 —— 这就是 RAII 的“获取即初始化”SmartPtr(T* ptr):_ptr(ptr){}// 析构函数:当 SmartPtr 对象生命周期结束时(如离开作用域),自动调用// 负责释放所管理的资源(这里使用 delete[],说明它专用于管理数组)// 即使程序因异常提前退出,C++ 保证局部对象的析构函数仍会被调用 → 实现异常安全!~SmartPtr(){ cout <<"delete[] "<< _ptr << endl;delete[] _ptr;// 注意:必须与 new[] 配对使用,否则行为未定义}// 重载解引用运算符 *,使得 SmartPtr 可以像普通指针一样使用// 例如:SmartPtr<int> sp(new int(5)); cout << *sp; // 输出 5 T&operator*(){return*_ptr;}// 重载成员访问运算符 ->,用于访问指针所指对象的成员// 例如:若 T 是类类型,则 sp->member 等价于 (*sp).member T*operator->(){return _ptr;}// 重载下标运算符 [],支持像数组一样访问元素// 这表明该智能指针设计用于管理动态数组(如 new int[10]) T&operator[](size_t i){return _ptr[i];}private: T* _ptr;// 保存被管理的裸指针,SmartPtr 对其拥有独占所有权};// 模拟一个可能抛出异常的除法函数// 当除数为 0 时,抛出 C 风格字符串异常(非标准异常类型,但合法)doubleDivide(int a,int b){// 当b == 0时抛出异常if(b ==0){throw"Divide by zero condition!";}else{return(double)a /(double)b;}}// 演示 RAII 如何简化资源管理和异常安全处理voidFunc(){// 这⾥使⽤RAII的智能指针类管理new出来的数组以后,程序简单多了// sp1 和 sp2 是局部对象,它们的析构函数会在函数结束(无论正常返回还是异常退出)时自动调用 SmartPtr<int> sp1 =newint[10]; SmartPtr<int> sp2 =newint[10];// 使用重载的 operator[] 初始化数组元素for(size_t i =0; i <10; i++){ sp1[i]= sp2[i]= i;}// 读取用户输入,并调用可能抛异常的 Divide 函数int len, time; cin >> len >> time; cout <<Divide(len, time)<< endl;// ✅ 关键点:即使 Divide 抛出异常,sp1 和 sp2所在栈帧销毁 sp1\sp2对象销毁 连带着申请的资源仍会正确析构并释放内存!// 这避免了传统写法中因异常导致的内存泄漏问题。}// 主函数:提供统一的异常处理入口intmain(){try{Func();}// 捕获 Divide 抛出的 const char* 类型异常catch(constchar* errmsg){ cout << errmsg << endl;}// 兜底:捕获任何其他未预期的异常catch(...){ cout <<"未知异常"<< endl;}return0;}
RAII核心关键代码:通过将资源(new[] 分配的内存)绑定到对象(SmartPtr)的生命周期,无论函数如何退出(正常 or 异常),资源都会被自动释放。【无论你正常结束函数还是因为异常被动销毁栈帧(这块不理解好好看看异常的机制!说明你异常抛出流程不熟悉!)栈帧释放,对象销毁,自动调用析构,连带着new的资源释放】
// 析构函数:当 SmartPtr 对象生命周期结束时(如离开作用域),自动调用// 负责释放所管理的资源(这里使用 delete[],说明它专用于管理数组)// 即使程序因异常提前退出,C++ 保证局部对象的析构函数仍会被调用 → 实现异常安全!~![请添加图片描述](https://i-blog.ZEEKLOGimg.cn/direct/c6b0c614adbd45faab094ca45aadd1e4.png)SmartPtr(){ cout <<"delete[] "<< _ptr << endl;delete[] _ptr;// 注意:必须与 new[] 配对使用,否则行为未定义}
智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类一样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。
// 重载解引用运算符 *,使得 SmartPtr 可以像普通指针一样使用// 例如:SmartPtr<int> sp(new int(5)); cout << *sp; // 输出 5 T&operator*(){return*_ptr;}// 重载成员访问运算符 ->,用于访问指针所指对象的成员// 例如:若 T 是类类型,则 sp->member 等价于 (*sp).member T*operator->(){return _ptr;}// 重载下标运算符 [],支持像数组一样访问元素// 这表明该智能指针设计用于管理动态数组(如 new int[10]) T&operator[](size_t i){return _ptr[i];}

2.1 智能指针拷贝所带来的隐藏问题

上述代码所写 SmartPtr 不支持拷贝或赋值,若尝试复制会导致双重释放double-delete)崩溃
 SmartPtr<int> sp1 =newint[10]; SmartPtr<int> sp2 =newint[10];//假设开辟的空间叫 AF2100 SmartPtr<int>sp3(sp2);
注意⚠️:首先 你要明白 智能指针和我们平常所见数据结构有本质区别!!比如说 vector的拷贝构造 是属于深拷贝!因为我们需要的是再开辟一个一样大小的空间 拷贝一样的数据进去;但是,智能指针 他本质属于浅拷贝默认拷贝构造导致浅拷贝,两个对象指向同一内存) 他不需要再开辟新空间 因为他存在的意义就是 帮忙托管资源 (所以智能指针的用途 决定了他的拷贝必须是浅拷贝) 也就是 sp1sp3共同管理 AF2100 这块空间!!!所以会导致 二次析构问题!
具体解决 参考share_ptr引用计数部分!

3. C++标准库智能指针的使⽤

C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就可以使用了。
智能指针有好几种,除了weak_ptr他们都符合RAII和像指针一样访问的行为,原理上而言主要是解决智能指针拷贝时的思路不同。
参考文档:https://legacy.cplusplus.com/reference/memory/

3.1 auto_ptr

auto_ptr 是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是一个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用这个智能指针的。

structDate{int _year;int _month;int _day;Date(int year =1,int month =1,int day =1):_year(year),_month(month),_day(day){}~Date(){ cout <<"~Date()"<< endl;}};intmain(){ auto_ptr<Date>ap1(new Date); ap1->_year++;//管理权转移,被拷贝对象悬空 auto_ptr<Date>ap2(ap1); ap2->_year++;// ap1->_year++; ap1已经悬空了!!!}

3.2 unique_ptr

unique_ptr 是C++11设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。
 unique_ptr<Date>up1(new Date);// 禁止拷贝// unique_ptr<Date> up2(up1);// 可以移动 unique_ptr<Date>up2(move(up1));//我们明确知道对象被移动了!

3.3 shared_ptr(引用计数)

shared_ptr 是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。
当拷贝时 “底层是用引用计数的方式实现的”
什么是“引用计数”(Reference Counting)?

引用计数是一种内存/资源管理技术:

为每个被管理的对象维护一个计数器,记录当前有多少个“使用者”正在引用它。当计数变为 0 时,说明没人再需要它了,就自动释放资源。

假设写了:

auto sp1 = std::make_shared<int>(42);

此时底层发生了什么?

  1. 分配两个东西(通常在一次内存分配中完成,高效):
  • 实际对象int(42)
  • 控制块(Control Block):包含:
    • 引用计数(use_count):当前有多少个 shared_ptr 指向这个对象
    • (可能还有弱引用计数、自定义删除器等)
内存布局示意: ┌─────────────┬──────────────┐ │ 控制块 │ 实际对象 int │ │ use_count=1 │ value = 42 │ └─────────────┴──────────────┘ ↑ sp1 指向这里(实际对象),但内部也持有控制块的指针 
  1. 拷贝 shared_ptr → 引用计数 +1
auto sp2 = sp1;// 拷贝构造
  • sp2sp1 指向同一个对象同一个控制块
  • 控制块中的 use_count1 变成 2
  1. 销毁 shared_ptr → 引用计数 -1
{auto sp3 = sp1;}// sp3 析构
  • sp3 被销毁 → use_count2 减到 1
  • 因为 use_count > 0对象不会被释放
  1. 当 use_count == 0 → 自动 delete 对象
sp1.reset();// 或 sp1 离开作用域 sp2.reset();// 或 sp2 离开作用域
  • 最后一个 shared_ptr 被销毁 → use_count 变为 0
  • 此时,控制块会自动调用 delete(或自定义删除器)释放对象内存
创建 shared_ptr ↓ 分配对象 + 控制块(use_count = 1) ↓ 拷贝 shared_ptr → use_count++ ↓ shared_ptr 析构或 reset() → use_count-- ↓ use_count == 0 ? → 是 → delete 对象和控制块 → 否 → 什么也不做 ⚠️:所以 shared_ptr 会多一个 use_count 的参数!! 
案例:
// 可以拷贝可以移动,通过底层引用计数实现 shared_ptr<Date>sp1(new Date); shared_ptr<Date>sp2(sp1); cout << sp1.use_count()<< endl; cout << sp2.use_count()<< endl;// 移动会导致sp1管理的资源被转移,sp1悬空 shared_ptr<Date>sp3(move(sp1)); cout << sp1 << endl; cout << sp2 << endl; cout << sp3 << endl;

3.4 Member functions(share_ptr为例)

篇幅原因 简单说明一下share_ptr 的成员函数 其实其他智能指针之间也大差不差,可能接口名字会有点不同。
#include<iostream>#include<memory>usingnamespace std;structDate{int _year, _month, _day;Date(int y =1,int m =1,int d =1):_year(y),_month(m),_day(d){ cout <<"Date("<< y <<","<< m <<","<< d <<") 构造\n";}~Date(){ cout <<"~Date("<< _year <<","<< _month <<","<< _day <<") 析构\n";}voidprint()const{ cout << _year <<"-"<< _month <<"-"<< _day <<"\n";}};

1. operator=(赋值操作)

// 演示:通过赋值共享所有权intmain(){auto sp1 =make_shared<Date>(2025,4,5);// use_count = 1 cout <<"sp1 创建后,引用计数: "<< sp1.use_count()<<"\n";auto sp2 = sp1;// 赋值:sp2 和 sp1 共享同一个对象 cout <<"sp2 = sp1 后,sp1 引用计数: "<< sp1.use_count()<<"\n"; cout <<"sp2 指向: "; sp2->print();return0;}

输出:

Date(2025,4,5) 构造 sp1 创建后,引用计数: 1 sp2 = sp1 后,sp1 引用计数: 2 sp2 指向: 2025-4-5 ~Date(2025,4,5) 析构 
作用:实现共享所有权。引用计数自动 +1。是 shared_ptr 最核心的操作之一。

2. swap()(交换内容)

// 演示:交换两个 shared_ptr 指向的对象intmain(){auto sp1 =make_shared<Date>(2025,1,1);auto sp2 =make_shared<Date>(2024,12,31); cout <<"交换前:\n"; cout <<"sp1: "; sp1->print(); cout <<"sp2: "; sp2->print(); sp1.swap(sp2);// 交换内部指针,不改变引用计数 cout <<"交换后:\n"; cout <<"sp1: "; sp1->print();// 现在指向 2024-12-31 cout <<"sp2: "; sp2->print();// 现在指向 2025-1-1return0;}

输出:

Date(2025,1,1) 构造 Date(2024,12,31) 构造 交换前: sp1: 2025-1-1 sp2: 2024-12-31 交换后: sp1: 2024-12-31 sp2: 2025-1-1 ~Date(2024,12,31) 析构 ~Date(2025,1,1) 析构 
作用:快速交换两个 shared_ptr 的内部指针。不涉及内存分配/释放,非常高效。常用于算法(如排序)或资源重定向。

3. reset()(重置指针)

// 演示:释放当前管理的对象intmain(){auto sp =make_shared<Date>(2025,4,5); cout <<"创建后引用计数: "<< sp.use_count()<<"\n"; sp.reset();// 放弃管理 → 对象被销毁(因为 use_count 变为 0) cout <<"reset 后,sp 是否为空? "<<(sp ?"否":"是")<<"\n";return0;}

输出:

Date(2025,4,5) 构造 创建后引用计数: 1 ~Date(2025,4,5) 析构 reset 后,sp 是否为空? 是 
作用:主动释放当前管理的资源。相当于“手动”减少引用计数。如果还有其他 shared_ptr 指向该对象,则不会立即析构。

4. get()(获取原始指针)

// 获取裸指针intmain(){auto sp =make_shared<Date>(2025,4,5); Date* raw = sp.get();// 获取原始指针 cout <<"原始指针地址: "<< raw <<"\n"; cout <<"通过 raw 访问: "; raw->print();// ⚠️ 千万不要 delete raw! 否则 double-free!// delete raw; // ❌ 危险!return0;}

输出:

Date(2025,4,5) 构造 原始指针地址: 0x... 通过 raw 访问: 2025-4-5 ~Date(2025,4,5) 析构 
作用:用于与 C 风格 API 交互(如传给只接受 T* 的函数)。⚠️:不能用于管理生命周期!只能读/写,不能 delete

5. operator 和 operator->(解引用)

// 演示:像普通指针一样使用intmain(){auto sp =make_shared<Date>(2025,4,5);// operator*(*sp)._year =2026;// operator-> sp->_month =6; cout <<"修改后: "; sp->print();// 输出 2026-6-5return0;}

输出:

Date(2025,4,5) 构造 修改后: 2026-6-5 ~Date(2026,6,5) 析构 
作用:让 shared_ptr用起来像原生指针*sp 返回对象引用(T&),sp-> 返回成员访问(T*)。

6. use_count()(查看引用计数)

// 演示:查询当前有多少个 shared_ptr 共享该对象intmain(){auto sp1 =make_shared<Date>(2025,4,5); cout <<"sp1 创建后: "<< sp1.use_count()<<"\n";// 1auto sp2 = sp1; cout <<"sp2 = sp1 后: "<< sp1.use_count()<<"\n";// 2 sp2.reset(); cout <<"sp2 reset 后: "<< sp1.use_count()<<"\n";// 1return0;}

输出:

Date(2025,4,5) 构造 sp1 创建后: 1 sp2 = sp1 后: 2 sp2 reset 后: 1 ~Date(2025,4,5) 析构 
作用:调试时非常有用。不要用于程序逻辑判断(多线程下可能变化)。

7. unique()(是否唯一所有者)

// 演示:检查是否只有自己拥有该对象intmain(){auto sp1 =make_shared<Date>(2025,4,5); cout <<"sp1.unique()? "<< sp1.unique()<<"\n";// true (1)auto sp2 = sp1; cout <<"sp2 = sp1 后,sp1.unique()? "<< sp1.unique()<<"\n";// false (2)return0;}

输出:

Date(2025,4,5) 构造 sp1.unique()? 1 sp2 = sp1 后,sp1.unique()? 0 ~Date(2025,4,5) 析构 
作用:等价于 use_count() == 1。可用于优化:如果是唯一所有者,可以安全地移动或修改对象。

8. operator bool(隐式转换为布尔值)

// 演示:检查 shared_ptr 是否为空intmain(){ shared_ptr<Date> sp;// 默认构造,为空if(!sp){ cout <<"sp 为空\n";} sp =make_shared<Date>(2025,4,5);if(sp){ cout <<"sp 不为空\n";}return0;}

输出:

sp 为空 Date(2025,4,5) 构造 sp 不为空 ~Date(2025,4,5) 析构 
作用:为空返回false 不为空返回true安全地判断指针是否有效。比 sp != nullptr 更简洁。

9 owner_before()(所有权顺序比较)

// 演示:比较两个 shared_ptr 的“所有权标识”intmain(){auto sp1 =make_shared<Date>(2025,1,1);auto sp2 =make_shared<Date>(2024,12,31);// owner_before 用于建立严格弱序(strict weak ordering) cout <<"sp1.owner_before(sp2): "<< sp1.owner_before(sp2)<<"\n"; cout <<"sp2.owner_before(sp1): "<< sp2.owner_before(sp1)<<"\n";return0;}

输出(示例):

Date(2025,1,1) 构造 Date(2024,12,31) 构造 sp1.owner_before(sp2): 1 sp2.owner_before(sp1): 0 ~Date(2024,12,31) 析构 ~Date(2025,1,1) 析构 
作用:用于 set<shared_ptr<T>>map<shared_ptr<T>, ...> 等容器的排序。比较的是控制块地址,而非对象地址,确保即使对象相同也能正确排序。

比如说:

int* p =newint(10);//假设 new的空间是 AF2000 std::shared_ptr<int>a(newint(20));//假设 new的空间是 AF3000 std::shared_ptr<int>b(a, p);// alias constructor
这是shared_ptr混合构造template <class U> shared_ptr (const shared_ptr<U>& x, element_type* p) noexcept;

这里面b 管理的对象和a一样都是 AF3000 但是b的地址是p的地址AF2000 此时如果想进行管理权比较就需要owner_before()

10. operator <<

作用:打印地址

总结

函数用途
operator=共享所有权
swap()交换指针
reset()释放资源
get()获取裸指针
operator* / ->访问对象
use_count()查看引用数
unique()是否唯一
operator bool判空
owner_before()所有权排序

unique_ptr:
函数用途
operator=转移所有权(仅支持移动赋值,拷贝被禁用)
swap()交换两个 unique_ptr 的内部指针(高效,不涉及资源复制)
reset()释放当前管理的对象,并可接管新对象(安全重置)
get()获取原始裸指针(用于兼容 C 接口,不可 delete)
operator* / ->解引用以访问所管理对象的成员(像普通指针一样使用)
release()放弃所有权,返回裸指针并将内部指针置空(调用者需手动管理内存)
operator bool判断是否持有有效指针(非空则为 true
get_deleter()获取删除器(用于自定义释放逻辑,如 fclosefree 等)

3.5 删除器

  • 智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式。
  • 因为new[]经常使用,所以为了简洁一点,unique_ptrshared_ptr都特化了一份[]的版本,使用时unique_ptr<Date[]> up1(new Date[5]); shared_ptr<Date[]> sp1(new Date[5]);就可以管理new []的资源。
 unique_ptr<Date[]>up1(new Date[10]); shared_ptr<Date[]>sp1(new Date[10]);

当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。

自定义删除器:
// Date 类:用于演示智能指针管理的对象// 析构时打印日志,便于观察资源释放时机structDate{int _year;int _month;int _day;// 构造函数支持默认参数,方便创建对象Date(int year =1,int month =1,int day =1):_year(year),_month(month),_day(day){}// 析构函数:输出提示,验证对象是否被正确销毁 ~Date(){ cout <<"~Date()"<< endl;}};// 自定义删除器类模板:用于配合 shared_ptr 管理动态数组// 重载 operator(),使其可像函数一样调用,执行 delete[]template<classT>classDeleteArray{public:voidoperator()(T* ptr){delete[] ptr;// 必须与 new[] 配对,否则行为未定义}};// 自定义删除器:用于安全关闭 C 风格 FILE* 资源classFclose{public:voidoperator()(FILE* ptr){ cout <<"fclose:"<< ptr << endl;fclose(ptr);// 实际关闭文件}};intmain(){// C++17 起,unique_ptr 支持数组特化语法:unique_ptr<T[]>// 此时会自动使用 delete[] 释放资源,无需自定义删除器 unique_ptr<Date[]>up1(new Date[10]);// C++17 起,shared_ptr 也支持数组特化:shared_ptr<T[]>// 同样自动调用 delete[],避免常见错误 shared_ptr<Date[]>sp1(new Date[10]);// 若使用 shared_ptr<T>(非数组特化)管理数组,// 必须显式传入 delete[] 删除器,否则默认调用 delete → 未定义行为! shared_ptr<Date>sp2(new Date[10],DeleteArray<Date>());// 同上,但使用 lambda 表达式作为删除器(更简洁) shared_ptr<Date>sp3(new Date[10],[](Date* ptr){delete[] ptr;});// unique_ptr 可通过模板第二个参数指定删除器类型// 此处使用 DeleteArray<Date> 作为删除器,适配 new Date[5] unique_ptr<Date, DeleteArray<Date>>up2(new Date[5]);// 使用 lambda 作为 unique_ptr 的删除器// 注意:lambda 类型是唯一的,需用 decltype 推导auto del =[](Date* ptr){delete[] ptr;};//必须显示定义 因为哪怕你写两个一样的lambda 它也会生成两个不同的仿函数(这块不懂 说明lambda表达式底层你学的不行) unique_ptr<Date,decltype(del)>up3(new Date[5], del);//第二个参数也要传del 是因为lambda无默认构造// C++20 起允许省略删除器构造实参(若删除器可默认构造)// 此处 lambda 无捕获,可默认构造,故 new Date[5] 后无需写 del unique_ptr<Date,decltype(del)>up4(new Date[5]);// shared_ptr 管理 C 资源(如 FILE*)的经典方式:// 传入资源指针 + 自定义删除器,确保资源被正确释放 shared_ptr<FILE>sp5(fopen("Test.cpp","r"),Fclose());// 同上,但使用 lambda 作为删除器(更紧凑) shared_ptr<FILE>sp6(fopen("Test.cpp","r"),[](FILE* ptr){ cout <<"fclose:"<< ptr << endl;fclose(ptr);});return0;}
⚠️:感觉是不是unique_ptrshared_ptr感觉构造有点割裂 尤其是unique_ptr用个删除器弯弯绕绕老难受了 说实话 我真的强烈觉得 这块底层是两个不同的人写的 他们模版参数都不一样 挺无语的老实说😓

3.6 make_shared

  • shared_ptr除了支持用指向资源的指针构造,还支持make_shared 用初始化资源对象的值直接构造。
template<classT,class... Args> shared_ptr<T>make_shared(Args&&... args);
// 这个是new好了把指针交给 sp10 shared_ptr<Date>sp10(newDate(2025,10,12));// make_shared 是创建 shared_ptr 的推荐方式://这里是 sp11 直接自己new// 更高效(控制块与对象同分配)、异常安全auto sp11 =make_shared<Date>(2025,10,12); ⚠️: // shared_ptr 不支持从裸指针隐式转换(防止意外资源泄漏)// 因此不能写:shared_ptr<Date> sp12 = new Date;// 必须显式调用构造函数(如下) shared_ptr<Date>sp12(new Date);
shared_ptrunique_ptr 都得构造函数都使用explicit修饰,防止普通指针隐式类型转换成智能指针对象。

3.7 补充

weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上⾯的智能指针,他不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr的⼀个循环引⽤导致内存泄漏的问题。具体细节下⾯我们再细讲。

4. 智能指针的原理包含模拟实现

  • auto_ptrunique_ptr,这两个智能指针的实现⽐较简单,⼤家了解⼀下原理即可。
  • auto_ptr的思路是拷⻉时转移资源管理权给被拷⻉对象,这种思路是不被认可的,也不建议使⽤
auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){// 管理权转移 sp._ptr =nullptr;} auto_ptr<T>&operator=(auto_ptr<T>& ap){// 检测是否为⾃⼰给⾃⼰赋值if(this!=&ap){// 释放当前对象中资源if(_ptr)delete _ptr;// 转移ap中资源到当前对象中 _ptr = ap._ptr; ap._ptr =NULL;}return*this;}
  • unique_ptr的思路是不⽀持拷⻉。
unique_ptr(const unique_ptr<T>& sp)=delete; unique_ptr<T>&operator=(const unique_ptr<T>& sp)=delete;

4.1 shared_ptr的模拟实现

重点要看看shared_ptr是如何设计的,尤其是引⽤计数的设计,主要这⾥⼀份资源就需要⼀个引⽤计数,所以引⽤计数才⽤静态成员的⽅式是⽆法实现的,要使⽤堆上动态开辟的⽅式,构造智能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就++引⽤计数,shared_ptr对象析构时就–引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象,则析构资源。

4.1.1 基础结构&&拷贝构造

其实大体结构是没变的 重点就是引用计数该怎么设计 我们可以通过一个动态分配的 int* _pcount 让多个 shared_ptr 共享同一个计数。
// Date 类:用于演示智能指针管理的对象// 析构时打印日志,便于观察对象何时被销毁structDate{int _year;int _month;int _day;// 构造函数支持默认参数,方便创建对象Date(int year =1,int month =1,int day =1):_year(year),_month(month),_day(day){}// 析构函数:输出提示,验证资源是否被正确释放 ~Date(){ cout <<"~Date()"<< endl;}};// 自定义命名空间 fcy,避免与标准库 std::shared_ptr 冲突namespace fcy {// 使用引用计数(reference counting)实现多所有权共享template<classT>classshared_ptr{public:// 构造函数:接管裸指针,并初始化引用计数为 1// 注意:此处假设传入的 ptr 是通过 new 分配的(非数组)shared_ptr(T* ptr =nullptr)//默认构造:_ptr(ptr),_pcount(newint(1))// 动态分配计数器,初始值为 1{}// 拷贝构造函数:实现“共享”语义// 多个 shared_ptr 共享同一对象和同一个引用计数shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){++(*_pcount);// 引用计数加 1}// 析构函数:当对象销毁时,引用计数减 1// 若减到 0,说明自己是最后一个管理者,负责释放资源 ~shared_ptr(){//最后一个管理资源的对象释放if(--(*_pcount)==0){delete _ptr;// 释放被管理的对象delete _pcount;// 释放引用计数器本身}}private: T* _ptr;// 指向被管理的对象int* _pcount;// 指向引用计数(多个 shared_ptr 共享同一个计数器)};}intmain(){// 创建第一个 shared_ptr,管理一个 Date 对象// 此时引用计数为 1 fcy::shared_ptr<Date>sp1(new Date);// 拷贝构造 sp2,与 sp1 共享同一个 Date 对象// 引用计数变为 2 fcy::shared_ptr<Date>sp2(sp1);// 创建另一个独立的 shared_ptr,管理新的 Date 对象// 它有自己的引用计数(值为 1),与 sp1/sp2 无关 fcy::shared_ptr<Date>sp3(new Date);// 无实际作用,调试用的int i;}
运行结果:

4.1.2 访问所用的额外功能

这个太简单 没什么好说的 直接给代码了
// 像指针一样使用 T&operator*(){return*_ptr;} T*operator->(){return _ptr;} T&operator[](int i){return _ptr[i];} T*get()const{return _ptr;}intuse_count()const{return*_pcount;}

4.1.3 赋值操作

在拷贝赋值时,我们先调用 release()(因为析构函数的函数体在后面有用 为了代码的复用,我们把它单独封装成一个函数) 对当前管理的资源引用计数减一,若减至零(说明自己是最后一个管理者),则释放对象和计数器;随后接管右侧 sp_ptr_pcount,并将新资源的引用计数加一,从而完成所有权的转移与共享。

// release() 函数:减少当前 shared_ptr 所管理资源的引用计数// 若计数减至 0,说明自己是最后一个管理者,则释放对象和计数器voidrelease(){//最后一个管理资源的对象释放if(--(*_pcount)==0){delete _ptr;// 释放被管理的对象(必须与 new 配对)delete _pcount;// 释放动态分配的引用计数器}}// 拷贝赋值运算符:实现 shared_ptr 的赋值语义(共享所有权)// 通过比较 _ptr 是否相同来避免不必要的操作 shared_ptr<T>&operator=(const shared_ptr<T>& sp){// 判断:如果当前管理的对象与 sp 管理的不是同一个,// 才执行释放旧资源、接管新资源的逻辑if(_ptr != sp._ptr){release();// 释放当前资源(引用计数-1,可能触发 delete) _ptr = sp._ptr;// 接管 sp 的对象指针 _pcount = sp._pcount;// 接管 sp 的引用计数器++(*_pcount);// 新资源的引用计数 +1}// 如果 _ptr == sp._ptr,说明赋值前后指向同一对象(如 sp1 = sp2,且 sp1 和 sp2 已共享同一资源),// 此时无需任何操作,直接返回(避免重复增减引用计数)return*this;}
效果:

4.1.4 删除器

我们将删除器作为成员变量存储:
std::function<void(T*)> _del =[](T* ptr){delete ptr;};
  • 使用 std::function<void(T*)> 作为删除器类型,统一支持任意可调用对象(普通函数、lambda、仿函数等)。
  • 提供默认删除器:[](T* ptr) { delete ptr; },适配 new 分配的单个对象。
这使得 shared_ptr 不再硬编码 delete,而是委托给用户提供的 _del
然后在构造时注入自定义删除器
template<classD>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(newint(1)),_del(del)// 保存用户传入的删除器{}
  • 通过模板参数 D 接收任意类型的删除器(如 DeleteArray、lambda 等)。
  • 在初始化列表中将其赋值给 _del,完成删除策略的注入
🌰 例如:

此处 lambda 会覆盖默认删除器,确保使用 delete[] 释放数组。

接着 资源释放统一由 _del(_ptr) 完成
voidrelease(){if(--(*_pcount)==0){_del(_ptr);// ← 关键:调用用户定义的删除逻辑delete _pcount;}}
  • 当引用计数归零时,不再写死 delete _ptr,而是调用 _del(_ptr)
  • 用户可通过删除器实现任意清理逻辑:
    • delete[](数组)
    • fclose()(文件句柄)
    • free()(malloc 内存)
    • 自定义资源回收
最后记得 拷贝/赋值时同步删除器 即可

4.1.5 完整代码

#include<iostream>#include<memory>#include<functional>usingnamespace std;structDate{int _year;int _month;int _day;Date(int year =1,int month =1,int day =1):_year(year),_month(month),_day(day){}~Date(){ cout <<"~Date()"<< endl;}};namespace fcy {template<classT>classshared_ptr{public:shared_ptr(T* ptr):_ptr(ptr),_pcount(newint(1)){}template<classD>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(newint(1)),_del(del){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount),_del(sp._del){++(*_pcount);}voidrelease(){//最后一个管理资源的对象释放if(--(*_pcount)==0){_del(_ptr);delete _pcount;}} shared_ptr<T>&operator=(const shared_ptr<T>& sp){if(_ptr!=sp._ptr){release(); _ptr=sp._ptr; _pcount=sp._pcount;++(*_pcount); _del = sp._del;}return*this;}~shared_ptr(){// //最后一个管理资源的对象释放// if (--(*_pcount)==0)// {// delete _ptr;// delete _pcount;// }release();} T*get()const{return _ptr;}intuse_count()const{return*_pcount;}// 像指针一样使用 T&operator*(){return*_ptr;} T*operator->(){return _ptr;} T&operator[](int i){return _ptr[i];}private: T* _ptr;int* _pcount; std::function<void(T*)> _del =[](T* ptr){delete ptr;};};}intmain(){ fcy::shared_ptr<Date>sp1(new Date); fcy::shared_ptr<Date>sp2(sp1); fcy::shared_ptr<Date>sp3(new Date); sp1 = sp1; sp1 = sp2; sp1 = sp3;// // 定制删除器 fcy::shared_ptr<Date>sp5(new Date[10],[](Date* ptr){delete[] ptr;});int i;}

5. shared_ptr和weak_ptr

5.1 shared_ptr循环引⽤问题

  • shared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。
structListNode{int _data; std::shared_ptr<ListNode> _next; std::shared_ptr<ListNode> _prev;ListNode(int val=0):_data(val){}~ListNode(){ cout <<"~ListNode()"<< endl;}};intmain(){// 循环引⽤ -- 内存泄露 std::shared_ptr<ListNode>n1(new ListNode); std::shared_ptr<ListNode>n2(new ListNode); cout << n1.use_count()<< endl;// 1 cout << n2.use_count()<< endl;// 1 n1->_next = n2; n2->_prev = n1; cout << n1.use_count()<< endl;// 2 cout << n2.use_count()<< endl;// 2return0;}
  • 下图所述场景,n1n2析构后,管理两个节点的引用计数减到1

注意:_nextprev 此时都是智能指针哦~(参考RAII的原理来理解下面的话)
当 程序结束后 n1 n2 进行一次析构(计数-1 此时计数都为 1)接着有意思的就来了:右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。_next什么时候析构呢,_next左边节点的的成员,左边节点释放,_next就析构了。左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。_prev什么时候析构呢,_prev右边节点的成员,右边节点释放,_prev就析构了。至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏
ListNode结构体中的_next_prev改成weak_ptrweak_ptr绑定到shared_ptr时不会增加它的引用计数,_next_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题

5.2 weak_ptr

  • weak_ptr不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr引用计数,那么就可以解决上述的循环引用问题。因为很简单 这里可以很快速的实现一个模拟的weak_ptr
// 不增加引用计数template<classT>classweak_ptr{public:weak_ptr(){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){} weak_ptr<T>&operator=(const shared_ptr<T>& sp){ _ptr = sp.get();return*this;}private: T * _ptr =nullptr;};}
⚠️:其实shared_ptr引用计数 是单独封装了一个类的 这是为了保证当有weak_ptr的时候 当use_count==0的时候 weak_ptr依然可以访问use_count来判断其是否有没有失效(了解即可)
当这里使用weak_ptr时:

此时use_count始终 为1:
  • weak_ptr也没有重载operator*operator->等,因为他不参与资源管理,那么如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

一些简单功能 可以看博主的 注释:
intmain(){// 创建一个 shared_ptr,管理字符串 "111111" std::shared_ptr<string>sp1(newstring("111111"));// sp2 拷贝 sp1,两者共享同一个 string 对象,引用计数为 2 std::shared_ptr<string>sp2(sp1);// wp 是一个 weak_ptr,观察 sp1 所管理的对象(不增加引用计数) std::weak_ptr<string> wp = sp1;// expired():检查被观察对象是否已被销毁(即 use_count == 0)// 此时 sp1 和 sp2 仍在,对象有效 → expired() 返回 false (0) cout << wp.expired()<< endl;// use_count():返回当前共享该对象的 shared_ptr 数量(不包括 weak_ptr)// 此时有 sp1 和 sp2 → use_count = 2 cout << wp.use_count()<< endl;// sp1 被重新赋值,指向新对象 "222222"// 原对象 "111111" 现在仅由 sp2 管理(use_count = 1),仍未销毁 sp1 =make_shared<string>("222222");// wp 仍观察原对象 "111111",它尚未被销毁 → expired() 仍为 false (0) cout << wp.expired()<< endl;// 原对象现在只有 sp2 引用 → use_count = 1 cout << wp.use_count()<< endl;// sp2 也被重新赋值,指向新对象 "333333"// 原对象 "111111" 不再被任何 shared_ptr 管理 → 被自动销毁 sp2 =make_shared<string>("333333");// wp 观察的对象已销毁 → expired() 返回 true (1) cout << wp.expired()<< endl;// 对象已销毁,use_count 为 0 cout << wp.use_count()<< endl;// wp 重新绑定到 sp1 当前管理的对象("222222") wp = sp1;// wp.lock():尝试将 weak_ptr 提升为 shared_ptr// 若对象仍存在,返回有效的 shared_ptr;否则返回空 shared_ptr// 此处 sp1 有效,故 sp3 成功指向 "222222"auto sp3 = wp.lock();// wp 现在观察的是 "222222",该对象由 sp1 和 sp3 共享(sp2 已离开)// 所以 expired() 为 false (0) cout << wp.expired()<< endl;// use_count = 2(sp1 和 sp3) cout << wp.use_count()<< endl;// 通过 sp3 修改对象内容(string 支持 +=)*sp3 +="###";// sp1 和 sp3 共享同一对象,因此 sp1 的内容也被修改 cout <<*sp1 << endl;// 输出: 222222###return0;}

6. shared_ptr的线程安全问题(了解)

这部分 跟线程挂钩 等学到线程自然就懂 看看就行
  • shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。
  • shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。
  • fcy::shared_ptr引用计数从int*改成atomic<int>*就可以保证引用计数的线程安全问题,或者使用互斥锁加锁也可以。

7. C++11和boost中智能指针的关系(了解)

Boost库是为C++语言标准库提供扩展的一些C++程序库的总称,Boost社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,Boost社区的发起人Dawes本人就是C++标准委员会的成员之一。在Boost库的开发中,Boost社区也在这个方向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从Boost中来的。C++ 98 中产生了第一个智能指针auto_ptr。C++ boost给出了更实用的scoped_ptr/scoped_arrayshared_ptr/shared_arrayweak_ptr等。C++ TR1,引入了shared_ptr等,不过注意的是TR1并不是标准版。C++ 11,引入了unique_ptrshared_ptrweak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

8. 内存泄漏(加餐)

  • 什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
  • 内存泄漏的危害:普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤(因为进程结束,内存会自动释放,最怕就是长期程序,而且每次只泄漏一点点🤏),如操作系统、后台服务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越慢,最终卡死。(这也就是电脑卡 很多时候重启就会好很多~)
所以 我们最怕的就是 慢泄漏 内存泄漏 绝对不能小瞧~

Read more

人工智能、机器学习和深度学习,其实不是一回事

人工智能、机器学习和深度学习,其实不是一回事

一、人工智能、机器学习与深度学习的真正区别 在当今科技领域,我们经常听到人工智能、机器学习和深度学习这三个词。它们虽然相关,但含义不同。 1.1 人工智能 人工智能是计算机科学的一个分支,旨在研究如何合成与分析能够像人一样行动的计算主体。简单来说,AI 的目标是利用计算机来模拟甚至替代人类大脑的功能。 一个理想的 AI 系统通常具备以下特征:像人一样思考、像人一样行动、理性地思考与行动。 1.2 机器学习 机器学习是实现人工智能的一种途径。它的核心定义是:赋予计算机在没有被显式编程的情况下进行学习的能力。 与传统的基于规则的编程不同,机器学习不依赖程序员手写每一条逻辑指令,而是通过算法让机器从大量数据中寻找规律,从而对新的数据产生预测或判断。 1.3 深度学习 深度学习是机器学习的一种特殊方法,也称为深度神经网络。它受人类大脑结构的启发,通过设计多层的神经元网络结构,来模拟万事万物的特征表示。 1.4 三者之间的层级关系 厘清这三者的关系对于初学者至关重要。人工智能 AI是最宏大的概念,包含了所有让机器变聪明的技术。机器学习 ML是 AI

By Ne0inhk
被问爆的Agent实战:从0到1搭建可落地AI智能体

被问爆的Agent实战:从0到1搭建可落地AI智能体

🎁个人主页:User_芊芊君子 🎉欢迎大家点赞👍评论📝收藏⭐文章 🔍系列专栏:AI 文章目录: * 【前言】 * 一、先搞懂:2026年爆火的AI Agent,到底是什么? * 1.1 Agent的核心定义 * 1.2 Agent的4大核心能力 * 1.3 2026年Agent的3个热门落地场景 * 二、框架选型:2026年6大主流Agent框架,新手该怎么选? * 三、实战环节:从0到1搭建可落地的“邮件处理Agent”(全程代码+步骤) * 3.1 实战准备:环境搭建(10分钟搞定) * 3.1.1 安装Python环境 * 3.1.2 创建虚拟环境(避免依赖冲突) * 3.

By Ne0inhk
2026年 Trae 收费模式改变 —— AI 编程“免费午餐”终结后的生存法则

2026年 Trae 收费模式改变 —— AI 编程“免费午餐”终结后的生存法则

关键词:Trae, Cursor, AI 编程成本, Token 计费, Agent 模式, 职业转型 大家好,我是飞哥!👋 2026年,AI编辑器Trae 也将收费模式改为按 Token 收费。 有些开发者开始动摇:“AI 编辑器越来越贵,是不是应该放弃使用,回归纯手写代码?” 对于用户来说,这无疑是一次涨价。但在飞哥看来,这次涨价背后释放了两个非常关键的信号: 1. AI 技术已进入稳定成熟期: 厂商不再需要通过“免费/低价补贴”来换取用户数据进行模型迭代。产品已经足够成熟,有底气接受市场真实定价的检验。 2. 倒逼用户进化,优胜劣汰: 涨价是一道筛子。它在要求用户大幅提升自己的 AI 使用水平(如 Prompt 技巧、Context 管理)。 * 低级使用者(只会问“怎么写代码”

By Ne0inhk

OpenClaw 控制你的 Mac 和 Windows 电脑,支持 SKill 的 GitHub 神器。

逛 GitHub 的时候,发现一个叫 TuriX-CUA 的开源项目。这是一个 Computer-Use Agent,电脑使用智能体框架。 它可以让 AI 大模型可以像人类一样,直接在桌面电脑上看屏幕 + 动手操作。 完成跨应用的复杂任务,而不是只在对话框里输出文字。 它不像传统 RPA 或基于 API 的集成方式,用如果人能点到的地方,TuriX 也能点的方式,实现跨应用自动化。 通过自然语言描述任务,AI 自动规划并执行,操纵的应用不提供 API 也没事儿。 而且,现在有专门的 Skill,能让你的 OpenClaw 或 Claude Code 使用TuriX-CUA。 目前在 Skill 广场中,Computer Use Agent 里排最高: 01 开源项目简介

By Ne0inhk