C++:智能指针

1. 智能指针的思想

1.1 RAII的概念

RAII 是 Resource(资源) Acquisition(请求) Is(立即) Initalization(初始化) 的缩写,是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄露(内存、文件指针、网络连接、互斥锁等等)。RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的声明周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄露问题。

1.2 智能指针的简单实现

来利用RAII的思想,实现一个智能指针,同时为了方便资源的访问智能指针类还会重载 **-> * []**等操作符

cpp 复制代码
#include<iostream>
#include<vector>
//RAII  Resource(资源) Acquisition(请求) Is(立即) Initalization(初始化)   的缩写,
using namespace std;
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr=nullptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		cout << "delete 执行" << endl;
		if (_ptr)
			delete _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}


private:
	T* _ptr;
};

利用智能指针来解决抛异常的内存泄漏问题,其原理是无论程序正常返回还是抛异常返回,创建的SmartPtr对象都会自动调用其析构函数,释放内存。

cpp 复制代码
double Divide(int a, int b)
{
	if (b == 0)
	{
		throw "Divide 除数不能为0";
	}
	return (double)a / (double)b;
}

void Func1()
{
	SmartPtr<int> array = new int[10];

	try
	{
		int a, b;
		cin >> a >> b;
		cout << Divide(a, b) << endl;
	}
	catch (...)
	{
		throw;//将异常重新抛出
	}

}

int main()
{
	try
	{
		Func1();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception" << endl;
	}

	return 0;
}
1.3 可能产生的问题

利用我们写的智能指针定义三个对象。执行以下操作

cpp 复制代码
int main()
{
	SmartPtr<int> ps1 = new int(0);
	SmartPtr<int> ps2(ps1);//拷贝构造
	SmartPtr<int> ps3 = new int(0);
	ps3 = ps1;//赋值

	return 0;
}

默认生成的拷贝构造是一个浅拷贝,而且默认生成的赋值重载也是浅拷贝,ps1与ps2与ps3指向同一块空间,析构时会对同一块空间析构三次,造成内存崩溃。ps3原本的空间就会内存泄露。

2. C++标准库智能指针的使用与实现

在C++标准库中提供了多种不同的智能指针。C++标准库中的智能指针都在<memory>这个头文件下面,除了weak_ptr都符合RAII和像指针一样的访问的行为,原理上主要是解决智能指针拷贝时的思路不同。

auto_ptr

(1).设计与使用

auto_ptr是C++98设计出来的智能指针,他的特点是拷贝时把拷贝对象的资源的管理权转移给拷贝对象,虽然保证了一个资源在任何时刻都只有一个对象对其进行管理但是会导致被拷贝对象悬空,访问报错。所以很不推荐使用它。

cpp 复制代码
int main()
{
	auto_ptr<int> ap1(new int(0));
	auto_ptr<int> ap2(ap1);
	*ap2 = *ap2 + 1;
	auto_ptr<int> ap3(new int(22));
	ap3 = ap2;

	cout << *ap3 << endl;
	cout << *ap2 << endl;
	cout << *ap1 << endl;

	return 0;
}

管理权先从ap1转移给了ap2,ap2将值从0变为1,管理权0再从ap2到ap3。此时ap1与ap2都是不能访问的,只有app3能访问运行如下

(2).auto_ptr的实现

auto_ptr相较于我们上面实现的SmartPtr没什么区别,只是在拷贝构造与赋值重载时需要对原来的指针置空。

