C++11 智能指针的深度理解


平时习惯使用cocos2d-x的Ref内存模式,回过头来在控制台项目中觉得c++的智能指针有点生疏,于是便重温一下。
首先有请c++智能指针们登场: std::auto_ptr、std::unique_ptr、std::shared_ptr 、std::weak_ptr


 

auto_ptr(已废弃的指针)

没有智能指针的c++时代,对堆内存的管理就是简单的new delete。
但是缺点是容易忘了delete释放,即使是资深码农,也可能会在某一个地方忘记delete它,造成内存泄漏。
在实际工程中,我们往往更希望把精力放在应用层上,而不是费尽心思在语言的细枝末节(内存的释放)。
于是就有了这个最原始的智能指针。

 

内部大概实现:
做成一个auto_ptr类,包含原始指针成员。
当auto_ptr类型的对象被释放时,利用析构函数,将拥有的原始指针delete掉。

//大概长这个样子(化简版)
template<class T>
class auto_ptr{
  T* ptr;
};

示例用法:

void runGame(){
    std::auto_ptr<Monster> monster1(new Monster());//monster1 指向 一个怪物
    monster1->doSomething();//怪物做某种事
}
//runGame函数执行完时,monster1被释放,然后它的析构函数也把指向的一个怪物释放了,要死带着一起死(o_o)

 

复制auto_ptr对象时,把指针指传给复制出来的对象,原有对象的指针成员随后重置为nullptr。
这说明auto_ptr是独占性的,不允许多个auto_ptr指向同一个资源。

void runGame(){
    std::auto_ptr<Monster> monster1(new Monster());//monster1 指向 一个怪物
    monster1->doSomething();//怪物做某种事
    std::auto_ptr<Monster> monster2 = monster1;//转移指针
    monster2->doSomething();//怪物做某种事
    monster1->doSomething();//Oops!monster1智能指针指向了nullptr,运行期崩溃。
}

 

注意:
虽然本文简单介绍了auto_ptr。
但是不要用auto_ptr! 不要用auto_ptr!

虽然它是c++11以前的最原始的智能指针,但是在c++11中已经被弃用(使用的话会被警告)了。
它的替代品,也就是c++11新智能指针unique_ptr,shared_ptr,weak_ptr将在下文出现

 


 

unique_ptr(一种强引用指针)

“它是我的所有物,你们都不能碰它!”——鲁迅
正如它的名字,独占 是它最大的特点。

 

内部大概实现:

它其实算是auto_ptr的翻版(都是独占资源的指针,内部实现也基本差不多).

但是unique_ptr的名字能更好的体现它的语义,而且在语法上比auto_ptr更安全(尝试复制unique_ptr时会编译期出错,而auto_ptr能通过编译期从而在运行期埋下出错的隐患)

假如你真的需要转移所有权(独占权),那么你就需要用std::move(std::unique_ptr对象)语法,尽管转移所有权后 还是有可能出现原有指针调用(调用就崩溃)的情况。
但是这个语法能强调你是在转移所有权,让你清晰的知道自己在做什么,从而不乱调用原有指针。

 

示例用法:

void runGame(){
  std::unique_ptr<Monster> monster1(new Monster());//monster1 指向 一个怪物
  std::unique_ptr<Monster> monster2 = monster1;//Error!编译期出错,不允许复制指针指向同一个资源。
  std::unique_ptr<Monster> monster3 = std::move(monster1);//转移所有权给monster3.
  monster1->doSomething();//Oops!monster1指向nullptr,运行期崩溃
}
 

(额外:boost库的boost::scoped_ptr也是一个独占性智能指针,但是它不允许转移所有权,从始而终都只对一个资源负责,它更安全谨慎,但是应用的范围也更狭窄。)

 


 

shared_ptr(一种强引用指针)

“它是我们(shared_ptr)的,也是你们(weak_ptr)的,但实质还是我们的”——鲁迅
共享对象所有权是件快乐的事情。

多个shared_ptr指向同一处资源,当所有shared_ptr都全部释放时,该处资源才释放。
(有某个对象的所有权(访问权,生命控制权) 即是 强引用,所以shared_ptr是一种强引用型指针)

 

