C++智能指针剖析

智能指针

什么是智能指针?

程序里有一些指针管理着资源,比如文件指针,指向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本身的析构函数,造成内存泄露问题。

猜你喜欢

转载自blog.csdn.net/hanzheng6602/article/details/81135111