欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++智能指针

程序员文章站 2022-06-02 13:53:55
...

为什么需要智能指针
首先是因为传统裸指针容易造成内存泄露问题,另外还有在使用异常时,如果在申请空间和释放空间之间抛异常,并且没在该函数处理,会造成内存泄露。因为这些原因引入了智能指针

智能指针原理
智能指针是使用了RAII的思想
RAII(Resource Acquisition is Initialization)直译为资源获取即初始化,即其在构造函数中获取资源,在析构函数中释放资源,因为C++类的机制,在创建时自动调用构造函数,当对象超出作用域的时候自动调用析构函数,所以可以将资源和某一个类进行绑定,其生命周期和该类相同,从而实现实现资源和状态的安全管理。
智能指针就是运用了上述原理实现,相当于将资源进行托管,并且其重载了* 、->可以让其可以和指针一样使用。
另外重载后的->本来应该是->->编译器对其进行了优化可以少写一个

  1. C++98 auto_ptr
    auto_ptr采用了简单的控制权转移的方法
template<class T>
class AutoPtr
{
public:
    AutoPtr(T * ptr = nullptr)
        :ptr_(ptr)
    {}
    AutoPtr(const AutoPtr<T>& ap)
        :ptr_(ap.ptr_)
    {
        ap.ptr_ = nullptr;
    }
    T & operator*()
    {
        return *ptr_;
    }
    T * operator->()
    {
        return ptr_;
    }
    ~AutoPtr()
    {
        if(ptr_ != nullptr)
        {
            delete ptr_;
        }
    }
private:
    T* ptr_;
};

因为auto_ptr存在缺陷,首先是其控制权转移只是简单的将被拷贝的指针置为空,如果后续有对其的使用就会出错
因此在boost库中随后出现了scoped_ptr智能指针其主要思想是因为拷贝会出问题所以其不允许拷贝和赋值

template <class T>
class ScopedPtr
{
public:
    ScopedPtr(T* ptr = nullptr)
        :ptr_(ptr)
    {}
    T & operator*()
    {
        return *ptr_;
    } 
    T * operator->()
    {
        return ptr_;
    }
    ~ScopedPtr()
    {
        if(ptr_ != nullptr)
        {
            delete ptr_;
        }
    }
private:
    ScopedPtr(const ScopedPtr<T>& sp);
    ScopedPtr& operator=(const ScopedPtr<T>& sp);
private:
    T* ptr_;
};

在C++11里面同样实现了和scoped_prt功能相同的智能指针unique_ptr,其实现是直接使用delete关键字直接删除构造函数,和赋值构造函数,但是这两个智能指针均不能进行拷贝,于是在boost库中就出现了第三种智能指针shared_ptr其采用了引用计数的方式解决该问题,下面的代码是一个很简化的方式,

template<class T>
class SharedPtr
{
public:
    SharedPtr(T* ptr = nullptr)
        : ptr_(ptr)
        , pcount_(new int(1))
        , pmutex_(new std::mutex)
    {
        if(ptr == nullptr)
        {
            *pcount_ = 0;
        }
    }
    SharedPtr(const SharedPtr & shp)
        : ptr_(shp.ptr_)
        , pcount_(shp.pcount_)
        , pmutex_(shp.pmutex_)
    {
        if(ptr_ != nullptr)
        {
            AddPCount();
        }
    }
    SharedPtr& operator=(const SharedPtr& shp)
    {
        if(ptr_ != shp.ptr_)
        {
            //释放旧的资源
            Release();
            //进行赋值
            ptr_ = shp.ptr_;
            pcount_ = shp.pcount_;
            pmutex_ = shp.pmutex_;
            if(ptr_ != nullptr)
            {
                AddPCount();
            }
        }
        return *this;
    }
    T* operator->()
    {
        return ptr_;
    }
    T& operator*()
    {
        return *ptr_;
    }
    ~SharedPtr()
    {
        Release();
    }
private:

    void AddPCount()
    {
        //加锁或原子操作
        pmutex_->lock();
        ++(*pcount_);
        pmutex_->unlock();
    }
    void SubPCount()
    {
        //加锁或原子操作
        pmutex_->lock();
        --(*pcount_);
        pmutex_->unlock();
    }
    void Release()
    {
        if(ptr_ != nullptr)
        {
            SubPCount();
            if(pcount_ == 0)
            {
                delete ptr_;
                delete pcount_;
                delete pmutex_;
            }
        }
    }
private:
    T * ptr_;
    int * pcount_;
    std::mutex* pmutex_;
};

