智能指针在 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_ = newstd::thread(workThread, this); } ReportClass::~ReportClass() { // 通过join来停止内部线程 stop_ = true; work_thread_->join(); delete work_thread_; work_thread_ = nullptr; }
这种方式看起来没问题了,但是由于这个对象一般是被多个线程使用。假如某个线程想要释放这个对象,但另外一个线程还在使用这个对象,可能会出现野指针问题。就算释放对象的线程将对象释放后将指针置为nullptr,但仍然可能在多线程下在指针置空前被另外一个线程取得地址并使用。
二、智能指针的基本用法
智能指针设计的初衷就是可以帮助我们管理堆上申请的内存,可以理解为开发者只需要申请,而释放交给智能指针。
目前 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(open) { // 不再需要手动释放内存 return; } // 不再需要手动释放内存 return; }
unique_ptr的核心特点就如它的名字一样,它拥有对持有对象的唯一所有权。即两个unique_ptr不能同时指向同一个对象。
那具体这个唯一所有权如何体现呢?
1、unique_ptr不能被复制到另外一个unique_ptr
2、unique_ptr所持有的对象只能通过转移语义将所有权转移到另外一个unique_ptr
std::unique_ptr<A> a1(new A()); std::unique_ptr<A> a2 = a1;//编译报错,不允许复制 std::unique_ptr<A> a3 = std::move(a1);//可以转移所有权,所有权转义后a1不再拥有任何指针
智能指针有一个通用的规则,就是->表示用于调用指针原有的方法,而.则表示调用智能指针本身的方法。
unique_ptr本身拥有的方法主要包括:
1、get() 获取其保存的原生指针,尽量不要使用
2、bool() 判断是否拥有指针
3、release() 释放所管理指针的所有权,返回原生指针。但并不销毁原生指针。
4、reset() 释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针
std::unique_ptr<A> a1(new A()); A *origin_a = a1.get();//尽量不要暴露原生指针 if(a1) { // a1 拥有指针 } std::unique_ptr<A> a2(a1.release());//常见用法,转义拥有权 a2.reset(new A());//释放并销毁原有对象,持有一个新对象 a2.reset();//释放并销毁原有对象,等同于下面的写法 a2 = nullptr;//释放并销毁原有对象
2.2 shared_ptr
与unique_ptr的唯一所有权所不同的是,shared_ptr强调的是共享所有权。也就是说多个shared_ptr可以拥有同一个原生指针的所有权。
std::shared_ptr<A> a1(new A()); std::shared_ptr<A> a2 = a1;//编译正常,允许所有权的共享
shared_ptr 是通过引用计数的方式管理指针,当引用计数为 0 时会销毁拥有的原生对象。
shared_ptr本身拥有的方法主要包括:
1、get() 获取其保存的原生指针,尽量不要使用
2、bool() 判断是否拥有指针
3、reset() 释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针
4、unique() 如果引用计数为 1,则返回 true,否则返回 false
5、use_count() 返回引用计数的大小
std::shared_ptr<A> a1(new A()); std::shared_ptr<A> a2 = a1;//编译正常,允许所有权的共享 A *origin_a = a1.get();//尽量不要暴露原生指针 if(a1) { // a1 拥有指针 } if(a1.unique()) { // 如果返回true,引用计数为1 } long a1_use_count = a1.use_count();//引用计数数量
2.3 weak_ptr
weak_ptr 比较特殊,它主要是为了配合shared_ptr而存在的。就像它的名字一样,它本身是一个弱指针,因为它本身是不能直接调用原生指针的方法的。如果想要使用原生指针的方法,需要将其先转换为一个shared_ptr。那weak_ptr存在的意义到底是什么呢?
由于shared_ptr是通过引用计数来管理原生指针的,那么最大的问题就是循环引用(比如 a 对象持有 b 对象,b 对象持有 a 对象),这样必然会导致内存泄露。而weak_ptr不会增加引用计数,因此将循环引用的一方修改为弱引用,可以避免内存泄露。
weak_ptr可以通过一个shared_ptr创建。
std::shared_ptr<A> a1(new A()); std::weak_ptr<A> weak_a1 = a1;//不增加引用计数
weak_ptr本身拥有的方法主要包括:
1、expired() 判断所指向的原生指针是否被释放,如果被释放了返回 true,否则返回 false
2、use_count() 返回原生指针的引用计数
3、lock() 返回 shared_ptr,如果原生指针没有被释放,则返回一个非空的 shared_ptr,否则返回一个空的 shared_ptr
4、reset() 将本身置空
std::shared_ptr<A> a1(new A()); std::weak_ptr<A> weak_a1 = a1;//不增加引用计数 if(weak_a1.expired()) { //如果为true,weak_a1对应的原生指针已经被释放了 } long a1_use_count = weak_a1.use_count();//引用计数数量 if(std::shared_ptr<A> shared_a = weak_a1.lock()) { //此时可以通过shared_a进行原生指针的方法调用 } weak_a1.reset();//将weak_a1置空
作者:lucasfan,腾讯 IEG Global Pub.Tech. 客户端工程师
声明:转载此文是出于传递更多信息之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与本网联系,我们将及时更正、删除,谢谢。