【C++】C++智能指针

🎬 个人主页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,这些局部变量都会销毁。但是由于arry1arry2内部是有资源的,而异常又跳过了释放资源的代码所以导致资源泄露。

1、当divide函数没有抛异常,执行资源释放的代码,资源正常释放

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

如果我们想要解决这个问题就需要在new以后捕获异常,捕获到没有释放的资源后就delete释放资源,然后再把异常抛出。而new本身也可能会抛异常,divide也会抛异常,这样处理起来就比较麻烦,正是因为这样的场景存在,智能指针便有了用武之地!

二、智能指针的使用及其原理

在了解智能指针的使用之前我们先来了解一下RAII:

2.1RAII

RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。

RAII的作用:

  1. 在获取资源时把资源委托给⼀个对象,通过这个对象控制对资源的访问。
  2. 该资源在该对象的生命周期内始终保持有效,最后在该对象调用析构的时候释放资源。

下面就来体会一下智能指针:

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;
}


上面我们写了一个智能指针的类,并创建了两个智能指针对象sp1sp2来管理资源,就是上面我们所说的将资源委托给智能指针对象。

通过上面程序的运行结果我们会发现,资源释放是在异常捕获之前就释放了 ,这是因为异常捕获以后沿着调用链所创建的局部变量都会销毁,而当智能指针被销毁的时候一定会去调用析构从而释放资源。 注意:使用了智能指针之后无论在哪里接收异常,都不会影响资源释放。

有些人也可能有疑问:如果将抛异常放在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函数中被捕获确实会跳过下面sp1sp2的代码,但是此时由于代码没有执行空间压根就没有开辟出来,又怎么会有资源呢?所以从而也佐证了使用了智能指针之后,解决了抛异常时资源泄露的问题!

所以总结一句话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_ptrshared_ptr定制删除器的使用:

删除器:

  • 所谓删除器本质就是⼀个可调用对象 ,这个可调用对象中实现你想要的释放资源的方式式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为new[]经常使用,所以为了简洁⼀点,unique_ptrshared_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_ptrweak_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定义出来的对象n1n2也交给智能指针来管理

此时就出现了一个问题:结点什么时候被释放呢?

虽然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、智能指针的使用方法

  1. 初始化 :智能指针必须通过new操作符或构造函数进行初始化。
  2. 赋值 :智能指针之间可以相互赋值,但std::unique_ptr不能给std::shared_ptr赋值。
  3. 解引用 :使用解引用运算符*和箭头->来访问智能指针指向的内容。
  4. 重置 :使用reset方法来重置智能指针、释放当前指向的内存,并可以重新指向新的内存。

2、注意事项:

1、避免循环引用: 在使用共享智能指针时,要注意避免循环引用,使用weak_ptr来避免循环引用,从而避免内存泄露。

2、不要使用原始指针: 尽量要避免使用原始的指针来管理内存,使用智能指针可以简化并提高安全性。

html 复制代码
MSTcheng 始终坚持用直观图解 + 实战代码,把复杂技术拆解得明明白白!
👁️ 【关注】 看普通程序员如何用实用派思路搞定复杂需求
👍 【点赞】 给 "不搞虚的" 技术分享多份认可
🔖 【收藏】 把这些 "好用又好懂" 的干货技巧存进你的知识库
💬 【评论】 来唠唠 ------ 你踩过最 "离谱" 的技术坑是啥?
🔄 【转发】把实用技术干货分享给身边有需要的程序员伙伴
技术从无唯一解,让我们一起用最接地气的方式,写出最扎实的代码! 🚀💻

感谢能够看到这里的小伙伴,如果这篇文章有帮到您,还请给个三连!你们的持续支持是我更新最大的动力!谢谢!

相关推荐
肆忆_1 天前
# 用 5 个问题学懂 C++ 虚函数(入门级)
c++
不想写代码的星星1 天前
虚函数表:C++ 多态背后的那个男人
c++
端平入洛3 天前
delete又未完全delete
c++
端平入洛4 天前
auto有时不auto
c++
郑州光合科技余经理5 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
feifeigo1235 天前
matlab画图工具
开发语言·matlab
dustcell.5 天前
haproxy七层代理
java·开发语言·前端
norlan_jame5 天前
C-PHY与D-PHY差异
c语言·开发语言
哇哈哈20215 天前
信号量和信号
linux·c++
多恩Stone5 天前
【C++入门扫盲1】C++ 与 Python:类型、编译器/解释器与 CPU 的关系
开发语言·c++·人工智能·python·算法·3d·aigc