C++智能指针

一、什么是内存泄漏

内存泄漏:指由于疏忽或错误造成程序未能及时释放不再使用的内存

内存泄漏并不是内存物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了软件层面的内存的浪费

内存泄漏的危害:长期运行的程序出现内存泄漏,会导致服务响应越来越慢,最终卡死

C/C++程序中一般关心两方面的内存泄漏:

堆内存泄漏(Heap leak):堆内存指程序执行过程中需要依据malloc/calloc/realloc/new等从堆中分配的一块内存,用完之后必须响应释放。如果没有释放,则会出现堆内存泄漏

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

二、异常引发的内存泄漏问题

C++的内存管理是由程序员手动进行开辟和释放,比如new对应delete,new []对应delete[]

//C++开辟空间的主要方式
void MemoryLeaks()
{
	int* p1 = (int*)malloc(sizeof(int));

	int* p2 = new int;

	int* p3 = new int[10];
}

而在C++11引入异常处理后,这种内存处理的方式可能由于异常throw的跳转导致未能成功释放内存,因此需要引入智能指针,用于解决内存泄漏问题

在下面这段执行代码中 Func没有使用智能指针

当p1如果new失败,并不会出现问题。是由于new失败并没有开辟空间,因此也不需要释放空间,所以不会出现内存泄漏

当p2如果new失败,会出现问题。是由于p2没有开辟空间,但是p1已经开辟了空间,而由于throw抛出异常跳转至main函数处理,导致Func中的delete p1没能执行,内存泄漏问题

#include
#include"SmartPtr.h"
using namespace std;


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

void Func()
{
	//1.如果p1的new出现异常 并不会出现问题,程序将会跳转至main处理异常
	int* p1 = new int;
	//2.如果p2的new出现异常,会出现问题,这会导致p1的delete被越过
	int* p2 = new int;
	//如果div的调用出现异常 也会导致p1,p2的delete被越过,导致内存泄露
	cout << div() << endl;

	delete p1;
	delete p2;
}


void fun()//智能指针解决内存泄漏问题
{
	SmartPtr sp1(new int);
	SmartPtr sp2(new int);
	cout << div() << endl;
}


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

	return 0;
}

三、智能指针的使用以及原理

智能指针最重要的功能是解决内存泄漏,因此引入RAII。同时还必须承担指针的功能

① RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源的技术。在对象构造时获取资源,在对象析构时释放资源。局部对象的生命周期由操作系统来管理,无需人工介入

优势在于:不用显示的释放资源,并且对象所需资源在其生命周期内始终有效

RAII机制是一种对资源申请、释放这种成对的操作的封装是么 通过这种方式实现在局部作用域内申请资源然后销毁资源

② 重载operator->与operator*

目的是使智能指针具有指针的行为,可以解引用与->访问自定义类型的内容

//SmartPtr.h
#pragma once

#include

using namespace std;

template
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}

	//智能指针还需要增加解引用与->访问
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()//用于自定义类型获取属性
	{
		return _ptr;
	}

private:
	T* _ptr;
};

四、auto_ptr

在SmartPtr中实现的智能指针,并没有拷贝构造函数。因此如果想要实现指针的拷贝构造,将会是浅拷贝。实际上也不能进行指针深拷贝

指针拷贝的问题在于,浅拷贝会导致程序执行结束后,对两个指针指向的同一块内存空间会出现重复释放问题

//指针浅拷贝 导致对同一块资源重复释放
void test()
{
	SmartPtr p1(new int);
	SmartPtr p2(p1);//由于Smart中并没有实现拷贝构造,因此为浅拷贝
	//执行完毕后,资源释放过程中,p1,p2重复释放同一块空间,导致重复释放报错

}

为什么链表/红黑树等迭代器也是浅拷贝就不会出错?

迭代器不需要管理资源的释放,资源释放是容器进行处理的

智能指针需要管理资源释放,所以不能单纯的浅拷贝

