一把王者的时间我学会了c++中的智能指针(含代码实现)

目录

1.什么是内存泄漏

2.如何避免内存泄漏

3.为什么要有智能指针

4. 智能指针的使用及其原理

4.1RAII

4.2利用RAII思想设计的类

4.3智能指针的实现

5.总结


1.什么是内存泄漏

1.内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。常指堆内存泄漏,因为堆是动态分配的,而且是用户来控制的,如果使用不当,会产生内存泄漏。

使用 malloc、calloc、realloc、new 等分配内存时,使用完后要调用相应的 free 或 delete 释放内存,否则这块内存就会造成内存泄漏。

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

2.如何避免内存泄漏

1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
2. 采用RAII思想或者智能指针来管理资源
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
5.总结一下:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工
具。

3.为什么要有智能指针

在c++中我们在堆上开辟内存,很容易就忘记了释放。或者在一些抛异常的情况下,没有释放堆上的空间。c++为了解决这一类的问题引入了智能指针帮我们管理内存。

4. 智能指针的使用及其原理

智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。C++11 中封装在了 <memory> 头文件中。

C++11 中智能指针包括以下三种:

共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。
独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用 move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。
弱指针(weak_ptr):指向 share_ptr 指向的对象,能够解决由shared_ptr带来的循环引用问题。

c++98提供了auto_ptr:auto_ptr 是C++标准库提供的类模板,auto_ptr对象通过初始化指向由new创建的动态内存,它是这块内存的拥有者,一块内存不能同时被分给两个拥有者。当auto_ptr对象生命周期结束时,其析构函数会将auto_ptr对象拥有的动态内存自动释放。即使发生异常,通过异常的栈展开过程也能将动态内存释放。auto_ptr不支持new 数组。

4.1RAII

 1.RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
2.在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
2.1.不需要显式地释放资源。
2.2.采用这种方式,对象所需的资源在其生命期内始终保持有效。

 4.2利用RAII思想设计的类

使用RAII思想设计的类我们一般会重载*和->让这个类可以有指针一样的行为。

template<class T>
	class SmartPtr {
		SmartPtr( T* ptr)//构造函数
			:_ptr(ptr)
		{}

		T& operator*() {//模拟指针一样的行为
			return *_ptr;
		}
		
		T* operator->() {
			return _ptr;
		}
		//RAII和智能指针的关系
		// RAII是一种托管资源的托管资源的思考,智能指针就是依靠这种RAII实现的,unique_lock,lock_guard
		//RAII利用对象的生命周期来控制程序资源
		~SmartPtr() {
			if (_ptr) {//对象生命周期结束释放调用析构函数释放资源
				delete _ptr;
				_ptr = nullptr;
			}
		}
	private:
		T* _ptr;
	};

 我们可以用上述设计出来的类来测试一个小程序。

#include<iostream>
using namespace std;
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr)//构造函数
		:_ptr(ptr)
	{}

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

	T* operator->() {
		return _ptr;
	}
	//RAII和智能指针的关系
	// RAII是一种托管资源的托管资源的思考,智能指针就是依靠这种RAII实现的,unique_lock,lock_guard
	//RAII利用对象的生命周期来控制程序资源
	~SmartPtr() {
		if (_ptr) {//对象生命周期结束释放调用析构函数释放资源
			delete _ptr;
			_ptr = nullptr;
		}
	}
private:
	T* _ptr;
};
int main() {
	SmartPtr<int>sp(new int(1));
	cout << *sp << endl;
	return 0;
	
}

这样我们就不用怕忘记释放空间导致内存泄漏,利用对象的生命周期来管理空间。

总结:

总结一下智能指针的原理:
1. RAII特性
2. 重载operator*和opertaor->,具有像指针一样的行为。

4.3智能指针的实现

上面实现的smart类看起来没有什么问题但是当一个smart对象赋值给另外一个smart对象时。

我们来看:

