
🎬 个人主页 :MSTcheng · CSDN
🌱 代码仓库 :MSTcheng · Gitee
🔥 精选专栏 : 《C语言》
《数据结构》
《算法学习》
《C++由浅入深》
💬座右铭: 路虽远行则将至,事虽难做则必成!
前言在上一篇文章中我们向大家介绍了异常,对于异常中忽略资源释放的情况就需要智能指针来解决。所以本篇文章我们来重点介绍一下智能指针。
文章目录
一、智能指针的介绍
1.1智能指针的概念
智能指针是C++中用于管理动态分配内存的模板类,通过封装原始指针并自动处理内存释放,避免内存泄漏。其核心功能是模拟指针行为的同时加入资源管理机制,确保对象在不再使用时被正确销毁。
智能指针被定义在<memory>这个头文件中:

其中:unique_ptr、shared_ptr以及weak_ptr是我们重点学习的智能指针。
1.2为什么要有智能指针?
下面来看看这样的场景:
cpp
#include<iostream>
#include<string>
using namespace std;
double Divide(int a, int b)
{
if (b == 0)
{
throw string("Divide by zero condition!");
}
else
{
return (double)a / (double)b;
}
}
void func()
{
int* arry1 = new int[10];
int* arry2 = new int[10];
try
{
int a = 0, b = 0;
cin >> a >> b;
Divide(a, b);
}
catch (...)
{
cout << "未知异常" << endl;
throw; //再次抛异常
}
//如果divide抛异常被捕获了 那么就会跳到main函数中的catch子句
delete[] arry1;
delete[] arry2;
}
int main()
{
try
{
func();
}
catch(const string& ermasg)
{
cout << ermasg << endl;
}
return 0;
}
观察上面的代码会发现,
func函数中存在内存泄露的问题,因为如果divide函数抛出异常被距离divide函数最近的catch子句接收后,又继续抛出异常让上一层的main函数捕获,那么就跳到了main函数中的catch子句中执行后面的内容了,此时相应的调用链所创建的局部对象会进行销毁,比如像func函数内部创建的变量a,b,arry1,arry2,这些局部变量都会销毁。但是由于arry1和arry2内部是有资源的,而异常又跳过了释放资源的代码所以导致资源泄露。
1、当divide函数没有抛异常,执行资源释放的代码,资源正常释放

2、当divide函数出现除零错误抛出异常,跳过一些释放资源的代码导致内存泄露

