浅谈智能指针

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lvdepeng123/article/details/81773329

一 什么是智能指针?

智能指针是一个类,用于封装一个普通指针的类,并且在这个类的构造函数中将这个普通指针初始化,并且在析构函数中对这个普通指针进行释放。而这个智能指针之所以这样做,是为了解决我们在以普通指针malloc或new申请空间之后,由于这里申请的空间需要手动释放,否则会造成内存泄漏,但是虽说大家在使用malloc或new申请空间之后,大多数人会牢记这点,但是这个问题还是防不胜防的,而且除了忘记释放的问题以外,很有可能会对一块空间释放多次,这也是需要提防的,所以C++中就引入了智能指针。如果将new 返回的地址赋给这些对象,在智能指针过期时,这些内存将自动释放

当你在创建一个类对象并以一个指向malloc或new出来的空间的普通指针进行初始化的时候,这个类对象就在栈上被开辟出来了,而且大家都知道栈上的空间是会被自动释放的,也就是说最终这个对象会被销毁,这样的话必然会调用析构函数,所以理所当然的就会将之前申请的堆空间给释放掉了。

常用的智能指针以及它们大致是如何实现的?(当然以下都是模拟实现)

C++标准库中从boost库当中引入了auto_ptr,unique_ptr(scoped_ptr),shared_ptr以及weak_ptr,这也是我们常用的几个智能指针。而模拟实现这些智能指针主要面对的问题是如何让它们在进行拷贝构造或者赋值的时候,防止浅拷贝,让一块空间被多个对象中的指针所指向,从而导致最后被析构多次

二 Java中的垃圾回收机制

        先谈谈Java中的垃圾回收机制。Java中的垃圾回收机制的目的在于回收释放不再被引用的实例对象占用的内存空间。垃圾回收通过确定内存单元是否还被对象引用来确定是否收集该内存区。垃圾回收首先要判断该对象是否是时候可以收集。两种常用的方法有2种,分别是引用计数器和对象引用遍历。

       引用计数器的原理是设置一整形变量来记录当前对象是否被引用以及引用的次数。当一个对象被创建时,将该对象对应计数器设置初值为1。当有新引用指向对象时,计数器+1;当其中某些引用超过生命期时,计数器-1.当计数器变为0时就可以当做垃圾收集了。这种方法实现简单,效率相对较高;但出现交叉引用时难以处理。下面是原文举例说明“父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0”。

       较早版本的JVM使用引用计数器,现在大多数JVM采用对象引用遍历。对象引用遍历从一组对象出发,沿着整个对象关系图上的每条链接线,递归确定当前可达的对象。如果某对象不能从这些根对象中的任何一个到达,则将它作为垃圾收集。等待它的就是删除,释放存储空间了。

1.auto_ptr

auto_ptr是最早被引入标准库的,也是被重点表明无论何种情况下都不要使用auto_ptr的,其中的原因就是在auto_ptr虽然简单,但是这里面存在严重的问题。

虽然这里我们说明了无论何种情况都不要使用auto_ptr,但是我们这里还是模拟实现一下,也好明白为什么要禁用auto_ptr。

关于auto_ptr差不多经历了三个版本,曾经多次想让auto_ptr变的不那么坑人,毕竟被引入了标准库啊,虽然说结果都失败了。

首先是第一个版本:

class AutoPtr
{
public:
	AutoPtr(T* ptr=NULL)   //默认构造函数,将类成员中_ptr置为空
		:_ptr(ptr)
	{}
 
	AutoPtr(AutoPtr& ap)   //拷贝构造函数
	{
		_ptr=ap._ptr;
		ap._ptr=NULL;
	}
 
	AutoPtr& operator=(AutoPtr& ap)    //赋值运算符重载
	{
		if(this!=&ap)//如果寄宿对象和宿主对象不是同一对象时,同一个对象没意义
		{
			_ptr=ap._ptr;
			ap._ptr=NULL;
		}
 
		return *this;
	}
 
	T* operator->()    //重载->
	{
		return _ptr;
	}
 
