C++智能指针使用及原理

在讲解之前,先讲述一种RAII思想.

目录

RAII

智能指针原理

auto_ptr

auto_ptr的介绍

auto_ptr的实现

unique_ptr

unique_ptr的介绍

unique_ptr实现

shared_ptr

shared_ptr的介绍

shared_ptr实现

weak_ptr

weak_ptr的介绍

weak_ptr的实现


RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
1.不需要显式地释放资源。
2.采用这种方式,对象所需的资源在其生命周期内始终保持有效

template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr)
        :_ptr(ptr)
    {}
    ~SmartPtr()
    {
        delete _ptr;
    }
private:
    T* _ptr;
};

这样我们最后便不再需要显式地释放资源了.

我们创建对象时,可以直接用匿名对象创建,也可以利用已有的指针创建.

int main()
{
    int* ptr1 = new int;
    SmartPtr<int> sp1(ptr1);

    SmartPtr<int> sp2(new int);
}

这样即使在过程中抛异常,对象资源也会正常释放.

智能指针原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因为SmartPtr类中还得需要将* ->重载下,才可让其像指针一样去使用.

    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }

这样才算是智能指针.

总结满足是智能指针必须有两个条件(原理)

1.RAII特性

2. 重载operator*和opertaor->,具有像指针一样的行为。

是不是满足这两个条件就万事大吉了呢,也不能算是,下面将讲解四种常见的智能指针。

auto_ptr

auto_ptr的介绍

auto_ptr在C++98中就已经存在了,库中也有对应的函数.

class A
{
public:
    A()
    {}
    ~A() 
    {
        cout << "~A()" << endl;
    }
private:
    int _a1 = 0;
    int _a2 = 0;
};
int main()
{
    auto_ptr<A> ap1(new A);
    return 0;
}

这段代码没有任何问题,包括运行起来也能正常释放资源.

但是智能指针存在一个严重的问题:拷贝问题. 

我们如果用我们刚才写的SmartPtr类进行拷贝:

    SmartPtr<A> sp1(new A);
    SmartPtr<A> sp2(ptr1);

这样会引发一个我们之前经常说的问题:同一块资源被析构两次.

 sp2先析构一次,sp1再次析构这时就会出错.说到底还是因为浅拷贝问题.

那么我们改成深拷贝不就可以了吗?

答案也是不能,因为深拷贝就违背了我们的功能需求.

智能指针也是模拟原生指针的行为,我们本身的目的就是把资源交给对象管理。

其实我们仔细想一下,我们把sp1托给sp2,不也就是相当于让sp1和sp2共同管理这份资源吗.

所以我们这里要的就是浅拷贝!

这里和迭代器类似,迭代器浅拷贝没问题的原因是:不管迭代器中资源的释放.

那么库里的auto_ptr是如何解决的呢?

先来说结论:是转移了资源管理权.

一开始在ap1还没有赋值给ap2时,资源还在ap1中.

 当执行完第二条语句:

 我们发现ap1被悬空了,资源管理交给了ap2.

这是一种极不负责的行为,当后面有人想在解引用ap1中的内容时,便会报错.

这算是设计的一种很大的缺陷吧,很多公司也禁止使用auto_ptr.

auto_ptr的实现

namespace hyx {
    template<class T>
    class auto_ptr {
    public:
        auto_ptr(T* ptr)
            :_ptr(ptr)
        {}
        ~auto_ptr()
        {
            cout << "~auto_ptr()" << endl;
            delete _ptr;
        }
        auto_ptr(auto_ptr<T>& ap)
            :_ptr(ap._ptr)
        {
            cout << "auto_ptr Delete()" << endl;
            //将原来的指针置空
            ap._ptr = nullptr;
        }
        //以ap1 = ap2为例
        auto_ptr<T>& operator=(auto_ptr<T>& ap)
        {
            if (this != &ap)
            {
                //如果当前ap1存在,则释放掉ap1
                if (_ptr)
                {
                    cout << "operator= Delete()" << endl;
                    delete _ptr;
                }
                //将ap2的资源转移给ap1
                _ptr = ap._ptr;
                //将ap2的指向置为空
                ap._ptr = nullptr;
            }
            return *this;
        }
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
    private:
        T* _ptr;
    };
}

unique_ptr

unique_ptr的介绍

unique_ptr对于拷贝问题设计思路非常简单粗暴:禁止拷贝和赋值.只适用于不需要拷贝的场景.

例如下面这段代码:

    unique_ptr<A> up1(new A);
    unique_ptr<A> up2(up1);//错误,unique_ptr不允许拷贝
    unique_ptr<A> up3 = up2;//错误,unique_ptr不允许赋值

