首先,为什么需要智能指针?
来看下面这段代码
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;
}
可以看到,在这段代码里,我们需要考虑一些异常情况,并且要非常非常注意释放资源。但是造成的问题就是,代码冗余,反复重复的释放代码,降低了可读性,并且反复多次的释放,如果有哪一个忘记写了或是漏写了,就会造成内存泄漏。
所以,这就体现出了智能指针的好处。
什么是智能指针
一个智能指针应具备以下的三点:
- RAII
- 像指针一样的使用
- 拷贝构造与赋值
先来说第一点:RAII
后面两点后面会说
RAII
资源分配即初始化,定义一个类来封装资源的分配与释放,在构造函数完成资源的分配与初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
再来看看都有哪些智能指针
auto_ptr
该智能指针是c98标准里规定的,需要注意的是,这个智能指针是不提倡使用的,某些情况下禁止使用的。所以只做了解。
上文种提到了智能指针的三点,再次讨论一下auto_ptr的后两点:
1.像指针一样使用,很容易就可以使用,重载*和->就可以实现
2.拷贝构造与赋值:拷贝构造默认的拷贝方式是浅拷贝,即把你的拷一份给我,对指针而言,就是你和我都指向同一空间,那么就会出现很大的问题,析构时会析构两次。auto_ptr对于该问题的解决方案是使用管理权转移的思想

我们来模拟实现一个auto_ptr:
template <class T>
class Auto_Ptr{
public:
Auto_Ptr(T* ptr)
:_ptr(ptr)
{}
~Auto_Ptr()
{
if(_ptr!=NULL)
{
cout<<"delete _ptr"<<endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//以下做了管理权转移
Auto_Ptr(Auto_Ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr=NULL;
}
//ap3=ap2;
T& operator=(Auto_Ptr<T>& ap)
{
if(this!=&ap)
{
if(_ptr!=NULL)
{
delete _ptr;
}
_ptr=ap._ptr;
ap._ptr=NULL;
}
return *this;
}
private:
T* _ptr;
};
scoped_ptr
以下智能指针都是针对于Boots库中的智能指针。
与auot_ptr相似,scoped_ptr也具有以上三点,与auto_ptr不同的是对于拷贝构造与赋值实现思想不同。
scoped_ptr采用了防拷贝的思想,意思是,如果你要拷贝或是赋值但是我不允许这种行为。如果采取默认的拷贝构造与赋值,默认是浅拷贝,也就意味着我们必须实现一个,但是如何实现呢?scoped_ptr采取的方法是只声明不实现。并且将接口设置为私有的,也就说明你想实现也实现不了。
template <class T>
class Scoped_Ptr
{
public:
Scoped_Ptr(T* ptr)
:_ptr(ptr)
{}
~Scoped_Ptr()
{
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
//防拷贝
//只声明不实现
//定义为私有的,也不允许你在外面实现
Scoped_Ptr(const Scoped_Ptr<T>& sp);
Scoped_Ptr<T>& operator=(const Scoped_Ptr<T>& sp);
private:
T* _ptr;
};
shared_ptr
shared_ptr使用了一种我们比较熟悉的做法:引用计数

template<class T>
class Shared_Ptr
{
public:
Shared_Ptr(T* ptr)
:_ptr(ptr)
{
_pcount=new int(1);
}
~Shared_Ptr()
{
if(--(*_pcount)==0)
{
delete _ptr;
delete _pcount;
}
}
Shared_Ptr(const Shared_Ptr& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
//sp1=sp2;
Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
{
if(_ptr!=sp._ptr)
{
if(--(*_pcount)==0)
{
delete _pcount;
delete _ptr;
}
_ptr=sp._ptr;
_pcount=sp._pcount;
++(*_pcount);
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
这种做法乍一看是很完美,但是shared_ptr有一些问题:
1. 线程安全问题:由于对引用计数++不是原子的,所以这里是有线程安全问题的
2. 循环引用问题:这个问题使用以上问题是不会有问题的,比如在以下场景:
struct ListNode
{
Shared_Ptr<ListNode> _next;
Shared_Ptr<ListNode> _prev;
ListNode()
:_next(NULL)
,_prev(NULL)
{}
~ListNode()
{
cout<<"~ListNode()"<<endl;
}
};
void TestCycle()
{
Shared_Ptr<ListNode> cur(new ListNode);
Shared_Ptr<ListNode> next(new ListNode);
cur->_next = next;
next->_prev = cur;
}

那么该如何解决这个问题,我们不妨思考一下,造成的原因是由于引用计数,那么我们就不要让引用计数自增就好了。所以就需要另外一个智能指针出场–weak_ptr
weak_ptr
这个智能指针不含RAII,因为他的作用就是为了解决shared_ptr的循环引用问题的。
如何解决呢?看代码:
struct ListNode
{
Weak_Ptr<ListNode> _next;
Weak_Ptr<ListNode> _prev;
ListNode()
:_next(NULL)
,_prev(NULL)
{}
~ListNode()
{
cout<<"~ListNode()"<<endl;
}
};
void TestCycle()
{
Shared_Ptr<ListNode> cur(new ListNode);
Shared_Ptr<ListNode> next(new ListNode);
cur->_next = next;
next->_prev = cur;
}
将_next 与 _prev指针都定义为weak_ptr指针就可以避免出现循环引用问题了。
下面来总结一下以上提到的智能指针:
| 智能指针 | 特点 | 思想 | 使用 |
|---|---|---|---|
| auto_ptr | 带有缺陷 | 管理权转移 | 严禁使用 |
| scoped_ptr | 简单粗暴 | 防拷贝 | 鼓励使用 |
| shared_ptr | 相对复杂 | 引用计数 | 鼓励使用,注意循环引用问题 |
| weak_ptr |
2545

被折叠的 条评论
为什么被折叠?



