C++智能指针

1. 为什么需要智能指针

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

Func()函数中,如果14,15,16行抛异常了,就需要先释放其他指针,然后再将异常重新抛出,如果指针很多,写起来就会很麻烦。

2. 智能指针的使用及原理

2.1 RAII

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

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

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

智能指针是RAII思想的一种实现

template<class T>
class SmartPtr
{
public:
	explicit SmartPtr(T* ptr) : _ptr(ptr) {}

	~SmartPtr()
	{
		cout << "~SmartPtr():" << _ptr << endl;
		delete _ptr;
	}

	// 重载*和->,使其像指针一样
	T& operator*()
	{
		return *_ptr;
	}
	
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};
void test1()
{
	SmartPtr<int> sp1(new int);
	*sp1 = 123;
	cout << *sp1 << endl;

	SmartPtr<pair<string, int>> sp2(new pair<string, int>("abcd", 666));
	// 实际上是sp2.operator->()->,先有一个pair*的指针,然后用这个pair*指针的->访问类内成员,编译器将其将其优化为一个->
	sp2->first += 'e';		
	cout << sp2->first << ' ' << sp2->second << endl;
}

image-20241005173020076

2.2 问题

上面的代码有浅拷贝的问题

SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(sp1);

sp2sp1指向同一块空间,析构会析构两次,代码崩溃。

这里不像之前vector,string,list那样,单纯实现一下深拷贝就可以。因为智能指针只是对指针资源进行管理,并不需要真的重新开一段空间,然后再次拷贝。这里有三种解决办法

2.3 std::auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针。
auto_ptr的实现原理:管理权转移的思想,下面简单模拟实现一下

namespace lyf
{
	template<class T>
	class auto_ptr
	{
	public:
		explicit auto_ptr(T* ptr) : _ptr(ptr) {}

		~auto_ptr()
		{
			cout << "~auto_ptr():" << _ptr << endl;
			delete _ptr;
		}

		explicit auto_ptr(auto_ptr<T>& ap) : _ptr(ap._ptr) { ap._ptr = nullptr; }
		auto_ptr& operator=(auto_ptr<T>& ap)
		{
			// 防止自己给自己复制
			if (this != &ap) {
				// 释放当前指针的资源
				if (_ptr)	delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

		// 重载*和->,使其像指针一样
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}
void test2()
{
	lyf::auto_ptr<int> ap1(new int);
	*ap1 = 123;
	lyf::auto_ptr<int> ap2(ap1);
	lyf::auto_ptr<int> ap3(new int);
	ap3 = ap2;
}

image-20241005175800259

但是auto_ptr有一个问题,如test2()中的代码,若不了解auto_ptr的人,可能会再次访问ap1ap2,但是此时该指针已经被悬空了,再次访问程序会崩溃。
auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr

2.4 std::unique_ptr

C++11中开始提供更靠谱的unique_ptr
unique_ptr的实现原理:简单粗暴的防拷贝

有两种做法,第一种是将其构造函数和赋值函数变为私有,只声明不实现

private:
	unique_ptr(const unique_ptr<T>& up);
	unique_ptr& operator=(const unique_ptr<T>& up);

为了防止别人再类外自己实现一份,也可以使用delete关键字

unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr& operator=(const unique_ptr<T>& up) = delete;

2.5 std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

namespace lyf
{	
	template<class T>
	class shared_ptr
	{
	private:
		void release()
		{
			if (--(*_pcount) == 0) {
				cout << "~shared_ptr  ():" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}
	public:
		explicit shared_ptr(T* ptr = nullptr) : _ptr(ptr), _pcount(new int(1)) {}

		~shared_ptr()
		{
			release();
		}

		explicit shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr), _pcount(sp._pcount)
		{
			++(*_pcount);
		}

		shared_ptr& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr) {
				/* 这种写法相比于if (this != &sp){},sp1和sp2就不会走这段逻辑。见test4();*/
				release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
			}
			return *this;
		}

		// 重载*和->,使其像指针一样
		T& operator*() const
		{
			return *_ptr;
		}

		T* operator->() const
		{
			return _ptr;
		}
        
        int use_count() const 
		{
			return *this;
		}

		T* get() const 
		{
			return _pcount;
		}
	private:
		T* _ptr;
		int* _pcount;		// 引用计数
	};
}
void test4()
{
	lyf::shared_ptr<string> sp1(new string("aaaaaaaa"));
	lyf::shared_ptr<string> sp2(sp1);
	lyf::shared_ptr<string> sp3(new string("bbbbbbbbbb"));
	sp3 = sp2;
}

image-20241005195718356

2.6 std::shared_ptr的循环引用

我们去掉shared_ptr析构中的打印,有下面的代码

struct ListNode
{
	int _val;
	lyf::shared_ptr<ListNode> _next;
	lyf::shared_ptr<ListNode> _prev;

	~ListNode() 
	{
		cout << "~ListNode() " << endl;
	}
};

void test5()
{
	lyf::shared_ptr<ListNode> n1(new ListNode);
	lyf::shared_ptr<ListNode> n2(new ListNode);
	n1->_next = n2;
	n2->_prev = n1;
}

运行后,发现并没有输出任何内容,说明ListNode资源没有释放,出现内存泄漏了。而去掉15或16行任意一行,就会正确打印两次~ListNode()


原因:

image-20241005205229755

这就叫做循环引用,即使没有任何外部引用指向 n1n2,它们的引用计数也不会降到零,因为它们相互持有对方的引用,这将导致内存泄漏。


解决方案,在ListNode中使用weak_ptr,不增加n1n2的引用计数,weak_ptr不增加引用计数

template<class T>
class weak_ptr
{
public:
	weak_ptr() {}
	weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {}

	weak_ptr& operator=(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();
		return *this;
	}

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

	T* operator->() const
	{
		return _ptr;
	}
private:
	T* _ptr = nullptr;
};

weak_ptr的特性

  1. 不是传统指针,不支持RAII
  2. 像指针一样使用
  3. 不参与(++)shared_ptr的引用计数

实际上,weak_ptr有一个方法叫做use_count()可以返回所指向的shared_ptr的引用计数,还有一个方法叫做expired(),用来判断 所指向的shared_ptr*_pcount是否为0

2.6 定制删除器

由于我们的shared_ptr的析构函数只是delete,当析构一个数组的时候就会报错

lyf::shared_ptr<string> n1(new string[5]);

std库里面提供了一个构造函数,可以传一个可调用对象

template <class U, class D> shared_ptr (U* p, D del);

所以当使用std的shared_ptr的时候,可以这样用

std::shared_ptr<ListNode> n1(new ListNode[5], [](ListNode* ln) { delete[] ln; });

image-20241009112443384


下面自己简单实现一下

template<class T>
class shared_ptr
{
private:
   void release()
   {
       if (--(*_pcount) == 0) {
           // cout << "~shared_ptr  ():" << _ptr << endl;
           _del(_ptr);
           delete _pcount;
       }
   }
public:
   explicit shared_ptr(T* ptr)
       : _ptr(ptr)
       , _pcount(new int(1)) {}

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

   ~shared_ptr()
   {
       release();
   }
  // ...
private:
   T* _ptr = nullptr;
   int* _pcount;		// 引用计数
   // 包装器
   function<void(T*)> _del = [](T* ptr) { delete ptr; };
};

3. 内存泄漏

3.1 什么是内存泄漏,内存泄漏的危害

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费

长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

3.2 内存泄漏分类

堆内存泄漏(Heap leak)

堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

系统资源泄漏

指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

3.3 如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值