shared_ptr首先需要注意的是线程安全问题,因为存在引用计数,如果有两个线程进行操作,其在加一或者减一过程中可能存在更新垃圾值的问题,如引用计数为2一个线程先减一然后判断引用计数值是否为0进行释放内存,另一个线程减一操作此时可能存在判断进行读取到的值为0(本来应该为1,以为它在判断时可能对方正在完成减一,并且内存已经改变为0)从而导致释放两次。
在boost库中和c++11中的shared_ptr都是线程安全的,其通过原子操作实现了线程安全
在boost库文档中这样描述
同一个shared_ptr对象可以被多线程同时读取,并发读本来就是安全的
不同的shared_ptr对象可以被多线程同时修改(即使这些shared_ptr对象管理着同一个对象的指针)
不同的对象指向不同的对象时肯定是线程安全的,其之间相互没有关联,当指向同一个对象时其通过原子操作来支持线程安全操作。
就是说shared_ptr对引用计数的操作设置了同步保护,但是其本身并没有任何同步保护,因此在使用shared_ptr提供的函数时需要注意线程安全问题。
shared_ptr存在的另一个循环引用问题,如下图
C++智能指针
当node1和node2出作用域的时候会执行析抅函数,其引用计数从2变成1并没有为0其管理的对象没有进行释放,因此会造成内存泄露问题。
在boost库和c++11库中使用了weak_ptr解决引用计数的问题,即将ListNode中的shared_ptr换成weak_ptr
weak_ptr和shared_ptr中的引用计数对象均含有指向同一引用计数类的指针,在第一次创建shared_ptr或weak_ptr的时候唯一的引用计数对象会被创建(管理同一对象)其类中含有两个计数use_count和weak_count,use_count初始值为0weak_ptr初始值为1

其中在shared_ptr构造weak_count对象或者weak_ptr构造weak_ptr对象时,weak_count 加1,use_count 不变
在weak_count构造shared_ptr或shared_ptr构造shared_ptr对象时use_count会加一就和上面的一样,weak_count 不变

其中在weak_count构造shared_ptr对象情况比较特殊,在构造shared_ptr对象时不能保证,被管理的对象没有被释放,如有两个线程一个线程构造shared_ptr对象,另一个线程有在释放另一个shared_ptr对象,并且这三者是管理同一个对象,如果此时构造shared_ptr线程首先判断use_count大于0所以此时,shared_ptr可以指向该对象,然后该线程停止另一个线程运行释放了shared_ptr同时该对象也被释放,所以此时shared_ptr构造出的是空的,但是
use_count为1,所以会发生错误,在库中为了避免这中错误采用了如下操作

//记录下use_count_
long tmp = static_cast< long const volatile& >( use_count_ );

//如果已经被别的线程抢先清0了,则被管理的对象已经或者将要被释放,返回false
if( tmp == 0 ) return false;

if( _InterlockedCompareExchange( &use_count_, tmp + 1, tmp ) == tmp )return true;
//先用use_count 和tmp进行比较如果相等则加1否则不执行,整个过程是锁内存的其它线程是无法操作的

对于上面的情况将ListNode中的shared_ptr换成weak_ptr后,首先use_count 均为1weak_ptr也均为1,
然后互相指向后,是用shared_ptr给weak_ptr赋值所以此时,weak_count加1变为2,use_count不变,
有四个weak_ptr node1 的_next变为2 node1的prev变为2 其它为1
然后在释放时,首先释放node2此时,use_count 为1所以直接释放并且其weak_count 减1并且node1的next的weak_count 因为管理的是和其相同的对象因此其使用相同的计数对象故其weak_count也更新为了1,此时node 2的next weak_ptr也释放weak_count减1并且对应的weak_count 为0因此其直接释放计数对象,而prev释放weak_count减一后为1故不释放计数对象,
然后释放node1, 此时use_count 为1直接释放并且其weak_count 减1变为0释放计数对象,然后node1中的prev释放 weak_count 减1变为0释放计数对象,然后next释放weak_count 减1变为0释放计数对象
到此所有的智能指针对象× 2 ,被管理对象 × 2, 引用计数对象 × 4均释放完 ,