秒懂C++之智能指针

目录

前言

智能指针的使用及原理

RAII

RAII弊端

std::auto_ptr

std::unique_ptr

std::shared_ptr

shared_ptr弊端

std::weak_ptr

扩展(删除器)


前言

为了解决抛异常所造成的内存泄漏等问题~秒懂C++之异常-CSDN博客~我们来学习智能指针的相关用法~

智能指针的使用及原理

RAII

RAII ( Resource Acquisition Is Initialization )是一种 利用对象生命周期来控制程序资源 (如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效, 最后在 对象析构的时候释放资源 。借此,我们实际上把管理一份资源的责任托管给了一个对象。

cpp 复制代码
template <class	T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}
    T& operator*() {return *_ptr;}
    T* operator->() {return _ptr;}
private:
	T* _ptr;
};

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);
	cout << div() << endl;
}

int main()
{
	try {
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

可以看到即使触发了异常最后也可以清理资源~避免内存泄漏~

RAII也可以和正常指针一样解引用与->访问其内容~

RAII弊端

智能指针的拷贝都是默认生成的,而默认生成的拷贝构造都为浅拷贝这就代表拷贝后会有两个指针指向同一处空间,最终析构两次造成报错~

其实多个指针指向同一处空间进行访问并没什么大碍,迭代器也这样。不过智能指针除了访问还有释放的职能,这才导致出现问题~

std::auto_ptr

这个稍微提一下就好了,因为它是一个及其失败的设计!

过于浮夸了,直接把人家资源给掠夺了~

std::unique_ptr

unique_ptr 的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份 UniquePtr 来了解它的原

cpp 复制代码
namespace lj
{
	template <class	T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				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;
	};
}

就是利用关键字delete防止生成默认拷贝与默认赋值,不过个人感觉实用性不大~

std::shared_ptr

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

  • shared_ptr 在其内部, 给每个资源都维护了着一份计数,用来记录该份资源被几个对象共
  • 对象被销毁时 ( 也就是析构函数调用 ) ,就说明自己不使用该资源了,对象的引用计数减一
  • 如果引用计数是 0 ,就说明自己是最后一个使用该资源的对象, 必须释放该资源
  • 如果不是 0 ,就说明除了自己还有其他对象在使用该份资源, 不能释放该资源 ,否则其他对象就成野指针了

shared_ptr中如何合理利用引用计数是一个难点~

普通int count计数肯定是不行的,那静态成员呢?

cpp 复制代码
namespace lj
{
	template<class T>
class shared_ptr
{
public:
	// RAII
	shared_ptr(T* ptr)
		:_ptr(ptr)
	{
		_count = 1;
	}

	// sp2(sp1)
	shared_ptr(const shared_ptr<T>& sp)
	{
		_ptr = sp._ptr;
		++_count;
	}

	~shared_ptr()
	{
		if (--_count == 0)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}

	int use_count()
	{
		return _count;
	}

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

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

	static int _count;
};

template<class T>
int shared_ptr<T>::_count = 0;
}

一开始还好,但当sp3被构建出来后会把count重新初始化为1,因为静态成员是属于类中的所有对象,这样一来就打乱了原本的计数。本来是想要释放两处空间的结果只释放了sp3的,由此看来静态也不合适~

我们想要的是给每一个资源都配上一个引用计数,而非全部资源共用一个!

cpp 复制代码
namespace lj
{
	template<class T>
	class shared_ptr
	{
	public:
		// RAII
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{
		}

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++*_pcount;
		}

		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;

			}
		}

		int use_count()
		{
			return *_pcount;
		}

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

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

		T* _pcount;
	};
	
}

最终给出的解决方案是在每一个对象中存一个指向计数的指针~

除了拷贝构造,赋值拷贝也要给上~

cpp 复制代码
        // sp1 = sp4
		// sp4 = sp4;
		// sp1 = sp2;
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr)//防止自身赋值,也防止与拷贝自身的赋值,即指向同一资源的不赋值
			{
				release();

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

				// 拷贝时++计数
				++(*_pcount);
			}

			return *this;
		}
		void release()
		{
			// 说明最后一个管理对象析构了,可以释放资源了
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}

shared_ptr弊端

cpp 复制代码
struct ListNode
{
	int _val;
	/*struct ListNode* _next;
	struct ListNode* _prev;*/

	//这里也要改为智能指针
	//n1->_next为内置类型,n2为自定义类型
	lj::shared_ptr<ListNode> _next;
	lj::shared_ptr<ListNode> _prev;

	

	ListNode(int val = 0)
		:_val(val)
		,_next(nullptr)
		,_prev(nullptr)
	{}

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

int main()
{
	/*ListNode* n1 = new ListNode(10); 
	ListNode* n2 = new ListNode(10);*/

	//库里的std::shared不支持隐式类型转化
	//std::shared_ptr<ListNode> n1 = new ListNode(10);


	lj::shared_ptr<ListNode> n1(new ListNode(10));
	lj::shared_ptr<ListNode> n2(new ListNode(20));

	n1->_next = n2;
	n2->_prev = n1;

	

	return 0;
}

当我们把智能指针应用到双链表时会出现循环引用的弊端~

完全就是死循环了,永远释放不了~

ps:_next作为节点成员,节点什么时候释放,它就什么时候析构~

为了解决这种特定场景下的问题,我们又引入了新的智能指针,weak_ptr

std::weak_ptr

weak指针其实也就是在shared指针的基础上放弃了对引用计数的职能~

cpp 复制代码
struct ListNode
{
	int _val;

