RAII(Resource Acquisition Initialization)
资源分配即初始化。
定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源正确的初始化和释放。
为什么要使用智能指针? 请看如下代码
bool doSomething()
{
// 如果时间执行失败了就返回false
return false;
}
// 为了避免内存泄漏和文件描述符泄漏,我们需要写出以下这样冗余的代码
// 我们需要一种方法让他自动的释放掉
void Test1()
{
int* p1 = new int[2];
FILE* pf = fopen("test","r");
// 1.如果打开文件失败,就需要释放p1的空间
if(pf == NULL)
{
delete[] p1;
}
// 2.如果执行事件失败,就需要释放p1的空间,并且关闭pf文件
if(!doSomething())
{
delete[] p1;
fclose(pf);
return;
}
// 3.如果抛出了异常,我们捕获异常以后,也需要释放p1的空间,并且关闭文件描述符
try
{
throw 1;
}
catch(int err)
{
delete[] p1;
fclose(pf);
return;
}
// 4.逻辑正常结束,也需要释放空间和关闭文件
delete[] p1;
fclose(pf);
return;
}
观察以上代码,我们为了避免内存泄漏,需要自己在很多的地方对申请的空间或者打开的文件描述符进行释放或关闭,这样的做法有两个缺点
-
代码冗余,写起来很不美观
-
容易写错,一旦某处忘记写了,就很可能导致内存泄漏
为了避免上面的两个缺点,于是我们就有了智能指针,智能指针主要就是解决动态申请内存和自动释放资源的问题
-
管理指针,正确的申请和释放内存
-
像指针一样来使用
auto_ptr
首先强调一下,auto_ptr虽然存在,但是并不推荐使用,因为我们有更好的工具,但是历史还是得要了解的嘛~
auto_ptr初级版本
(代码中的智能指针的名字叫做 AutoPtr是自己取的,为了和库中区别开,我们自己造轮子~)
template<class T>
class AutoPtr
{
public:
// 管理指针的操作
AutoPtr(T* ptr):_ptr(ptr = nullptr)
{}
~AutoPtr()
{
if(_ptr)
delete _ptr;
}
// 像指针一样使用的操作
// a) 解引用操作
T& operator*()
{
return *_ptr;
}
// b) 通过成员访问符去访问成员
T* operator->()
{
return _ptr;
}
// 成员函数
// c)拷贝构造函数的解决方法
// 让 ap 交出对资源的管理权限给this
AutoPtr(AutoPtr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr; // 这个解决浅拷贝的方法真是.....一言难尽....防不胜防啊....
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if(this != &ap)
{
// 如果当前_ptr管理了空间,那么其他对象一定不会管理这块空间
// 因为拷贝构造会将原对象的资源管理权会交出
// 也就述说 AutoPtr 中一个资源只能被一个对象所管理
if(_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
private:
T* _ptr;
};
-
构造函数和析构函数完成对象的初始化和内存的释放
-
为了更像指针一样,我们需要重载 * 和 -> 运算符
-
拷贝构造和赋值运算符的重载时这里的重点,这里使用的是资源管理权转移的方法,背拷贝的对象和被赋值的最想会直接将资源所有权交给拷贝和赋值的对象,自身就直接指向空。这个机制保证了每块资源只被一个对象管理着,并不会引起一块内存被多次释放的场景。
auto_ptr进阶版
把这个版本叫做进阶版,只是因为这个版本出的比较晚!!!只是因为这个版本出的比较晚!!!只是因为这个版本出的比较晚!!!
注意: 这个版本有很大的Bug,后面解释!! 建议不要使用
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = nullptr)
:_ptr(ptr)
,_owner(true)
{
cout<<"AutoPtr()"<<endl;
if(_ptr == nullptr)
_owner = false;
}
~AutoPtr()
{
if(_owner && _ptr)
{
cout<<"~AutoPtr()"<<endl;
delete _ptr;
_ptr = nullptr;
_owner = false;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
AutoPtr(const AutoPtr<T>& ap)
:_ptr(ap._ptr)
,_owner(ap._owner)
{
ap._owner = false;
}
AutoPtr<T>& operator=(const AutoPtr<T>& ap)
{
if(this != &ap)
{
if(_owner && _ptr)
delete _ptr;
_owner = ap._owner;
_ptr = ap._ptr;
ap._owner = false;
}
return *this;
}
public:
T* _ptr;
mutable bool _owner; // 为解决交出资源管理权的问题,_owner表示当前是不是这个对象 管理着资源
};
这个版本比第一个的"改进"点在于,多了一个 owner 的成员变量,来表示当前的对象是不是这块资源的管理着,在调用析构函数的时候,只有管理着可以释放这块资源,而其他的对象不可以
但是所有的指向这块空间的对象都对这块空间有操作的权利,可以读写这块资源。
注意:Bug在这里
观察一下以下的代码,有什么问题吗:
void FunTest()
{
AutoPtr<int> ap1(new int);
if(true)
{
AutoPtr<int> ap2(ap1);
}
*ap1 = 10;
}
look,以下的场景就是了 ->
ap2在if语句中被拷贝构造出来,那么ap1实际上已经将管理权交给了ap2
当ap2在大括号中被释放掉,外面的ap1根本感知不到
而我们也不能依靠_onwer来判断当前ap1是否有资源
如果在对他冒然解引用,就可能导致程序崩溃.
总结一下auto_ptr
第一种:
原理: 交出资源的管理权,在拷贝构造或者是赋值运算符中
AutoPtr<int> a(b);
a.拷贝构造(b); 用b去拷贝构造a,那就将b的资源管理权交出,b就是NULL;
a = b;
a.operaort(b); 将b赋值给a,那也将b的资源管理权交出,b就是NULL;
局限: 限制原指针的访问权
第二种:
原理: 在定义一个成员变量,表示当前这个对象是不是管理着这块资源,只有管理着才有资格释放这块资源
拷贝构造的时候: 将a的_owner置为b的_owner值,将b的_owner置为假.,赋值同理
缺陷: 上面的场景
所以在安全的角度上来说,第一种应该是比较好的吧,但是,最后再说一下,不要使用auto_ptr,因为我们有更好的智能指针,请看后续博客