智能指针——C++
在C和C++的代码编辑的过程中,我们经常会自己动态的开辟空间,但是随着代码量的增加难免在有些时候,会忘记释放动态
开辟的空间,此时就会造成内存的泄漏,那么我们怎么样才能避免这样的事情呢?就是用我们的智能指针。
智能指针:定义一个类,来封装资源的分配和释放,在构造函数完成资源的分配和初始化,在析构函数中完成资源的清理。
首先我们先来介绍第一种智能指针:AutoPtr
在AutoPtr中,我们要让对象代替我们管理语段空间,那么我们就可以利用析构函数进行空间的释放,但是又因为我们并不知道
我们
即将掌控的是什么类型的空间,所以我们在代码设计的时候就把类给成模版。
下面是AutoPtr的代码,我们在代码中讲解:
template<class T>
class AutoPtr
{
private:
T* _ptr;//用类的成员对象来管理一段空间
public:
//(构造函数)用传进来的空间给对象赋值
AutoPtr(const T* ptr = NULL):_ptr(ptr)
{}
//(拷贝构造函数)用赋值空间来覆盖被赋值空间
AutoPtr(const AutoPtr<T>& ap):_ptr(ap._ptr)
{
_ptr = NULL;//将被赋值空间置空
}
//赋值运算符重载
AutoPtr& operator=(const AutoPtr<T>& ap)
{
if(this != &ap)//检测是不是自己给自己赋值
{
if(_ptr)//如果当前对象还掌管着一段空间,那么先把这块空间销毁
delete _ptr;
_ptr = ap._ptr;//把ap的空间赋值给当前对象
ap._ptr = NULL;//再把ap的空间置空
}
return *this;
}
//*运算符重载
T& operator*()
{
return *_ptr;
}
//->运算符重载
T* operator->()
{
return _ptr;
}
//(析构函数)
~AutoPtr()
{
if(_ptr)//如果当前对象掌管了一块空间,再去销毁
{
delete _ptr;
}
}
};
AutoPtr的原理就是进行资源的转移,利用掌管空间对象的析构函数来进行资源的销毁,但是我们不难发现,如果我们用
拷贝
构造来创建对象的时候,先前来管理空间的对象就不能在对空间进行任何的操作了,而我们在实际的应用之中,在赋值
之后,原来的对象还是需要对空间进行访问的,此时我们的AutoPtr类就没有什么太大的作用了。
此时,我们就需要对AutoPtr类进行一定改造。
我们改造的方法就是在类中增加对于空间管理的标识,具体如下:
template<class T>
class AutoPtr
{
private:
T* _ptr;
mutable bool _owner;//bool类型的管理标识,如果是true就是可以对空间进行管理,如果是false,就表示不能对空间进行管理
public:
//构造函数
AutoPtr(T* ptr = NULL):_ptr(ptr),_owner(false)//默认把管理权限给成false,然后在函数体中进行判断,防止ptr是空
{
if(_ptr)//如果此时_ptr管理了一段空间,就把管理权限给成true
{
_owner = true;
}
}
//拷贝构造函数
//在初始化列表中,首先将ap的管理权交给当前对象
AutoPtr(const AutoPtr<T>& ap):_ptr(ap._ptr),_owner(ap._owner)
{
ap._owner = false;//在ap将管理权给当前对象之后,那么ap就是失去了对空间的管理权,就将管理权改成false
}
//赋值运算符重载
AutoPtr<T>& operator=(const AutoPtr<T>& ap)
{
if (this != &ap)//先判断是不是自己给自己赋值
{
if(_ptr && _owner)//如果当前对象有掌管空间,且对空间有管理的权利,在去把空间销毁掉
{
delete _ptr;
_ptr = NULL;
}
_ptr = ap._ptr;//把ap的空间交给当前对象来管理
_owner = ap._owner;//把ap的管理权交给当前对象来管理
ap._owner = false;//因为ap把管理权交给当前对象来管理,那么ap就失去了管理这段空间的全力,所 以把ap._owner改成false
}
return *this;
}
//析构函数
~AutoPtr()
{
if(_ptr && _owner)//如果当前对象掌管空间且对这段空间且对空间拥有管理权,在去释放
{
delete _ptr;
_ptr = NULL;
_owner = false;
}
}
};
在我们增加了空间的管理标致之后,那么在赋值拷贝之后,之前的对象还是可以对空间进行访问,但是此时就会有一个疑问,
为什么我们不用深拷贝的方式来解决这种问题呢?因为此时,我们的空间是类外给出,也就是用户给的,在类内并不能自主的
给出空间,如果我们在类内自主的给出空间,就违背了原则,所以我们才用,管理标致的方法来解决。
以上就是我们的AutoPtr,但是除了给出管理标记之外,我们还有还可以从根本来解决,就是一个对象只管理一段空间,
此时就是我们另外的一种智能指针:
ScopedPtr。
ScopedPtr的目的就是防止拷贝,一个对象只管理一段空间,我们给出ScopedPtr的代码,在代码中讲解:
template<class T>
class ScopedPtr
{
private:
T* _ptr;
public:
//构造函数
ScopedPtr(const T* ptr = NULL):_ptr(ptr)
{}
//析构函数
~ScopedPtr()
{
if(_ptr)
{
delete _ptr;
}
}
//运算符重载
T& operator*()
{
return *_ptr;
}
//运算符重载
T* operator->()
{
return _ptr;
}
private:
//拷贝构造
//我们在类中把拷贝构造函数和赋值运算符声明成私有的,但是我们不给定义
//根据类的性质,在类外是无法访问类内的成员的,此时就不能通过旧的对象给新的对象赋值了,也就实现了一个对象只掌管了一段对象
ScopedPtr(const ScopedPtr<T>& sp);
ScopedPtr& operator=(const ScopedPtr<T>& sp);
};
对于把拷贝构造函数和赋值运算符声明为私有的是因为,如果只是声明不给定义,但函数仍然是公有性质的,那么如果用户在
类外给出定义,那么就会把类的封装性给破坏掉,此时我们在类中只给声明,不给定义,并且把函数的性质给成私有的,那么就实现了一个类只掌管了一段空间,当用旧的对象来构造新的对象的时候,就会出错。此时我们的ScopedPtr就实现了。
但是之前我们写的智能指针,智能管理一个单个的对象,如果我们管理多个空间呢?此时在我们只需在原版的ScopedPtr改动
一点就可以,现在我们给出代码,和之前的SopedePtr区别就在于析构函数和运算符的重载函数的稍微不同:
template<class T>
class ScopedArray
{
private:
T* _ptr;
public:
ScopedArray(T* ptr):_ptr(ptr){}
~ScopedArray()
{
if(_ptr)
delete _ptr;
}
T& operator[](size_t index)
{
return _ptr[index];
}
const T& operator[](size_t index)
{
return _ptr[index];
}
private:
ScopedArray(const ScopedPtr<T>& sa);
ScopedArray& operator=(const ScopedArray<T>& sa);
};
现在,表面上看起来,ScopedPtr已经完善了,但是里面还是有一些的缺陷,因为类外在给出空间的时候,我们不能 确定是用new 给出的,还是用malloc给出,过着打开的是一个文件的指针,因为这三种方法对应的是不同的删除的方法,此时
如果我们只用一种delete来删除,就会出现错误。那么我们此时的改进的方法就是定制一个删除器,以此来给出对应的删除方法。
我们定制删除器有两种方法,一种就是用函数指针,另外一种就是模版参数
现在我们讲一下使用函数指针的方法,首先我们先给出删除器:
//用来删除new开辟的空间
template<class T>
void Delete(T* p)
{
if(p)
{
Delete( p;)
}
}
//用来删除malloc开辟的空间
template<class T>
void Free(T* p)
{
if(p)
{
free(p);
}
}
//用来关闭文件指针
void Fclose(FILE* p)
{
if(p)
{
fclose(p);
}
}
Tip:因为文件指针是固定的,所以就没有给成模版
此时我们的ScopedPtr类也应该改:
typedef void(*PFD)(void*);//我们把没有返回值,参数接收void*类型的函数重定义为PFD
template<class T>
class ScopedPtr
{
private:
T* _ptr;
PFD _pFDestory;//在对象中加入删除器类型的函数指针
public:
//构造函数
//默认把删除器类型给成销毁new类型空间
ScopedPtr( T* ptr = NULL,PFD defDestory= Delete<T>):_ptr(ptr),_pFDestory(defDestory)
{}
//析构函数
~ScopedPtr()
{
if(_ptr)
{
_pFDestory(_ptr);
}
}
//运算符重载
T& operator*()
{
return *_ptr;
}
//运算符重载
T* operator->()
{
return _ptr;
}
private:
ScopedPtr(const ScopedPtr<T>& sp);
ScopedPtr& operator=(const ScopedPtr<T>& sp);
};
那么此时我们在析构函数删除开辟空间的时候,就会调用对应的删除器,就不会出现错误了。
另外的一种办法就是:用模版的参数;
我们把之前给出的删除器改成类模版:
//用来删除new开辟的空间
template<class T>
class DefDes
{
public:
void operator()(T* p)//重载的符号是()
{
if(p)
delete p;
}
};
//用来删除malloc开辟的空间
template<class T>
class Free{
public:
void operator()(T* p)
{
if(p)
free(p);
}
};
//用来关闭文件指针
class Fclose
{
public:
void operator()(FILE* p)
{
if(p)
{
fclose(p);
}
}
};
在模版删除器中,我们使用了仿函数,在模版类中把()进行了重载,那么我们在使用的时候,就可以用无名对象来调用。
下面是使用模版参数来删除对用空间的ScopedPtr类:
template<class T, class Dp = DefDes<T> >
class ScopedPtr
{
private:
T* _ptr;
public:
//构造函数
//默认把删除器类型给成销毁new类型空间
ScopedPtr( T* ptr = NULL):_ptr(ptr)
{}
//析构函数
~ScopedPtr()
{
if(_ptr)
{
Dp()(_ptr);//用Dp创建一个无名对象,调用重载之后的()
}
}
//运算符重载
T& operator*()
{
return *_ptr;
}
//运算符重载
T* operator->()
{
return _ptr;
}
private:
ScopedPtr(const ScopedPtr<T>& sp);
ScopedPtr& operator=(const ScopedPtr<T>& sp);
};
此时我们使用模版参数来用删除器的ScopedPtr类就完成了。
Tip:限于编者水平,文章难免有缺漏之处,欢迎指正!
如需转载请注明出处~