C++——智能指针和RAII


该文章代码均在gitee中开源

C++智能指针hppicon-default.png?t=N7T8https://gitee.com/Ehundred/cpp-knowledge-points/tree/master/%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88​​​​​​​


智能指针

传统指针的问题

在C++自定义类型中,我们为了避免内存泄漏,会采用析构函数的方法释放空间。但是对于一些情况,系统往往并没有那么聪明,比如C语言里,我们malloc一块空间;C++里,我们new一块空间,系统不会对这些空间进行特别检查, 最后便造成了内存泄漏

void func()
{
	int* a = new int(1);

	//...一通操作

	if (true)
	{
		return;
	}
	//如果程序在中途就终止了,那这段delete便不会执行,内存泄漏了
	delete a;
}

有的时候并不是我们不想释放或者忘了释放,而是经常会发生函数异常终止或者中途结束,导致某一块空间的释放被跳过了

并且,在一些较大的程序中,某一个类似的函数会调用成千上万次, 每一次去泄漏一点点内存,极少成多,渐渐内存便开始以肉眼无法看见的速度渐渐泄漏。

此时,C++便想出了一个C++独有的解决方案:智能指针

为什么是C++独有?因为只有C++才把这种史甩给程序员去自己解决

智能指针的原理

我们在文章刚开始便解释到,对于自定义类型,C++会通过析构函数的方式将其释放,但是new出来的空间并没有析构函数。那为什么我们就不能强行给他一个析构函数呢? 

而这个想法的实现方法其实也很简单:只需要给一个类,让这个类来装这一个指针便可以了

template<class T>
class smart_ptr
{
public:
	smart_ptr(T* ptr=nullptr)
		:_ptr(ptr)
	{}

	~smart_ptr()
	{
		delete _ptr;
	}
private:
	T* _ptr;
};

我们在new一块空间之后,把这个指针装在smart_ptr这块盒子里,当函数结束时,smart_ptr会自动调用析构函数销毁,从而让这个野指针实现自动销毁的行为,这便是智能指针

void func()
{
	int* a = new int(1);
	smart_ptr<int> spa(a);
	//无论函数从哪里终止,只要函数被销毁,spa就会被销毁,从而释放a
}

同时,为了方便,我们完全可以改造一下只能指针,将智能指针改造成智能指针来使用

//改造后的智能指针,与普通指针的使用方法便一致了
template<class T>
class smart_ptr
{
public:
	smart_ptr(T* ptr=nullptr)
		:_ptr(ptr)
	{}

	~smart_ptr()
	{
		delete _ptr;
	}

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

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

改造之后,不仅可以实现指针的所有功能,而且被指针指向的空间也可以自动释放,相当于指针plus

同时,我们在初始化时,不需要引入新变量了

void func()
{
	/*int* a = new int(1);
	smart_ptr<int> spa(a);*/
	//直接简化成
	smart_ptr<int> spa(new int(1));

}

智能指针的问题

智能指针虽然看着好用,但是还是有着很多大问题。其中最大的便是赋值问题,如果我们想用一个智能指针去赋值另一个智能指针,那我们会发现一个严重的问题: 

那咋整?

而为了解决这一问题,C++标准库给出了三种解决方案,这也便是C++智能指针的发展历史。


std中的智能指针

其实智能指针的发展史很早。早在C++98中,std库中便有了一个智能指针名为auto_ptr,但是一个字便可以概括:

不仅被开发者们诟病,而且很多公司还明确要求:不许使用库中的智能指针。而这也导致了一个结果:不同的库智能指针千奇百怪,程序和程序间的兼容依托稀烂。

而后C++11,对备受诟病的智能指针进行了改造,产生了两种应用场景的智能指针:unique_ptr和shared_ptr,至此,智能指针的发展便已经完美画上了句号,而我们如今最常用也最需要去学习的便是在C++11新加入的两种智能指针

auto_ptr

C++98在刚开始接触智能指针这一问题的时候,可能是项目经理开始催命了,便展现出了及其离谱的操作:权限转移。这个操作虽然理论上确实解决了两次delete的问题,但是就相当于饿到没办法才去赤石,没有任何实际使用的价值

什么是权限转移?就是在a赋值b的时候,将a装着的指针清空,而原本的指针到了b身上,就相当于把a变成了b,然后a这一变量销毁掉。

下面只展示赋值的情况代码

template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr=nullptr)
		:_ptr(ptr)
	{}

	auto_ptr(const auto_ptr& ptr)
	{
		if (ptr != *this)
			swap(*this, ptr);
	}

	auto_ptr& operator=(const auto_ptr& ptr)
	{
		if(ptr!=*this)
			swap(*this, ptr);
		
		return *this;
	}

	~auto_ptr()
	{
		delete _ptr;
	}
