C++语法——详解智能指针的概念、实现原理、缺陷

目录

一.智能指针的概念

(一).智能指针的历史

(二).智能指针的使用

插曲.auto_ptr

①unique_ptr

②shared_ptr

③weak_ptr

二.智能指针的实现

三.智能指针的缺陷

(一).循环引用

(二).定制删除器


一.智能指针的概念

智能指针指在通过RAII(Resource Acquisition Is Initialization,即资源获取就是初始化)技术,免去手动释放指针指向资源的步骤,希望能自动释放内存资源。底层而言,就是通过封装一个类,使用析构函数来释放资源。

当类对象出作用域被销毁时,会自动调用析构函数从而完成释放资源。

(一).智能指针的历史

最初的智能指针是C++98提出来的auto_ptr,不过因为使用时缺陷很大,Beman G.Dawes(C++委员会成员之一)所成立的boost社区(专门面向C++程序员,提供许多免费好用的自制库)贡献了scoped_ptr、shared_ptr、weak_ptr被C++11采纳,修改为官方的unique_ptr、shared_ptr、weak_ptr。

(二).智能指针的使用

头文件<memory>

ps:虽然下面的auto_ptr、unique_ptr之类均是库模板类,但为了便于理解均称为指针。 

插曲.auto_ptr

对于auto_ptr,小编认为它很鸡肋。虽然它也能够自动释放资源,但是一旦对类进行拷贝,那么会将资源转移给拷贝对象,而被拷贝者将失去资源(很像移动拷贝)。因此,一旦后续有调用被拷贝对象,那将引发巨大安全问题。

①unique_ptr

顾名思义,“独一无二”的指针,即只能有一个指针指向申请的资源空间,不存在两个指针同时指向一个空间,可以说这个就是C++11中对auto_ptr的改进,将它作为智能指针的一种形式。

与auto_ptr不同,unique_ptr禁止拷贝行为,当然赋值也是禁止的

②shared_ptr

“可以分享”的指针,说明允许拷贝与赋值行为,即允许多个指针指向同一片空间。

同时不管有多少指针指向一个空间,这个空间只会释放一次。 

std::shared_ptr<int> p1(new int);
std::shared_ptr<int> p2;
p2 = p1;//不会有任何问题,资源只会释放一次

③weak_ptr

“无权”的指针(小编认为这里翻译成无权更好,而不是虚弱),该指针不会释放指向的资源,用于解决shared_ptr的一些缺陷(循环引用),第三部分会详细讲解。

二.智能指针的实现

 这里我们重点实现shared_ptr。

首先定义一个模板类,其中一个指针成员负责指向申请的空间,析构函数中delete该指针成员即可完成资源自动释放

但是难点在于怎么拷贝,如果单纯拷贝的话那会引起资源重复释放的问题。

因此我们采用引用计数的方式解决。

专门动态开辟一个int空间用于记录我们申请的资源空间有多少个指针指向。

当调用构造、拷贝、赋值时计数++,调用析构时计数--,直到计数空间值为0时真正释放资源

值得注意的是,赋值需要考虑被赋值者原资源空间以及原来的计数空间。

代码如下:

template<class T>
class Shared_ptr
{
	typedef Shared_ptr<T> Self;
private:
    bool Destory()//销毁
	{
		if (pCount == nullptr || (*pCount) == 0) return false;

		(*pCount)--;//计数--

		if ((*pCount) == 0 && _ptr)
		{
			delete _ptr;
			delete pCount;
		}
		_ptr = nullptr;
		pCount = nullptr;
		return true;
	}
public:
	Shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, pCount(new int(1))
	{}
	Shared_ptr(const Self& sp)//拷贝构造
		:_ptr(sp._ptr)
		, pCount(sp.pCount)
	{
		(*pCount)++;
	}

	Self& operator=(const Self& sp)//赋值
	{
		if (_ptr == sp._ptr) return *this;
		Destory();
		_ptr = sp._ptr;
		pCount = sp.pCount;
		(*pCount)++;
		return *this;
	}
	bool release()//释放本指针空间(还有其他指向时不释放空间)
	{
		return Destory();
	}
	~Shared_ptr()//析构
	{
		std::cout << "~" << std::endl;
		Destory();
	}

	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T* get()
	{
		return _ptr;
	}
    bool expired()//检查是否过期(指向空间是否已被释放)
	{
        
		return pCount != nullptr && *pCount == 0;
	}
	size_t get_count() const
	{
		return *pCount;
	}
private:
	T* _ptr;//指向空间
	int* pCount;//指向计数空间
};

