背景
c++中智能指针shared_ptr用于自动管理资源,通过引用计数来记录资源被多少出地方使用。在不使用资源时,减少引用计数,如果引用计数为0,表示资源不会再被使用,此时会释放资源。本文记录对c++中std::shared_ptr的源码学习。
为什么需要“智能”指针
当我们需要在堆上分配对象时,可以通过new来创建,在不需要使用对象时,需要使用delete来释放对象占用的资源,如下所示:
void foo() {
...
int *pi = new int(0);
...
delete pi;
...
}
如果在delete之前,函数提前return,则new出来的内存不会被释放,导致内存泄露。另外,如果一个对象在函数中被new出来,并作为返回值返回,但是使用者如果不清楚函数内部实现,也可能忘记delete调new出来的对象,导致内存泄露,如下所示:
int* foo() {
return new int(0);
}
void bar() {
int *pi = foo();
...
return; // 因为不知道foo返回的int*是new出来的,所以没有delete pi
}
对上面这种场景,还可能会出现一个指针被多处使用。为了确保指针在使用过程中没有被delete,还需要关注指针使用的先后顺序。但在迭代的过程中,这会导致指针的使用变得很难维护。为此,我们需要有一个“智能”的指针,维护我们new出来的对象,并在不需要的时候自动delete,释放资源。
智能指针如何“智能”
c++智能指针利用class的构造函数和析构函数来对指针管理,构造函数中将new出来的指针传入,在析构函数中判断如果没人再使用,则会释放指针。
如何判断指针还被使用呢?答案是使用引用计数。只要指针被引用了,引用计数加1,当某一个引用不再使用(析构之后),则引用计数减1,如果指针不再被使用时,引用计数为0,此时即可delete指针,释放资源。
那如何知道一个指针被引用了呢?答案就是复制构造和赋值构造。如果不知道什么是复制构造和赋值构造,可以先去学习下,以下是个简单的例子:
class A {
public:
// 构造函数
A() {
x = 0;
}
// 析构函数
~A() {
}
// 复制构造
A(const A &a) {
x = a.x;
}
// 赋值构造
A& operate=(cosnt A &a) {
x = a.x;
return *this;
}
private:
int x;
}
int main() {
A a1; // 构造函数
A a2(a1); // 复制构造
A a3 = a1; // 赋值构造
}
// 离开作用域,a1, a2, a3析构
shared_ptr概览
shared_ptr定义如下:
/**
* @brief A smart pointer with reference-counted copy semantics.
*
* The object pointed to is deleted when the last shared_ptr pointing to
* it is destroyed or reset.
*/
template<typename _Tp>
class shared_ptr : public __shared_ptr<_Tp>
{
...
}
即shared_ptr继承__shared_ptr,__shared_ptr定义如下:
template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr
: public __shared_ptr_access<_Tp, _Lp>
{
public:
using element_type = typename remove_extent<_Tp>::type;
...
private:
element_type* _M_ptr; // Contained pointer.
__shared_count<_Lp> _M_refcount; // Reference counter.
}
从注释中可以看出,__shared_ptr中有两个成员变量:_M_ptr和_M_refcount。_M_ptr是智能指针管理的资源,_M_refcount是引用计数。再看下_M_refcount的类型定义,即__shared_count:
template<_Lock_policy _Lp>
class __shared_count
{
...
private:
_Sp_counted_base<_Lp>* _M_pi;
};
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base
: public _Mutex_base<_Lp>
{
...
private:
_Atomic_word _M_use_count; // #shared
...
};
typedef int _Atomic_word;
可以看出,引用计数本质是一个int值:_M_use_count。
shared_ptr简单的类图示意如下:
根据智能指针实现方式,我们主要考虑构造函数、析构函数、复制构造函数、赋值构造函数。
构造函数
用以下代码作为示例说明智能指针构造过程:
class A {
public:
A() {
cout << "construct A" << endl;
}
~A() {
cout << "deconstruct A" << endl;
}
};
void test_init() {
shared_ptr<A> pa(new A);
}
当test函数被调用是,创建了一个A指针,该指针作为shared_ptr入参传入构造函数,shared_ptr构造函数调用过程如下:
// shared_ptr构造函数
/**
* @brief Construct a %shared_ptr that owns the pointer @a __p.
* @param __p A pointer that is convertible to element_type*.
* @post use_count() == 1 && get() == __p
* @throw std::bad_alloc, in which case @c delete @a __p is called.
*/
template<typename _Yp, typename = _Constructible<_Yp*>>
explicit
shared_ptr(_Yp* __p) : __shared_ptr<_Tp>(__p) { }
// __shared_ptr构造函数,本文考虑指针不是数组指针,即is_array<_Tp>::type() == false_type
template<typename _Yp, typename = _SafeConv<_Yp>>
explicit
__shared_ptr(_Yp* __p)
: _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type())
{
...
}
// 引用计数__shared_ptr::_M_refcount(__shared_count类型)构造函数
template<typename _Ptr>
__shared_count(_Ptr __p, /* is_array = */ false_type)
: __shared_count(__p)
{ }
template<typename _Ptr>
explicit
__shared_count(_Ptr __p) : _M_pi(0)
{
__try
{
_M_pi = new _Sp_counted_ptr<_Ptr, _Lp>(__p);
}
__catch(...)
{
delete __p;
__throw_exception_again;
}
}
// __shared_count::_M_pi构造函数
// Counted ptr with no deleter or allocator support
template<typename _Ptr, _Lock_policy _Lp>
class _Sp_counted_ptr final : public _Sp_counted_base<_Lp>
{
public:
explicit
_Sp_counted_ptr(_Ptr __p) noexcept
: _M_ptr(__p) { }
...
private:
_Ptr _M_ptr;
};
_Sp_counted_base() noexcept
: _M_use_count(1), _M_weak_count(1) { }
可以看到,new创建的指针被存在__shared_ptr::_M_ptr中,同时,__shared_ptr::_M_refcount构造函数中,new创建了_M_refcount::_M_pi,这样,引用计数才不会因为__shared_ptr析构而消失,引用计数的生命周期应该与指针的生命周期一致,才能记录指针被引用的次数。__Sp_counted_base构造函数中,初始化引用计数_M_use_count为1,表示当前只有1处在使用该指针。
构造函数调用结束之后,shared_ptr数据可简单表示为:
(为什么内存布局是这样?可查看附录中的测试代码。)
复制构造
以下代码被调用时,智能指针的复制构造函数被调用:
int test_copy() {
shared_ptr<A> pa(new A);
shared_ptr<A> pa_copy(pa); // 复制构造
}
复制构造主要实现代码如下:
template<typename _Tp>
class shared_ptr : public __shared_ptr<_Tp>
{
...
public:
...
shared_ptr(const shared_ptr&) noexcept = default; // 默认复制构造
...
}
template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr
: public __shared_ptr_access<_Tp, _Lp>
{
public:
using element_type = typename remove_extent<_Tp>::type;
...
__shared_ptr(const __shared_ptr&) noexcept = default; // 默认复制构造
...
private:
...
element_type* _M_ptr; // Contained pointer.
__shared_count<_Lp> _M_refcount; // Reference counter.
}
复制构造使用编译器生成的默认复制构造函数,伪代码可以表示成:
template<typename _Tp>
class shared_ptr : public __shared_ptr<_Tp>
{
...
public:
...
shared_ptr(const shared_ptr& ptr) noexcept {
_M_ptr = __ptr._M_ptr;
_M_refcount.__shared_count::__shared_count(__ptr._M_refcount);
}
...
}
继续看__shared_count的复制构造函数:
template<_Lock_policy _Lp>
class __shared_count
{
...
public:
__shared_count(const __shared_count& __r) noexcept
: _M_pi(__r._M_pi)
{
if (_M_pi != 0)
_M_pi->_M_add_ref_copy();
}
...
private:
...
_Sp_counted_base<_Lp>* _M_pi;
}
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base
: public _Mutex_base<_Lp>
{
public:
...
void
_M_add_ref_copy()
{ __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }
...
private:
_Sp_counted_base(_Sp_counted_base const&) = delete;
_Sp_counted_base& operator=(_Sp_counted_base const&) = delete;
_Atomic_word _M_use_count; // #shared
_Atomic_word _M_weak_count; // #weak + (#shared != 0)
};
namespace __gnu_cxx _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION
// Functions for portable atomic access.
// To abstract locking primitives across all thread policies, use:
// __exchange_and_add_dispatch
// __atomic_add_dispatch
#ifdef _GLIBCXX_ATOMIC_BUILTINS
static inline _Atomic_word
__exchange_and_add(volatile _Atomic_word* __mem, int __val)
{ return __atomic_fetch_add(__mem, __val, __ATOMIC_ACQ_REL); }
...
static inline void
__attribute__ ((__unused__))
__atomic_add_dispatch(_Atomic_word* __mem, int __val)
{
#ifdef __GTHREADS
if (__gthread_active_p())
__atomic_add(__mem, __val);
else
__atomic_add_single(__mem, __val);
#else
__atomic_add_single(__mem, __val);
#endif
}
_GLIBCXX_END_NAMESPACE_VERSION
} // namespace
可以看到,复制构造函数最终就是将引用计数加1,且是通过原子操作来进行加1的,由此可见:智能指针是线程安全的!至于如何实现的线程安全,本文暂不深究。
赋值构造
类似地,以下函数调用时,智能指针的赋值构造函数被调用:
int test_assgin() {
shared_ptr<A> pa(new A);
shared_ptr<A> pa_assign(new A);
pa_assign = pa; // 赋值构造
}
比复制构造稍微复杂一点,赋值构造的实现如下:
template<typename _Tp>
class shared_ptr : public __shared_ptr<_Tp>
{
...
public:
...
shared_ptr& operator=(const shared_ptr&) noexcept = default;
...
}
template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr
: public __shared_ptr_access<_Tp, _Lp>
{
public:
using element_type = typename remove_extent<_Tp>::type;
...
__shared_ptr& operator=(const __shared_ptr&) noexcept = default;
...
private:
...
element_type* _M_ptr; // Contained pointer.
__shared_count<_Lp> _M_refcount; // Reference counter.
}
template<_Lock_policy _Lp>
class __shared_count
{
...
public:
...
__shared_count&
operator=(const __shared_count& __r) noexcept
{
_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
if (__tmp != _M_pi)
{
if (__tmp != 0)
__tmp->_M_add_ref_copy();
if (_M_pi != 0)
_M_pi->_M_release();
_M_pi = __tmp;
}
return *this;
}
...
private:
...
_Sp_counted_base<_Lp>* _M_pi;
}
template<typename _Ptr, _Lock_policy _Lp>
class _Sp_counted_ptr final : public _Sp_counted_base<_Lp>
{
public:
...
virtual void
_M_dispose() noexcept
{ delete _M_ptr; }
virtual void
_M_destroy() noexcept
{ delete this; }
...
}
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base
: public _Mutex_base<_Lp>
{
public:
...
void
_M_add_ref_copy()
{ __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }
...
void
_M_release() noexcept
{
// Be race-detector-friendly. For more info see bits/c++config.
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
_M_dispose();
// There must be a memory barrier between dispose() and destroy()
// to ensure that the effects of dispose() are observed in the
// thread that runs destroy().
// See http://gcc.gnu.org/ml/libstdc++/2005-11/msg00136.html
if (_Mutex_base<_Lp>::_S_need_barriers)
{
__atomic_thread_fence (__ATOMIC_ACQ_REL);
}
// Be race-detector-friendly. For more info see bits/c++config.
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count,
-1) == 1)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
_M_destroy();
}
}
}
...
private:
_Sp_counted_base(_Sp_counted_base const&) = delete;
_Sp_counted_base& operator=(_Sp_counted_base const&) = delete;
_Atomic_word _M_use_count; // #shared
_Atomic_word _M_weak_count; // #weak + (#shared != 0)
};
namespace __gnu_cxx _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION
...
static inline _Atomic_word
__attribute__ ((__unused__))
__exchange_and_add_dispatch(_Atomic_word* __mem, int __val)
{
#ifdef __GTHREADS
if (__gthread_active_p())
return __exchange_and_add(__mem, __val);
else
return __exchange_and_add_single(__mem, __val);
#else
return __exchange_and_add_single(__mem, __val);
#endif
}
...
_GLIBCXX_END_NAMESPACE_VERSION
} // namespace
赋值构造会将被复制的对象引用计数加1,同时,将自身的引用计数减1,如果减1之前,自身的引用计数已经为1,则将释放持有的指针:_M_dispose(),并且weak_count=1时,也会将自身delete掉:_M_destroy()。整个操作也是线程安全的。
析构函数
当类离开其作用域时,析构函数会被调用:
int test_destroy() {
{
shared_ptr<A> pa(new A);
}
// 此时, pa的析构函数已经被调用
}
析构函数的实现如下:
// shared_ptr没有显式定义析构函数
template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr
: public __shared_ptr_access<_Tp, _Lp>
{
public:
using element_type = typename remove_extent<_Tp>::type;
...
~__shared_ptr() = default;
...
private:
...
element_type* _M_ptr; // Contained pointer.
__shared_count<_Lp> _M_refcount; // Reference counter.
}
template<_Lock_policy _Lp>
class __shared_count
{
...
public:
...
~__shared_count() noexcept
{
if (_M_pi != nullptr)
_M_pi->_M_release();
}
...
private:
...
_Sp_counted_base<_Lp>* _M_pi;
}
析构函数调用引用计数的_M_release(),即引用计数减1,且如果资源不再使用,则释放资源。
像使用指针一样
在使用智能指针时,为了能像使用指针一样,智能指针对操作符*、->进行了重载,我们就能像下面这样使用智能指针了:
struct B {
int b;
};
void test() {
B *pb = new B;
shared_ptr<B> shared_pb(pb);
shared_pb->b = 1; // 类似于pb->b = 1
B b = *shared_pb; // 类似于B b = *pb;
}
重载操作符的实现如下:
template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr_access<_Tp, _Lp, true, false>
{
public:
using element_type = typename remove_extent<_Tp>::type;
#if __cplusplus <= 201402L
[[__deprecated__("shared_ptr<T[]>::operator* is absent from C++17")]]
element_type&
operator*() const noexcept
{
__glibcxx_assert(_M_get() != nullptr);
return *_M_get();
}
[[__deprecated__("shared_ptr<T[]>::operator-> is absent from C++17")]]
element_type*
operator->() const noexcept
{
_GLIBCXX_DEBUG_PEDASSERT(_M_get() != nullptr);
return _M_get();
}
#endif
...
private:
element_type*
_M_get() const noexcept
{ return static_cast<const __shared_ptr<_Tp, _Lp>*>(this)->get(); }
};
template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr
: public __shared_ptr_access<_Tp, _Lp>
{
public:
using element_type = typename remove_extent<_Tp>::type;
...
element_type*
get() const noexcept
{ return _M_ptr; }
...
private:
element_type* _M_ptr; // Contained pointer.
__shared_count<_Lp> _M_refcount; // Reference counter.
};
总结
总结一下,智能指针,就是申请资源(本文中的例子是指针)之后,将资源交给智能指针,智能指针复制、赋值时原子增加、减少引用计数,并在引用计数为0,即没有人再使用资源时,释放资源,并将引用计数释放掉(引用计数也是new申请出来的),以此来实现“智能”管理资源。
附录
shared_ptr内存布局测试
测试代码如下:
# include <memory>
# include <iostream>
# include <stdint.h>
using namespace std;
class A {
public:
A() {
cout << "construct A" << endl;
}
~A() {
cout << "deconstruct A" << endl;
}
};
/*
__shared_ptr:
element_type* _M_ptr; // Contained pointer.
__shared_count<_Lp> _M_refcount; // Reference counter.
__shared_count:
_Sp_counted_base<_Lp>* _M_pi;
_Sp_counted_ptr:
void *vptr;
_Atomic_word _M_use_count; // #shared
_Atomic_word _M_weak_count; // #weak + (#shared != 0)
_Ptr _M_ptr;
*/
int main() {
{
A *pa = new A();
shared_ptr<A> shared_pa(pa);
char *addr = (char*)&shared_pa;
void *shared_pa_addr = *((void**)addr);
void *_M_pi = *((void**)(addr+8));
printf("pa: 0x%X, shared_pa_addr: 0x%X, _M_pi: 0x%X\n", pa, shared_pa_addr, _M_pi);
addr = (char*)_M_pi;
void *vptr = *((void**)addr);
int _M_use_count = *((int*)(addr+8));
int _M_weak_count = *((int*)(addr+12));
void *_M_ptr = *((void**)(addr+16));
printf("vptr: 0x%X, _M_use_count: %d, _M_weak_count: %d, _M_ptr: 0x%X\n", vptr, _M_use_count, _M_weak_count, _M_ptr);
}
cout << "move scope" << endl;
}
面试要求写一个智能指针
面试中可能会被要求实现一个智能指针,先不考虑线程安全、各种场景的兼容,可以对上面代码进行精简,仅对核心逻辑做实现:
#ifndef M_SMART_POINTER_H
#define M_SMART_POINTER_H
template<typename T>
class MSmartPointer {
public:
MSmartPointer(T *ptr_) {
ptr = ptr_;
ref_count_ptr = new int(1);
}
MSmartPointer(const MSmartPointer &r) {
Copy(r);
}
MSmartPointer& operator=(const MSmartPointer &r) {
if (r.ptr != ptr) {
Release();
Copy(r);
}
return *this;
}
~MSmartPointer() {
Release();
}
T& operator*() {
return *ptr;
}
T* operator->() {
return ptr;
}
int ref_count() {
return *ref_count_ptr;
}
private:
void Copy(const MSmartPointer &r) {
ptr = r.ptr; // 持有指针
ref_count_ptr = r.ref_count_ptr; // 持有指针对应的引用计数
if (ref_count_ptr)
(*ref_count_ptr)++; // 引用计数加1
}
void Release() {
if (!ref_count_ptr) return;
if (--(*ref_count_ptr) <= 0) { // 引用计数减1,如果引用计数小于等于0,代表没人用了,需要释放资源
delete ref_count_ptr; // 释放引用计数
ref_count_ptr = nullptr;
delete ptr; // 释放持有的指针
ptr = nullptr;
}
}
private:
T *ptr;
int *ref_count_ptr;
};
#endif
附上测试代码:
void test_m_smart_pointer() {
cout << "test_m_smart_pointer" << endl;
{
MSmartPointer<Test> p(new Test);
cout << "1. ref_count: " << p.ref_count() << endl;
p->t = 1;
cout << "1. t: " << p->t << endl;
{
MSmartPointer<Test> p1(p);
cout << "2. ref_count: " << p.ref_count() << endl;
(*(p1)).t = 2;
cout << "2. t: " << p->t << endl;
MSmartPointer<Test> p2(new Test);
p2 = p;
cout << "3. ref_count: " << p.ref_count() << endl;
}
cout << "4. ref_count: " << p.ref_count() << endl;
}
cout << "move scope" << endl;
}
线程安全思考
在对智能指针的引用计数进行修改时,都使用额原子操作,因此引用计数的加减都是现成安全的。但是,在释放资源时,其整个释放的操作为非原子的。如果在释放资源的过程,发生了复制、赋值,则就可能出现资源被释放,但是还有地方在引用的问题。不考虑多线程场景,以下面的代码举例:
void test_atomic_thread_safe() {
A *pa = new A;
shared_ptr<A> shared_pa(pa);
shared_pa.__shared_ptr<A>::~__shared_ptr<A>();
shared_ptr<A> shared_pb(pa);
cout << "test call deconstruct: " << shared_pb.use_count() << endl;
}
这段代码运行结果如下:
construct A
deconstruct A
test call deconstruct: 1
deconstruct A
deconstruct A
free(): double free detected in tcache 2
[1] 6372 abort (core dumped) ./output/main
可以看到,在shared_pa被析构之后,再去对其复制,将导致持有的指针不再有效。但是,正常情况下,谁会主动掉析构函数呢?如果我们不主动调析构函数,则析构函数被调用时,我们也取不到对象了,更别提对其引用。所以总体看来,智能指针还是线程安全的。