private:
	T* _ptr;
};

你说他赋值了吗?好像赋值了,但是又好像没有赋值

我们想要对智能指针进行赋值,为的就是产生两份智能指针,但是你这一通转移,最后还是只给了我一份智能指针,而且还到了最后连我自己都不知道转移到哪去了。解决问题了吗?好像解决了,但是实际上让问题变得更麻烦了,这也是auto_ptr一直被诟病的原因——为了修一个小bug,引入了一个更大的bug

unique_ptr

 C++11里,为了解决掉auto_ptr乱赋值这一毛病,干脆采用了一个简单粗暴的方法——既然赋值会有bug,那就都别赋值了

unique_ptr在最初的智能指针上加了一个新特性:私有化operator=和赋值构造函数,让unique_ptr无法被赋值

template<class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr=nullptr)
		:_ptr(ptr)
	{}

	~unique_ptr()
	{
		delete _ptr;
	}

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

	T* operator->()
	{
		return _ptr;
	}
private:
	unique_ptr& operator=(const unique_ptr& ptr) = default;
	unique_ptr(const unique_ptr& ptr) = default;

	T* _ptr;
};

这样,和他的名字一样,unique_ptr就是独一无二的智能指针,只能产生一次,无法多次使用。

虽然这种方法听起来也是拖史,但是我们不可否认,unique_ptr解决了赋值的问题,而且也没有产生新的bug

shared_ptr

而从shared_ptr开始,才算是直视多次delete这一问题。既然不断去赋值会导致delete很多次,那我就记录一下指向某块空间的智能指针的个数,当最后一个智能指针也被销毁,我再去delete,这样就不会产生delete多次的问题了。而这实际也是引用计数的思想。 

不过这种想法虽然看起来简单,真正实现起来却还是有着一些障碍:

  1. 引用计数怎么实现?
  2. 如果某一个智能指针已经指向了一块空间,之后再对其进行赋值,那原来被指向的空间怎么办?
  3. 自己赋值自己又是什么情况?

我们来一个一个看

引用计数怎么实现?

最直观直接的方法便是,在类中加入一个新变量count来记录指向这块空间的数量,如果有一个新的智能指针指向了这块空间,就将count++,然后将++后的count赋值给新的智能指针。虽然想法很好,但是也有着一个巨大的问题——count无法同步

比如count==3,表示有三个智能指针a,b,c指向了这块空间,我们再将c赋值给d,然后count++变成4, c和d中的count也变成了4,那a和b怎么办?a和b里的count还是3

此刻便可以想出一个很简单的解决方案——在类中存放一个count的地址,这样一个count改变,所有的count也便随之改变了。

如何赋值给已存放地址的智能指针

在之前,我们都只考虑了用智能指针进行初始化。但是其实赋值还有一种情况——改变智能指针的值。这种情况,如果我们直接修改,显然会导致原先的内存泄漏,所以我们在赋值的时候,还需要将原先的count--,不然会导致多出一次count 的问题。

如何自己赋值给自己

这是在所有类型的赋值中,我们都要考虑的情况。一般,如果自己赋值给自己,我们直接跳过就可以了,否则最好的情况是效率的损耗,而最坏的情况则会导致野指针。