内部大概实现:
每个shared_ptr都占指针的两倍空间,一个装着原始指针,一个装着计数区域(SharedPtrControlBlock)的指针

(用原始指针构造时,会new一个SharedPtrControlBlock出来作为计数存放的地方,然后用指针指向它,计数加减都通过SharedPtrControlBlock指针间接操作。)

//shared计数放在这个结构体里面,实际上结构体里还应该有另一个weak计数。下文介绍weak_ptr时会解释。
struct SharedPtrControlBlock{
  int shared_count;
};
//大概长这个样子(化简版)
template<class T>
class shared_ptr{
  T* ptr;
  SharedPtrControlBlock* count;
};

 

每次复制,多一个共享同处资源的shared_ptr时,计数+1。每次释放shared_ptr时,计数-1。
当shared计数为0时,则证明所有指向同一处资源的shared_ptr们全都释放了,则随即释放该资源(哦,还会释放new出来的SharedPtrControlBlock)。
这也是常说的引用计数技术(好绕口)

 

示例用法:

void runGame(){
  std::shared_ptr<Monster> monster1(new Monster());   //计数加到1
  do{std::shared_ptr<Monster> monster2 = monster1;    //计数加到2   }while(0);          
  //该栈退出后,计数减为1,monster1指向的堆对象仍存在
  std::shared_ptr<Monster> monster3 = monster1;      //计数加到2 } //该栈退出后,shared_ptr都释放了,计数减为0,它们指向的堆对象也能跟着释放.

 

缺陷:模型循环依赖(互相引用或环引用)时,计数会不正常

假如有这么一个怪物模型,它有2个亲人关系

class Monster{
  std::shared_ptr<Monster> m_father;
  std::shared_ptr<Monster> m_son;
public:
  void setFather(std::shared_ptr<Monster>& father);//实现细节懒得写了
  void setSon(std::shared_ptr<Monster>& son);    //
  ~Monster(){std::cout << "A monster die!";}    //析构时发出死亡的悲鸣
};

然后执行下面函数

void runGame(){
    std::shared_ptr<Monster> father = new Monster();
    std::shared_ptr<Monster> son = new Monster();
    father->setSon(son);
    son->setFather(father);
}

猜猜执行完runGame()函数后,这对怪物父子能正确释放(发出死亡的悲鸣)吗?
答案是不能。

那么我们来模拟一遍(自行脑海模拟一遍最好),函数退出时栈的shared_ptr对象陆续释放后的情形:
开始:
father,son指向的堆对象 shared计数都是为2

son智能指针退出栈:
son指向的堆对象 计数减为1,father指向的堆对象 计数仍为2。

father智能指针退出栈:
father指向的堆对象 计数减为1 , son指向的堆对象 计数仍为1。

函数结束:所有计数都没有变0,也就是说中途没有释放任何堆对象。

为了解决这一缺陷的存在,弱引用指针weak_ptr的出现很有必要。

 


 

weak_ptr(一种弱引用指针)

“它是我们(weak_ptr)的,也是你们(shared_ptr)的,但实质还是你们的”——鲁迅

weak_ptr是为了辅助shared_ptr的存在,它只提供了对管理对象的一个访问手段,同时也可以实时动态地知道指向的对象是否存活。

(只有某个对象的访问权,而没有它的生命控制权 即是 弱引用,所以weak_ptr是一种弱引用型指针)

 

内部大概实现:

计数区域(SharedPtrControlBlock)结构体引进新的int变量weak_count,来作为弱引用计数。
每个weak_ptr都占指针的两倍空间,一个装着原始指针,一个装着计数区域的指针(和shared_ptr一样的成员)。
weak_ptr可以由一个shared_ptr或者另一个weak_ptr构造。
weak_ptr的构造和析构不会引起shared_count的增加或减少,只会引起weak_count的增加或减少。

被管理资源的释放只取决于shared计数,当shared计数为0,才会释放被管理资源,
也就是说weak_ptr不控制资源的生命周期。

但是计数区域的释放却取决于shared计数和weak计数,当两者均为0时,才会释放计数区域。

//shared引用计数和weak引用计数
//之前的计数区域实际最终应该长这个样子
struct SharedPtrControlBlock{
  int shared_count;
  int weak_count;
};
//大概长这个样子(化简版)
template<class T>
class weak_ptr{
  T* ptr;
  SharedPtrControlBlock* count;
};

 

针对空悬指针问题

空悬指针问题是指:无法知道指针指向的堆内存是否已经释放。

得益于引入的weak_count,weak_ptr指针可以使计数区域的生命周期受weak_ptr控制,

从而能使weak_ptr获取 被管理资源的shared计数,从而判断被管理对象是否已被释放。(可以实时动态地知道指向的对象是否被释放,从而有效解决空悬指针问题)

它的成员函数expired()就是判断指向的对象是否存活。

 

针对循环引用问题

class Monster{
  //尽管父子可以互相访问,但是彼此都是独立的个体,无论是谁都不应该拥有另一个人的所有权。
  std::weak_ptr<Monster> m_father;    //所以都把shared_ptr换成了weak_ptr
  std::weak_ptr<Monster> m_son;      //同上
public:
  void setFather(std::shared_ptr<Monster>& father); //实现细节懒得写了
  void setSon(std::shared_ptr<Monster>& son);    //
  ~Monster(){std::cout << "A monster die!";}     //析构时发出死亡的悲鸣
};

然后执行下面的函数

void runGame(){
  std::shared_ptr<Monster> father(new Monster());
  std::shared_ptr<Monster> son(new Monster());
  father->setSon(son);
  son->setFather(father);
}

那么我们再来模拟一遍,函数退出时栈的shared_ptr对象陆续释放后的情形:
一开始:
father指向的堆对象 shared计数为1,weak计数为1
son指向的堆对象 shared计数为1,weak计数为1

son智能指针退出栈:
son指向的堆对象 shared计数减为0,weak计数为1,释放son的堆对象,发出第一个死亡的悲鸣
father指向的堆对象 shared计数为1,weak计数减为0;

father智能指针退出栈:
father指向的堆对象 shared计数减为0,weak计数为0;释放father的堆对象和father的计数区域,发出第二个死亡的悲鸣。
son指向的堆对象 shared计数为0,weak计数减为0;释放son的计数区域。

函数结束,释放行为正确。

(可以说,当生命控制权没有彼此互相掌握时,才能正确解决循环引用问题,而弱引用的使用可以使生命控制权互相掌握的情况消失)

此外:
weak_ptr没有重载 * 和 -> ,所以并不能直接使用资源。但可以使用lock()获得一个可用的shared_ptr对象,
如果对象已经死了,lock()会失败,返回一个空的shared_ptr。

void runGame(){
  std::shared_ptr<Monster> monster1(new Monster());
  std::weak_ptr<Monster> r_monster1 = monster1;
  r_monster1->doSomething();//Error! 编译器出错!weak_ptr没有重载* 和 -> ,无法直接当指针用
  std::shared_ptr<Monster> s_monster1 = r_monster1.lock();//OK!可以通过weak_ptr的lock方法获得shared_ptr。
}

 


 

总结(语义)

1、不要使用std::auto_ptr(已经在C++11或以上标准中弃用)

2、当你需要一个独占资源所有权(访问权+生命控制权)的指针,且不允许任何外界访问,使用std::unique_ptr

3、当你需要一个共享资源所有权(访问权+生命控制权)的指针,使用std::shared_ptr

4、当你需要一个能访问资源,但不控制其生命周期的指针,使用std::weak_ptr

 

推荐用法:
一个shared_ptr和n个weak_ptr搭配使用而不是n个shared_ptr。
因为一般模型中,最好总是被一个指针控制生命周期,然后可以被n个指针控制访问。

逻辑上,大部分模型的生命在直观上总是受某一样东西直接控制而不是多样东西共同控制。
程序上,能够完全避免生命周期互相控制引发的 循环引用问题。

posted @ 2018-05-27 17:04  KillerAery  阅读(14560)  评论(4编辑  收藏  举报