因此在智能指针库中,实现了auto_ptr用于解决多个指针指向同一片空间的问题

auto_ptr采用的方式称为管理权转移,即将旧指针对空间的管理权转换给拷贝的指针,并将被拷贝指针(旧指针)置空

 通过管理权转移的方式,对内存空间的指向只有最新的指针拥有管理权

但这种方式存在的缺陷最终导致auto_ptr被弃用

缺陷:在程序开发的过程中,开发者如果使用auto_ptr则一定知道管理权转移导致的原指针悬空问题,因此并不会对悬空指针进行操作,但是大型项目中其他的开发者并不知道。可能对悬空指针操作导致出错

#include
#include"SmartPtr.h"
using namespace std;


namespace my_auto_ptr
{
	template
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		auto_ptr(auto_ptr& sp)
			: _ptr(sp._ptr)
		{
			//管理权转移
			sp._ptr = nullptr;//确保参数对象在拷贝构造函数执行完后不再拥有对原始对象的所有权
			//避免两个auto_ptr同时管理一个对象,导致的内存重复释放问题
		}

		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		auto_ptr& operator=(auto_ptr& ap)
		{
			if (this != &ap)
			{
				if (_ptr)
				{
					delete _ptr;
				}
				_ptr = ap._ptr;
				ap._ptr = NULL;
			}
			return *this;
		}

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

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

	private:
		T* _ptr;
	};
}



void  auto_ptr_test()
{
	//auto_ptr存在的问题
	my_auto_ptr::auto_ptr a1(new int);
	my_auto_ptr::auto_ptr a2(a1);//auto_ptr转移管理权导致a1悬空,变为空指针
	*a2 = 10;
	cout << "a1:" << &a1<<*a1 << endl;//无法对空指针解引用
	cout << "a2:" << &a2<<*a2 << endl;
}

int main()
{
	
	auto_ptr_test();
    return 0;
}

五、unique_ptr

unique_ptr为了解决指针拷贝的问题,采取了更加简单粗暴的方法:禁止拷贝

#include
using namespace std;



namespace my_unique_ptr
{
	template
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		unique_ptr(const unique_ptr& sp) = delete;//为了避免浅拷贝重复释放问题,直接禁止拷贝构造

		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		unique_ptr& operator=(const unique_ptr& sp) = delete;//同理,直接禁止赋值构造

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

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


void test()
{
	my_unique_ptr::unique_ptr u1(new int);
	//my_unique_ptr::unique_ptr u2(u1);//直接禁止拷贝构造,从而避免重复释放问题
}

int main()
{
	test();
	return 0;
}

六、shared_ptr

前面的管理权转移和禁止拷贝都没有根本上的解决指针拷贝的问题,因此出现了shared_ptr

shared_ptr原理:通过引用技术的方式来实现多个shared_ptr对象之间共享资源

shared_ptr在其内部,每个资源都维护了一份计数,用来记录该分资源被几个对象共享

在对象被销毁时(调用析构函数时)说明对象不再使用该资源,对象的引用计数减一

如果引用计数是0,就说明自己是该资源的最后使用者,必须释放该资源

如果引用计数不是0,说明除了自己之外还有其他对象在共享该资源,不能释放该资源,否则其他对象将成为野指针

引用计数操作

核心思想:使用一个计数器来标识当前指针指向的对象被多少类的对象所使用(即记录指针指向对象被引用的次数

引用计数必须使用int* _pcount而不是使用静态变量,因为static会导致指向不同资源的对象指向同一个计数值

线程安全问题 

线程安全分为两个方面:智能指针本身与智能指针管理的对象

1.智能指针对象中,引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,会导致线程安全问题,导致资源未释放或程序崩溃问题。因此智能指针中的引用计数++或--操作需要加锁,保障引用计数操作线程安全

2.智能指针管理的对象存放在堆上,两个线程同时去访问,也会导致线程安全问题。因此在对智能指针管理的对象进行操作时,也需要进行加锁操作

包装器删除

在前面的各种实现中,都只考虑了new的释放,也就是通过delete释放。实际上使用中也会出现new int [10] 这类开辟数组,此时使用delete就不能完全释放空间

因此需要采用包装器,识别delete与delete[],在相应的环境下释放智能指针

#include
#include
#include
#include
using namespace std;

namespace my_shared_ptr
{
	template
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex)
		{}

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

		shared_ptr(const shared_ptr& p)
			:_ptr(p._ptr), _pcount(p._pcount), _pmtx(p._pmtx)
		{
			AddCount();
		}

		~shared_ptr()
		{
			Release();
		}

		void AddCount()
		{
			_pmtx->lock();

			++(*_pcount);

			_pmtx->unlock();
		}

		void Release()
		{
			//_pmtx->lock();

			//if (--(*_pcount) == 0)
			//{
			//	cout << "delete:" << endl;
			//	delete _ptr;
			//	delete _pcount;
			//	delete _pmtx;//这里需要思考 如何解决锁的释放问题
			//}
			//_pmtx->unlock();

			_pmtx->lock();
			bool deleteFlag = false;
			if (--(*_pcount) == 0)
			{
				if (_ptr)
				{
					_del(_ptr);//用删除器进行删除
				}
				delete _pcount;
				deleteFlag = true;
			}
			_pmtx->unlock();

			if (deleteFlag)
			{
				delete _pmtx;
			}
		}

		shared_ptr& operator=(const shared_ptr& p)//赋值拷贝有以下几种情况:p1=p2,p1=p1,p3=p1
		{
			if (_ptr != p._ptr)
			{
				Release();
				_ptr = p._ptr;
				_pcount = p._pcount;
				_pmtx = p._pmtx;

				AddCount();
			}
			return *this;
		}

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

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

		T* get() const
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}



