【C++智能指针】

本文讨论了智能指针在C++中的应用,包括auto_ptr、unique_ptr和shared_ptr的概念、原理以及它们在防止内存泄漏和循环引用问题上的作用。重点介绍了RAII原则和智能指针的生命周期管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么使用智能指针?

考虑以下场景:

void div()
{
	int a, b;
 	cin >> a >> b;
 	if (b == 0)
 		throw invalid_argument("除0错误");
 	return a / b;
}
void Func()
{
	int* ptr=new int;
	div();
	delete ptr;
}
int main()
{
	try{
		Func();
	}
 	catch (exception& e)
 	{
 	cout << e.what() << endl;
 	}
 	return 0;
}

Func()函数中我们在堆上申请了资源(new int对象),当div()调用抛异常时,程序会根据异常处理机制,找到最近的catch捕获异常,这使得程序没有进行到释放资源就结束(未delete int 对象),进而会导致内存泄漏。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死,所以需要一定的方法来优化。

异常处理机制

  1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
  2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
  3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(…)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
  4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

常用的解决方案是:
1、事前预防型。如智能指针等。
2、事后查错型。如泄漏检测工具。

概念

智能指针是一种特殊的指针类型,用于自动管理动态分配的内存。相比于传统的裸指针,智能指针提供了更安全、更方便的内存管理方式。
智能指针的原理主要基于对象的生命周期管理和引用计数机制。通过在自动释放内存和避免悬挂指针方面提供便利,它们可以帮助开发人员更轻松地处理动态内存分配和释放的问题。

RAII(Resource Acquisition Is Initialization)
对象的生命周期控制程序资源的技术,在对象构造时获取资源,在对象析构时释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
1.不需要显式地释放资源。(对象析构了资源就释放了)
2.采用这种方式,对象所需的资源在其生命期内始终保持有效。(资源和对象绑定)

分类

在C++98,有智能指针auto_ptr,但在C++11后,auto_ptr被弃用。标准库中提供了三种常见的智能指针:unique_ptr、shared_ptr和weak_ptr。

auto_ptr

auto_ptr是一种独占所有权的智能指针,实现原理是管理权的转移。当一个auto_ptr将其所有权转移给另一个auto_ptr时,原始的auto_ptr将不再拥有对资源的所有权,即两个auto_ptr不能管理同一份资源。
缺陷:
1.拷贝和赋值操作会改变资源的所有权

auto_ptr<string> p1(new string("hhe"));
auto_ptr<string> p2(new string("aab"));
p1=p2; 
//p2赋值给p1后,p1释放掉管理"hhe"的指针并接收p2交给它的管理"aab"的指针
//p2交付完毕后释放自己内部的指针并置NULL。

2.指针被悬空成野指针容易解引用造成错误

auto_ptr<int> sp1(new int);
auto_ptr<int> sp2(sp1); // 管理权转移
//sp1悬空
*sp2 = 10;
cout << *sp2 << endl;
cout << *sp1 << endl;

自C++11起,该类模板已被弃用,用更严谨的unique_ptr 取代了auto_ptr

unique_ptr

unique_ptr对auto_ptr的缺陷做了修改,和auto_ptr类似,unique_ptr是一种独占所有权的智能指针。它具有以下特点:

  1. 提供了唯一拥有的语义,即同一时间只能有一个unique_ptr指向同一块内存。
  2. 当unique_ptr被销毁时,会自动释放其所拥有的对象。
  3. 不允许多个unique_ptr指针共享同一块内存,避免了悬空指针和内存泄漏的风险。
  4. 无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
unique_ptr<string> p1(new string("hhe"));
unique_ptr<string> p2(new string("aab"));
p1 = p2;					// 禁止左值赋值
unique_ptr<string> p3(p2);	// 禁止左值赋值构造
unique_ptr<string> p3(std::move(p1));
p1 = std::move(p2);	// 使用move把左值转成右值就可以赋值了,效果和auto_ptr赋值一样

下面给出unique_ptr指针的模拟实现:

template<class T>
class unique_ptr
{
public:
	unique_ptr( T* ptr):_ptr(ptr)
	{}
	~unique_ptr()
	{
		if(_ptr)
			delete _ptr;
	}
	T& operator*()
		return *_ptr;
	T* operator->()
		return _ptr;
	unique_ptr(const unique_ptr<T>& sp) =delete;
	unique_ptr<T>& operator =(const unique_ptr<T>& sp) =delete;
private:
	T* _ptr;
}

shared_ptr

为了解决一份资源只能被一个智能指针管理的问题,新增了shared_ptr智能指针。shared_ptr是一种共享所有权的智能指针。它使用引用计数的方式来跟踪对象的引用数量。每次创建shared_ptr时,引用计数增加;每次销毁或重置shared_ptr时,引用计数减少。当引用计数变为0时,shared_ptr会自动释放所管理的对象的内存。

引用计数
因为shared_ptr支持一份资源被多个指针管理,未避免对一块空间资源的重复释放,shared_ptr不必在每次析构时都释放资源,而是在管理该资源的最后一个shared_ptr析构时释放资源。那如何知道一个资源被几个shared_ptr管理?析构时如何知道该指针是最后一个管理该资源的指针?这些问题需要在被管理的资源对象上装一个计数器,供这些shared_ptr共享。
在这里插入图片描述

当创建shared_ptr时,会同时创建一个控制块,并将所管理的对象指针和引用计数等信息存储在该控制块中。多个shared_ptr可以共享同一个控制块,通过引用计数来追踪所有引用该对象的shared_ptr个数。

template<class T>
	class shared_ptr
	{
		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:" << _ptr << 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 (this != &sp)
			{
				--(*_pcount);
				if (*_pcount == 0)
				{
					delete _ptr;
					delete _pcount;
				}
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				*_pcount++;
			}
			return *this;
		}
		int use_count()
		{
			return *_pcount;
		}
		T* get()
		{
			return _ptr;
		}
		
	private:
		T* _ptr;
		int* _pcount;
		function<void(T*)> _del = [](T* ptr) {delete ptr;};
	};

循环引用

shared_ptr在使用不当容易引发循环引用问题。循环引用就是一组对象彼此持有对方的智能指针,从而导致它们的引用计数永远不会变为零。当存在循环引用时,智能指针可能会导致内存泄漏,因为循环引用会阻止引用计数减到零。这将导致相关的内存资源无法被释放,从而造成内存泄漏。
下面是一个产生了循环引用问题的示例:

struct Father
{
...//此处省略成员变量及函数
	shared_ptr<Child> child;
}
struct Child
{
...//此处省略成员变量及函数
	shared_ptr<Father> father;
}
//调用
shared_ptr<Father> a(new Father);
shared_ptr<Child> b(new Child);
a->child=b;
b->father=a;

循环计数情况如下图所示:
在这里插入图片描述

weak_ptr

weak_ptr是一种弱引用的智能指针,通过使用weak_ptr作为循环引用中的其中一个对象,可以打破循环引用并允许相关对象被正确地销毁。它可以观测shared_ptr所管理的对象,并不会增加引用计数。当最后一个shared_ptr被销毁时,即使还有weak_ptr存在,也不会阻止所管理对象的内存释放。(不参与资源的管理,解决智能指针交叉使用问题)

struct Father
{
...//此处省略成员变量及函数
	weak_ptr<Child> child;
}
struct Child
{
...//此处省略成员变量及函数
	weak_ptr<Father> father;
}

使用weak_ptr可以有效避免循环引用问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值