举个例子,如果有一个智能指针sp,其中的count只有1,我们自己赋值给自己,上述情况是count--,最终count==0,sp指向的空间被销毁。然后再去赋值,指针指向了一块被销毁的空间,count++,就导致了指向野指针的问题。

所以,自己赋值给自己必须要进行判断并跳过,否则或大或小都会产生一些意料之外的问题。

而解决了上述的问题,shared_ptr也算是被暴力解决了

template<class T>
class share_ptr
{
public:
	share_ptr(T* ptr = nullptr)
		:_ptr(ptr)
	{
		_count = new int(1);
	}

	share_ptr(const share_ptr& ptr)
	{
		_ptr = ptr._ptr;
		_count = ptr._count;

		++(*_count);
	}

	share_ptr& operator=(const share_ptr& ptr)
	{
		if (_ptr != ptr._ptr)
		{
			delete_ptr();

			_ptr = ptr._ptr;
			_count=ptr._count;

			++(*_count);
		}

		return *this;
	}

	~share_ptr()
	{
		delete_ptr();
	}

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

	T* operator->()
	{
		return _ptr;
	}
private:
	void delete_ptr()
	{
		--(*_count);

		if (*_count == 0)
		{
			delete _ptr;
			delete _count;
		}
	}

	T* _ptr;
	int* _count;
};

循环引用

shared_ptr虽然强大,但是shared_ptr也会有着内存泄漏的问题

我们来看双向链表

struct ListNode
{
	ListNode()
		:_pre(nullptr),
		_next(nullptr)
	{}

	share_ptr<ListNode> _pre;
	share_ptr<ListNode> _next;
};

void func()
{
	share_ptr<ListNode> head(new ListNode);
	share_ptr<ListNode> tail(new ListNode);

	head->_next = tail;
	tail->_pre = head;
}

int main()
{
	func();
}

一个很经典的双向链表问题,但是最终却暗藏玄机。我们来看func函数内部

void func()
{
	share_ptr<ListNode> head(new ListNode);
	share_ptr<ListNode> tail(new ListNode);
  
	head->_next = tail;
	tail->_pre = head;
    //赋值之后,很正常的head和tail指向的空间count都为2
         
    //但是到了最后,调用析构函数,head的count--,tail的count--,两个count都为1
    //最后head和tail都没有被清理掉,内存泄漏了
}

而导致这个问题的本质原因是什么?是智能指针指向的对象,其内部还有一个无法被自动释放的指针。 

而为了避免这个问题,C++采用了一个新的指针——weak_ptr。

weak_ptr顾名思义,是弱指针,其特性和shared_ptr基本相同,只不过在赋值的时候,count并不会增加

 也就是说,在类内部的智能指针,我们定义成weak_ptr,这样就可以避免count异常的问题

unique_ptr和shared_ptr

光看解说量,我们都会发现,unique_ptr已经被shared_ptr完爆了。虽然如此,我们仍还是让两个不同的智能指针都进入了std标准库,因为shared_ptr虽然在功能上远远战胜了unique_ptr,但是产生的性能代价仍是非常大的。unique_ptr简单粗暴,空间开销少,性能极高,所以在不同的场合还是会在两种智能指针之间取舍。

而auto_ptr


RAII

 看看得了,经常看我文章的都知道,我最不喜欢甩概念。

简单说,RAII就是将空间的释放自动化,我们不需要特意去delete,也不需要检查内存是否泄漏,我们只需要把地址抛给一个对象,让这个对象帮我们干这些事情就可以了

其实在很多语言中,都有一个垃圾回收机制,定期去回收掉被泄露的内存,而C++将这个责任甩给了程序员。但是,这并不是C++没能力弄或者懒得弄,而是为了极致的性能,不得不去舍弃掉这个垃圾回收机制。往后无论C++如何发展,一些其他语言便捷的地方如果会导致性能的损耗,C++都不会去尝试利用他们,而是让我们程序员去想更好的解决方案,没办法,谁叫我们是站在语言歧视链顶端的程序员呢。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值