	private:
		T* _ptr;//智能指针
		int* _pcount;//引用计数 这里使用int*,而不是static int _pcount
		mutex* _pmtx;//加锁

		function _del = [](T* ptr) {
			cout << "包装器 lambda delete:" << ptr << endl;
			delete ptr;
		};

	};


	void test_shared()
	{
		shared_ptr sp1(new int(1));
		shared_ptr sp2(sp1);
		shared_ptr sp3(sp2);

		shared_ptr sp4(new int(10));

		//sp1 = sp4;
		sp4 = sp1;

		sp1 = sp1;
		sp1 = sp2;
	}

	struct Date
	{
		int _year = 0;
		int _month = 0;
		int _day = 0;

		~Date()
		{}
	};

	// shared_ptr本身是线程安全的,因为计数是加锁保护
	// shared_ptr管理的对象是否是线程安全?
	void SharePtrFunc(shared_ptr&sp, size_t n, mutex& mtx)
	//void SharePtrFunc(std::shared_ptr&sp, size_t n, mutex& mtx)
	{
		//cout << &sp << endl;

		//cout << sp.get() << endl;

		for (size_t i = 0; i < n; ++i)
		{
			// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
			shared_ptr copy(sp);
			//std::shared_ptr copy(sp);

			mtx.lock();

			sp->_year++;
			sp->_day++;
			sp->_month++;

			mtx.unlock();
		}
	}

	void test_shared_safe()
	{
		my_shared_ptr::shared_ptr p(new Date);
		//std::shared_ptr p(new Date);

		cout << p.get() << endl;
		//cout << &p << endl;

		const size_t n = 50000;
		mutex mtx;
		thread t1(SharePtrFunc, ref(p), n, ref(mtx));
		thread t2(SharePtrFunc, ref(p), n, ref(mtx));

		t1.join();
		t2.join();

		cout << p.use_count() << endl;

		cout << p->_year << endl;
		cout << p->_month << endl;
		cout << p->_day << endl;
	}
}



int main()
{
	//my_shared_ptr:: test_shared();//拷贝构造与赋值测试
	my_shared_ptr::test_shared_safe();//线程安全测试



	return 0;
}

