【C++】初步认识智能指针

初步认识智能指针

为什么需要智能指针

先看以下代码

cpp 复制代码
#include<iostream>

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

void func()
{
	int* p1 = new int;
	int* p2 = new int;
	std::cout << div() << std::endl;
	delete p1;
	delete p2;
}

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

当在func中没有捕捉异常时,此时没有执行到后面的delete语句,整个func函数就已经全部退出了,但是由于p1p2所指向的空间是在堆区申请的,它们并没有被释放,但是当程序返回到main函数后,也无法再次访问到申请的空间资源,这里就造成了内存泄漏的问题。

一个长期运行的程序出现内存泄漏,影响很大,比如操作系统或者后台服务等,出现内存泄漏后会导致响应越来越慢,最终卡死。想要在C++中避免内存泄漏的问题,智能指针是一个很好的选择

智能指针的使用及基本原理

RAII

RAII(Resource Acquisition is initialization)是一种利用对象生命周期来控制程序资源 的技术。在对象构造时获取资源 ,让资源的访问在该对象的生命周期内始终有效,最后在对象析构时释放资源 。就相当于是将管理一份资源的责任交给了一个对象。

将RAII思想运用到刚才的代码中

cpp 复制代码
#include<iostream>
template<class T>
class smart_ptr
{
public:
	smart_ptr(T* ptr = nullptr)
		:_ptr(ptr)
	{}
	~smart_ptr()
	{
		std::cout << "~smart_ptr()" << std::endl;
		if (_ptr)delete _ptr;
	}
private:
	T* _ptr;
};

int div()
{
	int a = 2, b = 0;
	std::cout << "a=" << a << " " << "b=" << b << std::endl;
	if (b == 0)
	{
		throw std::invalid_argument("除0错误");
	}
	return a / b;
}

void func()
{
	smart_ptr<int>p1(new int);
	smart_ptr<int>p2(new int);
	std::cout << div() << std::endl;
}

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

这里看到,即使是中间发生了异常,申请到的资源可以随着对象生命周期的结束而自动释放。作为一个指针,还需要实现解引用和->访问所知空间中的内容,使其拥有普通指针一样的行为

std::auto_ptr

C++98的库中就提供了auto_ptr的智能指针,auto_ptr的实现原理是:管理权转移思想。下面我用代码来演示以下

在以上代码中,先用p1管理申请出来的int大小的空间,然后让p2也来管理这块空间,最后通过p1来查看空间的内容,发现已经无法查看了,这是因为在用p2拷贝p1时,p1就将所申请空间的使用权交给了p2,自己所维护的指针置空。

以下就是我简单模拟实现的auto_ptr,帮助理解它的原理