如果我们想要解决这个问题就需要在
new以后捕获异常,捕获到没有释放的资源后就delete释放资源,然后再把异常抛出。而new本身也可能会抛异常,divide也会抛异常,这样处理起来就比较麻烦,正是因为这样的场景存在,智能指针便有了用武之地!
二、智能指针的使用及其原理
在了解智能指针的使用之前我们先来了解一下RAII:
2.1RAII
RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。
RAII的作用:
- 在获取资源时把资源委托给⼀个对象,通过这个对象控制对资源的访问。
- 该资源在该对象的生命周期内始终保持有效,最后在该对象调用析构的时候释放资源。
下面就来体会一下智能指针:
cpp
template<class T>
class Smartptr
{
public:
Smartptr(T* ptr)
:_ptr(ptr)
{
}
~Smartptr()
{
cout << "delete[]" << _ptr <<endl;
delete[] _ptr;
}
//为了方便智能指针对资源的访问,还需重载operator*/operator->/operator[]
//这样方便访问
T& operator *()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
double Divide(int a, int b)
{
if (b == 0)
{
throw string("Divide by zero condition!");
}
else
{
return (double)a / (double)b;
}
}
void func()
{
/*int* arry1 = new int[10];
int* arry2 = new int[10];*/
Smartptr<int> sp1 = new int[10];
Smartptr<int> sp2 = new int[10];
//给sp1和sp2分别赋值
for (size_t i = 0;i < 10;i++)
{
sp1[i] = sp2[i] = i;
}
int a = 0, b = 0;
cin >> a >> b;
Divide(a, b);
}
int main()
{
try
{
func();
}
catch (const string& ermasg)
{
cout << ermasg << endl;
}
catch(const exception & e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << "未知异常" << endl;
}
return 0;
}

上面我们写了一个智能指针的类,并创建了两个智能指针对象sp1和sp2来管理资源,就是上面我们所说的将资源委托给智能指针对象。
通过上面程序的运行结果我们会发现,资源释放是在异常捕获之前就释放了 ,这是因为异常捕获以后沿着调用链所创建的局部变量都会销毁,而当智能指针被销毁的时候一定会去调用析构从而释放资源。 注意:使用了智能指针之后无论在哪里接收异常,都不会影响资源释放。
有些人也可能有疑问:如果将抛异常放在sp1和sp2的下面在divide函数抛异常在main函数捕获时势必会跳过下面的代码,那么这样资源是不是就没有被释放?
cpp
void func()
{
/*int* arry1 = new int[10];
int* arry2 = new int[10];*/
int a = 0, b = 0;
cin >> a >> b;
Divide(a, b);
Smartptr<int> sp1 = new int[10];
Smartptr<int> sp2 = new int[10];
//给sp1和sp2分别赋值
for (size_t i = 0;i < 10;i++)
{
sp1[i] = sp2[i] = i;
}
}
仔细想想其实就会发现,
divide函数抛异常到main函数中被捕获确实会跳过下面sp1和sp2的代码,但是此时由于代码没有执行空间压根就没有开辟出来,又怎么会有资源呢?所以从而也佐证了使用了智能指针之后,解决了抛异常时资源泄露的问题!
所以总结一句话 :RAII是智能指针的指导思想,它通过一个智能指针类,有效的将资源全部管理起来,最大的优点就是在智能指针对象销毁时会去调用析构,从而释放资源,避免内存泄露。
2.2智能指针的使用
前面我们说过了,对于智能指针的使用我们重点学习 ,unique_ptr、shared_ptr、weak_ptr 这三个指针,下面我们就来依次介绍这三个指针。
1、unique_ptr

unique_ptr是C++11设计出来的指针,它的名字叫唯一指针 ,也就是该类定义出来的对象智能唯一的指向一块资源,因此它不允许拷贝,从而保证唯一性,但是支持移动赋值和移动构造。因为移动赋值移动构造的本质就是抢夺资源,交换指针之后依然能保证唯一性。
cpp
#include<iostream>
#include<memory>
using namespace std;
struct Date
{
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;
}
};
int main()
{
unique_ptr<int> up1(new int[10]);
unique_ptr<Date> up2(new Date);
//unique_ptr禁止拷贝 因为要确保唯一性
//unique_ptr<Date> up3(up2);
//但是可以移动 move了之后就相当于将up2强转为右值引用类型 调用右值移动构造抢夺资源
//调用右值移动构造 移动后up2不再拥有资源 权限给了up4 由up4来管理资源
unique_ptr<Date> up4(move(up2));
return 0;
}
2、shared_ptr

shared_ptr同样也是C++11设计出来的指针,它的名字叫共享指针 ,从名字上我们就能知道它支持共享资源,所以它支持拷贝,也支持移动 。所以shared_ptr对象可以共享对指针的所有权,即同时指向同一个对象。
cpp
//延续上面的日期类代码
int main()
{
//shared_ptr支持拷贝、也支持移动
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);
shared_ptr<int> sp3(new int(1));
//移动会导致sp3 管理的资源被转移,sp3悬空
shared_ptr<int> sp4(move(sp3));
}

