C++智能指针
为什么需要智能指针
首先是因为传统裸指针容易造成内存泄露问题,另外还有在使用异常时,如果在申请空间和释放空间之间抛异常,并且没在该函数处理,会造成内存泄露。因为这些原因引入了智能指针
智能指针原理
智能指针是使用了RAII的思想
RAII(Resource Acquisition is Initialization)直译为资源获取即初始化,即其在构造函数中获取资源,在析构函数中释放资源,因为C++类的机制,在创建时自动调用构造函数,当对象超出作用域的时候自动调用析构函数,所以可以将资源和某一个类进行绑定,其生命周期和该类相同,从而实现实现资源和状态的安全管理。
智能指针就是运用了上述原理实现,相当于将资源进行托管,并且其重载了* 、->可以让其可以和指针一样使用。
另外重载后的->本来应该是->->编译器对其进行了优化可以少写一个
- 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存在的另一个循环引用问题,如下图
当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均释放完 ,