首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【C++篇】智能指针

【C++篇】智能指针

作者头像
我想吃余
发布2025-08-11 08:27:55
发布2025-08-11 08:27:55
15200
代码可运行
举报
文章被收录于专栏:C语言学习C语言学习
运行总次数:0
代码可运行

为什么需要智能指针?

我们知道,在程序中,内存泄漏是一个很严重的问题。之前我们说过,手动开出的空间,只要记得释放资源即可。但在C++推出异常后,这种方式已经不可靠了,因为异常的捕捉会扰乱程序的流,导致我们即使手动释放了,空间也不一定得到释放。如下:

代码语言:javascript
代码运行次数:0
运行
复制
int div()
{
	 int a, b;
	 cin >> a >> b;
	 if (b == 0)
	 throw invalid_argument("除0错误");
	 
	 return a / b;
}

void Func()
{
	 // 1、如果p1这里new 抛异常会如何?
	 // 2、如果p2这里new 抛异常会如何?
	 // 3、如果div调用这里又会抛异常会如何?
	 int* p1 = new int;
	 int* p2 = new int;
	 cout << div() << endl;
	 
	 delete p1;
	 delete p2;
}

int main()
{
	 try
	 {
	 	Func();
	 }
	 catch (exception& e)
	 {
		 cout << e.what() << endl;
	 }
	 
	 return 0;
}

分析上面的问题,你能发现什么?

这里是有可能出现内存泄漏的,即使我们手动释放资源了:

  1. 如果p1这里new 抛异常,程序的流会直接跳到catch 块中,然后结束程序,p1未被释放,内存泄漏。
  2. 如果p2这里new 抛异常程序的流会直接跳到catch 块中,然后结束程序,p1、p2未被释放,内存泄漏。
  3. 如果div调用抛异常,程序的流会直接跳到catch 块中,然后结束程序,p1、p2未被释放,内存泄漏。

解决方法:

用异常的重新捕获解决

我们可以在func函数中先对div函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出。

代码语言:javascript
代码运行次数:0
运行
复制
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void func()
{
	int* p1 = new int;
	int* p2 = new int;
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete p1;
		delete p2;
		throw;
	}
	
	delete p1;
	delete p2;
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

可以看到,代码非常冗余,这代码看起来就像是💩做的。

智能指针

1. 智能指针的核心思想:RALL

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显示释放资源
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

我们来用此思想解决上文问题:

代码语言:javascript
代码运行次数:0
运行
复制
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr)
       : _ptr(ptr)
   {}
    ~SmartPtr()
   {
        if(_ptr)
        delete _ptr;
   }
private:
    T* _ptr;
};

int div()
{
 int a, b;
 cin >> a >> b;
 if (b == 0)
 throw invalid_argument("除0错误");
 return a / b;
}

void Func()
{
	 ShardPtr<int> sp1(new int);
	 ShardPtr<int> sp2(new int);
	 cout << div() << endl;
}

int main()
{
	  try 
	  {
	 		Func();
	  }
	  catch(const exception& e)
	  {
	        cout<<e.what()<<endl;
	  }
	  
	 return 0;
}
2. 智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将* ->重载下,才可让其像指针一样去使用。

如下:

代码语言:javascript
代码运行次数:0
运行
复制
template<class T>
class SmartPtr {
public:
	 //RALL
	 SmartPtr(T* ptr = nullptr)
	     : _ptr(ptr)
	 {}
	 ~SmartPtr()
	 {
	     if(_ptr)
	         delete _ptr;
	 }
	 
	 //可以和指针一样使用
	 T& operator*() 
	 {
	 	return *_ptr;
	 }
	 T* operator->() 
	 {
	 	return _ptr;
	 }
private:
	 T* _ptr;
};

总结一下智能指针的原理:

  1. RAII特性
  2. 重载operator*和opertaor->,具有像指针一样的行为。

除此之外,我们还需要去解决智能指针对象的拷贝问题,在C++的发展历史上,官方推出了一系列智能指针,它们在对象的拷贝问题解决方式都不同,不同场景适用不同的智能指针。

3. C++中的智能指针
auto_ptr(管理权转移)

C++98版本的库中就已经提供了auto_ptr的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。

代码语言:javascript
代码运行次数:0
运行
复制
int main()
{
	std::auto_ptr<int> ap1(new int(1));
	std::auto_ptr<int> ap2(ap1);
	*ap2 = 2;//管理权转移给新的对象
	//*ap1 = 3; //被拷贝的对象会被置空,访问就会报错

	std::auto_ptr<int> ap3(new int(1));
	std::auto_ptr<int> ap4(new int(2));
	ap3 = ap4;
	
	return 0;
}

