看下面的代码我们就会发现shared_ptr在多线程的情况下会出问题:
void test_multi_thread_copy(yan::shared_ptr<int>& sp,size_t n)//把智能指针拷贝n次
{
for (size_t i = 0; i < n; i++)
{
yan::shared_ptr<int> copy(sp);
}
}
void test_shared_ptr_safe()
{
yan::shared_ptr<int> sp(new int);
std::thread t1(test_multi_thread_copy, sp, 1000);
std::thread t2(test_multi_thread_copy, sp, 1000);
cout << sp.use_count() << endl;
t1.join();
t2.join();
}
此时sp1的引用计数应该从1加两次,加到3,两个线程同时对引用计数进行 ++操作时,如果两个线程同时执行加加操作的话,引用计数只加了1次,而我们期望它加两次,这样会导致最终引用计数已经为0了,该资源已被释放,但是却还有一个对象指向该资源,也就是出现野指针问题,所以不是线程安全的。
在多线程的情况下,要保证这引用计数的加加操作是原子性的,就需要加锁,完善后的代码如下:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))//把引用计数置为1
, _pmtx(new std::mutex)
{}
void AddRef()//引用计数++,加锁
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
void Release()//释放
{
bool flag = false;
_pmtx->lock();
if (--(*_pcount) == 0)//保证释放是线程安全的
{
cout << "delete: " << _ptr << endl;
delete _ptr;
delete _pcount;
flag = true;
}
_pmtx->unlock();
if (flag)
delete _pmtx;
//错误,这三个指针有可能会被释放2次,因为它是解锁之后,才判断pcount并释放的
/*_pmtx->lock();
--(*pcount);
_pmtx->unlock();
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
delete _pmtx;
}*/
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//拷贝构造-->++引用计数即可
shared_ptr(const shared_ptr<T>& sp)//拷贝构造
:_ptr(sp._ptr)
,_pcount(sp._pcount)
, _pmtx(sp._pmtx)
{
AddRef();
}
//sp1=sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp)//赋值
{
if (_ptr!=sp._ptr)//防止自己给自己赋值
{
Release();//释放的是sp1
//两个指针指向同一块空间,再++引用计数
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
AddRef();//增加的是sp3的引用计数
}
return *this;
}
//查看引用计数
int use_count()
{
return *_pcount;
}
~shared_ptr()
{
Release();
}
private:
T* _ptr;
int* _pcount;
std::mutex* _pmtx;
};
加锁之后,shared_ptr的引用计数的加加和减减操作变成了原子性的。
智能指针对象本身是线程安全的,它的引用计数的加加和减减操作是加过锁的,但是它指向管理的资源不一定是线程安全的,资源通常都是动态开辟的,动态开辟在堆上,多线程通过智能指针解引用这份资源,不是线程安全的,智能指针只是可以访问这份资源,去修改资源时,并不会加锁,不是线程安全的,想要实现资源的线程安全要通过资源本身的加锁来实现。
shared_ptr的循环引用问题
shared_ptr的循环引用问题是发生在特定场景下的问题:
struct ListNode
{
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_shared_ptr_cycleRef()
{
std::shared_ptr<ListNode> cur(new ListNode);
std::shared_ptr<ListNode> next(new ListNode);
cur->_next = next;
next->_prev = cur;
}
int main()
{
test_shared_ptr_cycleRef();
system("pause");
return 0;
}
在上面代码中,shared_ptr不仅管理了cur和next,同时也管理了cur里面的_prev和next里面的_next,当shared_ptr指向cur和next时,两个智能指针的引用计数都是1,再让cur的_next指向next,让next的_prev指向cur,此时两个智能指针的引用计数都变成了2,出了作用域,cur和next的生命周期结束,cur销毁,其引用计数减1,next销毁,其引用计数减1,此时next结点由cur中的_next管理,cur结点由next中的_prev管理。也就是说cur的释放取决于next,而next的释放又依赖于cur,这就是循环引用,具体过程如下图所示:
为了解决shared_ptr的循环引用问题,引入了weak_ptr,weak_ptr是一个特殊的智能指针,它不具备其它智能指针所具备的特点:RAII,像指针一样。它是专门解决shared_ptr的循环引用问题的。
struct ListNode
{
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
weak_ptr解决循环引用的原理是:当已经有对象管理这份资源时,如果再发生赋值之类的操作生成新的对象来二次管理资源时,weak_ptr不会增加shared_ptr的引用计数,也就是不让新的对象接管这份资源。 还有一个需要注意的是:weak_ptr在常规场景下,是不能用的,它只能用在shared_ptr的循环引用的场景。
shared_ptr的定制删除器(定制删除的方式)
shared_ptr里面管理的都是单独的指针,释放时都是用delete进行释放的,如果现在使用new[]申请出来的,就应该用delete[]进行释放,资源不仅仅可以使用new和new[],还可以使用malloc,这时候,就要用对应的接口进行释放。这时shared_ptr使用定制删除器来解决此类问题,而删除器又是通过仿函数来定制的。 具体实现代码如下:
template<class T>
struct DeleteArray
{
void operator()(T* ptr)//仿函数
{
delete[] ptr;
}
};
struct B
{
~B()
{
cout << "~B()" << endl;
}
};
//定制删除器(定制删除的方式,一般默认是delete)
void test_shared_ptr_deletor()
{
DeleteArray<B> del;
std::shared_ptr<B> sp(new B[10],del);
}
int main()
{
test_shared_ptr_deletor();
system("pause");
return 0;
}
通过定制删除器,shared_ptr不仅可以释放new出来的,还可以释放new[]出来的,也可以释放malloc出来的,只用给定一个free的仿函数传过去即可。
C++智能指针的发展历史
所有的智能指针,除了weak_ptr,其它智能指针都符合:RAII和像指针一样这两个特性。
- C++ 98 中产生了第一个智能指针auto_ptr,但是后来被标记为有缺陷的智能指针.
- C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
- C++ TR1,引入了shared_ptr等。不过TR1并不是标准版。
- C++ boost的scoped_ptr相当于C++ 11的unique_ptr,也是防拷贝的,而boost的shared_ptr和weak_ptr和C++11中的和shared_ptr和weak_ptr作用是一样的。
- C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。