智能指针
什么是智能指针?
程序里有一些指针管理着资源,比如文件指针,指向malloc动态分配空间的指针。这些指针在使用完后必须被释放,否则有内存泄露问题。而程序员往往会因为程序执行流复杂而疏忽了释放这些指针指向的资源,所以,智能指针智能在自动释放资源。
这里的概念和RAII很相似,用类管理资源,构造即分配,析构即释放。
最简单的模型
template<class T>
class AutoPtr {
public:
AutoPtr(T* ptr = NULL)
: _ptr(ptr)
{}
~AutoPtr() {
if(_ptr)
delete _ptr;
}
private:
T* _ptr;
};
智能指针的版本
auto_ptr
第一个版本是auto_ptr,多个智能指针之间是浅拷贝的方式。当多个智能指针指向同一块资源,会有多次释放的错误问题,因此auto_ptr同时只允许一个智能指针对象管理资源,发生复制/拷贝对象时,把资源的管理权限交出去。
template <typename T>
class AutoPtr {
public:
AutoPtr(T* ptr = NULL)
:_ptr(ptr)
{
}
~AutoPtr()
{
if (_ptr) {
delete _ptr;
}
}
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;
}
T& operator*() {
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T * _ptr;
};
但如此又引入新的问题
AutoPtr<int> ap1(new int(5));
AutoPtr<int> ap2(ap1);
cout << *ap2 << endl;
cout << *ap1 << endl;
ap2复制ap1,此时ap2拥有资源,ap1指针被置空。本想复制一份指针对象,却把自己搞丢了。所以禁用auto_ptr.
scoped_ptr
scoped的做法就很粗暴了,直接禁止智能指针对象的赋值和拷贝。
private:
//对象不可调用
ScopedPtr(const ScopedPtr<T>& p);
ScopedPtr<T>& operator=(const ScopedPtr<T>& p);
以此为原型,诞生出scoped_array,该类管理一个动态申请内存的数组。
调用scoped_array
int n;
boost::scoped_array<int> array(new int[n]);
for(int i=0; i<n; i++){
cout<<array[i]<<endl;
}
shared_ptr
目前最好的智能指针版本,采用引用计数。
模拟实现一下:
template <typename T>
class Delete {
public:
void operator ()(T*& p) {
if (p) {
delete p;
p = NULL;
}
}
};
class FClose {
public:
void operator()(FILE*& p)
{
if (p) {
fclose(p);
p = NULL;
}
}
};
template <typename T, class Dx = Delete<T>>
class SharedPtr {
public:
SharedPtr(T* ptr = NULL)
:_ptr(ptr),_pCount(NULL)
{
if (ptr) {
_pCount = new int(1);
}
}
~SharedPtr()
{
if (_pCount && 0 == --*_pCount) {
//定制删除器
Dx()(_ptr);
delete _pCount;
}
}
SharedPtr(const SharedPtr<T>& sp)
:_ptr(sp._ptr), _pCount(sp._pCount)
{
if (_pCount) {
++(*_pCount);
}
}
//考虑this可能为NULL或者指向资源,sp为空或指向资源
SharedPtr<T>& operator=(const SharedPtr<T>& sp) {
if (this != &sp) {
if (_pCount && 0 == --*_pCount) {
//独占销毁
Dx()(_ptr);
delete _pCount;
}
_ptr = sp._ptr;
_pCount = sp->_pCount;
if (_pCount) {
++(*_pCount);
}
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T * _ptr;
int * _pCount;//引用计数
};
测试用例:
void TestShared()
{
SharedPtr<ListNode<int> > ap1(new ListNode<int>(10));
SharedPtr<int> ap(new int(50));
SharedPtr<FILE, FClose> ap3(fopen("xx.txt", "w"));
}
循环引用问题
看了很多网上的解释,大都对循环引用过程的解释有问题。
主要的误区是分不清智能指针的析构和资源自己的析构函数什么时候被调用。
本着探索精神,在vs2017中不断使用F11调试,得出了和结果一致的解释,如有不妥欢迎评论。
先看一个正常使用的例子
template <class T>
struct ListNode {
shared_ptr<ListNode<T> > _pPre;
shared_ptr<ListNode<T> > _pNext;
T _data;
ListNode(const T& data)
:_pNext(NULL), _pPre(NULL),_data(data)
{
cout << this << endl;
}
};
void Test(){
shared_ptr<ListNode<int> > sp1(new ListNode<int>(10));//node1
shared_ptr<ListNode<int> > sp2(new ListNode<int>(20));//node2
sp1->_pNext = sp2;
}
结果分析:
034CB100 //node1
034C5658
Destory 034CB100 //node1析构
Destory 034C5658
shared_ptr为其管理的每一块资源设置一个引用计数,sp1的Next指针指向sp2,sp2的引用计数被增加到2.
此时sp1的引用比sp2的引用:1:2
出了Test函数,首先调用sp2的析构函数, 智能指针的析构只做一件事 ,–当前资源的引用,若为0则销毁。所以此时sp2的引用变为1.
接着调用了sp1的析构,此时sp1:sp2 = 0 : 1.这时sp1引用为0,调用node1的真正析构函数,Node1生命周期结束之后需要销毁在Node1里创建的智能指针对象 ,因此调用Next智能指针的析构函数,也就是调用sp2的析构函数,–sp2的引用后发现为0,再调用node2的析构销毁node2。
循环引用
//循环引用
void Test()
{
shared_ptr<ListNode<int> > sp1(new ListNode<int>(10));
shared_ptr<ListNode<int> > sp2(new ListNode<int>(20));
//此时sp1,sp2引用为1
sp1->_pNext = sp2;
sp2->_pPre = sp1;
//此时sp1,sp2引用均为2
}
出了Test函数调用sp2的析构函数,sp1:sp2 = 2 : 1.
再调用sp1的析构函数,sp1 : sp2 = 1 : 1.
引用计数均不为0,所以不会调用node本身的析构函数,造成内存泄露问题。
标准库为了解决循环引用问题
库的实现版本通过原子操作实现线程安全。并且是维护了一个use计数,一个weak计数。shared_ptr管理的资源里面若有指向同类资源的指针替换为weak智能指针。
weak指针表达对资源的临时引用权限,其引用的资源随时可能被销毁。在需要使用weak所指对象时,升级为shared_ptr,若对象已经被销毁,则升级失败。
这样在析构资源时,只需要判断use计数就可以了,use为0立即析构。