今天我们要学习的内容是智能指针,在此之前我们先来看两行代码
int *p=new int;
delete p;
new一个对象,再delete掉它,这样看起来没什么问题,但是有时候程序代码过长,new对象了,但是忘记delete了或者是发生下面的情况
int *p=new int;
bool isEnd=true;
if(isEnd)
return;
delete p;
这样在delete之前,发生了执行流跳转,导致无法delete,这样就造成了内存泄漏。
除了return,还有以下情况会造成执行流跳转:
- break
- goto
- 抛异常
那么为了解决这个问题引出了我们的智能指针,在学习指针之前,我们首先来了解什么是RAII思想?
RAII就是资源分配既初始化,按照我的自己的理解这句话的意思是:获得指向资源的指针后,在构造函数中将其保存,这样析构函数会将它自动回收。
那么我们的智能指针就是RAII思想的一种实现。我们先来看下面的代码:
template<class T>
class AutoPtr
{
public:
AutoPtr(T*ptr)
:_ptr(ptr)
{}
~AutoPtr()
{
if (_ptr)
{
printf("~AutoPtr:%p\n",_ptr);
delete _ptr;
}
}
private:
T* _ptr;
};
void TestAutoPtr()
{
AutoPtr<int> p1(new int(2));//p1是智能指针类对象,把new出来的资源保存起来,最后能自动的调用析构函数
}
运行结果:
这样就实现了我们的RAII思想,智能指针不是真正的指针,他只是像指针一样,既然像指针一样我们就要实现它的*操作和->操作:
template<class T>
class AutoPtr
{
public:
AutoPtr(T*ptr)
:_ptr(ptr)
{}
~AutoPtr()
{
if (_ptr)
{
printf("~AutoPtr:%p\n",_ptr);
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
struct AA{
int _a1;
int _a2;
};
void TestAutoPtr()
{
AutoPtr<int> p1(new int(2));
*p1=10;
AA* p2=new AA;
p2->_a1=1;
p2->_a2=2;
}
代码运行成功后,通过调试我们可以看到如下结果:
这样我们的智能指针就实现可像指针一样,但是这不就代表问题彻底解决了~
我们把测试代码改为:
AutoPtr<int> p1(new int(2));
AutoPtr<int> p2(p1);
结果如下:
这个时候会发现程序会崩掉,什么原因呢?就是一个指针被析构了两次。
为了解决这一个问题:我们引出了以下的几类智能指针来实现资源的赋值与管理
- auto_ptr
auto_ptr采用的是管理权转移法,既然多个对象指向同一空间,会被释放多次。那么我们就让一个空间只让一个人管理。
意思就是 AutoPtr<int>p2(p1),p1赋给p2后,将p1释放,代码如下
AutoPtr(AutoPtr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr=NULL;
}
实现了拷贝构造,我们接下来实现它的赋值运算符重载
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if (this!=ap)
{
if(_ptr)
{
delete _ptr;
_ptr=ap._ptr;
ap._ptr=NULL;
}
}
return *this;
}
与拷贝构造是同样的道理,但是管理权转移这种方法有很大的缺陷,就是我把p1的空间置空了,但是别人不知道他已经是空了呀,他就会操作这块空间,这样就会出错,为了解决这一问题,我们引出了scoped_ptr
- scoped_ptr
这是一种比较简单粗暴的方法,既然你拷贝和赋值会出问题,那么我就不让你拷贝和赋值了
template<class T>
class ScopedPtr
{
public:
ScopedPtr(T*ptr)
:_ptr(ptr)
{}
~ScopedPtr()
{
if (_ptr)
{
printf("~ScopedPtr:%p\n",_ptr);
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
ScopedPtr(ScopedPtr<T> &sp);
ScopedPtr<T>& operator=(const ScopedPtr<T> &sp);
private:
T* _ptr;
};
我们把拷贝构造函数和赋值运算符弄成私有的且只声明不实现的原因有两个
(1)类外的成员调不了
(2)只声明不实现,万一类内出现了拷贝构造,这样做就导致它链接不成功,找不到栈帧中call指令的地址
但是这样做还是不好的,就好像说不能说因为你胖,我就彻底不让你吃饭了,这也是不现实的。同理我们也不能完全不让它拷贝和赋值,这样我们就引出了shared_ptr
- shared_ptr
这是采用一个引用计数的方法,来释放空间,什么意思呢?我们来看图解释
#pragma once
template<class T>
class SharedPtr
{
public:
SharedPtr(T*ptr)
:_ptr(ptr)
,_refcount(new int(1))
{}
~SharedPtr()
{
Get_Ref();
if (--(*_refcount)==0)
{
printf("~SharedPtr:%p\n",_ptr);
delete _ptr;
delete _refcount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
SharedPtr(SharedPtr<T> &sp)
:_ptr(sp._ptr)
,_refcount(sp._refcount)
{
(*_refcount)++;
}
SharedPtr<T>& operator=(const SharedPtr<T> &sp)
{
if(_ptr!=sp._ptr)
{
if (--(*_refcount)==0)
{
delete _ptr;
delete _refcount;
}
_ptr=sp._ptr;
_refcount=sp._refcount;
(*_refcount)++;
}
return *this;
}
void Get_Ref()
{
cout<<*_refcount<<endl;
}
private:
T* _ptr;
int* _refcount;
};
void TestSharedPtr()
{
SharedPtr<int> p1(new int(2));
SharedPtr<int> p3(new int(3));
p3=p1;
}
结果如图:
赋值运算符重载的实现我们通过一张图来分析:
有人看代码会有这样的疑问,为什么要给引用计数重新开辟空间,为什么不像string的深拷贝一样,在_str上为引用计数开空间
我们来看string类的深拷贝是这样添加引用计数的
string(const char*str)
{
_str=new char[strlen(str)+1];
}
_str的空间是自己开辟的,你自己想开辟多大就开辟多大
而我们来看SharedPtr的相关代码:
SharedPtr(T*ptr)
:_ptr(ptr)
{}
_ptr的空间用的是ptr的空间,你用别人的空间,还想让别人给你多开一个字节,是不是有点不好呢?
看到这,小伙伴是不是以为这样问题就解决了,问题哪有这么简单的啊,我们再来看一个场景,这个时候我们使用库里面的sharedptr
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
ListNode(int x)
:_data(x)
, _prev(NULL)
,_next(NULL)
{ }
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main()
{
shared_ptr<ListNode> cur(new ListNode(1));
shared_ptr<ListNode> next(new ListNode(2));
cur->_next = next;
next->_prev = cur;
return 0;
}
cout << "~ListNode" << endl;
}
};
int main()
{
shared_ptr<ListNode> cur(new ListNode(1));
shared_ptr<ListNode> next(new ListNode(2));
cur->_next = next;
next->_prev = cur;
return 0;
}
这个时候呢会造成一种缺陷叫做循环引用,那么什么是循环引用,我们画图来说
这个时候我们又引来一个东西叫做weak_ptr,这并不是一个智能指针,它只是配合着shared_ptr来使用。
它并不引进引用计数,不会对引用计数进行操作,我们来看一下代码
class WeakPtr
{
public:
WeakPtr()
:_ptr(NULL)
{}
WeakPtr(SharedPtr<T> &sp)
:_ptr(sp._ptr)
{}
WeakPtr<T>& operator=(const SharedPtr<T> &sp)
{
_ptr=sp._ptr;
return *this;
}
private:
T* _ptr;
};
我们不对_refcount进行操作,将上述场景中_next和_prev的类型改成WeakPtr的会出现什么情况呢?
这样就解决了循环引用的缺陷。
最后一个问题! new/delete , new[]/delete[] ,malloc/free ,fopen/fclose等四种类型都可能会造成资源泄漏
我们的智能指针中的析构是由delete实现的,那如果我们要想对后面两中类型进行进行自动清理的工作,我们就要
自己订制删除器。我们以fopen/fclose为例看以下的代码:
class Fclose
{
public:
void operator()(FILE*f)//仿函数,重载operator()
{
cout<<"Fclose"<<endl;
fclose(f);
}
};
void test()
{
Fclose fc;
std::shared_ptr<FILE> sp(fopen("in.txt","w",fc));
}
这样我们就完成了一个订制的删除器,细心的小伙伴就会发现我的这份代码中的shared_ptr前加上了作用域解析符,这是为什么呢?这就跟我们智能指针的发展历史有关,接下来我们就要总结智能指针的发展历史
最后的最后!!!我们来总结关于实现智能指针的三点要素
(1)要实现RAII
(2)要像指针一样
(3)资源的赋值与管理
好了,关于智能指针的内容我们就说到这里,如果你在这篇文章中发现了什么错误!请在下方评论哦!