C++智能指针

写在前面

在学习智能指针原理及使用前,先需要搞懂几个问题:
1、什么是RAII
2、为什么需要智能指针
不买关子,解答如下:
1、RAII(Resource Acquisition Is Initialization),是C++语言的一种管理资源、避免泄漏的技术、策略。C++标准保证任何情况下,已构造的对象最终会被销毁,即它的析构函数最终会被调用。体现在智能指针中,就是将指针封装成一个对象,在构造时分配资源,在对象生命周期控制对资源的访问使之始终有效,在析构时释放资源。借此,我们把管理资源的责任托管给了一个对象,这想做的好处为(1)不需要显式释放资源;(2)采用这种方式,对象所需的资源在其生命周期内始终保持有效
2、一句话,运用RAII的策略,对系统资源进行有效的管理,避免产生资源泄漏的情况

根据上面两个问题,相信对智能指针有了一定的了解,下面,对智能指针进行最基本的模拟:

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
		{
			delete _ptr;
			_ptr = nullptr;
		}
	}
	//增加指针的特性:*、->
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

struct Date
{
	int _year;
	int _month;
	int _day;
};

int main()
{
	SmartPtr<int> sp1(new int);
	*sp1 = 10;
	cout << *sp1 << endl;

	SmartPtr<Date> sp2(new Date);
	sp2->_day = 2019;
	sp2->_month = 12;
	sp2->_year = 10;

	cout << sp2->_day << "-" << sp2->_month << "-" << sp2->_year << endl;
	return 0;
}

如此,就模拟出来了一个简单的智能指针的模拟实现,但是,细心地朋友会发现,这里仅仅是模拟了指针的实现,但是,如果多个指针同时指向一份资源,当释放的时候,就会发生浅拷贝的问题
由此,引出了C++标准库中定义的几种智能指针。

智能指针使用及原理

这里,将介绍4中智能指针的使用及原理:auto_ptr、unique_ptr、shared_ptr、weak_ptr,其中,第一种是C++98中提出的,后三种是在C++11中介绍的

auto_ptr

使用
#include <memory>

class Date
{
public:
	Date()
	{
		cout << "Date()" << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
	int _year;
	int _month;
	int _day;
};
int main()
{
	auto_ptr<Date> ap1(new Date);
	ap1->_year = 2019;
	ap1->_month = 12;
	ap1->_day = 10;
	auto_ptr<Date> ap2(ap1);

	ap1->_year = 2020;  //错误,原始版本的auto_ptr在拷贝构造新的对象后,不能访问和使用原来的对象
	return 0;
}
原理实现

auto_ptr的实现原理:管理权转移的思想,若存在拷贝、赋值等操作,就将资源转移到当前对象中,并且断开之前的连接,这样就解决了一块空间被多个对象使用而造成程序崩溃的问题

auto_ptr的原始版本

当前C++标准库中使用的该auto_ptr的原始版本

namespace a
{
	template<class T>
	class auto_ptr
	{
	public:
		//构造 析构
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~auto_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}
		//指针特性
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _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;
		}
	private:
		T* _ptr;
	};
}

int main()
{
	a::auto_ptr<int> ap1(new int);
	*ap1 = 10;
	a::auto_ptr<int> ap2(ap1);
	*ap2 = 20;
	a::auto_ptr<int> ap3;
	ap3 = ap1;

	return 0;
}

显然地,auto_ptr存在着致命的缺陷,当拷贝构造或赋值后,将原对象的指针置空了,在访问原对象时就会出现问题

auto_ptr的改进

为解决不能同时访问的问题,引入了bool类型的成员变量,看当前对象对资源是否具有释放的权利
对auto_ptr的改进如下:

namespace a
{
	template<class T>
	class auto_ptr
	{
	public:
		//构造 析构
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _owner(false)
		{
			if (_ptr)
			{
				owner = true;
			}
		}
		~auto_ptr()
		{
			if (_ptr && _owner == true)
			{
				delete _ptr;
				_ptr = nullptr;
			}
		}
		//指针特性
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//解决浅拷贝
		auto_ptr(auto_ptr<T>& ap)
			: _ptr(ap._ptr)
			, _owner(ap._owner)
		{
			ap._owner = false;
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				if (_ptr && _owner)
				{
					delete _ptr;
				}
				_ptr = ap._ptr;
				_owner = ap._owner;
				ap._owner = false;
			}
			return *this;
		}
	private:
		T* _ptr;
		bool owner;  //标记对资源是否具有释放的权利
	};
}

解决了不能同时访问的缺陷,但是,它可能会造成野指针,而导致代码崩溃,示例如下,此时,ap1会成为野指针

void TestAutoPtr()
{
	a::auto_ptr<int> ap1(new int);
	if (true)
	{
		a::auto_ptr<int> ap2(ap1);
		*ap2 = 10;
	}
	*ap1 = 20;
}

该版本仅供对auto_ptr的缺陷的解决办法了解,但是由于引出了更大的问题——野指针,所以C++11中未采用该版本,而是使用的是auto_ptr的原始版本

小结:

std::auto_ptr 可用来管理单个对象的资源,但是,请注意如下几点:
(1)、std::auto_ptr只是通过转移管理权的思想来进行管理资源,但是并没有使用深拷贝的解决策略
(2)、尽量不要使用operator=和拷贝构造函数,如果使用了,请不要再使用先前对象
(3)、由于 std::auto_ptr 的operator=问题,有其管理的对象不能放入 std::vector等容器中
(4)、 ……

unique_ptr

使用

在这里插入图片描述
如上图所示,不能对unique_ptr进行拷贝构造和赋值

原理实现

实现原理:简单粗暴的防拷贝,禁止掉拷贝构造函数和赋值来防止浅拷贝的发生

namespace a
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
			}
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}

	private:
		/*
		//C++98防拷贝的方式:只声明不实现+声明私有
		//只声明:防止编译器生成默认拷贝构造和赋值运算符重载
		//声明私有:防止用户在类外定义拷贝构造和赋值运算符重载
		unique_ptr(unique_ptr<T> const&);
		unique_ptr& operator=(unique_ptr<T> const&);
		*/
		//C++11防拷贝方式:delete
		//C++11中提供了=delete和=default,分别表示不默认生成成员函数和默认生成成员函数
		unique_ptr(unique_ptr<T> const&) = delete;
		unique_ptr& operator=(unique_ptr<T> const&) = delete;
	private:
		T* _ptr;
	};
}

实现浅拷贝方式:资源独占,只能一个对象管理一份资源,但同样的,它的应用场景受到了很大的限制

shared_ptr

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

使用
int main()
{
	shared_ptr<Date> sp1(new Date);
	shared_ptr<Date> sp2(sp1);

	cout << "ref count" << sp1.use_count() << endl;
	cout << "ref count" << sp2.use_count() << endl;

	return 0;
}

原理实现

shared_ptr的原理:通过引用计数的方式实现多个shared_ptr对象之间资源共享,具体表现如下:
1、shared_ptr在其内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享
2、在对象被销毁时(析构函数被调用),就说明当前对象不使用该资源了,引用计数减一
3、如果引用计数是0,说明当前对象是最后一个使用该资源的对象,必须释放资源
4、如果不是0,说明还有其他对象正在使用该资源,不能释放该资源

下面是对shared_ptr的简单模拟实现:

//定制删除器,让用户控制资源具体的释放操作
template<class T>
class DFdef
{
public:
	void operator()(T*& p)
	{
		if (p)
		{
			delete p;
			p = nullptr;
		}
	}
};
template<class T>
class Free
{
public:
	void operator()(T*& p)
	{
		if (p)
		{
			free(p);
			p = nullptr;
		}
	}
};
class FClose
{
public:
	void operator()(FILE*& p)
	{
		if (p)
		{
			fclose(p);
			p = nullptr;
		}
	}
};

#include <mutex>  //保证线程安全
//mutex用来加锁解锁可以保证shared_ptr的引用计数是安全的,但不能保证用户数据的安全
namespace a
{
	template<class T, class DF = DFdef<T>>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pCount(nullptr)
			, _pMutex(nullptr)
		{
			if (_ptr)
			{
				_pCount = new int(1);
				_pMutex = new mutex;
			}
		}
		~shared_ptr()
		{
			Release();
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}

		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pCount(sp._pCount)
			, _pMutex(sp._pMutex)
		{
			if (_ptr)
			{
				AddRef();
			}
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (this != &sp)
			{
				Release();

				_ptr = sp._ptr;
				_pCount = sp._pCount;
				_pMutex = sp._pMutex;

				AddRef();
			}
			return *this;
		}
	private:
		void AddRef()
		{
			if (_pCount)
			{
				_pMutex->lock();//加锁
				++*_pCount;
				_pMutex->unlock();//解锁
			}
		}
		int SubRef()
		{
			if (_pCount)
			{
				_pMutex->lock();//加锁
				--*_pCount;
				_pMutex->unlock();//解锁
			}
			return *_pCount;
		}

		void Release()
		{
			if (_ptr && 0 == SubRef())
			{
				DF()(_ptr);
				delete _pCount;
			}
		}
	private:
		T* _ptr;  //指向管理资源的指针
		int* _pCount;  //引用计数
		mutex* _pMutex;  //互斥锁
	};
}
struct Date
{
	Date()
	{
		_year = _month = _day = 0;
	}
	int _year;
	int _month;
	int _day;
};

void SharedPtrFunc(a::shared_ptr<Date>& sp, int n)
{
	for (int i = 0; i < n; i++)
	{
		a::shared_ptr<Date> copy(sp);

		copy->_year++;
		copy->_month++;
		copy->_day++;
	}
}

#include <thread>
//测试能否保证用户数据安全
int main()
{
	a::shared_ptr<Date> sp(new Date);
	thread t1(SharedPtrFunc, sp, 10000);
	thread t2(SharedPtrFunc, sp, 10000);

	t1.join();
	t2.join();
	return 0;
}

对上述线程安全测试的说明:
1、智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++两次,可能还是2,这样引用计数就错乱了,会导致资源未释放或者程序崩溃的问题,所以智能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的
2、智能指针管理的对象存放在堆上,两个线程同时去访问,会导致线程安全问题

【注意】:删除器定制

在上面的原理实现中,若是采用delete的删除方式只能删除部分内存资源(即通过new出来的空间),但是资源可不止内存资源,还有文件资源,等等,所以我们需要定制删除器,形如代码中开头部分的内容,通过模板参数将它传到shared_ptr的析构函数中去,为其定制资源的释放方式

std::shared_ptr的循环引用

先来看一段问题代码:

#include <memory>

struct ListNode
{
	ListNode(int data = 0)
		: _pre(nullptr)
		, _next(nullptr)
		, _data(data)
	{
		cout << "ListNode(int):" << this << endl;
	}
	~ListNode()
	{
		cout << "~ListNode()" << this << endl;
	}
	shared_ptr<ListNode> _pre;
	shared_ptr<ListNode> _next;
	int _data;
};

void TestListNode()
{
	std::shared_ptr<ListNode> sp1(new ListNode(10));
	std::shared_ptr<ListNode> sp2(new ListNode(20));

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	sp1->_next = sp2;
	sp2->_pre = sp1;

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
}

int main()
{
	TestListNode();

	return 0;
}

运行结果:在这里插入图片描述
由结果可以看出,该段代码未调用ListNode的析构函数,造成内存泄漏
分析如下:
在这里插入图片描述
为方便区分,将两个ListNode节点分别称为节点1和节点2
1、sp1和sp2两个智能指针对象指向两个ListNode节点,引用计数分别变为1
2、sp1中的_next这个智能指针对象指向节点2,对应的节点2的引用计数变为2
3、同理,sp2中的_pre这个智能指针对象指向节点1,对应的节点1的引用计数也变为2
4、执行完TestListNode()函数体中的语句后,开始对该函数作用域中定义的局部变量进行销毁
5、由于函数栈先定义的后销毁的原则,先销毁sp2这个智能指针对象,将它的指向断开连接后引用计数不为0,所以不对节点2进行销毁
6、同理,销毁sp1智能指针对象,将它的指向断开连接后引用计数也不为0,所以也不销毁节点1
此时,函数退出,但是存在着未释放的内存空间,内存泄漏
这种情况称之为循环引用

解决方式:
在引用计数场景下,把节点中的_pre和_next改成weak_ptr即可
原理:sp1->_next = sp2; sp2->_pre = sp1;时weak_ptr的_pre和_next不会增加sp1和sp2的引用计数
需要注意的是:weak_ptr的对象不能独立的管理资源,必须配合shared_ptr,它唯一的作用就是解决循环引用问题

对上述代码修改如下:

#include <memory>

struct ListNode
{
	ListNode(int data = 0)
		: _data(data)
	{
		cout << "ListNode(int):" << this << endl;
	}
	~ListNode()
	{
		cout << "~ListNode()" << this << endl;
	}
	weak_ptr<ListNode> _pre;
	weak_ptr<ListNode> _next;
	int _data;
};

void TestListNode()
{
	std::shared_ptr<ListNode> sp1(new ListNode(10));
	std::shared_ptr<ListNode> sp2(new ListNode(20));

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	sp1->_next = sp2;
	sp2->_pre = sp1;

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;
}

int main()
{
	TestListNode();

	return 0;
}

运行结果:
在这里插入图片描述
通过运行结果可以看出,调用析构函数成功,未造成资源泄漏,由此我们成功解决了循环引用的问题
具体分析结果如下:
在这里插入图片描述
图有点丑,勿怪
在weak_ptr中,它给引用计数_Ref增加了一份weak_ptr智能指针的计数,当有weak_ptr指向当前对象时,weak计数加一
1、构造开始阶段,图中表示为为黑体部分,用shared_ptr构造出两个智能指针对象sp1和sp2分别指向节点1和节点2,此时引用计数use和weak都为1(只有一个对象使用这份资源)
2、执行完赋值语句后,图中表示为不带叉号的所有部分,节点1中weak_ptr定义的_next智能指针对象指向sp2的资源,其引用计数_Ref指向节点2的引用计数,所以对应的weak计数加一,同理,节点2中_pre指向sp1的资源,对应引用计数加一
3、执行完输出语句后,函数准备退出,开始析构
4、由函数栈的特性后定义的先释放,首先释放sp2所指向的资源,图中表示为红色叉号:将sp2所指向的引用计数use减一后为零,表示当前对象的资源可以被释放,有:将节点2中_pre指向的资源先释放,但是此时还有其他对象正在使用该资源(节点1的空间),不能释放,所以只是将_pre的_ptr和节点1断开连接后,将weak计数减一,释放_pre智能指针对象,由于_next对象未管理任何资源,直接释放,再将整个sp2管理的资源释放,请注意,此时引用计数use计数为0,weak计数为1,引用计数暂不能释放
5、释放完sp2对象管理的资源中所能释放的部分后,同理,释放sp1,此时,节点1的引用计数use计数减一为0,该对象可以释放,有:_pre未管理资源,直接释放,_next所指向的资源已经被释放,但引用计数仍未释放,所以此时对节点2的引用计数中weak计数减一后为零,释放节点2的引用计数部分,再释放sp1指向的节点其它资源,weak计数减一为零,该引用计数空间可以释放
6、至此,循环引用的内存泄漏问题得以解决

至此,C++智能指针简单的介绍完了......

总结

1、RAII是一种管理资源,避免泄漏的策略,它利用对象的生命周期来控制程序资源
2、智能指针是基于RAII的思想实现的,它将普通指针封装成一个类,使其具有指针特性(*、->),从而有效的避免内存泄漏。
3、但是同样的,使智能指针具有指针的特性,那么就存在着多个指针指向同一份资源的情况,此时,若不能采用某种方法,就会导致同一份资源被释放多次,浅拷贝导致的程序崩溃问题,基于这个问题,C++标准库中定义了多种方法来避免该情况的发生,其中,本文讲述了四种auto_ptr 、 unique_ptr 、 shared_ptr 、 weak_ptr,第一种是C++98中提供的,后三种是C++11中提供的
4、auto_ptr采用的方式是管理权转移的思想,即在赋值和拷贝构造中让当前对象直接管理该份资源,而将原来的对象置为空,但是这样同时访问一份资源就不可能了,为此,后来的版本对auto_ptr进行修改,增加了owner的属性,将释放权利只交给使用资源的一个对象,但是,又可能会造成野指针的情况,所以,现在的C++标准中仍然使用悬空的auto_ptr版本
5、unique_ptr采用的方式是禁止复制和拷贝构造(C++98中为私有声明+只声明不实现,C++11中为声明后加“=delete”),这样,就不会引发浅拷贝问题,但是,同样的,这样会是的只能指针的引用场景受到限制
6、shared_ptr采用的方式是引用计数,当且仅当资源的引用计数为1时,使用该资源的对象才可以释放该资源,但是,可能会造成循环引用的问题,导致内存泄漏,解决办法是引入weak_ptr,把会产生循环引用的对象定义为weak_ptr,就能避免内存泄漏
7、shared_ptr还有就是会涉及线程安全的问题,解决办法就是引入互斥锁mutex
8、weak_ptr的唯一作用是配合shared_ptr解决循环引用问题,weak_ptr的对象不能独立管理资源
9、对于其他类型的资源,可以采用定制删除器的方式进行资源的释放
10、............

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值