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和智能指针的设计思路
RAII是Resource 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++ 保证局部对象的析构函数仍会被调用 → 实现异常安全!~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的拷贝构造 是属于深拷贝!因为我们需要的是再开辟一个一样大小的空间 拷贝一样的数据进去;但是,智能指针 他本质属于浅拷贝(默认拷贝构造导致浅拷贝,两个对象指向同一内存) 他不需要再开辟新空间 因为他存在的意义就是 帮忙托管资源 (所以智能指针的用途 决定了他的拷贝必须是浅拷贝) 也就是sp1和sp3共同管理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);此时底层发生了什么?
- 分配两个东西(通常在一次内存分配中完成,高效):
- 实际对象:
int(42) - 控制块(Control Block):包含:
- 引用计数(use_count):当前有多少个
shared_ptr指向这个对象 - (可能还有弱引用计数、自定义删除器等)
- 引用计数(use_count):当前有多少个
内存布局示意: ┌─────────────┬──────────────┐ │ 控制块 │ 实际对象 int │ │ use_count=1 │ value = 42 │ └─────────────┴──────────────┘ ↑ sp1 指向这里(实际对象),但内部也持有控制块的指针 - 拷贝 shared_ptr → 引用计数 +1
auto sp2 = sp1;// 拷贝构造sp2和sp1指向同一个对象和同一个控制块- 控制块中的
use_count从1变成2
- 销毁 shared_ptr → 引用计数 -1
{auto sp3 = sp1;}// sp3 析构sp3被销毁 →use_count从2减到1- 因为
use_count > 0,对象不会被释放
- 当 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() | 获取删除器(用于自定义释放逻辑,如 fclose、free 等) |
3.5 删除器
- 智能指针析构时默认是进行
delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式。 - 因为
new[]经常使用,所以为了简洁一点,unique_ptr和shared_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_ptr和shared_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_ptr和unique_ptr都得构造函数都使用explicit修饰,防止普通指针隐式类型转换成智能指针对象。
3.7 补充
weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上⾯的智能指针,他不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr的⼀个循环引⽤导致内存泄漏的问题。具体细节下⾯我们再细讲。
4. 智能指针的原理包含模拟实现
auto_ptr和unique_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;}- 如下图所述场景,
n1和n2析构后,管理两个节点的引用计数减到1。
注意:_next和prev此时都是智能指针哦~(参考RAII的原理来理解下面的话)
当 程序结束后n1 n2进行一次析构(计数-1此时计数都为1)接着有意思的就来了:右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。_next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。_prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏
把ListNode结构体中的_next和_prev改成weak_ptr,weak_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_array和shared_ptr/shared_array和weak_ptr等。C++ TR1,引入了shared_ptr等,不过注意的是TR1并不是标准版。C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
8. 内存泄漏(加餐)
- 什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
- 内存泄漏的危害:普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤(因为进程结束,内存会自动释放,最怕就是长期程序,而且每次只泄漏一点点🤏),如操作系统、后台服务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越慢,最终卡死。(这也就是电脑卡 很多时候重启就会好很多~)
所以 我们最怕的就是 慢泄漏 内存泄漏 绝对不能小瞧~