当有多个对象共同管理同一个资源的时候,我们如何知道资源什么时候被释放,或则由哪个对象销毁的时候去释放呢? 这就要介绍引用计数了。
引用计数本质就是一个计数器,由一个指针指向一块空间,每个
shared_ptr对象中都会存储一块资源且还而外指向一个计数器 , 每次新增了对象来共同管理这块资源的时候计数器就加一,每次销毁一个对象之后计数器就减一 。直到计算器减到了0,说明该对象是最后一个管理该资源的对象了,此时就要调用析构释放资源。 其他情况下只是让计数器减减不要调用析构。 这点在后面模拟实现shared_ptr的时候会展示。
cpp
int main()
{
unique_ptr<int> up1(new int[10]);
unique_ptr<Date> up2(new Date);
//unique_ptr禁止拷贝 因为要确保唯一性
//unique_ptr<Date> up3(up2);
//但是可以移动 move了之后就相当于将up2强转为右值引用类型 调用右值移动构造抢夺资源
//调用右值移动构造 移动后up2不再拥有资源 权限给了up4 由up4来管理资源
unique_ptr<Date> up4(move(up2));
//shared_ptr支持拷贝、也支持移动
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);
shared_ptr<int> sp3(new int(1));
shared_ptr<int> sp4(move(sp3));
//shared_ptr还提供了use_count()接口来获取引用计数
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
//shared_ptr 和 unique_ptr 还支持operator bool的类型转换,如果智能指针对象是⼀个
//空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断
//是否为空。
//sp3被移动了此时没有管理资源返回false 取反一下就是true
if (!sp3) //其本本质是调用if(sp3.operator bool())
{
cout << "sp3 空!" << endl;
}
if (sp2)
{
sp2.reset();//reset()接口就是手动删除共享对象 如果sp2不是最后一个对象
//减减计数即可
cout << "sp2 非空!" << endl;
}
shared_ptr<Date> sp5(new Date);
Date* ptr = sp5.get(); //get()接口 获取sp5所指向的资源的地址
//int* ptr = new int(10);
cout << ptr << endl;
//同时share_ptr还支持make_shared来构造
//shared_ptr<Date> sp10(new Date(2025, 10, 12));
shared_ptr<Date> sp11 = make_shared<Date>(2025, 10, 12);
//auto sp11 = make_shared<Date>(2025, 10, 12); //使用auto自动推类型
return 0;
}
unique_ptr和shared_ptr定制删除器的使用:
删除器:
- 所谓删除器本质就是⼀个可调用对象 ,这个可调用对象中实现你想要的释放资源的方式式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为
new[]经常使用,所以为了简洁⼀点,unique_ptr和shared_ptr都特化了⼀份[]的版本,使用时unique_ptr<Date[]> up1(newDate[5]);shared_ptr<Date[]> sp1(new Date[5]);就可以管理new []的资源。 - 智能指针析构时默认是进⾏
delete释放资源, 这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。所以智能指针支持在构造时给⼀个删除器。
cpp
//延续上面的Date代码
#include<memory>
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
//使用智能指针特化的版本 Date[] 要搭配delete[]来析构
unique_ptr<Date[]> up1(new Date[10]);
shared_ptr<Date[]> sp1(new Date[10]);
//库中在第二个参数调用删除器
shared_ptr<Date> sp2(new Date[10], DeleteArray<Date>());
//定制删除器 定制一个删除器 让他满足我们的需求 调用delete[]
shared_ptr<Date[]> sp3(new Date[10],[](Date*ptr){delete[] ptr});
//unique_ptr有点不一样 是在模板参数的第二个参数传删除器
unique_ptr<Date,DeleteArray<Date>> up2(new Date[10]);
//也可以使用lambda表达式来创建一个可调用对象 然后传给第二个参数
auto del = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(del)> up3(new Date[5], del);
}
3、weak_ptr

