智能指针

什么是智能指针?

  • 智能指针是一个句柄

  • 智能指针在离开作用域时自动调用实际指针的析构函数

第一点句柄代表了智能指针本身不会失效,因此没有判空问题

第二点代表了智能指针可以自动管理内存

shared_ptr 和 make_shared

如下代码:

shared_ptr<string> str(nullptr);
auto str2 = make_shared<string>();
*str2 = "haha";
assert(str == nullptr);
assert(*str2 == "haha");

使用 shared_ptr 创建出来的对象用于绑定一个已有的堆对象,其参数为一个对象指针,因此 可以绑定到空指针 ,但是 make_shared 是直接创建了一个匿名的堆对象,并直接使用 share_ptr 管理它,其参数是参数 T 的构造参数,因此不能绑定到空指针。

share_ptr 的注意事项

根据 C++ shared_ptr四宗罪) 可以由以下结论:

  • 一个对象只能由一个 shared_ptr 管理,否则会出现多次析构。因此 shared_ptr 适用于移动语义

  • shared_ptr 应当至少与原对象的声明周期相同,但是一定不能比原对象的声明周期短。

  • shared_ptr 无法转移对象的所有权

  • 一旦使用 shared_ptr,则 shared_ptr 将会从库中传播到用户代码中,则唯一的方法就是使用回调函数。

  • 若资源在外部已经由 shared_ptr 管理了,这时候资源需要获取指向自己的 shared_ptr,那么唯一的办法就是获取一个引用计数与外部 shared_ptr 共享的智能指针。这唯一的方法就是让对象继承 enable_shared_from_this

  • 智能指针在多线程中性能不佳。

enable_shared_from_this

以下为摘录内容

我们来看看具体的代码实现逻辑:

struct Good: std::enable_shared_from_this<Good> // 注意:继承
{
   std::shared_ptr<Good> getptr() {
      return shared_from_this();
   }
};

struct Bad
{
   // 错误写法:用不安全的表达式试图获得 this 的 shared_ptr 对象
   std::shared_ptr<Bad> getptr() {
      return std::shared_ptr<Bad>(this);
   }
};

这里我们可以看到,Good类继承了std::enable_shared_from_this,并且自己是作为模板参数传递给父类的。这就给让代码看起来有些”唬人”,看起来像是继承自己一样。但其实呢?这里只是用到了模板派生,让父类能够在编译器感知到子类的模板存在,二者不是真正意义上的继承关系。

这里只分析下面两个问题:

  1. 为什么Bad类直接通过this构造shared_ptr会存在问题?

    答:因为原本的this指针就是被shared_ptr管理的,通过getprt函数构造的新的智能指针和和原本管理this指针的的shared_ptr并不互相感知。这会导致指向Bad的this指针被二次释放!!!

  2. 为什么通过继承std::enable_shared_from_this之后就没有上述问题了?

    答:这里截取了部分std::enable_shared_from_this的源码并且简化了一下:

template<typename _Tp>
class enable_shared_from_this
{
protected:

  enable_shared_from_this(const enable_shared_from_this&) noexcept { }
  ~enable_shared_from_this() { }

public:
  shared_ptr<_Tp>
  shared_from_this()
  { return shared_ptr<_Tp>(this->_M_weak_this); }

  shared_ptr<const _Tp>
  shared_from_this() const
  { return shared_ptr<const _Tp>(this->_M_weak_this); }

private:
  mutable weak_ptr<_Tp>  _M_weak_this;
};

std::enable_shared_from_this 的实现由于有些复杂,受限于篇幅。笔者就不展开来分析它具体是怎么样实现的了。它的能够规避上述问题的原因如下:

通过自身维护了一个 std::weak_ptr 让所有从该对象派生的 shared_ptr 都通过了 std::weak_ptr 构造派生。

std::shared_ptr 的构造函数判断出对象是 std::enable_shared_from_this 的之类之后也会同样通过对象本身的 std::weak_ptr 构造派生。这个这样引用计数是互通的,也就不会存在上述 double delete 的问题了。

weak_ptr

weak_ptr 用于关联到一个 shared_ptr 上。而且这个关联不会导致 shared_ptr 中的引用计数增加。主要目的是防止环形引用

weak_ptr 比较重要的一点是用来做资源的 handle,这样就不用担心空指针的问题了,因为 handle 是不会失效的

另外

析构动作在创建时被捕获

这是一个非常有用的特性,这意味着:

  • 虚析构不再是必需的

  • shared_ptr<void> 可以持有任何对象,而且能安全地释放

  • shared_ptr 对象可以安全地跨越模块边界,比如从 DLL 里返回,而不会造成从模块 A 分配的内存在模块 B 里被释放这种错误

  • 二进制兼容性,即便 Foo 对象的大小变了,那么旧的客户代码仍然可以使用新的动态库,而无须重新编译。前提是 Foo 的头文件中不出现访问对象的成员的 inline 函数,并且 Foo 对象的由动态库中的 Factory 构造,返回其 shared_ptr

  • 析构动作可以定制

析构所在的线程

对象的析构是同步的,当最后一个指向 x 的 shared_ptr 离开其作用域的时候,x 会同时在同一个线程析构。这个线程不一定是对象诞生的线程。

这个特性是把双刃剑:如果对象的析构比较耗时,那么可能会拖慢关键线程的速度(如果最后一个 shared_ptr 引发的析构发生在关键线程);同时,我们可以用一个单独的线程来专门做析构,通过一个 BlockingQueue<shared_ptr<void> > 把对象的析构都转移到那个专用线程,从而解放关键线程。

std::shared_ptr<Node> getptr() {
   return shared_ptr<Node>(this);
}

没问题,但是

this->shared_from_this()

会出现问题

对象池

对象池算是对智能指针指针的一次综合运用

class Stock {
   string key_;

public:
   Stock(string key) : key_(key) { }
   const string& key() {
      return key_;
   }
};

class StockFactory : enable_shared_from_this<StockFactory> {
   mutable mutex                mutex_;
   map<string, weak_ptr<Stock>> stocks_;

public:
   shared_ptr<Stock> get(const string& key) {
      std::lock_guard<mutex> lock(mutex_);

      auto& wkStock = stocks_[key];
      auto  pStock  = wkStock.lock();
      if(!pStock) {
            pStock.reset(new Stock(key), bind(&StockFactory::weakDeleteCallBack,
                                             weak_ptr<StockFactory>(this->shared_from_this()), std::placeholders::_1));
      }
      return pStock;
   }
   static void weakDeleteCallBack(const weak_ptr<StockFactory>& wkFactory, Stock* stock) {
      auto factory = wkFactory.lock();
      if(factory) factory->removeStock(stock);
      delete stock;
   }
   void removeStock(Stock* stock) {
      if(stock) {
            lock_guard<mutex> lock(mutex_);
            stocks_.erase(stock->key());
      }
   }
};