可以看到,被拷贝的对象会悬空,如果不慎调用,程序会出现错误。 因此使用auto_ptr之前必须先了解它的机制,否则程序很容易出问题,于是很多公司也都明确规定了禁止使用auto_ptr。 所以auto_ptr是比较鸡肋的。

模拟实现auto_ptr

  1. RALL思想
  2. 可以像指针一样使用
  3. 管理权转移:拷贝后将被拷贝的对象置空
代码语言:javascript
代码运行次数:0
运行
复制
namespace surplus
{
	template<class T>
	class auto_ptr
	{
		//auto_ptr核心逻辑
		//1. RALL:利用生命周期特性来管理资源
		//2. 可以像指针一样使用
	public:
		//RALL
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{ }

		~auto_ptr()
		{
			delete _ptr;
		}

		//可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		//支持资源的转移:被拷贝的对象置空
		//缺陷:被拷贝对象悬空,若被访问会出问题
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			_ptr = ap._ptr;
			ap._ptr = nullptr;
			return *this;
		}
	private:
		T* _ptr;
	};
}
unique_ptr(防拷贝)

unique_ptr通过禁止拷贝的方式解决智能指针的拷贝问题,以简单粗暴的防拷贝方式解决了auto_ptr拷贝不安全的问题,适合不需要拷贝的场景。

代码语言:javascript
代码运行次数:0
运行
复制
std::unique_ptr<int> up1(new int(1));
std::unique_ptr<int> up2(up1);//直接报错

std::unique_ptr<int> up3(new int(1));
std::unique_ptr<int> up4(new int(2));
ap3 = ap4;//直接报错

模拟实现unique_ptr

核心逻辑:

  1. RALL:利用生命周期特性来管理资源
  2. 可以像指针一样使用
  3. 禁止拷贝:用关键字delete禁止编译器生成拷贝构造和赋值重载
代码语言:javascript
代码运行次数:0
运行
复制
namespace Surplus
{
	template<class T>
	class unique_ptr
	{
		//unique_ptr核心逻辑
		//1. RALL:利用生命周期特性来管理资源
		//2. 可以像指针一样使用
		//3. 禁止转移资源解决指针悬空的缺陷
	public:
		//RALL
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
	
		~unique_ptr()
		{
			delete _ptr;
		}
	
		//可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
	
		T* operator->()
		{
			return _ptr;
		}
	
		//禁止转移资源
		unique_ptr(const unique_ptr<T>& up) = delete;
	
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
	
	private:
		T* _ptr;
	};
}
shared_ptr(引用计数)

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr,这也是我们最常用的智能指针。

shared_ptr解决对象的拷贝问题的原理:通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

举个栗子:老师下晚自习之前都会通知,让最后走的学生记得把门锁下。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数器,用来记录该份资源被几个对象共享。
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
    • 减一之后如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
    • 减一之后如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了
  3. 在对象被拷贝时(也就是拷贝构造函数调用),就说明有新的对象需要共享该资源了,对象的引用计数加一

shared_ptr存在循环引用的缺陷

wake_ptr可以帮助解决缺陷,后文讲解wake_ptr时细说。

模拟实现