除了拷贝,其它方面和别的智能指针作用几乎是一样的.正常用即可.

unique_ptr实现

不能拷贝和赋值,我们直接将这两个函数设为delete即可,这是C++11中的特性.

而在C++98中,我们只能把它设为私有(防止有人可以在外部实现),并且只声明不实现(防止编译器自己生成).

namespace hyx {
    template<class T>
    class unique_ptr {
    public:
        unique_ptr(T* ptr)
            :_ptr(ptr)
        {}
        ~unique_ptr()
        {
            cout << " unique_ptr" << endl;
            if (_ptr)
                delete _ptr;
        }
        //C++11 将两个函数设为delete
        unique_ptr (unique_ptr <T>& ap) = delete;
        unique_ptr <T>& operator=(unique_ptr <T>& ap) = delete;
        
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
        //C++98防拷贝的方式:私有+只声明不实现
        //private:
        //    unique_ptr(unique_ptr <T>& ap);
        //    unique_ptr <T>& operator=(unique_ptr <T>& ap);
    private:
        T* _ptr;
    };
}

shared_ptr

shared_ptr的介绍

那如果我们就是想要拷贝呢?这个时候shared_ptr便登场了.

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr.

那它是怎么支持的呢?浅拷贝还是深拷贝还是别的方法呢?

看下面这段代码

class A
{
public:
    A()
    {}
    ~A()
    { }

    int _a1 = 0;
    int _a2 = 0;
};
int main()
{
    shared_ptr<A> sp1(new A);
    shared_ptr<A> sp2(sp1);
    sp1->_a1++;
    sp1->_a2++;
    cout << sp2->_a1 << "   " << sp2->_a2 << endl;
}

我们把sp1拷贝给了sp2,然后让sp1的两个成员分别+1,然后观察sp2的情况:

发现sp2两个成员也都+1,这就说明它们是共享这块资源的

这不就是浅拷贝吗,那为什么不会出现析构多次出错的情况?

shared_ptr内部其实是采用引用计数的方式来实现的.

具体是怎么做的呢?

有一个计数器count,每当有新的对象来管理时,计数器便++,而每当有对象释放时,这个计数器便--,直到最后一个对象析构时,即count=1时,此时再把这块空间资源释放掉。如此便很好解决了问题.

比如刚才的sp1和sp2共同管理那一块空间资源,此时count=2,当sp2释放时,count--,此时count=1,说明还有其它对象也在管理这块空间,不要清理释放这块资源,所以直接count--即可.

sp1释放时,count=0,说明没有对象再管理这块空间了,此时便清理释放掉这块空间即可.

shared_ptr实现

刚才是我们的想法,但是具体要怎么实现呢?

我们把计数器_count放在哪呢?

如果直接加到类成员里面去,那么每个对象都会有一份独立的_count,这样就无法再进行计数了.

我们想要的_count一定是共享的,那我们把它设置成static可不可以?

答案也是不可以的.看下面的场景就知道了:

    test::shared_ptr<A> sp1(new A);
    test::shared_ptr<A> sp2(sp1);
    test::shared_ptr<A> sp3(sp1);

其中test域是static修饰的_count,主要目的是为了演示.

这样目前看起来没有问题,sp1,sp2,sp3各管理一份,此时_count=3,没任何问题.

但如果此时我又加了一句这样的代码呢?

    test::shared_ptr<int> sp4(new int);

此时我们期望的应该是重新生成一份_count,然后sp4再管理。

但按刚才说的static那种方法,_count会直接++,会让_count=4,这样肯定就错误了.

因为当sp4想释放资源时,由于_count并不为0,会导致sp4的资源不能正常释放.

而按常理来说,sp4和前面的1,2,3管理的并不是同一块资源,sp4不应该和他们公用计数器.此时sp4的_count=0理应被释放的.

这就是static修饰所带来的问题.也是为什么不能用static修饰的原因:所有资源都只有一个计数.

我们想要的是每个资源都有一个计数.

这里直接说正确的设计思路:在类内部加了一个指针.int* _pCount;

我们什么时候知道资源 来了呢?构造函数!

来一个新资源我们就new一个,这样就能保证每一份资源都能有计数了.

拷贝构造的时候,不仅把要把_ptr拷贝,也要把_pCount拷贝一份,同时让(*_pCount)++.

析构时,每次先--(*pCount),如果此时_pCount==0说明已经没有对象管理这块资源了,此时再释放掉这块资源.

比较需要注意且麻烦的是 赋值重载运算符.

