背景
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*>>explicitshared_ptr(_Yp* __p) : __shared_ptr<_Tp>(__p) { }// __shared_ptr构造函数,本文考虑指针不是数组指针,即is_array<_Tp>::type() == false_typetemplate<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 supporttemplate<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_BUILTINSstatic 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 __GTHREADSif (__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.htmlif (_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 __GTHREADSif (__gthread_active_p())return __exchange_and_add(__mem, __val);elsereturn __exchange_and_add_single(__mem, __val);
#elsereturn __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 = 1B 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_Htemplate<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被析构之后,再去对其复制,将导致持有的指针不再有效。但是,正常情况下,谁会主动掉析构函数呢?如果我们不主动调析构函数,则析构函数被调用时,我们也取不到对象了,更别提对其引用。所以总体看来,智能指针还是线程安全的。