首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【C++篇】“内存泄露”的宝藏手段:智能指针

【C++篇】“内存泄露”的宝藏手段:智能指针

作者头像
用户11456817
发布2025-08-28 08:47:14
发布2025-08-28 08:47:14
8400
代码可运行
举报
文章被收录于专栏:学习学习
运行总次数:0
代码可运行

智能指针的使用场景分析

下面这段程序在“cout << Divide(len, time) << endl;“可能会抛出异常,如果不捕获这个异常,也就是不加对应的catch语句,那么后面的delete[]就执行不到位,这样就会造成内存泄漏。所以,我们应该加上对应的catch语句,将异常捕获后释放资源,再将异常重新抛出。

除了Divide会抛出异常,new的部分也会抛出异常,若是”int* array1 = new int[10];“处抛异常,倒没什么事,因为抛出异常代表内存申请失败,但若是”int* array2 = new int[10];“处抛异常呢?此时array1已经成功申请内存了,如果不delete掉array1的资源,就会造成内存泄漏,为了避免这样的情况,还需在array2处在写一个try语句。但是当存在多个变量进行new时,代码会变的很搓,所以这里就可以使用到智能指针。

代码语言:javascript
代码运行次数:0
运行
复制
double Divide(int a, int b)
{
	// 当b == 0时抛出异常

		if (b == 0)
		{
			throw "Divide by zero condition!";
		}
		else
		{
			return (double)a / (double)b;
		}
}
void Func()
{
		// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。

		// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。

		// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案

		// 是智能指针,否则代码太戳了

		int* array1 = new int[10];
		int* array2 = new int[10];   // 抛异常呢

		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;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

RAII和智能指针的设计思路

RAII是ResourceAcquisition Is Initialization的缩写,本质是一种利用对象⽣命周期来管理获取到的动态资源。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问, 资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常 释放,避免资源泄漏问题。

简单讲就是,申请了内存空间,但这个内存空间不需要自己管理,而是交给一个对象进行管理,当这个对象生命周期结束时,会析构,同时也会把管理资源释放掉。 构造函数保存资源,析构函数释放资源

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>

using namespace std;

template<class T>
class SmartPtr//智能指针
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
      //析构的是被智能指针管理的资源
		cout << "delete[]" << _ptr << endl;
		delete[] _ptr;
	}
    
   // 重载运算符,模拟指针的⾏为,⽅便访问资源
 
    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)
{
  //如果这里抛出异常,会跳到main函数中去。根据智能指针资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源。所以在跳转到main函数中去之前,会调用智能指针的析构函数将资源释放掉
	if (b==0)
	{
		throw "Divide:分母不能为0";
	}
	else
	{
		return ((double)a / (double)b);
	}

}

void Func()
{
	SmartPtr<int>sp1 = new int[10];
	SmartPtr<int>sp2 = new int[10];
	SmartPtr<int>sp3 = new int[10];
	SmartPtr<pair<int,int>>sp4 = new pair<int, int>[10];

	int len, time;
	cin >> len >> time;
	cout << Divide(len,time) << endl;

	sp1[5] = 50;
	sp4->first = 1;
	sp4->second = 2;

	cout << sp1[5] << endl;

}

