作者:lucasfan,腾讯 IEG Global Pub.Tech. 客户端工程师
智能指针在 C++11 标准中被引入真正标准库(C++98 中引入的 auto_ptr 存在较多问题),但目前很多 C++开发者仍习惯用原生指针,视智能指针为洪水猛兽。但很多实际场景下,智能指针却是解决问题的神器,尤其是一些涉及多线程的场景下。本文将介绍智能指针可以解决的问题,用法及最佳实践。并且根据源码分析智能指针的实现原理。
一、为什么需要使用智能指针
1.1 内存泄漏
C++在堆上申请内存后,需要手动对内存进行释放。代码的初创者可能会注意内存的释放,但随着代码协作者加入,或者随着代码日趋复杂,很难保证内存都被正确释放。
尤其是一些代码分支在开发中没有被完全测试覆盖的时候,就算是内存泄漏检查工具也不一定能检查到内存泄漏。
void test_memory_leak(bool open)
{
A *a = new A();
if(open)
{
// 代码变复杂过程中,很可能漏了 delete(a);
return;
}
delete(a);
return;
}
1.2 多线程下对象析构问题
多线程遇上对象析构,是一个很难的问题,稍有不慎就会导致程序崩溃。因此在对于 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 主要支持的智能指针为以下几种
unique_ptr
shared_ptr
weak_ptr
2.1 unique_ptr
先上代码
class A
{
public:
void do_something() {}
};
void test_unique_ptr(bool open)
{
std::unique_ptr<A> a(new A());
a->do_something();
if