	lj::weak_ptr<ListNode> _next;
	lj::weak_ptr<ListNode> _prev;



	ListNode(int val = 0)
		:_val(val)
		//weak指针无法用普通指针构造
		/*, _next(nullptr)
		, _prev(nullptr)*/
	{}

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

int main()
{


	lj::shared_ptr<ListNode> n1(new ListNode(10));
	lj::shared_ptr<ListNode> n2(new ListNode(20));

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	n1->_next = n2;
	n2->_prev = n1;

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;


	return 0;
}
cpp 复制代码
namespace lj
{
	template<class T>
	class shared_ptr
	{
	public:
		// RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{
		}

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++*_pcount;
		}
		// sp1 = sp4
		// sp4 = sp4;
		// sp1 = sp2;
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr)//防止自身赋值,也防止与拷贝自身的赋值,即指向同一资源的不赋值
			{
				release();

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

				// 拷贝时++计数
				++(*_pcount);
			}

			return *this;
		}
		void release()
		{
			// 说明最后一个管理对象析构了,可以释放资源了
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}
		~shared_ptr()
		{
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

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

		T* operator->()
		{
			return _ptr;
		}
		//方便weak指针拿到shared指针
		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
	};
	
}
namespace lj
{
	// 不支持RAII,不参与资源管理
	template<class T>
	class weak_ptr
	{
	public:
		// RAII
		// 取消了引用计数
		weak_ptr()
			:_ptr(nullptr)
		{}
		//用share指针作拷贝
		weak_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
		}
		//用share指针作赋值重载
		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;
	};
}

我们把互相牵制的_next与_prev指针变为weak指针,使得其无法进行引用计数,这样就可以把资源成功释放~

最终在weak指针的作用下解决了循环引用的问题~

扩展(删除器)

在了解删除器之前我们需要认识一点:[ ] 是要匹配使用的,如果new用了[ ] 那么delete也要使用[ ] ,否则就会报错~

而且也不是所有被智能指针修饰的资源都是delete,例如打开文件的时候我们反而是要用close,

所以一般是搞关于删除的仿函数。

cpp 复制代码
template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		cout << "delete[]" << ptr << endl;
		delete[] ptr;
	}
};

int main()
{
	std::shared_ptr<ListNode> p1(new ListNode(10));
	//C11下作出的改进优化
	//std::shared_ptr<ListNode[]> p2(new ListNode[10]);

	std::shared_ptr<ListNode> p2(new ListNode[10], DeleteArray<ListNode>());

	return 0;

}

不过比起仿函数,我们前面所学的lambda表达式会更好用一点~

cpp 复制代码
int main()
{
	std::shared_ptr<ListNode> p1(new ListNode(10));
	//C11下作出的改进优化
	//std::shared_ptr<ListNode[]> p2(new ListNode[10]);

	//std::shared_ptr<ListNode> p2(new ListNode[10], DeleteArray<ListNode>());
	std::shared_ptr<FILE> p3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
	std::shared_ptr<ListNode> p2(new ListNode[10], [](ListNode* ptr){ delete[] ptr; });

	return 0;

}

认识到删除器后,我们再来对自己实现的shared指针进行改进调整~

所以我们再引入前面所学的包装器function,解决这个参数问题~

cpp 复制代码
#include<functional>
namespace lj
{
	template<class T>
	class shared_ptr
	{
	public:
		// RAII
		template<class D>
		shared_ptr(T* ptr, D del)
			:_ptr(ptr)
			, _pcount(new int(1))
			, _del(del)
		{
		}

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++*_pcount;
		}
		// sp1 = sp4
		// sp4 = sp4;
		// sp1 = sp2;
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr)//防止自身赋值,也防止与拷贝自身的赋值,即指向同一资源的不赋值
			{
				release();

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

				// 拷贝时++计数
				++(*_pcount);
			}

			return *this;
		}
		void release()
		{
			// 说明最后一个管理对象析构了,可以释放资源了
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				//delete _ptr;
				_del(_ptr);
				delete _pcount;
			}
		}
		~shared_ptr()
		{
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

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

		T* operator->()
		{
			return _ptr;
		}
		//方便weak指针拿到shared指针
		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
		function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};
	
}
相关推荐
C-SDN花园GGbond44 分钟前
【探索数据结构与算法】插入排序:原理、实现与分析(图文详解)
c语言·开发语言·数据结构·排序算法
迷迭所归处2 小时前
C++ —— 关于vector
开发语言·c++·算法
架构文摘JGWZ2 小时前
Java 23 的12 个新特性!!
java·开发语言·学习
leon6252 小时前
优化算法(一)—遗传算法(Genetic Algorithm)附MATLAB程序
开发语言·算法·matlab
CV工程师小林2 小时前
【算法】BFS 系列之边权为 1 的最短路问题
数据结构·c++·算法·leetcode·宽度优先
white__ice3 小时前
2024.9.19
c++
天玑y3 小时前
算法设计与分析(背包问题
c++·经验分享·笔记·学习·算法·leetcode·蓝桥杯
锦亦之22333 小时前
QT+OSG+OSG-earth如何在窗口显示一个地球
开发语言·qt
我是苏苏3 小时前
Web开发:ABP框架2——入门级别的增删改查Demo
java·开发语言
姜太公钓鲸2333 小时前
c++ static(详解)
开发语言·c++