智能指针的设计思想
为什么会出现智能指针?
首先我们先看一个可能会引发异常安全的例子:
#include <iostream>
using namespace std;
int div(int num1,int num2)
{
if(num2 == 0)
{
throw string("除0错误");
}
return num1/num2;
}
void catch_div(int num1,int num2)
{
try
{
div(num1,num2);
}
catch(const string& s)
{
cout<<s<<endl;
}
}
int main()
{
int* p = new int();
catch_div(10,0); // 在调用函数catch_div中有捕获异常,则程序直接跳到捕获异常的地方
delete p; // 不能正确完成delete指针,可能造成内存泄漏,引发异常安全的问题
return 0;
}
在main()函数运行时可以看出,若在调用catch_div() 函数时存在捕获异常,则程序会跳到捕获的地方去,从而不会调用到后面的delete p, 可能就会造成内存泄漏的问题。
那如何解决呢?两种思路:(1)异常的重新抛出 (2)采用智能指针
采用智能指针的方式我们可以理解为,就像普通指针一样,但是它可以在调用的时候自动进行资源申请,调用结束后自动释放。
智能指针的设计思想:
(1)RAII思想:资源获得即初始化;
不同于普通类型指针,定义一个类对象指针,在构造的时候申请资源,在析构的时候释放资源,并且该类一定是一个模板类以适应不同类型的指针。
(2)像指针一样使用;
重载operator* operator-> 使这个类可以向指针一样来使用。
auto_ptr ( 管理权转移 )
(1)儿童时期的auto_ptr
在auto_ptr还是小孩的时候,他觉得自己只需要实现作为一个智能指针的普通功能就好(乖乖听妈妈的话),
在构造函数的时候申请资源,析构函数的时候释放资源,并且重载operator* operator-> 使他能够向指针一样使用。
但是随着年龄的增长他发现,当进行拷贝构造和赋值操作的时候进行的是浅拷贝,也就是多个智能指针管理的是同一块空间,在调用结束释放空间的时候也就会对这块空间进行多次释放,这肯定是不可以的!
(2)少年时期的auto_ptr
小auto_ptr想了很久,终于想到了一个办法
在进行拷贝构造和赋值操作的时候,将原来这块空间的管理权从原指针 ap1 转移到拷贝构造的新指针 ap2 上;
在实际编写代码时,我们只需要在进行浅拷贝之后,将原指针ap1赋为空指针NULL。
但是还有一个问题,进行拷贝构造完成后 ap1 == NULL ,当我们再想访问 ap1 的时候会程序崩溃(因为解引用空指针了),所以当前的这种方式也不可以!!!
(3)成年后的auto_ptr
成年后的auto_ptr有了一定自己的思维,决定在类的成员变量中加一个布尔类型的_owner,用来判断该对象是否具有对这块空间的管理权;
所以在进行拷贝构造和赋值运算符重载的时候,在浅拷贝之后,将原指针ap1的_owner 赋给 现在的指针ap2 的_owner, 之后对原指针ap1的_owner赋为false,这样对这块空间的管理权就从ap1手中转移到了ap2手中。
虽然auto_ptr思考了很多解决方案,但是它本身还是存在很大缺陷的,所以一般不建议使用auto_ptr。
下面是对于auto_ptr的简单实现:
#include <iostream>
using namespace std;
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
,_owner(true)
{
cout<<"auto_ptr()"<<endl;
}
auto_ptr(auto_ptr& ap)
:_ptr(ap._ptr)
,_owner(ap._owner)
{
// ap._ptr = NULL; // 将对这块空间的管理权转移给当前对象
ap._owner = false;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if(this != &ap)
{
if(_owner && _ptr)
{
delete _ptr;
}
_ptr = ap._ptr;
// ap._ptr = NULL;
_owner = ap._owner;
ap._owner = false;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~auto_ptr()
{
if(_owner && _ptr)
{
delete _ptr;
_owner = false;
}
cout<<"~auto_ptr()"<<endl;
}
private:
T* _ptr;
bool _owner;
};
typedef struct
{
int a;
double c;
}A;
int main()
{
/////////////////////////////////////////////////////////////////////////////
// 1 查看能否实现智能指针的功能:
// 在初始化时即开辟空间,析构时即释放空间
// 能够向一般指针一样进行* -> 操作
// auto_ptr<int> ap1(new int);
// cout<<*ap1<<endl;
// *ap1 = 10;
// cout<<*ap1<<endl;
// auto_ptr<A> ap1(new A);
// ap1->a = 10;
// ap1->c = 3.14;
// cout<<(*ap1).a<<endl;
// cout<<ap1->c<<endl;
//////////////////////////////////////////////////////////////////////////////
// 2 验证拷贝构造和 = 时会析构两次
// 通过管理权转移能否解决该问题
// auto_ptr<int> ap1(new int);
// auto_ptr<int> ap2(ap1);
// auto_ptr<int> ap3(new int);
// ap3 = ap1;
////////////////////////////////////////////////////////////////////////////
// 3 验证加入_owner 成员变量,
// 解决访问已经将管理权转移的指针已经释放的问题
auto_ptr<int> ap1(new int);
*ap1 = 10;
auto_ptr<int> ap2(ap1);
auto_ptr<int> ap3(new int);
ap3 = ap1;
cout<<*ap1<<endl;
return 0;
}
简单粗暴的scoped_ptr(防拷贝)
scoped_ptr 见到 auto_ptr 在拷贝上存在那么大的问题,就非常简单粗暴的想:不是只有拷贝构造和赋值运算符重载函数在调用时会存在问题吗,那我不让别人调用这两个函数就好了。
实现防拷贝只需要两步
(1)只声明,不实现
(2)定义为私有
下面是对于scoped_ptr的简单实现:
#include <iostream>
using namespace std;
template <class T>
class scoped_ptr
{
public:
scoped_ptr(T* ptr)
:_ptr(ptr)
{
cout<<"scoped_ptr()"<<endl;
}
// 只声明,不实现,就不会出现别人拷贝构造和赋值的情况
// 但是,有可能别人在类外面进行定义!!!
// scoped_ptr(scoped_ptr<T>& sp);
// scoped_ptr<T>& operator=(scoped_ptr<T>& sp);
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~scoped_ptr()
{
if(_ptr)
{
delete _ptr;
}
cout<<"~scoped_ptr()"<<endl;
}
// 将拷贝构造和赋值定义为私有的,这样在类外面也不能访问它
// 为什么不能只将这两个函数定义为私有,但是还实现它??
// 因为也要防止有可能有人将访问的函数定义为友元friend,这样更保险一些
private:
scoped_ptr(scoped_ptr<T>& sp);
scoped_ptr<T>& operator=(scoped_ptr<T>& sp);
private:
T* _ptr;
};
int main()
{
scoped_ptr<int> sp1(new int);
*sp1 = 10;
cout<<*sp1<<endl;
// scoped_ptr<int> sp2(sp1);
// scoped_ptr<int> sp3(new int);
// sp3 = sp1;
return 0;
}