int main()
{
	try
	{
		Func();
	}
	catch(const char*errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

C++标准库智能指针的使用

智能指针解决了抛出异常可能会导致内存泄漏的问题,但是它自身也存在一些问题。比如:智能指针的拷贝:

代码语言:javascript
代码运行次数:0
运行
复制
template<class T>
class SmartPtr
{
public:
	// RAII
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{
	}
	~SmartPtr()
	{
		cout << "delete[] " << _ptr << endl;
		delete[] _ptr;
	}

	// 重载运算符,模拟指针的⾏为,⽅便访问资源
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator[](size_t i)
	{
		return _ptr[i];
	}

private:
	T* _ptr;
};

int main()
{
  //我们自己没有实现拷贝构造,编译器自己生成的默认拷贝构造是浅拷贝
  //这样就会导致两个智能指针指向同一块资源,析构时就会析构两次
	SmartPtr<int>sp1 = new int[10];
	SmartPtr<int>sp2(sp1);

	return 0;
}

根据前面所学,当存在申请空间时,就要调用深拷贝。但是智能指针模拟的是原生指针,原生指针1拷贝给原生指针2的目的是赋值,他们指向的资源依然是同一块。智能指针1拷贝给智能指针2的目的是为了让两个智能指针共同管理这块资源

  • C++标准库中的智能指针都在这个头⽂件下⾯,我们包含了就可以使用。 智能指针有好⼏种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解决智能指针拷⻉时的思路不同的问题
  • auto_ptr是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给拷贝对象,这是⼀个⾮常糟糕的设计,因为他会使被拷⻉对象悬空,访问报错的问题,也就是出现野指针的问题
  • unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点是不⽀持拷⻉,只⽀持移动。如果不需要拷贝的场景就非常建议使用他。
  • shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是⽀持拷⻉, 也⽀持移动。如果需要拷贝的场景就需要使⽤他了。底层是用引用计数的⽅式实现的。
  • weakptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上⾯的智能指针,他不⽀持RAII,也就意味着不能用它直接管理资源,weakptr的产生本质是要解决shared_ptr 的⼀个循环引用导致内存泄漏的问题。

auto_ptr的使用:

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
#include<memory>
using namespace std;

struct Date
{
public:
	Date(int year=1,int month=1,int day=1)
		:_year(year)
		,_month(month)
		,_day(day)
	{ }

	~Date()
	{
		cout << "析构" << endl;
	}

	int _year;
	int _month;
	int _day;
};

int main()
{
	auto_ptr<Date>ap1(new Date);
    //拷贝发生资源管理权的转移,ap1悬空
	auto_ptr<Date>ap2(ap1);
    //空指针的访问,这也是auto_ptr会存在的问题
    //ap1->_year;
  
	return 0;
}

拷贝完后,在对ap1进行访问,这就是对空指针进行访问:

unique_ptr的使用:

代码语言:javascript
代码运行次数:0
运行
复制
struct Date
{
public:
	Date(int year=1,int month=1,int day=1)
		:_year(year)
		,_month(month)
		,_day(day)
	{ }

	~Date()
	{
		cout << "析构" << endl;
	}


	int _year;
	int _month;
	int _day;
};

int main()
{
  //unique_ptr只支持移动构造,不支持拷贝
  unique_ptr<Date>up1(new Date);
  //当移动完后,up1会悬空
  unique_ptr<Date>up2(move(up1));

	return 0;
}

unique_ptr与auto_ptr的区别:前者是告知使用者,自己只支持移动构造,不支持拷贝构造,使用完后会造成指针悬空的情况;后者是告知使用者自己是拷贝构造,但是会造成指针悬空的问题并未告知使用者

shared_ptr的使用:

代码语言:javascript
代码运行次数:0
运行
复制
struct Date
{
public:
	Date(int year=1,int month=1,int day=1)
		:_year(year)
		,_month(month)
		,_day(day)
	{ }

	~Date()
	{
		cout << "析构" << endl;
	}


	int _year;
	int _month;
	int _day;
};

int main()
{
	shared_ptr<Date>sp1(new Date);
	shared_ptr<Date>sp2(sp1);
	shared_ptr<Date>sp3(sp1);
    //输出引用计数
	cout << sp1.use_count() << endl;
  
	sp1->_year++;
	cout << sp1->_year << endl;
	cout << sp2->_year << endl;
	cout << sp3->_year << endl;
  
  //shared_ptr还能像如下这样赋值
  shared_ptr<Date>sp4=make_share<Date>(2024,1,1);

	return 0;
}

引用计数:当多个对象管理同一块资源时,用一个count记录对象个数,当count==0时,再将资源释放

模拟shared_ptr:
代码语言:javascript
代码运行次数:0
运行
复制
namespace liu
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T*ptr=nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{ }

		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				cout << "析构" << endl;
				delete _pcount;
				delete _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)++;

			return *this;
		}


		T& operator*()
		{
			return *_ptr;
		}

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


	private:
		T* _ptr;
		int* _pcount;
	};
}

如果以int count的方式来计数:

如果以静态成员变量的方式来计数:

定制删除器:

智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指 针管理,析构时就会崩溃。