核心逻辑:

  1. RALL:利用生命周期特性来管理资源
  2. 可以像指针一样使用
  3. 可以共享资源(利用引用计数实现)
    • 在shared_ptr类中增加一个成员变量pcount,表示智能指针对象管理的资源对应的引用计数。
    • 在构造函数中获取资源,并将该资源对应的引用计数初始值设置为1,表示当前只有一个对象在管理这个资源。
    • 在拷贝构造函数中,与传入对象一起管理它管理的资源,将该资源对应的引用计数++
    • 在拷贝赋值函数中,先将当前对象管理的资源对应的引用计数--(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++
    • 在析构函数中,将管理资源对应的引用计数--,如果减为0则需要将该资源释放
  4. 定制删除器:动态开辟内存是多方式的,可以是malloc、new、new[],不同方式其释放资源的方式也不同,malloc->free、new->delete、new[]->delete[]
    • 目的:使智能指针可以识别开辟内存方式,然后用对应的释放方式与其匹配
    • 解决方法:用function包装器解决
      1. 将构造函数设置为模板函数,模板参数为D,意为资源释放的函数类型(function<T>)。
      2. 设置function<void(T*)> _del成员变量,并设置缺省值[](T* ptr) {delete ptr; }为默认资源释放的函数。
      3. 此时,_del可以在构造函数中用外部传入的资源释放函数初始化,获取用户所需的资源释放方法。
      4. 最后,在析构函数中,用_del释放资源。
代码语言:javascript
代码运行次数:0
运行
复制
template<class T>
class shared_ptr
{
	//shared_ptr核心逻辑
	//1. RALL:利用生命周期特性来管理资源
	//2. 可以像指针一样使用
	//3. 可以共享资源(利用引用计数实现)
	//4. 存在循环引用缺陷(用wake_ptr解决)
public:
	//RALL

	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}
	template<class D>
	shared_ptr(T* ptr, D del)
		:_ptr(ptr)
		,_pcount(new int(1))
		,_del(del)
	{}

	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << "delete" << endl;
			_del(_ptr);
			delete _pcount;
		}
	}

	//可以像指针一样使用
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
	
	//可以共享资源
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
	{
		++(*_pcount);
	}
	
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr == sp._ptr)
			return *this;

		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}

		_ptr = sp._ptr;
		_pcount = sp._pcount;
		++(*_pcount);
	}

	//获取当前对象管理的资源对应的引用计数。
	int use_count()const
	{
		return *_pcount;
	}
	
	//获取原生指针
	T* get()const
	{
		return _ptr;
	}

private:
	T* _ptr;
	int* _pcount;

	function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
wake_ptr(shared_ptr的小弟)

shared_ptr的缺陷:循环引用问题

shared_ptr的循环引用问题在一些特殊的场景下才会产生,比如定义下面的节点类: 说明:在节点类的析构函数添加打印一条提示语,便于我们观察资源是否释放

代码语言:javascript
代码运行次数:0
运行
复制
//节点类
struct Node
{
	int _val;
	shared_ptr<Node> _next;
	shared_ptr<Node> _prev;
	
	~Node(){ cout << "~Node()" << endl; }
};

int main()
{
	shared_ptr<Node>sp1(new Node);
	shared_ptr<Node>sp2(new Node);

	sp1->_next = sp2;
	sp2->_prev = sp1;
	
	//打印计数器
	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	return 0;
}

可以看到,程序并没有打印释放资源提示语,内存泄漏了,智能指针失效。

循环引用分析:

  1. sp1和sp2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. sp1的_next指向sp2,sp2的_prev指向sp1,引用计数变成2。
  3. sp1和sp2析构,引用计数减到1,sp1的_next还指向下一个节点sp2且sp2的_prev还指向上一个节点sp1。
  1. 也就是说_next析构了,sp2就释放了。
  2. 也就是说_prev析构了,sp1就释放了。
  3. 但是_next属于sp1的成员,sp1释放了,_next才会析构,而sp1由_prev管理,_prev又属于sp2成员,然后就会出现这样的一个循环:

到头来谁也不会被释放。

解决方案:在引用计数的场景下,把节点类中的_prev和_next成员的类型改成weak_ptr就可以了

代码语言:javascript
代码运行次数:0
运行
复制
//节点类
struct Node
{
	int _val;
	weak_ptr<Node> _next;
	weak_ptr<Node> _prev;
};

weak_ptr设计出来的目的就是解决shared_ptr的循环引用问题的。 解决原理:让_next和_prev与其他对象共享资源时,不参与引用计数。

  1. 无RALL
  2. 也无引用计数
  3. 可以与shared_ptr共享资源
  4. 无引用计数(功能上没有,但库中在底层实现上是有的,引用计数的类型和核心与shared_ptr不同,感兴趣的读者可以去深入学习)

模拟实现weak_ptr

代码语言:javascript
代码运行次数:0
运行
复制
template<class T>
class weak_ptr
{
	//无RALL
	//可以像指针一样使用
	//可以与shared_ptr共享资源
	//无引用计数,唯一作用是解决shared_ptr的缺陷
public:
	weak_ptr()
		:_ptr(nullptr)
	{ }
	//可以像指针一样使用
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	//可以与shared_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;
};
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-08-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么需要智能指针?
  • 智能指针
    • 1. 智能指针的核心思想:RALL
    • 2. 智能指针的原理
    • 3. C++中的智能指针
      • auto_ptr(管理权转移)
      • unique_ptr(防拷贝)
      • shared_ptr(引用计数)
      • wake_ptr(shared_ptr的小弟)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档