#include<iostream>
using namespace std;
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr)//构造函数
		:_ptr(ptr)
	{}

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

	T* operator->() {
		return _ptr;
	}
	//RAII和智能指针的关系
	// RAII是一种托管资源的托管资源的思考,智能指针就是依靠这种RAII实现的,unique_lock,lock_guard
	//RAII利用对象的生命周期来控制程序资源
	~SmartPtr() {
		if (_ptr) {//对象生命周期结束释放调用析构函数释放资源
			delete _ptr;
			_ptr = nullptr;
		}
	}
private:
	T* _ptr;
};
int main() {
	SmartPtr<int>sp1(new int);
	SmartPtr<int>sp2(new int);
	sp1 = sp2;
	return 0;
}

运行结果:

 此时代码崩溃了这是我们在我们没有写opeator=,编译器生成opeator=默认是值拷贝也就是浅拷贝,这样会使两个指针指向同一块空间。对象生命周期结束,调用析构函数同一块空间析构两次导致同一块空间析构两次从而导致程序崩溃。

 为了解决上述问题c++提出了下列解决方案:

        1.管理权转移:   c++98 auto_ptr管理权转移
        2.防拷贝:c++11 unique_ptr 防拷贝
        3.引用计数 :    c++11 shared_ptr //共享拷贝循环引用的问题又需要weak_ptr来解决。 

  

 1.auto_ptr

c++98中为了解决这个问题采用的是管理权转移,当发生赋值时。以下面的代码为例:

#include<iostream>
using namespace std;
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr)//构造函数
		:_ptr(ptr)
	{}

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

	T* operator->() {
		return _ptr;
	}
	//RAII和智能指针的关系
	// RAII是一种托管资源的托管资源的思考,智能指针就是依靠这种RAII实现的,unique_lock,lock_guard
	//RAII利用对象的生命周期来控制程序资源
	~SmartPtr() {
		if (_ptr) {//对象生命周期结束释放调用析构函数释放资源
			delete _ptr;
			_ptr = nullptr;
		}
	}
private:
	T* _ptr;
};
int main() {
	SmartPtr<int>sp1(new int);
	SmartPtr<int>sp2(new int);
	sp2 = sp1;
	return 0;
}

sp2=sp1.此时auto_ptr的做法是将sp2所管理的空间释放在让sp2管理sp1的空间,并将sp1里面的_ptr置空。具体实现请看代码:

	template<class T>
	class auto_ptr {
	public:
		auto_ptr(const T* ptr)//构造函数
			:_ptr(ptr)
		{}
		auto_ptr(auto_ptr<T>& ap)//拷贝构造
			:_ptr(ap._ptr) {
			ap._ptr = nullptr;//将自己管理的置空
		}
		auto_ptr<T>& 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;
		}
		
		~auto_ptr() {
			if (_ptr) {
				delete _ptr;
				_ptr = nullptr;
			}
		}
	private:
		T* _ptr;
	};

但是这种写法十分的不好,我赋值给你最后我自己管理的空间还变空了,有点盗窃的感觉,在公司里面也是被禁止使用的。 