	T& operator*()  //重载*
	{
		return *_ptr;
	}
 
	~AutoPtr()     //析构函数
	{
		if(_ptr)
		{
			delete _ptr;
			_ptr=NULL;
		}
	}
 
private:
	T* _ptr;
};
int main()
{
	AutoPtr<string> p1 (new string ("auto"));
	AutoPtr<string> p2;
	p2 = p1;
	/*p2接管了p1对象所有权之后,P1置为空,将不能再用p1操作这块空间*/
}

分析第一个版本,这里在解决拷贝构造函数以及赋值运算符重载的时候,通过进行资源转移,也就是在进行拷贝构造或赋值之后,将用于赋值或拷贝的对象中的指针置空。

这里主要存在两个大问题:首先,由于我们要进行拷贝构造或是赋值的时候需要对传入对象的指针进行置空修改,所以这里形参就不能为const,也就是说我们无法拷贝构造const对象,或用const对象进行赋值;其次,就是在进行了拷贝构造或赋值之后,再就不能对原对象进行操作了(其中的指针已经为空了),也就是说我们无法让多个智能指针指针对象管理一块空间

第二个版本:

template<class T>
class AutoPtr
{
public:
	AutoPtr(T* ptr=NULL)
		:_ptr(ptr)
		,owner(true)
	{}
 
	AutoPtr(const AutoPtr& ap)
	{
		_ptr=ap._ptr;
		owner=true;
		ap.owner=false;
	}
 
	AutoPtr& operator=(const AutoPtr& ap)
	{
		if(this!=&ap)
		{
			_ptr=ap._ptr;
			ap.owner=false;
		}
 
		return *this;
	}
 
	T* operator->()
	{
		return _ptr;
	}
 
	T& operator*()
	{
		return *_ptr;
	}
 
	~AutoPtr()
	{
		if(_ptr&&owner)
		{
			delete _ptr;
			_ptr=NULL;
		}
	}
 
private:
	T* _ptr;
	mutable bool owner; //用mutable修饰的成员变量不受const成员方法的限制。
};
int main()
{
	AutoPtr<string> p1 (new string ("auto"));
	AutoPtr<string> p2;
	p2 = p1;
	
}

第二个版本是为了解决第一个版本所面对的问题,其一,通过对这个对象再封装一个bool类型的变量,让其表示当前对象的管理权限,当具有管理权限的时候为true,并且在这个对象进行析构时,对对应的空间进行释放,反之则无法释放这块空间;其二以mutable来定义这个bool变量,使得能够在拷贝构造函数与赋值运算符重载的形参为const的引用的时候,能在函数内对这个bool变量进行修改(即转移管理权限)。

这样一来,表面上貌似是解决了之前所面临的两个问题,但是实际上呢?看下面这个测试用例:


void funtest()
{
	int* p=new int(0);
	AutoPtr<int> ap(p);
	if(true)
	{
		AutoPtr<int> ap1(ap);//ap1出了这个局部范围就操作空间进行释放
	}
 
	cout<<*ap<<endl;//执行了那块释放的空间
 
}

这里存在一个很严重的问题,在局部范围内进行拷贝构造或是赋值的话,出了这个作用域,空间就会被释放,那么在这个局部作用域之外,再对对象进行操作的话,很容易造成崩溃,就算不崩溃,那原空间已经被释放了,再对其进行访问也是毫无意义。

所以,这样看来这个版本还不如上面的第一个版本,最起码不会造成这么隐晦的崩溃。

第三个版本:

template<class T>//辅助类指针
class AutoPtrRef
{
	explicit AutoPtrRef(T* ptr)//防止进行隐式转换
		:_ptr(ptr)
	{}
 
	T* _ptr;
};
 
template<class T>
class AutoPtr
{
public:
	AutoPtr(T* ptr=NULL)
		:_ptr(ptr)
	{}
 
	AutoPtr(AutoPtr& ap)
	{
		_ptr=ap._ptr;
		ap._ptr=NULL;
	}
 