cpp 复制代码
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr=nullptr)
			:_ptr(ptr)
		{
		}
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		~auto_ptr()
		{
			if (_ptr)
				delete _ptr;
		}
		auto_ptr& operator=(auto_ptr<T>& ap)
		{
			//if (*this != ap)//不可以
			if (this != &ap)
			{	
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
unique_ptr

(1). 使用

是C++11设计出来的智能指针,特点是不支持拷贝只支持移动。如果不需要拷贝的场景就非常建议使用它

cpp 复制代码
int main()
{
	unique_ptr<int> up1(new int(0));

	unique_ptr<int> up2(move(up1));

	cout << *up2 << endl;
	cout << *up1 << endl;

	return 0;
}

利用move可以进行移动初始化,但是up1就不能调用了,因为里面的值已经被转移了。

(2). unique_ptr的实现

cpp 复制代码
	template<class T>
	class unique_ptr
	{
	public:
	 	explicit unique_ptr(T* ptr=nullptr)防⽌普通指针隐式类型转换成智能指针对象。
			:_ptr(ptr)
		{
		}
		unique_ptr(unique_ptr<T>&& up)
			:_ptr(up._ptr)
		{
			up._ptr = nullptr;
		}
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
		~unique_ptr()
		{
			if (_ptr)
				delete _ptr;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}

	private:
		T* _ptr=nullptr;
	};
shared_ptr

(1)简单使用

shared_ptr的特点是支持拷贝也支持移动。需要拷贝的场景就需要使用他。底层是引用计数的方式实现的。

cpp 复制代码
int main()
{
    shared_ptr<int> sp1(new int(0));
	shared_ptr<int>sp2(sp1);
	shared_ptr<int> sp3;
	sp3 = sp1;
	
	cout << *sp1 << endl;
	*sp1 = *sp1 + 1;
	cout << *sp2 << endl;
	*sp2 = *sp2 + 1;
	cout << *sp3 << endl;
	return 0;
}

通过下图结果可知,上面绑定的同一个资源,通过计数来判断是否释放开辟的内存。

(2)简单实现shared_ptr

分析参数

第一个参数肯定是_ptr来存储资源,第二个参数是一个计数器,但是为了保证这个资源的引用计数共通需要动态开辟这个计数器。

cpp 复制代码
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr=nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))

		{
		}

		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			*_pcount++;
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if(this!=&sp)
			if (_ptr != sp._ptr)//防止自己赋值自己的情况
			{
				(*_pcount)--;
				if (*_pcount == 0)
				{
					delete _ptr;
					delete _pcount;
				}

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				(*_pcount)++;
			}
			return *this;
		}
		~shared_ptr()
		{
			*_pcount--;
			if (*_pcount == 0)
			{
				if(_ptr)
				delete _ptr;
				delete _pcount;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}
		long int use_count()
		{
			return *_pcount;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr=nullptr;
		int* _pcount;
	};

但是这样还有一个问题,当智能指针对象生命周期结束调用析构函数时,都会用delete方式释放资源,但是智能指针所管理的资源不一定是new出来的资源,还可能是new [] 或者是文件指针亦或者是其他非new出来的资源,析构时就会崩溃。

如上我们可以在构造时给定一个删除器,在析构时利用这个删除器来释放资源。

我们在这里是用function包装器来实现的

cpp 复制代码
	template<class T>
	class shared_ptr
	{
	public:
		explicit shared_ptr(T* ptr=nullptr)//防⽌普通指针隐式类型转换成智能指针对象。
			:_ptr(ptr)
			, _pcount(new int(1))

		{
		}
		template<class D>
		shared_ptr(T* ptr = nullptr, D del = [](T* ptr) {delete ptr; })//缺省值为lambda表达式
			:_ptr(ptr)
			,_pcount(new int(1))
			,_del(del)
		{

		}

		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			*_pcount++;
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if(this!=&sp)
			if (_ptr != sp._ptr)//防止自己赋值自己的情况
			{
				(*_pcount)--;
				if (*_pcount == 0)
				{
					_del(_ptr);
					delete _pcount;
				}

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				(*_pcount)++;
			}
			return *this;
		}
		~shared_ptr()
		{
			*_pcount--;
			if (*_pcount == 0)
			{
				if(_ptr)
				_del(_ptr);
				delete _pcount;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}
		long int use_count()
		{
			return *_pcount;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr=nullptr;
		int* _pcount;
		function<void(T*)> _del = [](T* ptr) {delete ptr; };//定制删除器
	};
make_shared构造与shared_ptr直接构造的区别

shared_ptr除了支持用指向资源的指针构造,还支持make_shared用初始化资源对象直接构造。

内存分配上:

make_shared:使用单一内存分配策略,即同时分配被管理的对象和引用计数块,减少了内存分配的次数提高效率。

shared_ptr直接构造:可能涉及两次内存分配,一次是分配被管理的对象,另一次用于分配引用计数块,可能导致额外消耗和内存碎片。

效率方面:

make_shared:通常比直接构造更高效,内存连续分配,也可能更好利用内存。

shared_ptr直接构造:可能涉及两次内存分配,效率相对较低

shared_ptr线程安全问题...待补充
看shared_ptr与unique_ptr是否在管理资源

由于shared_ptr和unique_ptr都支持operator bool的转换,如果智能指针对象是一个空对象没有管理资源返回false反之返回true。

shared_ptr的缺陷

shared_ptr在特定情况下有很大缺陷(循环引用),比如存链表节点

那链表内部的next节点与prev节点也必须变成智能指针,如下代码

cpp 复制代码
struct ListNode
{
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
		cout << "delete" << endl;
	}
};

int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	node1->_next = node2;
	node2->_prev = node1;
	return 0;
}