cpp 复制代码
template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr = nullptr)
		:_ptr(ptr)
	{}
	~auto_ptr()
	{
		if (_ptr)delete _ptr;
	}
	auto_ptr(const auto_ptr<T>& ptr)
		:_ptr(ptr._ptr)
	{
		ptr = nullptr;//管理权转移
	}
	auto_ptr& operator=(const auto_ptr<T>& ptr)
	{
		if (this != &ptr)
		{
			if (_ptr)
			{
				delete _ptr;
			}
			//管理权转移
			_ptr = ptr._ptr;
			ptr = nullptr;
		}
		return *this;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

std::unique_ptr

C++11中提供了相较于std::auto_ptr更为靠谱的std::auto_ptr
std::unique_ptr的实现原理就是:简单粗暴的防止拷贝

cpp 复制代码
template<class T>
class unique_ptr
{
public:
	unique_ptr(T* p)
		:_ptr(p)
	{}
	~unique_ptr()
	{
		if (_ptr)
			delete _ptr;
	}
	unique_ptr(const unique_ptr<T>& p) = delete;
	unique_ptr<T> operator=(const unique_ptr<T>&p) = delete;

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

std::shared_ptr

C++11中还提供了靠谱且支持拷贝的shared_ptr

shared_ptr支持拷贝的原理就是通过引用计数的方法来实现多个shared_ptr对象之间共享资源

  1. shared_ptr内部都维护一个计数器,用来记录所管理的资源一共被几个对象所共享
  2. 对象在销毁 时(调用析构函数),就说明自己已经不在管理该资源了,这时,计数器里的值减一
  3. 如果计数为0,说明当前对象是最后一个指向该资源的对象 ,就需要该对象承担释放资源的任务
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源 ,就不能释放该资源,否则其他对象就找不到该资源了

std::shared_ptr的循环引用问题

cpp 复制代码
#include<iostream>
#include<memory>
struct Node
{
	int _val = 0;
	std::shared_ptr<Node>_next;
	std::shared_ptr<Node>_prev;
	Node(int val = 0, std::shared_ptr<Node>next = nullptr, std::shared_ptr<Node>prev = nullptr)
		:_val(val)
		, _next(next)
		, _prev(prev)
	{}
	~Node()
	{
		std::cout << "~Node()" << std::endl;
	}
};

void test1()
{
	std::shared_ptr<Node>node1(new Node);
	std::shared_ptr<Node>node2(new Node);
	std::cout << node1.use_count() << std::endl;
	std::cout << node2.use_count() << std::endl;
	node1->_next = node2;
	node2->_prev = node1;
	std::cout << node1.use_count() << std::endl;
	std::cout << node2.use_count() << std::endl;
}

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

可以看到在test1函数执行结束之后,两个指针所指向的空间并没有释放,下面就来分析以下原因

要解决这个问题,可以将结点中的_prev_next改成weak_ptr就可以了。原理就是node1->_next = node2;node2->_prev = node1;使用weak_ptr并不会增加node1node2内部的引用计数。

weak_ptr不支持RAII,并不直接参与资源管理(所以不支持使用一个普通指针构造对象)。但是weak_ptr内部也会存在一个引用计数,这个计数的作用就是为了防止在shared_ptr已经被释放的情况下,weak_ptr再去访问被释放的资源(当内部计数器为0的时候就说明该资源已经被释放了)

std::shared_ptr删除器

如果shared_ptr指向的资源并不是在堆区开辟的(new出来的空间),那么在析构时就不能直接delete。这个时候就需要在构造对象时传入一个自定义的"删除器",这个删除器就是一个仿函数

以下面为例

这里直接在构造的时候传入一个lambda表达式,这样shared_ptr在析构时就直接调用这个lambda表达式来管理对应的资源

std::shared_ptr的简单模拟实现

cpp 复制代码
#include<atomic>

namespace lsh
{
	template<class T>
	struct DefaultDeleter
	{
		void operator()(T* p)
		{
			delete p;
		}
	};

	struct spControlBlock
	{
		std::atomic<long> refcnt;

		spControlBlock()
			:refcnt(1)
		{}

		spControlBlock(spControlBlock&&) = delete;

		void inref()
		{
			refcnt.fetch_add(1);
		}

		void decref()
		{
			if (refcnt.fetch_sub(1) == 1)//相当于(refcnt--) == 1
			{
				delete this;
			}
		}

		long cntref()
		{
			return refcnt.load();
		}

		virtual ~spControlBlock() = default;
	};

	template<class T,class Deleter>
	struct spControlBlockImpl :spControlBlock
	{
		T* _ptr;
		Deleter _del;

		spControlBlockImpl(T* ptr)
			:_ptr(ptr)
		{}

		spControlBlockImpl(T* ptr, Deleter del)
			:_ptr(ptr)
			, _del(del)
		{}

		virtual ~spControlBlockImpl() override
		{
			//delete _ptr;
			_del(_ptr);
		}
	};

	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _cb(new spControlBlockImpl<T, DefaultDeleter<T>>(ptr))
		{}

		template<class Deleter>
		shared_ptr(T* ptr, Deleter del)
			: _ptr(ptr)
			, _cb(new spControlBlockImpl<T, Deleter>(ptr, del))
		{}

		shared_ptr(const shared_ptr& that)
			:_ptr(that._ptr)
			, _cb(that._cb)
		{
			_cb->inref();
		}
		~shared_ptr()
		{
			_cb->decref();
		}
		long use_count()
		{
			return _cb->cntref();
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		spControlBlock* _cb;
	};
}

在模拟实现中,我创建了一个类spControlBlock来对计数器进行封装,由于智能指针还可能运行在多线程环境下,所以内部对于计数器的操作必须是原子的,所以我将计数器定义为std::atomic<long>类型,

spControlBlockImpl 负责删除的设计主要是为了分离内存管理和对象删除的逻辑。通过这种设计将删除逻辑与引用计数管理分开,使得 spControlBlock 只关注引用计数,而spControlBlockImpl负责处理资源的销毁细节

相关推荐
一点媛艺3 小时前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风3 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生4 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功4 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
闲晨4 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程5 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
UestcXiye5 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
Chrikk5 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*6 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue6 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang