【C++】一文详解智能指针以及模拟实现

前言

auto_ptr

unique_ptr

shared_ptr

定值删除器

循环引用

weak_ptr

总结

前言

由于C++引入的抛异常会影响程序执行流,导致new申请的资源因抛异常而无法释放,造成内存泄漏问题,Java采用垃圾回收机制解决此问题,而C++引入智能指针,将指针封装为一个类,利用类在生命周期结束后会自动调用析构函数的特性,通过析构函数释放申请的资源。


auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针,auto_ptr文档

以下代码演示了auto_ptr的使用及问题。

struct Date
{
	friend std::ostream& operator<<(std::ostream& out, Date ptr);
	int _year = 1970;
};

std::ostream& operator<<(std::ostream& out, Date ptr)
{
	out << ptr._year;
	return out;
}

int main()
{
	//test auto_ptr
	std::auto_ptr<int> ptr1(new int(10));
	std::auto_ptr<Date> ptr2(new Date());
	cout << *ptr1 << endl;
	cout << *ptr2 << endl;
	cout << ptr2->_year << endl;

    return 0;
}

如果对auto_ptr类型的指针进行一次拷贝呢?

struct Date
{
	friend std::ostream& operator<<(std::ostream& out, Date ptr);
	int _year = 1970;
};

std::ostream& operator<<(std::ostream& out, Date ptr)
{
	out << ptr._year;
	return out;
}

int main()
{
	//test auto_ptr
	std::auto_ptr<int> ptr1(new int(10));
	std::auto_ptr<int> ptrCopy(ptr1);
	std::auto_ptr<Date> ptr2(new Date());
	cout << *ptr1 << endl;
	cout << *ptrCopy << endl;
	cout << *ptr2 << endl;
	cout << ptr2->_year << endl;

    return 0;
}

通过监视窗口可以看到,在将ptr1拷贝给ptrCopy后,ptr1的值为empty。

实际上,auto_ptr的实现原理类似管理权转移的思想,在拷贝过后,会使被拷贝指针为空,C++标准委员会建议不要使用auto_ptr,下面简化模拟实现了一份auto_ptr来了解它的原理。

namespace zyh
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr()
			:_ptr(nullptr)
		{}

		auto_ptr(T* const ptr)
			:_ptr(ptr)
		{}

		// 管理权转移
		auto_ptr(auto_ptr<T>& ap)
		{
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

		~auto_ptr()
		{
			if (_ptr)
			{
				std::cout << "delete:" << _ptr << std::endl;
				delete _ptr;
			}
		}

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

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

	private:
		T* _ptr = nullptr;
	};
}
	// 测试代码
    // test auto_ptr
	zyh::auto_ptr<int> ptr1(new int(10));
	//zyh::auto_ptr<int> ptrCopy(ptr1);
	zyh::auto_ptr<Date> ptr2(new Date());
	cout << *ptr1 << endl;
	//cout << *ptrCopy << endl;
	cout << *ptr2 << endl;
	cout << ptr2->_year << endl;

可以看到,模拟实现的auto_ptr在将ptr1拷贝给ptrCopy后,ptr1为空。


unique_ptr

C++11提供更靠谱的unique_ptr,unique_ptr文档

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

namespace zyh
{
    template<class T>
	class unique_ptr
	{
	public:
		unique_ptr()
		{}

		unique_ptr(T* const ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			if (_ptr)
			{
				std::cout << "delete:" << _ptr << std::endl;
				delete _ptr;
			}
		}

		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T> operator=(const unique_ptr<T>& up) = delete;

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

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

	private:
		T* _ptr = nullptr;
	};
}
	// 测试代码
    // test unique_ptr
	zyh::unique_ptr<int> ptr1(new int(10));
	// 禁止拷贝行为
	zyh::unique_ptr<int> ptr2(ptr1);
	zyh::unique_ptr<int> ptr2(new int(20));
	zyh::unique_ptr<Date> ptr3(new Date());
	cout << *ptr3 << endl;
	cout << ptr3->_year << endl;

使用unique_ptr无法进行拷贝构造。


shared_ptr

C++11提供更靠谱的并且支持拷贝的shared_ptr,shared_ptr文档

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如:
办公室的同事在下班之前都会通知剩下的人,让最后走的人记得把门锁下。

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

下面简化模拟实现了一份shared_ptr来了解它的原理。

namespace zyh
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr()
		{}

		shared_ptr(T* const ptr)
			: _ptr(ptr)
			, _pcount(new int(1))
		{}

		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			(*_pcount)++;
		}

		void Release()
		{
			if (_ptr != nullptr && --(*_pcount) == 0)
			{
				std::cout << "delete:" << _ptr << std::endl;
				delete _ptr;
				delete _pcount;
			}
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				(*_pcount)++;
			}
			return *this;
		}

		~shared_ptr()
		{
			Release();
		}

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

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

		int use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr = nullptr;
		int* _pcount = nullptr;
	};
}
    // 测试代码
	// test shared_ptr
	zyh::shared_ptr<int> ptr1(new int(10));
	zyh::shared_ptr<int> ptr2(new int(20));
	zyh::shared_ptr<int> ptr3 = ptr1;
	ptr2 = ptr3;
	zyh::shared_ptr<int> ptr4(new int(40));
	ptr1 = ptr4;
	cout << "ptr1: " << *ptr1 << endl 
		<< "ptr2: " << *ptr2 << endl 
		<< "ptr3: " << *ptr3 << endl 
		<< "ptr4: " << *ptr4 << endl;

定值删除器

如果想要用智能指针管理不是用new产生的对象呢?其实shared_ptr设计了一个删除器来解决这个问题,通过传入仿函数或Lambda表达式提供的方法来释放资源。

在我们模拟实现的shared_ptr里增加定值删除器。

namespace zyh
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr()
		{}

		shared_ptr(T* const ptr)
			: _ptr(ptr)
			, _pcount(new int(1))
		{}

		template<class D>
		shared_ptr(T* const ptr, D del)
			: _ptr(ptr)
			, _pcount(new int(1))
			, _del(del)
		{}

		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			(*_pcount)++;
		}

		void Release()
		{
			if (_ptr != nullptr && --(*_pcount) == 0)
			{
				//std::cout << "delete:" << _ptr << std::endl;
				//delete _ptr;
				_del(_ptr);
				delete _pcount;
			}
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				(*_pcount)++;
			}
			return *this;
		}

		~shared_ptr()
		{
			Release();
		}

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

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

		int use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr = nullptr;
		int* _pcount = nullptr;
		std::function<void(T*)> _del = [](T* ptr) {
			std::cout << "delete:" << ptr << std::endl; 
			delete ptr; 
		};
	};
}
template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		std::cout << "delete[]:" << ptr << std::endl;
		delete[] ptr;
	}
};	

int main()
{
    // 测试代码
    // 定值删除器
	zyh::shared_ptr<ListNode> ptr1(new ListNode[10], DeleteArray<ListNode>());
	zyh::shared_ptr<ListNode> ptr2(new ListNode[10], [](ListNode* ptr) {
		std::cout << "delete[]:" << ptr << std::endl; 
		delete[] ptr; });
	zyh::shared_ptr<FILE> ptr3(fopen("./Test.cpp", "r"), [](FILE* fp) {
		std::cout << "fclose: " << fp << std::endl;
		fclose(fp); });

    return 0;
}

循环引用

如果用shared_ptr管理一个双向链表呢?

struct ListNode
{
	ListNode(int value = 0)
		:_value(value)
	{}

	int _value;
	zyh::shared_ptr<ListNode> _next;
	zyh::shared_ptr<ListNode> _prev;
};

int main()
{
    // 测试代码
	// shared_ptr的循环引用问题
	zyh::shared_ptr<ListNode> ptr1(new ListNode(10));
	zyh::shared_ptr<ListNode> ptr2(new ListNode(20));

	ptr1->_next = ptr2;
	ptr2->_prev = ptr1;

	cout << ptr1.use_count() << " " << ptr2.use_count() << endl;

    return 0;
}

我们模拟实现的shared_ptr的析构函数是会打印delete的信息的,而实际上没打印,说明根本没有释放申请的资源,产生了内存泄漏

为什么会产生这种现象?

观察上图

  • 要释放ptr1对象的资源,得先delete ptr2._prev;
  • 要delete ptr2._prev,得先释放ptr2对象;
  • 要释放ptr2对象的资源,得先delete ptr1._next;
  • 要delete ptr1._next,得先释放ptr1对象的资源;
  • 要释放ptr1对象的资源,得先delete ptr2._prev。

于是就构成了shared_ptr的循环引用

这里采用zyh::shared_ptr是为了方便观察,换成std::shared_ptr也会造成循环引用。

C++11采用weak_ptr解决这种问题。


weak_ptr

weak_ptr文档,weak_ptr的原理是让weak_ptr类型的指针指向一个对象时,引用计数不会加1。

下面简化模拟实现了一份shared_ptr来了解它的原理。

namespace zyh
{
    template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

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

		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;
	};
}
struct ListNode
{
	ListNode(int value = 0)
		:_value(value)
	{}

	int _value;
	//zyh::shared_ptr<ListNode> _next;
	//zyh::shared_ptr<ListNode> _prev;
	zyh::weak_ptr<ListNode> _next;
	zyh::weak_ptr<ListNode> _prev;
};

int main()
{
    // 测试代码
    // shared_ptr的循环引用问题
	zyh::shared_ptr<ListNode> ptr1(new ListNode(10));
	zyh::shared_ptr<ListNode> ptr2(new ListNode(20));
	cout << ptr1.use_count() << " " << ptr2.use_count() << endl;

	ptr1->_next = ptr2;
	ptr2->_prev = ptr1;
	cout << ptr1.use_count() << " " << ptr2.use_count() << endl;

    return 0;
}

可以看到,在使用weak_ptr后,ptr1和ptr2指向的资源可以正常释放了。


总结

  1. 任何时候都不要使用auto_ptr;
  2. 不需要拷贝指针的情况,可以使用unique_ptr;
  3. 使用shared_ptr时,如果出现了循环引用,将某些指针设置为weak_ptr,通常是shared_ptr指向的对象内部的指针。
  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

毕瞿三谲丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值