	AutoPtr(AutoPtrRef<T>& ap)
	{
		_ptr=ap._ptr;
		ap._ptr=NULL;
	}
 
	AutoPtr& operator=(AutoPtr& ap)
	{
		if(this!=&ap)
		{
			_ptr=ap._ptr;
			ap._ptr=NULL;
		}
 
		return *this;
	}
 
	AutoPtr& operator=( AutoPtrRef<T>& ap)
	{
		if(this!=&ap)
		{
			_ptr=ap._ptr;
			ap._ptr=NULL;
		}
 
		return *this;
	}
 
	operator AutoPtr<T>()const //如果传过来的是const对象,则返回当前对象
	{
		return *this;
	}
 
	operator AutoPtrRef<T>()const
	{
		AutoPtrRef<T> tmp(_ptr);
		_ptr=NULL;
		return tmp;
	}
 
	T* operator->()
	{
		return _ptr;
	}
 
	T& operator*()
	{
		return *_ptr;
	}
 
	~AutoPtr()
	{
		if(_ptr)
		{
			delete _ptr;
			_ptr=NULL;
		}
	}
 
private:
	T* _ptr;
};
int main()
{
	AutoPtr<string> p1 (new string ("auto"));
	AutoPtr<string> p2;
	p2 = p1;
	const AutoPtr<string> p1 (new string ("auto"));
	const AutoPtr<string> p2;
	p2 = p1;//突破const限制
}

第三个版本是在第一个版本的基础上进行改进,不可避免的是原对象的指针还是被置空了,无法解决让多个对象管理同一块空间的问题;但是这里它解决了对const对象进行拷贝构造或赋值,通过利用类型转换运算符。
综上,auto_ptr的三个版本各有各的缺陷,而且都是挺关键的缺陷,所以慎用auto_ptr,或者干脆就别用了。

2.scoped_ptr(unique_ptr)

上面讨论了auto_ptr的各种缺陷,归结到底都是由于拷贝构造函数以及赋值运算符重载造成的,这里scoped_ptr(unique_ptr)干脆就禁止拷贝构造和赋值了,也就是通过防拷贝来解决:

template<class T>
class ScopedPtr
{
public:
	ScopedPtr(T* ptr=NULL)
		:_ptr(ptr)
	{}
 
	T* operator->()
	{
		return _ptr;
	}
 
	T& operator*()
	{
		return *_ptr;
	}
 
 
	~ScopedPtr()
	{
		if(_ptr)
		{
			delete _ptr;
			_ptr=NULL;
		}
	}
 
private:
	ScopedPtr(ScopedPtr& sp);
 
	ScopedPtr& operator=(ScopedPtr& sp);//将拷贝构造与赋值运算符重载两个函数给成私有的,但都给出定义;禁止拷贝和赋值
 
 
private:
	T* _ptr;
}
int main()
{
	AutoPtr<string> p1 (new string ("auto"));
	AutoPtr<string> p2;
	p2 = p1;
	// p2(p1)编译器将要报错
}

这里要说明一点scoped_ptr(unique_ptr)是针对单个类型空间,对于多个类型空间的管理,boost库当中是用scoped_array来管理的(无非就是在释放空间是将delete变成delete[ ]),但是在我们标准库当中却并没有引入scoped_array,原因在于我们C++标准库当中管理一段连续空间用什么?std::vector容器,更何况scoped_array当中还没有std::vector所具有的功能强大。

3.shared_ptr与weak_ptr

scoped_ptr所说可以解决部分智能指针所要解决的问题,但是对于让多个对象管理同一块空间的问题,scoped_ptr还是无能为力。这里就引入了shared_ptr:

template<class T>
class Dele
{
public:
	void operator()(T* ptr)//重载()运算符
	{
		delete ptr;
		ptr=NULL;
	}
};
 