三.智能指针的缺陷

(一).循环引用

shared_ptr隐藏了一个极大的bug。

看看下面代码:

class A {
public:
	~A() {
		std::cout << "析构" << std::endl;
	}
	std::shared_ptr<A> next;
	std::shared_ptr<A> prev;
	int i = 0;
};

int main()
{
	
	std::shared_ptr<A> p1(new A);
	std::shared_ptr<A> p2(new A);
	p1->next = p2;
	p2->prev = p1;

 	return EXIT_SUCCESS;
}

 正常情况下,应该会打印两次“析构”,分别是调用p1和p2析构函数delete指向空间时。

但实际是一个都没打印:

这是因为出现了循环引用的情况。

p1指向空间的next指向p2空间,导致p2空间的计数变成2,同理p2指向空间的prev指向p1空间,p1空间计数变成2。

但是当析构时只有p1和p2出栈,也就是分别调用一次析构函数,两个空间的计数均-1,变成1。此时并不会真正释放空间,但“明面上”又找不到指向这两个空间的指针。因为这两个堆空间又分别被对方堆空间上的指针指向,算是一种“死锁”吧。

画图如下:

解决方式:

专门定义一个模板类智能指针,只能指向空间但不能改变计数数量即可

即weak_ptr(名字也就是这么来的)。

代码如下:

ps:小编采用的是将weak_ptr作为shared_ptr的友元类,也可以是普通模板类。

class Shared_ptr
{
	typedef Shared_ptr<T> Self;
	template<class T>
	friend class Weak_ptr;
    . . .
}

template<class T>
class Weak_ptr
{
	typedef Weak_ptr<T> Self;
	typedef Shared_ptr<T> Sptr;
public:
	Weak_ptr()
		:_ptr(nullptr)
	{}
	Weak_ptr(const Self& wp)
		:_ptr(wp._ptr)
		,pCount(wp.pCount)
	{}
	Weak_ptr(const Sptr& sp)
		:_ptr(sp._ptr)
		,pCount(sp.pCount)
	{}
	~Weak_ptr()
	{}
	Self& operator=(const Self& wp)
	{
		_ptr = wp._ptr;
		pCount = wp.pCount;
		return *this;
	}
	Self& operator=(const Sptr& sp)
	{
		_ptr = sp._ptr;
		pCount = sp.pCount;
		return *this;
	}
	bool expired()
	{
		return *pCount == 0;
	}
	T& operator*()
	{
		assert(!expired());
		return *_ptr;
	}
	T* operator->()
	{
		assert(!expired());
		return _ptr;
	}
private:
	T* _ptr;
	int* pCount;
};

(二).定制删除器

细心的可能发现了,析构函数只有delete一种方式,那如果我们需要的是delete[]呢?

比如这种情况:

std::shared_ptr<A> p1(new A[5]);

这时将报错:

但如果是内置类型比如int又不会报错了。

这是因为有析构函数的存在,如果自定义类型(weak_ptr)没有析构函数那也不会报错。

下面我们仔细梳理一下:

如果自定义类型没有析构函数,那么对于编译器而言只需要把它按内置类型处理即可。

但对于有析构函数的自定义类型,编译器会根据new空间的个数,在空间之前再开辟4个字节记录该类型开辟的数量。当调用delete[]时会根据这个记录调用对应次的析构函数。

但因为我们使用的是delete,根据记录本应调用n次,而delete只会调用一次,与记录有冲突从而报错。

解决方式:

根据需要删除的对象,确定需要delete或delete[],传一个仿函数给智能指针模板,删除时调用仿函数即可。

代码如下:

template<class T>//默认仿函数,默认使用delete
class DefaultDelete {
public:
	void operator()(T* _ptr) {
		delete _ptr;
	}
};

template<class T, class D = DefaultDelete<T>>//传一个默认的仿函数
class Shared_ptr
{
private:
    bool Destory()//销毁
	{
		. . .
		if ((*pCount) == 0 && _ptr)
		{
			D()( _ptr);
			delete pCount;
		}
		. . .
	}
    . . .
}

template<class T>//可以自制一个传delete[]
class Free {
public:
	void operator()(T* _ptr) {
		delete[] _ptr;
	}
};

值得注意的是,官方的shared_ptr是在构造函数中传仿函数对象,而unique_ptr是传仿函数类型给模板参数。

我不是一个伟大的程序员,我只是一个具有良好习惯的优秀程序员—— Kent Beck


如有错误,敬请斧正 

猜你喜欢

转载自blog.csdn.net/weixin_61857742/article/details/127975548