2.unique_ptr

 针对上面这种问题c++引入了unique_ptr(scope_ptr,私有化。它的做法简单粗暴既然你会出现这种问题那我就不允许你拷贝和赋值。

对应代码:

	template<class T>
	class unique_ptr {
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		unique_ptr(const unique_ptr<T>& ap) = delete;
		//将拷贝和赋值全部禁掉不允许赋值
		unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete;
		T& operator*() {

			return *_ptr;
		}
		
		T* operator->() {
			return _ptr;
		}
		
		~unique_ptr() {
			if (_ptr) {
				delete _ptr;
				_ptr = nullptr;
			}
		}
	private:
		T* _ptr;
	};

3.shared_ptr

c++为了解决unique_ptr不能支持拷贝和赋值,c++11中引入了引用计数的方案。

当需要多个智能指针指向同一个资源时,使用带引用计数的智能指针。

每增加一个智能指针指向同一资源,资源引用计数加一,反之减一。当引用计数为零时,由最后一个指向资源的智能指针将资源进行释放。

下图表示带引用计数智能指针的工作过程。sp1 对象和 sp2 对象通过指针指向同一资源,引用计数器记录了引用资源的对象个数。

 当 sp1 对象发生析构时,引用计数器的值减 1,由于引用计数不等于 0,资源并未释放,如下图所示:

                                                                                                   sp1析构

 当 sp2 对象也发生析构,引用计数减为 0,资源释放,如下图所示:

                                                                                                                sp2析构

 即引用计数可以保证多个智能指针指向资源时资源在所有智能对其取消引用再释放,避免过早释放产生空悬指针。

template<class T>
	struct Delete
	{
		void operator()(const T* ptr)
		{
			delete ptr;
		}
	};
	//c++11中shared_ptr采用引用计数的方式来解决
	template<class T, class D = Delete<T>>
	class shared_ptr
	{
	private:
		void AddRef()
		{
			_pmutex->lock();
			++(*_pcount);
			_pmutex->unlock();
		}

		void ReleaseRef()
		{
			_pmutex->lock();
			bool flag = false;
			if (--(*_pcount) == 0)
			{
				if (_ptr)
				{
					cout << "delete:" << _ptr << endl;
					//delete _ptr;
					_del(_ptr);
				}

				delete _pcount;
				flag = true;
			}
			_pmutex->unlock();

			if (flag == true)
			{
				delete _pmutex;
			}
		}
	public:
		// RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))
			, _pmutex(new mutex)
		{}

		shared_ptr(T* ptr, D del)
			: _ptr(ptr)
			, _pcount(new int(1))
			, _pmutex(new mutex)
			, _del(del)
		{}

		~shared_ptr()
		{
			ReleaseRef();
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _pmutex(sp._pmutex)
		{
			AddRef();
		}

		// sp1 = sp1
		// sp1 = sp2
		// sp3 = sp1;
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				ReleaseRef();

				_pcount = sp._pcount;
				_ptr = sp._ptr;
				_pmutex = sp._pmutex;

				AddRef();
			}

			return *this;
		}


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

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

		int use_count()
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmutex;
		D _del;
	};

在上述代码实现中我们引入了删除器也就是仿函数来控制如何来释放空间,以及多线程问题,通过加锁来解决 。

 4.weak_ptr

weak_ptr是用来解决shared_ptr在这样情况下的循环引用的问题:

struct ListNode
{
int _data;

shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}

//循环引用分析:
 1.,node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
 2.node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
 3.node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
4. 也就是说_next析构了,node2就释放了。
5. 也就是说_prev析构了,node1就释放了。
6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2
成员,所以这就叫循环引用,谁也不会释放。

此时引入了weak_ptr,weak_ptr其实也非常的简单, weak_ptr一般被称为弱智能指针,其对资源的引用不会引起资源的引用计数的变化,通常作为观察者,用于判断资源是否存在,并根据不同情况做出相应的操作。具体请看实现:


	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

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

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

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

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

5.总结

  1. shared_ptr 会增加资源的引用计数,常用于管理对象的生命周期。

  2. weak_ptr 不会增加资源的引用计数,常作为观察者用来判断对象是否存活。

  3. 使用 shared_ptr 的普通拷贝构造函数会产生额外的引用计数对象,可能导致对象多次析构。使用 shared_ptr 的拷贝构造函数则只影响同一资源的同一引用计数的增减。

  4. auto_ptr 进行拷贝构造时,会对之前的auto_ptr的资源置nullptr操作;

  5. scoped_ptr 通过私有化了拷贝构造和赋值函数杜绝浅拷贝;

  6. unique_ptr 通过删除了拷贝构造和赋值函数函数杜绝浅拷贝,但引入了带右值引用的拷贝构造和赋值函数。

猜你喜欢

转载自blog.csdn.net/qq_56999918/article/details/123300148