template<class T>
class Free
{
public:
	void operator()(T* ptr)
	{
		free(ptr);
		ptr=NULL;
	}
};
 
 
template<class T>
class File
{
public:
	void operator()(T* ptr)
	{
		fclose(ptr);
	}
}
template<class T,class Des=Dele<T>>
class SharedPtr
{
public:
	SharedPtr(T* ptr=NULL)
		:_ptr(ptr)
		,count(0)
	{
		if(_ptr)
		{
			count=new int(1);  //开始引用的时候计数置为1
		}
	}
 
	SharedPtr(const SharedPtr& sp)
	{
		_ptr=sp._ptr;
		count=sp.count;
		(*count)++;
	}
 
	SharedPtr& operator=(const SharedPtr& sp)
	{
		if(this!=&sp)
		{
			if(_ptr&&!(--*count))//保护指针为空
			{
				Destory();
			}
			_ptr=sp._ptr;
			count=sp.count;
			(*count)++;
		}
		return *this;
	}
 
	~SharedPtr()
	{
		if(_ptr&&!(--(*count)))
		{
			Destory();
		}
	}
	
private:
	void Destory()
	{
		Des()(_ptr);
		delete count;
	}
 
private:
	T* _ptr;
	int* count;
};
int main()
{
	AutoPtr<string> p1 (new string ("auto"));
	AutoPtr<string> p2;
	p2 = p1;
	
}

这里对于shared_ptr是利用了之前string类用过的引用计数,除了封装一个原生指针外还封装了一个int*的指针,当然标准库当中肯定没这么简单,上面只是简单的进行了模拟大致的方法,仅对于上述的模拟而言,shared_ptr增加了一个引用计数空间用于保存当前管理这块原生指针指向的空间的对象有多少,而根据这个引用计数来决定在析构某一对象的时候要不要释放空间;而除此之外这里还解决了一个问题,就是对于不同的指针而言,最后进行的处理是不同的,例如文件指针需要进行关闭文件,malloc出来的空间需要的是进行free而不是delete等,上述模拟根据这一问题对于不同的指针设计了对应的删除器来进行解决。

但是这里还有一个严重的问题,就是关于循环引用的问题。

对于什么是循环引用?我们用下面这个测试用例来解释:

#include <iostream>
using namespace std;
 
#include <memory>
#include "SharedPtr.h"
 
struct Node
{
	Node(int va)
		:value(va)
		
	{
		cout<<"Node()"<<endl;
	}
 
 
	~Node()
	{
		cout<<"~Node()"<<endl;
	}
	shared_ptr<Node> _pre;
	shared_ptr<Node> _next;
	int value;
};
 
void funtest()
{
	shared_ptr<Node> sp1(new Node(1));
	shared_ptr<Node> sp2(new Node(2));
 
	sp1->_next=sp2;
	sp2->_pre=sp1;
}
int main()
{
	funtest();
	return 0;
}

这里模拟了双向链表中的两个节点的情况,对于节点中的_pre和_next都用shared_ptr来进行管理,而节点本身也由shared_ptr进行管理,这样的话当funtest()运行结束后,我们会发现:



很明显,并没有调用~Node(),也就是说节点在这里并没有被释放,这里原因在于有两个shared_ptr的对象管理同一个节点,而其中的一个对象就是另一个节点当中的_pre或是_next,当你对sp2进行释放的时候,由于引用计数为2,所以这里并不能把对应节点给释放掉,而是要等待对方也就是sp1的_next释放时才能将sp2的节点释放掉,反过来对于sp1与sp2同理,也要等待sp2中的_pre释放,所以这里两者都未能释放。

因此,在这里标准库就引用了weak_ptr,将上面的_pre和_next的类型换成weak_ptr,由于weak_ptr并不会增加引用计数use的值,所以这里就能够打破shared_ptr所造成的循环引用问题。但是这里要注意一点,就是weak_ptr并不能单独用来管理空间。

警告:使用new 分配内存时,使用auto_ptr和shared_ptr,使用new或者new【】 分配时,使用unique_ptr;

std::unique_ptr<double[]>pda (new double(5));

猜你喜欢

转载自blog.csdn.net/lvdepeng123/article/details/81773329
今日推荐