七、weak_ptr

前面描述的shared_ptr已经很完善了,但是引用计数仍然存在问题。比如如下情况

class A  
{  
public:  
    shared_ptr ptrA_B;  
public:  
    A()  
    {  
        cout << "调用class A的默认构造函数" << endl;  
    }  
    ~A()  
    {  
        cout << "调用class A的析构函数" << endl;  
    }  
};  
  
class B  
{  
public:  
    shared_ptr ptrB_A;  
public:  
    B()  
    {  
        cout << "调用class B的默认构造函数" << endl;  
    }  
    ~B()  
    {  
        cout << "调用class B的析构函数" << endl;  
    }  
};  

class A中包含着指向堆区构建的class B对象的引用计数指针,而class B中也包含着指向堆区构建的class A对象的引用计数指针。这样就形成了一个环形结构

C++智能指针_第1张图片

对于class A对象来说,每当class A对象生命结束时,指向在堆区构建的class B对象的shared_ptr指针就会调用class B的析构函数结束class B对象的声明,但此时class B对象中也包含有指向“在堆区构架的class A对象”的shared_ptr指针,也得执行对class A对象的析构,如此往复进入了死循环

同样类似的结构还包括双向链表,如下代码实现

#include
#include
#include

using namespace std;


namespace my_weak_ptr
{
	template
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex)
		{}

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

		shared_ptr(const shared_ptr& p)
			:_ptr(p._ptr), _pcount(p._pcount), _pmtx(p._pmtx)
		{
			AddCount();
		}

		~shared_ptr()
		{
			Release();
		}

		void AddCount()
		{
			_pmtx->lock();

			++(*_pcount);

			_pmtx->unlock();
		}

		void Release()
		{
			//_pmtx->lock();

			//if (--(*_pcount) == 0)
			//{
			//	cout << "delete:" << endl;
			//	delete _ptr;
			//	delete _pcount;
			//	delete _pmtx;//这里需要思考 如何解决锁的释放问题
			//}
			//_pmtx->unlock();

			_pmtx->lock();
			bool deleteFlag = false;
			if (--(*_pcount) == 0)
			{
				if (_ptr)
				{
					_del(_ptr);//用删除器进行删除
				}
				delete _pcount;
				deleteFlag = true;
			}
			_pmtx->unlock();

			if (deleteFlag)
			{
				delete _pmtx;
			}
		}




		shared_ptr& operator=(const shared_ptr& p)//赋值拷贝有以下几种情况:p1=p2,p1=p1,p3=p1
		{
			if (_ptr != p._ptr)
			{
				Release();
				_ptr = p._ptr;
				_pcount = p._pcount;
				_pmtx = p._pmtx;

				AddCount();
			}
			return *this;
		}




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

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

		T* get() const
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}



	private:
		T* _ptr;
		int* _pcount;//这里使用int*,而不是static int _pcount
		mutex* _pmtx;

		function _del = [](T* ptr) {
			cout << "包装器 lambda delete:" << ptr << endl;
			delete ptr;
		};

	};

	//shared_ptr的问题 循环引用导致内存泄漏
	struct ListNode
	{
		//ListNode* _next;
		//ListNode* _prev;

		shared_ptr _next;
		shared_ptr _prev;

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

	void test_shared_cycle()//循环引用测试
	{

		//shared_ptr智能指针链接链表
		shared_ptr n1(new ListNode);
		shared_ptr n2(new ListNode);
		cout << "链接前" << endl;
		cout << n1.use_count() << endl;
		cout << n2.use_count() << endl;

		n1->_next = n2;
		n2->_prev = n1;
		cout << "链接后" << endl;
		cout << n1.use_count() << endl;
		cout << n2.use_count() << endl;
		//如果ListNode内为shared_ptr类型,运行后发现,并没有执行析构函数
		//因此出现了weak_ptr用于解决shared_ptr的内存泄漏问题

	}
}


int main()
{
	my_weak_ptr::test_shared_cycle();

	return 0;
}

