智能指针在 C++11 标准中被引入真正标准库(C++98 中引入的 auto_ptr 存在较多问题),但目前很多 C++开发者仍习惯用原生指针,视智能指针为洪水猛兽。但很多实际场景下,智能指针却是解决问题的神器,尤其是一些涉及多线程的场景下。本文将介绍智能指针可以解决的问题,用法及最佳实践。并且根据源码分析智能指针的实现原理。
C++在堆上申请内存后,需要手动对内存进行释放。代码的初创者可能会注意内存的释放,但随着代码协作者加入,或者随着代码日趋复杂,很难保证内存都被正确释放。
尤其是一些代码分支在开发中没有被完全测试覆盖的时候,就算是内存泄漏检查工具也不一定能检查到内存泄漏。
void test_memory_leak(bool open)
{
A *a = new A();
if(open)
{
// 代码变复杂过程中,很可能漏了 delete(a);
return;
}
delete(a);
return;
}
多线程遇上对象析构,是一个很难的问题,稍有不慎就会导致程序崩溃。因此在对于 C++开发者而言,经常会使用静态单例来使得对象常驻内存,避免析构带来的问题。这势必会造成内存泄露,当单例对象比较大,或者程序对内存非常敏感的时候,就必须面对这个问题了。
先以一个常见的 C++多线程问题为例,介绍多线程下的对象析构问题。
比如我们在开发过程中,经常会在一个 Class 中创建一个线程,这个线程读取外部对象的成员变量。
// 日志上报Class
class ReportClass
{
private:
ReportClass() {}
ReportClass(const ReportClass&) = delete;
ReportClass& operator=(const ReportClass&) = delete;
ReportClass(const ReportClass&&) = delete;
ReportClass& operator=(const ReportClass&&) = delete;
private:
std::mutex mutex_;
int count_ = 0;
void addWorkThread();
public:
void pushEvent(std::string event);
private:
static void workThread(ReportClass *report);
private:
static ReportClass* instance_;
static std::mutex static_mutex_;
public:
static ReportClass* GetInstance();
static void ReleaseInstance();
};
std::mutex ReportClass::static_mutex_;
ReportClass* ReportClass::instance_;
ReportClass* ReportClass::GetInstance()
{
// 单例简单实现,非本文重点
std::lock_guard<std::mutex> lock(static_mutex_);
if (instance_ == nullptr) {
instance_ = new ReportClass();
instance_->addWorkThread();
}
return instance_;
}
void ReportClass::ReleaseInstance()
{
std::lock_guard<std::mutex> lock(static_mutex_);
if(instance_ != nullptr)
{
delete instance_;
instance_ = nullptr;
}
}
// 轮询上报线程
void ReportClass::workThread(ReportClass *report)
{
while(true)
{
// 线程运行过程中,report可能已经被销毁了
std::unique_lock<std::mutex> lock(report->mutex_);
if(report->count_ > 0)
{
report->count_--;
}
usleep(1000*1000);
}
}
// 创建任务线程
void ReportClass::addWorkThread()
{
std::thread new_thread(workThread, this);
new_thread.detach();
}
// 外部调用
void ReportClass::pushEvent(std::string event)
{
std::unique_lock<std::mutex> lock(mutex_);
this->count_++;
}
使用 ReportClass 的代码如下:
ReportClass::GetInstance()->pushEvent("test");
但当这个外部对象(即ReportClass
)析构时,对象创建的线程还在执行。此时线程引用的对象指针为野指针,程序必然会发生异常。
解决这个问题的思路是在对象析构的时候,对线程进行join
。
// 日志上报Class
class ReportClass
{
private:
//...
~ReportClass();
private:
//...
bool stop_ = false;
std::thread *work_thread_;
//...
};
// 轮询上报线程
void ReportClass::workThread(ReportClass *report)
{
while(true)
{
std::unique_lock<std::mutex> lock(report->mutex_);
// 如果上报停止,不再轮询上报
if(report->stop_)
{
break;
}
if(report->count_ > 0)
{
report->count_--;
}
usleep(1000*1000);
}
}
// 创建任务线程
void ReportClass::addWorkThread()
{
// 保存线程指针,不再使用分离线程
work_thread_ = new std::thread(workThread, this);
}
ReportClass::~ReportClass()
{
// 通过join来停止内部线程
stop_ = true;
work_thread_->join();
delete work_thread_;
work_thread_ = nullptr;
}
这种方式看起来没问题了,但是由于这个对象一般是被多个线程使用。假如某个线程想要释放这个对象,但另外一个线程还在使用这个对象,可能会出现野指针问题。就算释放对象的线程将对象释放后将指针置为nullptr
,但仍然可能在多线程下在指针置空前被另外一个线程取得地址并使用。
线程 A | 线程 B |
---|---|
ReportClass::GetInstance()->ReleaseInstance(); | ReportClass *report = ReportClass::GetInstance(); if(report) { // 此时切换到线程 A report->pushEvent("test"); } |
此种场景下,锁机制已经很难解决这个问题。对于多线程下的对象析构问题,智能指针可谓是神器。接下来我们先对智能指针的基本用法进行说明。
智能指针设计的初衷就是可以帮助我们管理堆上申请的内存,可以理解为开发者只需要申请,而释放交给智能指针。
目前 C++11 主要支持的智能指针为以下几种
先上代码
class A
{
public:
void do_something() {}
};
void test_unique_ptr(bool open)
{
std::unique_ptr a(new A());
a->do_something();
if(open)
{
// 不再需要手动释放内存
return;
}
// 不再需要手动释放内存
return;
}
unique_ptr
的核心特点就如它的名字一样,它拥有对持有对象的唯一所有权。即两个unique_ptr
不能同时指向同一个对象。
1、unique_ptr
不能被复制到另外一个unique_ptr
2、unique_ptr
所持有的对象只能通过转移语义将所有权转移到另外一个unique_ptr
std::unique_ptr
a1(new A());
std::unique_ptr a2 = a1;//编译报错,不允许复制
std::unique_ptr a3 = std::move(a1);//可以转移所有权,所有权转义后a1不再拥有任何指针
智能指针有一个通用的规则,就是->
表示用于调用指针原有的方法,而.
则表示调用智能指针本身的方法。
3、release() 释放所管理指针的所有权,返回原生指针。但并不销毁原生指针。
4、reset() 释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针
std::unique_ptr
a1(new A());
A *origin_a = a1.get();//尽量不要暴露原生指针
if(a1)
{
// a1 拥有指针
}
std::unique_ptr a2(a1.release());//常见用法,转义拥有权
a2.reset(new A());//释放并销毁原有对象,持有一个新对象
a2.reset();//释放并销毁原有对象,等同于下面的写法
a2 = nullptr;//释放并销毁原有对象
与unique_ptr
的唯一所有权所不同的是,shared_ptr
强调的是共享所有权。也就是说多个shared_ptr
可以拥有同一个原生指针的所有权。
std::shared_ptr
a1(new A());
std::shared_ptr a2 = a1;//编译正常,允许所有权的共享
shared_ptr
是通过引用计数的方式管理指针,当引用计数为 0 时会销毁拥有的原生对象。
3、reset() 释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针
4、unique() 如果引用计数为 1,则返回 true,否则返回 false
std::shared_ptr
a1(new A());
std::shared_ptr a2 = a1;//编译正常,允许所有权的共享
A *origin_a = a1.get();//尽量不要暴露原生指针
if(a1)
{
// a1 拥有指针
}
if(a1.unique())
{
// 如果返回true,引用计数为1
}
long a1_use_count = a1.use_count();//引用计数数量
std::shared_ptr
a1(new A());
std::weak_ptr weak_a1 = a1;//不增加引用计数
1、expired() 判断所指向的原生指针是否被释放,如果被释放了返回 true,否则返回 false
3、lock() 返回 shared_ptr,如果原生指针没有被释放,则返回一个非空的 shared_ptr,否则返回一个空的 shared_ptr
std::shared_ptr
a1(new A());
std::weak_ptr weak_a1 = a1;//不增加引用计数
if(weak_a1.expired())
{
//如果为true,weak_a1对应的原生指针已经被释放了
}
long a1_use_count = weak_a1.use_count();//引用计数数量
if(std::shared_ptr shared_a = weak_a1.lock())
{
//此时可以通过shared_a进行原生指针的方法调用
}
weak_a1.reset();//将weak_a1置空
这一节我们会基于基本用法,进一步说明智能指针的实践用法,一起驯服智能指针这头野兽。
1、unique_ptr
独占对象的所有权,由于没有引用计数,因此性能较好
3、weak_ptr
配合shared_ptr
,解决循环引用的问题
由于性能问题,那么可以粗暴的理解:优先使用unique_ptr
。但由于unique_ptr
不能进行复制,因此部分场景下不能使用的。
unique_ptr
一般在不需要多个指向同一个对象的指针时使用。但这个条件本身就很难判断,在我看来可以简单的理解:这个对象在对象或方法内部使用时优先使用unique_ptr
。
class TestUnique
{
private:
std::unique_ptra_ = std::unique_ptr(new A());
public:
void process1()
{
a_->do_something();
}
void process2()
{
a_->do_something();
}
~TestUnique()
{
//此处不再需要手动删除a_
}
};
void test_unique_ptr()
{
std::unique_ptra(new A());
a->do_something();
}
shared_ptr
一般在需要多个执行同一个对象的指针使用。在我看来可以简单的理解:这个对象需要被多个 Class 同时使用的时候。
class B
{
private:
std::shared_ptra_;
public:
B(std::shared_ptr& a): a_(a) {}
};
class C
{
private:
std::shared_ptr a_;
public:
C(std::shared_ptr& a): a_(a) {}
};
std::shared_ptr b_;
std::shared_ptrc_; a = std::make_shared();
void test_A_B_C()
{
std::shared_ptr
b_ = std::make_shared(a);
c_ = std::make_shared(a);
}
在上面的代码中需要注意,我们使用std::make_shared
代替new
的方式创建shared_ptr
。
因为使用new
的方式创建shared_ptr
会导致出现两次内存申请,而std::make_shared
在内部实现时只会申请一个内存。因此建议后续均使用std::make_shared
。
如果A
想要调用B
和C
的方法怎么办呢?可否在A
中定义B
和C
的shared_ptr
呢?答案是不可以,这样会产生循环引用,导致内存泄露。
class A
{
private:
std::weak_ptr b_;
std::weak_ptr c_;
public:
void do_something() {}
void set_B_C(const std::shared_ptr& b, const std::shared_ptr& c)
{
b_ = b;
c_ = c;
}
};
a->set_B_C(b_, c_);
如果想要在A
内部将当前对象的指针共享给其他对象,需要怎么处理呢?
class D
{
private:
std::shared_ptra_;
public:
std::shared_ptr& a): a_(a) {}
};
class A
{
//上述代码省略
public:
void new_D()
{
//错误方式,用this指针重新构造shared_ptr,将导致二次释放当前对象
std::shared_ptr this_shared_ptr1(this);
std::unique_ptrd1(new D(this_shared_ptr1)) ;
}
};
此时就需要引入shared_from_this
。对象继承了enable_shared_from_this
后,可以通过shared_from_this()
获取当前对象的shared_ptr
指针。
class A: public std::enable_shared_from_this
{
//上述代码省略
public:
void new_D()
{
//错误方式,用this指针重新构造shared_ptr,将导致二次释放当前对象
std::shared_ptr this_shared_ptr1(this);
std::unique_ptrd1(new D(this_shared_ptr1)) ;
//正确方式
std::shared_ptr this_shared_ptr2 = shared_from_this();
std::unique_ptrd2(new D(this_shared_ptr2)) ;
}
};
智能指针的使用时有较多常见的错误用法,可能会导致程序异常。下面我会列举这些错误用法,开发时需要避免。
很多开发同学(包括我在内)在最开始使用智能指针的时候,对同一个对象会混用智能指针和原生指针,导致程序异常。
void incorrect_smart_pointer1()
{
A *a= new A();
std::unique_ptrunique_ptr_a(a);
// 此处将导致对象的二次释放
delete a;
}
如果将一个原生指针交个多个智能指针,这些智能指针释放对象时会产生对象的多次销毁
void incorrect_smart_pointer2()
{
A *a= new A();
std::unique_ptrunique_ptr_a1(a);
std::unique_ptr unique_ptr_a2(a);// 此处将导致对象的二次释放
}
void incorrect_smart_pointer3()
{
std::shared_ptrshared_ptr_a1 = std::make_shared();
A *a= shared_ptr_a1.get();
std::shared_ptr shared_ptr_a2(a);// 此处将导致对象的二次释放
delete a;// 此处也将导致对象的二次释放
}
class E
{
void use_this()
{
//错误方式,用this指针重新构造shared_ptr,将导致二次释放当前对象
std::shared_ptr this_shared_ptr1(this) ;
}
};
std::shared_ptr e = std::make_shared();
栈上对象本身在出栈时就会被自动销毁,如果将其指针交给智能指针,会造成对象的二次销毁
void incorrect_smart_pointer5()
{
int int_num = 3;
std::unique_ptr<int> int_unique_ptr(&int_num);
}
有了智能指针之后,我们就可以使用智能指针解决多线程下的对象析构问题。
// 日志上报Class
class ReportClass: public std::enable_shared_from_this
{
//...
private:
static void workThread(std::weak_ptr weak_report_ptr) ;
private:
static std::shared_ptr instance_;
static std::mutex static_mutex_;
public:
static std::shared_ptr GetInstance() ;
static void ReleaseInstance();
};
std::mutex ReportClass::static_mutex_;
std::shared_ptr ReportClass::instance_;
std::shared_ptr ReportClass::GetInstance()
{
// 单例简单实现,非本文重点
std::lock_guard<std::mutex> lock(static_mutex_);
if (!instance_) {
instance_ = std::shared_ptr(new ReportClass());
instance_->addWorkThread();
}
return instance_;
}
void ReportClass::ReleaseInstance()
{
std::lock_guard<std::mutex> lock(static_mutex_);
if(instance_)
{
instance_.reset();
}
}
// 轮询上报线程
void ReportClass::workThread(std::weak_ptr weak_report_ptr)
{
while(true)
{
std::shared_ptr shared_report_ptr = weak_report_ptr.lock();
if(!shared_report_ptr)
{
return;
}
std::unique_lock<std::mutex>(shared_report_ptr->mutex_);
if(shared_report_ptr->count_ > 0)
{
shared_report_ptr->count_--;
}
usleep(1000*1000);
}
}
// 创建任务线程
void ReportClass::addWorkThread()
{
std::weak_ptr weak_report_ptr = shared_from_this();
std::thread work_thread(workThread, weak_report_ptr);
work_thread.detach();
}
// 外部调用
void ReportClass::pushEvent(std::string event)
{
std::unique_lock<std::mutex> lock(mutex_);
this->count_++;
}
在介绍智能指针源码前,需要明确的是,智能指针本身是一个栈上分配的对象。根据栈上分配的特性,在离开作用域后,会自动调用其析构方法。智能指针根据这个特性实现了对象内存的管理和自动释放。
本文所分析的智能指针源码基于 Android ndk-16b 中 llvm-libc++的 memory 文件。
先看下 unique_ptr
的声明。unique_ptr
有两个模板参数,分别为_Tp
和_Dp
。
函数声明中typename __pointer_type<_Tp, deleter_type>::type
可以简单理解为_Tp*
,即原生指针类型。
template >
class _LIBCPP_TEMPLATE_VIS unique_ptr {
public:
typedef _Tp element_type;
typedef _Dp deleter_type;
typedef typename __pointer_type<_Tp, deleter_type>::type pointer;
//...
}
unique_ptr
中唯一的数据成员就是原生指针和析构器的 pair。
private:
__compressed_pair __ptr_;
template <class _Tp, class _Dp = default_delete<_Tp> >
class _LIBCPP_TEMPLATE_VIS unique_ptr {
public:
// 默认构造函数,用pointer的默认构造函数初始化__ptr_
constexpr unique_ptr() noexcept : __ptr_(pointer()) {}
// 空指针的构造函数,同上
constexpr unique_ptr(nullptr_t) noexcept : __ptr_(pointer()) {}
// 原生指针的构造函数,用原生指针初始化__ptr_
explicit unique_ptr(pointer __p) noexcept : __ptr_(__p) {}
// 原生指针和析构器的构造函数,用这两个参数初始化__ptr_,当前析构器为左值引用
unique_ptr(pointer __p, _LValRefType<_Dummy> __d) noexcept
: __ptr_(__p, __d) {}
// 原生指针和析构器的构造函数,析构器使用转移语义进行转移
unique_ptr(pointer __p, _GoodRValRefType<_Dummy> __d) noexcept
: __ptr_(__p, _VSTD::move(__d)) {
static_assert(!is_reference::value,
"rvalue deleter bound to reference");
}
// 移动构造函数,取出原有unique_ptr的指针和析构器进行构造
unique_ptr(unique_ptr&& __u) noexcept
: __ptr_(__u.release(), _VSTD::forward(__u.get_deleter())) {
}
// 移动赋值函数,取出原有unique_ptr的指针和析构器进行构造
unique_ptr& operator=(unique_ptr&& __u) _NOEXCEPT {
reset(__u.release());
__ptr_.second() = _VSTD::forward(__u.get_deleter());
return *this;
}
}
template <class _Tp, class _Dp = default_delete<_Tp> >
class _LIBCPP_TEMPLATE_VIS unique_ptr {
// 返回原生指针
pointer get() const _NOEXCEPT {
return __ptr_.first();
}
// 判断原生指针是否为空
_LIBCPP_EXPLICIT operator bool() const _NOEXCEPT {
return __ptr_.first() != nullptr;
}
// 将__ptr置空,并返回原有的指针
pointer release() _NOEXCEPT {
pointer __t = __ptr_.first();
__ptr_.first() = pointer();
return __t;
}
// 重置原有的指针为新的指针,如果原有指针不为空,对原有指针所指对象进行销毁
void reset(pointer __p = pointer()) _NOEXCEPT {
pointer __tmp = __ptr_.first();
__ptr_.first() = __p;
if (__tmp)
__ptr_.second()(__tmp);
}
}
// 返回原生指针的引用
typename add_lvalue_reference<_Tp>::type
operator*() const {
return *__ptr_.first();
}
// 返回原生指针
pointer operator->() const _NOEXCEPT {
return __ptr_.first();
}
// 通过reset()方法进行对象的销毁
~unique_ptr() { reset(); }
shared_ptr
与unique_ptr
最核心的区别就是比unique_ptr
多了一个引用计数,并由于引用计数的加入,可以支持拷贝。
先看下shared_ptr
的声明。shared_ptr
主要有两个成员变量,一个是原生指针,一个是控制块的指针,用来存储这个原生指针的shared_ptr
和weak_ptr
的数量。
template<class _Tp>
class shared_ptr
{
public:
typedef _Tp element_type;
private:
element_type* __ptr_;
__shared_weak_count* __cntrl_;
//...
}
// 共享计数类
class __shared_count
{
__shared_count(const __shared_count&);
__shared_count& operator=(const __shared_count&);
protected:
// 共享计数
long __shared_owners_;
virtual ~__shared_count();
private:
// 引用计数变为0的回调,一般是进行内存释放
virtual void __on_zero_shared() _NOEXCEPT = 0;
public:
// 构造函数,需要注意内部存储的引用计数是从0开始,外部看到的引用计数其实为1
explicit __shared_count(long __refs = 0) _NOEXCEPT
: __shared_owners_(__refs) {}
// 增加共享计数
void __add_shared() _NOEXCEPT {
__libcpp_atomic_refcount_increment(__shared_owners_);
}
// 释放共享计数,如果共享计数为0(内部为-1),则调用__on_zero_shared进行内存释放
bool __release_shared() _NOEXCEPT {
if (__libcpp_atomic_refcount_decrement(__shared_owners_) == -1) {
__on_zero_shared();
return true;
}
return false;
}
// 返回引用计数,需要对内部存储的引用计数+1处理
long use_count() const _NOEXCEPT {
return __libcpp_relaxed_load(&__shared_owners_) + 1;
}
};
class __shared_weak_count
: private __shared_count
{
// weak ptr计数
long __shared_weak_owners_;
public:
// 内部共享计数和weak计数都为0
explicit __shared_weak_count(long __refs = 0) _NOEXCEPT
: __shared_count(__refs),
__shared_weak_owners_(__refs) {}
protected:
virtual ~__shared_weak_count();
public:
// 调用通过父类的__add_shared,增加共享引用计数
void __add_shared() _NOEXCEPT {
__shared_count::__add_shared();
}
// 增加weak引用计数
void __add_weak() _NOEXCEPT {
__libcpp_atomic_refcount_increment(__shared_weak_owners_);
}
// 调用父类的__release_shared,如果释放了原生指针的内存,还需要调用__release_weak,因为内部weak计数默认为0
void __release_shared() _NOEXCEPT {
if (__shared_count::__release_shared())
__release_weak();
}
// weak引用计数减1
void __release_weak() _NOEXCEPT;
// 获取共享计数
long use_count() const _NOEXCEPT {return __shared_count::use_count();}
__shared_weak_count* lock() _NOEXCEPT;
private:
// weak计数为0的处理
virtual void __on_zero_shared_weak() _NOEXCEPT = 0;
};
class __shared_ptr_pointer
: public __shared_weak_count
{
__compressed_pair<__compressed_pair<_Tp, _Dp>, _Alloc> __data_;
public:
_LIBCPP_INLINE_VISIBILITY
__shared_ptr_pointer(_Tp __p, _Dp __d, _Alloc __a)
: __data_(__compressed_pair<_Tp, _Dp>(__p, _VSTD::move(__d)), _VSTD::move(__a)) {}
#ifndef _LIBCPP_NO_RTTI
virtual const void* __get_deleter(const type_info&) const _NOEXCEPT;
#endif
private:
virtual void __on_zero_shared() _NOEXCEPT;
virtual void __on_zero_shared_weak() _NOEXCEPT;
};
了解了引用计数的基本原理后,再看下shared_ptr
的实现。
// 使用原生指针构造shared_ptr时,会构建__shared_ptr_pointer的控制块
shared_ptr<_Tp>::shared_ptr(_Yp* __p,
typename enable_if::value, __nat>::type)
: __ptr_(__p)
{
unique_ptr<_Yp> __hold(__p);
typedef typename __shared_ptr_default_allocator<_Yp>::type _AllocT;
typedef __shared_ptr_pointer<_Yp*, default_delete<_Yp>, _AllocT > _CntrlBlk;
__cntrl_ = new _CntrlBlk(__p, default_delete<_Yp>(), _AllocT());
__hold.release();
__enable_weak_this(__p, __p);
}
// 如果进行shared_ptr的拷贝,会增加引用计数
template<class _Tp>
inline
shared_ptr<_Tp>::shared_ptr(const shared_ptr& __r) _NOEXCEPT
: __ptr_(__r.__ptr_),
__cntrl_(__r.__cntrl_)
{
if (__cntrl_)
__cntrl_->__add_shared();
}
// 销毁shared_ptr时,会使共享引用计数减1,如果减到0会销毁内存
template<class _Tp>
shared_ptr<_Tp>::~shared_ptr()
{
if (__cntrl_)
__cntrl_->__release_shared();
}
class _LIBCPP_TEMPLATE_VIS weak_ptr
{
public:
typedef _Tp element_type;
private:
element_type* __ptr_;
__shared_weak_count* __cntrl_;
}
// 通过shared_ptr构造weak_ptr。会将shared_ptr的成员变量地址进行复制。增加weak引用计数
weak_ptr<_Tp>::weak_ptr(shared_ptr<_Yp> const& __r,
typename enable_if::value, __nat*>::type)
_NOEXCEPT
: __ptr_(__r.__ptr_),
__cntrl_(__r.__cntrl_)
{
if (__cntrl_)
__cntrl_->__add_weak();
}
// weak_ptr析构器
template<class _Tp>
weak_ptr<_Tp>::~weak_ptr()
{
if (__cntrl_)
__cntrl_->__release_weak();
}