weak_ptr是C++11设计出来的智能指针,他的名字叫弱指针 ,他完全不同于上面的智能指针,他不支持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产生本质是要解决shared_ptr的⼀个循环引⽤导致内存泄漏的问题。 具体细节下面我们再细讲
三、智能指针的模拟实现
这里我们着重模拟实现shared_ptr,通过模拟实现shared_ptr有助于我们更好的理解智能指针:
cpp
#include<iostream>
using namespace std;
namespace my_shared_ptr
{
template<class T>
class shared_ptr
{
public:
template<class D>
shared_ptr(T* ptr=nullptr,D del)//D是删除器的类型
:_ptr(ptr)
, _pcount(new int(1))
,_del(del)
{}
//拷贝构造
//sp1(sp2) 拷贝一般不允许修改sp加上const
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)//pcount也要拷贝两个共享的对象得到pcount保持一致
,_del(sp._del)
{
(*_pcount)++;
}
//赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//防止给自己赋值
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
//每多一个对象管理资源 pcount就要加加
(*_pcount)++;
}
return *this;
}
void release()
{
if (--(*_pcount) == 0)
{
//此时删除对象即可
delete _pcount;
//delete _ptr;
_del(_ptr);//使用删除器对象 来进行删除
}
}
//析构
~shared_ptr()
{
release();
}
T* get()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
//=================
//迭代器
//=================
T& operator*()
{
//模拟指针的行为 解引用拿到资源里面的值
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](int i)
{
return _ptr[i];
}
private:
//
T* _ptr;
int* _pcount;
//定制删除器 包装了一个lambda表达式 能够创建lambda的可调用对象
std::functional<void(T*)> _del=[](T*ptr){delete ptr};
};
}
int main()
{
my_shared_ptr::shared_ptr<Date> sp1(new Date);
my_shared_ptr::shared_ptr<Date> sp2(sp1);
my_shared_ptr::shared_ptr<Date> sp3(new Date);
sp1 = sp1;
sp1 = sp2;
sp1 = sp3;
cout << sp1.use_count() << endl;;
my_shared_ptr::shared_ptr<Date> sp4;
//
// 定制删除器
my_shared_ptr::shared_ptr<Date> sp5(new Date[10], [](Date* ptr) {delete[] ptr; });
return 0;
}
对于
shared_ptr重点要注意它的构造,拷贝构造,赋值重载,以及析构,因为智能指针设计出来就是管理资源的,所以这几个构造一定要搞清楚!要求能够手撕,而删除器是为了满足在特殊情况下的删除而已,理解原理即可。
注意:对于unique_ptr和weak_ptr的实现可以去我的代码仓库中拿:
四、shared_ptr循环引用问题
1、问题引入:
cpp
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
ListNode(int val)
:_data(val)
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
my_shared_ptr::shared_ptr<ListNode> n1(new ListNode(1));
my_shared_ptr::shared_ptr<ListNode> n2(new ListNode(2));
}
在上面我们定义了一个双向链表,链表的指针我们同一使用智能指针来管理,因为每一个结点都代表一块资源,其次在
main函数中我们对于使用ListNode定义出来的对象n1和n2也交给智能指针来管理
此时就出现了一个问题:结点什么时候被释放呢?

虽然shared_ptr在大多数情况下都是适用,但是在这种特殊的情况下,会造成循环引用,导致资源泄露的情况,这时候我们就要请出weak_ptr来解决问题了。
前面说过weak_ptr不支持RAII,也不支持资源访问,所以weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,当绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决循环引用问题。
cpp
// 链表节点定义(无需修改)
struct ListNode
{
int _data;
my_weak_ptr::weak_ptr<ListNode> _next;
my_weak_ptr::weak_ptr<ListNode> _prev;
ListNode(int val)
:_data(val)
{
}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
my_shared_ptr::shared_ptr<ListNode> n1(new ListNode(1));
my_shared_ptr::shared_ptr<ListNode> n2(new ListNode(2));
cout << n1.use_count() << endl; // 输出 1
cout << n2.use_count() << endl; // 输出 1
//这⾥改成weak_ptr,当n1->_next = n2;绑定shared_ptr时
//不增加n2的引⽤计数,不参与资源释放的管理,就不会形成循环引⽤了
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl; // 输出 1(weak_ptr 不增加引用计数)
cout << n2.use_count() << endl; // 输出 1
return 0;
}
五、总结
1、智能指针的使用方法
- 初始化 :智能指针必须通过
new操作符或构造函数进行初始化。- 赋值 :智能指针之间可以相互赋值,但
std::unique_ptr不能给std::shared_ptr赋值。- 解引用 :使用解引用运算符
*和箭头->来访问智能指针指向的内容。- 重置 :使用
reset方法来重置智能指针、释放当前指向的内存,并可以重新指向新的内存。
2、注意事项:
1、避免循环引用: 在使用共享智能指针时,要注意避免循环引用,使用weak_ptr来避免循环引用,从而避免内存泄露。
2、不要使用原始指针: 尽量要避免使用原始的指针来管理内存,使用智能指针可以简化并提高安全性。
html
MSTcheng 始终坚持用直观图解 + 实战代码,把复杂技术拆解得明明白白!
👁️ 【关注】 看普通程序员如何用实用派思路搞定复杂需求
👍 【点赞】 给 "不搞虚的" 技术分享多份认可
🔖 【收藏】 把这些 "好用又好懂" 的干货技巧存进你的知识库
💬 【评论】 来唠唠 ------ 你踩过最 "离谱" 的技术坑是啥?
🔄 【转发】把实用技术干货分享给身边有需要的程序员伙伴
技术从无唯一解,让我们一起用最接地气的方式,写出最扎实的代码! 🚀💻
感谢能够看到这里的小伙伴,如果这篇文章有帮到您,还请给个三连!你们的持续支持是我更新最大的动力!谢谢!