C++智能指针_第2张图片

C++中的变量是后定义的先释放。因此n2将会先释放,n2的引用计数--。但由于n1的_next指向n2,所以n2暂时还不能释放,需要先释放n1的_next。释放_next就需要释放n1,但n1目前仍有智能指针_prev指向,所以不能释放,需要释放n2_prev。因此形成循环

C++智能指针_第3张图片

因此出现了weak_ptr用于辅助shared_ptr解决循环引用问题

原理:node1->_next = node2;和node2->_prev = node1;链接时weak_ptr的_next和 _prev不会增加node1和node2的引用计数

weak_ptr并不满足智能指针RAII的需求,因为一般并不独立使用weak_ptr

#include
#include
#include

using namespace std;


namespace my_weak_ptr
{
	template
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex)
		{}

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

		shared_ptr(const shared_ptr& p)
			:_ptr(p._ptr), _pcount(p._pcount), _pmtx(p._pmtx)
		{
			AddCount();
		}

		~shared_ptr()
		{
			Release();
		}

		void AddCount()
		{
			_pmtx->lock();

			++(*_pcount);

			_pmtx->unlock();
		}

		void Release()
		{
			//_pmtx->lock();

			//if (--(*_pcount) == 0)
			//{
			//	cout << "delete:" << endl;
			//	delete _ptr;
			//	delete _pcount;
			//	delete _pmtx;//这里需要思考 如何解决锁的释放问题
			//}
			//_pmtx->unlock();

			_pmtx->lock();
			bool deleteFlag = false;
			if (--(*_pcount) == 0)
			{
				if (_ptr)
				{
					_del(_ptr);//用删除器进行删除
				}
				delete _pcount;
				deleteFlag = true;
			}
			_pmtx->unlock();

			if (deleteFlag)
			{
				delete _pmtx;
			}
		}




		shared_ptr& operator=(const shared_ptr& p)//赋值拷贝有以下几种情况:p1=p2,p1=p1,p3=p1
		{
			if (_ptr != p._ptr)
			{
				Release();
				_ptr = p._ptr;
				_pcount = p._pcount;
				_pmtx = p._pmtx;

				AddCount();
			}
			return *this;
		}




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

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

		T* get() const
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}



	private:
		T* _ptr;
		int* _pcount;//这里使用int*,而不是static int _pcount
		mutex* _pmtx;

		function _del = [](T* ptr) {
			cout << "包装器 lambda delete:" << ptr << endl;
			delete ptr;
		};

	};



	template
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{
		}

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

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

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

		T* get()
		{
			return _ptr;
		}

	
	private:
		T* _ptr;
	};


	//shared_ptr的问题 循环引用导致内存泄漏
	struct ListNode
	{
		//ListNode* _next;
		//ListNode* _prev;

		//shared_ptr _next;
		//shared_ptr _prev;

		weak_ptr _next;
		weak_ptr _prev;
		int val;

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

	void test_shared_cycle()//循环引用测试
	{
		//普通链表链接
		/*ListNode* n1 = new ListNode;
		ListNode* n2 = new ListNode;
		n1->_next = n2;
		n2->_prev = n1;

		delete n1;
		delete n2;*/

		//shared_ptr智能指针链接链表
		shared_ptr n1(new ListNode);
		shared_ptr n2(new ListNode);
		cout << "链接前" << endl;
		cout << n1.use_count() << endl;
		cout << n2.use_count() << endl;

		n1->_next = n2;
		n2->_prev = n1;
		cout << "链接后" << endl;
		cout << n1.use_count() << endl;
		cout << n2.use_count() << endl;
		//如果ListNode内为shared_ptr类型,运行后发现,并没有执行析构函数
		//因此出现了weak_ptr用于解决shared_ptr的内存泄漏问题

	}
}


int main()
{
	my_weak_ptr::test_shared_cycle();

	return 0;
}

你可能感兴趣的:(C++,c++,开发语言)