我们创建两个节点并将其链接

此时

资源1的管理者是Node1与Node2的_next

资源2的管理者是Node2与Node1的_prev

当这两个对象生命周期结束时,这两个资源对应的引用计数最终都减少到了1,此时问题出现了

必须引用计数变为0资源才会释放,但是资源1的释放取决于资源2的_prev成员,资源2的释放取决于资源1的_next成员,这样就陷入了一个死循环使资源1和资源2都无法释放,这就是循环引用。

运行结果如下,并没有调用析构函数

weak_ptr

为了解决这个问题C++11提供了一个智能指针weak_ptr。weak_ptr不是用来管理资源的释放的(不支持RAII),产生是为了要解决shared_ptr的循环引用导致内存泄露的问题。

我们将上面_next与_prev成员的类型换成weak_ptr就不会发生循环引用的问题了

cpp 复制代码
struct ListNode
{
	weak_ptr<ListNode> _next;
	weak_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
		cout << "delete" << endl;
	}
};

int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	node1->_next = node2;
	node2->_prev = node1;
	return 0;
}

将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象也会被释放。

实现一个最简单的weak_ptr

cpp 复制代码
	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
		{
			_ptr = nullptr;
		}
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())//返回存储的指针
		{

		}
		weak_ptr& operator=(const weak_ptr<T>& wp) {
			if (this != &wp) {
				_ptr = wp._ptr;
			}
			return *this;
		}
	private:
		T* _ptr;
	};

这篇就到这里啦,ヾ( ̄▽ ̄)Bye~Bye~

(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤

相关推荐
霁月风5 分钟前
设计模式——工厂方法模式
c++·设计模式·工厂方法模式
夜阳朔13 分钟前
《C++ Primer》第三章知识点
c++·编程语言
凡人的AI工具箱13 分钟前
每天40分玩转Django:Django管理界面
开发语言·数据库·后端·python·django
每天写点bug28 分钟前
【go每日一题】:并发任务调度器
开发语言·后端·golang
一个不秃头的 程序员29 分钟前
代码加入SFTP Go ---(小白篇5)
开发语言·后端·golang
sjyioo34 分钟前
【C++】类和对象.1
c++
数据小爬虫@39 分钟前
Python爬虫抓取数据,有哪些常见的问题?
开发语言·爬虫·python
逊嘘1 小时前
【Java数据结构】ArrayList相关的算法
java·开发语言
基哥的奋斗历程1 小时前
初识Go语言
开发语言·后端·golang
煤泥做不到的!1 小时前
挑战一个月基本掌握C++(第六天)了解函数,数字,数组,字符串
开发语言·c++