1.以sp1=sp5为例,由于sp1要管理sp5指向的空间资源,此时应该先把sp1的计数--,如果sp1的计数为0,说明sp1是最后一个对象,需要释放掉当前的资源和计数,然后再指向sp5.

2.还有防止自己给自己赋值,以及管理相同资源的对象互相赋值,注意不要写this==&sp,因为这样只能解决前者,不能解决后者问题

namespace hyx
{
    template<class T>
    class shared_ptr {
    public:
        shared_ptr(T* ptr = nullptr)
            :_ptr(ptr)
            ,_pCount(new int(1))
        {}
        ~shared_ptr()
        {
            if (--(*_pCount) == 0)
            {
                cout << "Delete:"  << _ptr  << endl;
                delete _ptr;
                delete _pCount;
            }
        }
        shared_ptr(shared_ptr<T>& sp)
            :_ptr(sp._ptr)
            ,_pCount(sp._pCount)
        {
            (*_pCount)++;
        }
        //以sp1=sp5为例.
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            //防止自己给自己赋值,以及管理相同资源的对象互相赋值,注意不要写this==&sp,因为这样只能解决前者,不能解决后者问题
            if (_ptr == sp._ptr)
            {
                return *this;
            }
            //由于sp1要管理sp5指向的空间资源,此时应该先把sp1的计数--,如果sp1的计数为0,
            //说明sp1是最后一个对象,需要释放掉当前的资源和计数,然后再指向sp5
            if (--(*_pCount) == 0)
            {
                delete _ptr;
                delete _pCount;
            }

            _ptr = sp._ptr;
            _pCount = sp._pCount;

            (*_pCount)++;

            return *this;
        }
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
    private:
        T* _ptr;
        int* _pCount;
    };
}

我们测试一下下面代码:

int main()
{
    hyx::shared_ptr<A> sp1(new A);
    hyx::shared_ptr<A> sp2(sp1);
    hyx::shared_ptr<A> sp3(sp1);

    hyx::shared_ptr<int> sp4(new int);
    hyx::shared_ptr<A> sp5(new A);
    hyx::shared_ptr<A> sp6(sp5);
}

一共创建了3份资源,所以最后也应该析构3次.

这样完美符合了我们的预期.

weak_ptr

weak_ptr的介绍

shared_ptr看起来很完美了,但是还存在一些小问题:循环引用.

看如下代码

struct ListNode
{
    int _data;
    shared_ptr<ListNode> _prev;
    shared_ptr<ListNode> _next;
    ~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
    shared_ptr<ListNode> node1(new ListNode);
    shared_ptr<ListNode> node2(new ListNode);
    node1->_next = node2;
    node2->_prev = node1;
    return 0;
}

 我们画图来演示问题所在:

 循环引用分析:

1. node1和node2两个智能指针对象分别指向对应的结点,此时引用计数都是1.

2. node1的_next指向node2,node2的引用计数变为2,node2的_prev指向node1,node1的引用计数变成2。

3. node1和node2析构,引用计数分别减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。

4.也就是说node1中_next析构了,node2就释放了;node2中_prev析构了,node1就释放了.

5.但是_next属于node的成员,node1释放了,_next才会析构,而node1由node2中的_prev管理,只有node2释放了,_prev才会释放,而node2又有node1的_next管理,形成了循环,所以这就叫循环引用,谁也不会释放。

为解决循环引用问题,便引用了weak_ptr,但weak_ptr不是常规智能指针,没有RAII,不支持直接管理资源.

 设计出来就是为了解决shared_ptr循环引用问题的.

_prev和_prev是weak_ptr时,它不参与资源释放管理,但是可以访问和修改资源.但是不增加计数,就不存在循环引用的问题了.

// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加
node1和node2的引用计数
struct ListNode
{
    int _data;
    weak_ptr<ListNode> _prev;
    weak_ptr<ListNode> _next;
    ~ListNode() { cout << "~ListNode()" << endl; }
}; 

weak_ptr的实现

/ 辅助性智能指针,使命就是配合解决shared_ptr循环引用问题
template<class T>
class weak_ptr
{
public:
    weak_ptr()
        :_ptr(nullptr)
    {}
    weak_ptr(const shared_ptr<T>& sp)
        :_ptr(sp.get())
    {}
    weak_ptr(const weak_ptr<T>& wp)
        :_ptr(wp._ptr)
    {}
    weak_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        _ptr = sp.get();
        return *this;
    }
    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }
public:
    T* _ptr;
};

这样,智能指针的部分就讲解的差不多了.

其中,shared_ptr是最重要的一个智能指针,如何实现的一定要牢记!

猜你喜欢

转载自blog.csdn.net/weixin_47257473/article/details/131645306