代码语言:javascript
代码运行次数:0
运行
复制
//原生指针的空间、malloc来的空间、new[]出来的空间或者其他不是new出来的空间,都不能交给智能指针管理
shared_ptr<Date>sp1(new Date[10]);

面对这种情况,有两种解决方案:

第一种:

代码语言:javascript
代码运行次数:0
运行
复制
//模板特化
shared_ptr<Date[]>sp2(new Date[10]);

第二种:

代码语言:javascript
代码运行次数:0
运行
复制
//定制删除器

struct Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
	}

	~Date()
	{
		cout << "析构" << endl;
	}


	int _year;
	int _month;
	int _day;
};


class Fclose
{
public:
	void operator()(FILE*ptr)
	{
		cout << "flcose()" << endl;
		fclose(ptr);
	}
	
};

template<class T>
void DeleteArrayFunc(T *ptr)
{
	cout << "函数指针" << endl;
	delete[] ptr;
}


int main()
{
  //传仿函数
	shared_ptr<FILE>sp3(fopen("智能指针.cpp","r"),Fclose());
  //传lambda
    shared_ptr<FILE>sp4(fopen("智能指针.cpp", "r"), [](FILE* ptr) {
	cout << "fclose()" << endl;
      
    shared_ptr<Date>sp5(new Date[10], [](Date* ptr) {
	cout << "delete[]" << endl;
	delete[] ptr; })
      
    shared_ptr<Date>sp6(new Date[10], DeleteArrayFunc<Date>);
      
	fclose(ptr); });
	return 0;
}

unique_ptr定制删除器智是在模板处,shared_ptr定制删除器是在构造函数参数处

uniqueptr传定制删除器如果不想传仿函数想以其他形式传如删除器的话,会很麻烦。 sharedptr定制删除器的位置是在函数参数的位置,编译器会自动推导类型。

代码语言:javascript
代码运行次数:0
运行
复制
//unique_ptr不以传仿函数的方式传入定制删除器
//lambda
auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };
unique_ptr<FILE, decltype(fcloseFunc)>up1(fopen("智能指针.cpp", "r"), fcloseFunc);
//函数指针
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);

shared_ptr的循环引用

shared_ptr⼤多数情况下管理资源非常合适,⽀持RAII,也⽀持拷贝。但是在循环引用的场景下会 导致资源没得到释放造成内存泄漏,

  • 如下图所述场景:这样循环引用的问题就会造成内存泄漏
  • 把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这⾥的问题

weak_ptr

weakptr不⽀持RAII,也不⽀持访问资源,所以我们看⽂档发现weakptr构造时不⽀持绑定到资 源,只⽀持绑定到sharedptr,绑定到sharedptr时,不增加shared_ptr的引用计数,那么就可以 解决上述的循环引⽤问题。

  • weakptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的 sharedptr已经释放了资源,那么他去访问资源就是很危险的。
  • weakptr⽀持expired检查指向的 资源是否过期,usecount也可获取sharedptr的引⽤数,weakptr想访问资源时,可以调用lock返回⼀个管理资源的sharedptr,如果资源已经被释放,返回的sharedptr是⼀个空对象,如 果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

sharedptr中的count计数在sharedptr释放时不会立即释放,因为它还需要提供给weakptr使用,如果立即释放了,就会造成weakptr野指针的情况。

weak_ptr中还有expired接口来检查资源是否过期。

代码语言:javascript
代码运行次数:0
运行
复制
	shared_ptr<string>sp1(new string("11111"));
	shared_ptr<string>sp2(sp1);
	weak_ptr<string>wp1 = sp1;

	cout << wp1.expired() << endl;
	cout << wp1.use_count() << endl;

如果shareptr的资源是weakptr所需要的,那么可以使用lock()接口在资源释放前将锁住锁住。

锁住资源实际上就是再用一个shared_ptr指针来管理该资源。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-08-01,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 智能指针的使用场景分析
  • RAII和智能指针的设计思路
  • C++标准库智能指针的使用
    • auto_ptr的使用:
    • unique_ptr的使用:
    • shared_ptr的使用:
      • 模拟shared_ptr:
  • 定制删除器:
  • shared_ptr的循环引用
  • weak_ptr
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档