C++“准”标准库Boost学习指南(1):智能指针Boost.smart_ptr

我们学习C++都知道智能指针,例如STL中的std::auto_ptr,但是为什么要使用智能指针,使用它能带给我们什么好处呢?

最简单的使用智能指针可以不会因为忘记delete指针而造成内存泄露。还有如果我们开发或者使用第三方的lib中的某些函数需要返回指针,这样的返回的指针被client使用的时候,lib就会失去对返回的指针的控制,这样delete的指针的任务一般就会交给调用方client,但是如果 client忘记调用delete或是调用的时机不正确,都有可能导致问题,在这种情况下就最好使用智能指针。还有使用智能指针可以保证异常安全,保证程序在有异常抛出时仍然无内存泄露。
   
std::auto_ptr很多的时候并不能满足我们的要求,比如她不能用在STL的container中。boost的smart_ptr中提供了4种智能指针和2种智能指针数组来作为std::auto_ptr的补充。   
  • shared_ptr<boost/shared_ptr.hpp>:使用shared_ptr进行对象的生存期自动管理,使得分享资源所有权变得有效且安全.
  • scoped_ptr<boost/scoped_ptr.hpp>: 用于确保能够正确地删除动态分配的对象。scoped_ptr 有着与std::auto_ptr类似的特性,而最大的区别在于它不能转让所有权而auto_ptr可以。事实上,scoped_ptr永远不能被复制或被赋值!scoped_ptr 拥有它所指向的资源的所有权,并永远不会放弃这个所有权。
  • weak_ptr<boost/weak_ptr.hpp>:weak_ptr 是 shared_ptr 的观察员。它不会干扰shared_ptr所共享的所有权。当一个被weak_ptr所观察的 shared_ptr 要释放它的资源时,它会把相关的 weak_ptr的指针设为空。使用此辅助指针一般是防止悬空指针。
  • intrusive_ptr<boost/intrusive_ptr.hpp>:shared_ptr的插入是版本,一般不使用,因为需要对使用此指针的类型中增加ref计数功能。但是可以保证不增加指针的大小。
  • scoped_array<boost/scoped_array.hpp>: scoped_array 为数组做了scoped_ptr为单个对象的指针所做的事情:它负责释放内存。shared_array<boost/shared_array.hpp>: shared_array 用于共享数组所有权的智能指针。一般指向std::vector的shared_ptr提供了比shared_array更多的灵活性,所以一般使用 std::vector<shared_ptr>。



Smart_ptr库如何改进你的程序?
  • 使用shared_ptr进行对象的生存期自动管理,使得分享资源所有权变得有效且安全。
  • 使用weak_ptr可以安全地观测共享资源,避免了悬挂的指针。
  • 使用scoped_ptr 和 scoped_array限制资源的使用范围,使得代码更易于编写和维护,并有助于写出异常安全的代码。

智能指针解决了资源生存期管理的问题(尤其是动态分配的对象). 智能指针有各种不同的风格。多数都有一种共同的关键特性:自动资源管理。这种特性可能以不同的方式出现:如动态分配对象的生存期控制,和获取及释放资源 (文件, 网络连接)。Boost的智能指针主要针对第一种情况,它们保存指向动态分配对象的指针,并在正确的时候删除这些对象。你可能觉得奇怪为什么这些智能指针不多做一点工作。它们不可以很容易就覆盖所有资源管理的不同情况吗?是的,它们可以(在一定范围内它们可以),但不是没有代价的。通用的解决方案意味着更高的复杂性,而对于Boost的智能指针,可用性比灵活性具有更高的优先级。但是,通过对可定制删除器的支持,Boost的最智能的智能指针(boost::shared_ptr)可以支持那些不是使用delete进行析构的资源。Boost.Smart_ptr的五个智能指针类型是专门特制的,适用于每天的编程中最常见的需求。

何时我们需要智能指针?

有三种典型的情况适合使用智能指针:
  • 资源所有权的共享
  • 要编写异常安全的代码时
  • 避免常见的错误,如资源泄漏

共享所有权是指两个或多个对象需要同时使用第三个对象的情况。这第三个对象应该如何(或者说何时)被释放?为了确保释放的时机是正确的,每个使用这个共享资源的对象必须互相知道对方,才能准确掌握资源的释放时间。从设计或维护的观点来看,这种耦合是不可行的。更好的方法是让这些资源所有者将资源的生存期管理责任委派给一个智能指针。当没有共享者存在时,智能指针就可以安全地释放这个资源了。

异常安全,简单地说就是在异常抛出时没有资源泄漏并保证程序状态的一致性。如果一个对象是动态分配的,当异常抛出时它不会自动被删除。由于栈展开以及指针离开作用域,资源可以会泄漏直至程序结束(即使是程序结束时的资源回收也不是由语言所保证的)。不仅可能程序会由于内存泄漏而耗尽资源,程序的状态也可能变得混乱。智能指针可以自动地为你释放这些资源,即使是在异常发生的情况下。

避免常见的错误。忘记调用 delete 是书本中最古老的错误(至少在这本书中)。一个智能指针不关心程序中的控制路径;它只关心在它所指向的对象的生存期结束时删除它。使用智能指针,你不再需要知道何时删除对象。并且,智能指针隐藏了释放资源的细节,因此使用者不需要知道是否要调用 delete, 有些特殊的清除函数并不总是删除资源的。

安全和高效的智能指针是程序员的军火库中重要的武器。虽然C++标准库中提供了 std::auto_ptr, 但是它不能完全满足我们对智能指针的需求。例如,auto_ptr不能用作STL容器的元素。Boost的智能指针类填充了标准所留下来的缺口。

本章主要关注 scoped_ptr, shared_ptr, intrusive_ptr, 和 weak_ptr. 虽然剩下的 scoped_array 和 shared_array 有时候也很有用,但它们用的不是很多,而且它们与已讨论的非常相近,这里就不重复讨论它们了。

Smart_ptr如何适应标准库?

Smart_ptr库已被提议包含进标准库中,主要有以下三个原因:
  • 标准库现在只提供了一个auto_ptr, 它仅是一类智能指针,仅仅覆盖了智能指针族谱中的一个部分。shared_ptr 提供了不同的,也是更重要的功能。
  • Boost的智能指针专门为了与标准库良好合作而设计,并可作为标准库的自然扩充。例如,在 shared_ptr之前,还没有一个标准的智能指针可用作容器的元素。
  • 长久以来,现实世界中的程序员已经在他们的程序中大量使用这些智能指针类,它们已经得到了充分的验证。

以上原因使得Smart_ptr库成为了C++标准库的一个非常有用的扩充。Boost.Smart_ptr的 shared_ptr (以及随同的助手 enable_shared_from_this) 和 weak_ptr 已被收入即将发布的Library Technical Report。


scoped_ptr

头文件: "boost/scoped_ptr.hpp"

boost::scoped_ptr 用于确保动态分配的对象能够被正确地删除。scoped_ptr 有着与std::auto_ptr类似的特性,而最大的区别在于它不能转让所有权而auto_ptr可以。事实上,scoped_ptr永远不能被复制或被赋值!scoped_ptr 拥有它所指向的资源的所有权,并永远不会放弃这个所有权。scoped_ptr的这种特性提升了我们的代码的表现,我们可以根据需要选择最合适的智能指针(scoped_ptr 或 auto_ptr)。

要决定使用std::auto_ptr还是boost::scoped_ptr, 就要考虑转移所有权是不是你想要的智能指针的一个特性。如果不是,就用scoped_ptr. 它是一种轻量级的智能指针;使用它不会使你的程序变大或变慢。它只会让你的代码更安全,更好维护。

下面是scoped_ptr的摘要,以及其成员的简要描述:

namespace boost {

  template<typename T> class scoped_ptr : noncopyable {
  public:
    explicit scoped_ptr(T* p = 0); 
    ~scoped_ptr(); 

    void reset(T* p = 0); 

    T& operator*() const; 
    T* operator->() const; 
    T* get() const; 
   
    void swap(scoped_ptr& b); 
  };

  template<typename T> 
    void swap(scoped_ptr<T> & a, scoped_ptr<T> & b); 
}


成员函数

  1. explicit scoped_ptr(T* p=0)
构造函数,存储p的一份拷贝。注意,p 必须是用operator new分配的,或者是null. 在构造的时候,不要求T必须是一个完整的类型。当指针p是调用某个分配函数的结果而不是直接调用new得到的时候很有用:因为这个类型不必是完整的,只需要类型T的一个前向声明就可以了。这个构造函数不会抛出异常。
  1. ~scoped_ptr()
删除被指物。类型T在被销毁时必须是一个完整的类型。如果scoped_ptr在它被析构时并没有保存资源,它就什么都不做。这个析构函数不会抛出异常。
  1. void reset(T* p=0);
重置一个 scoped_ptr 就是删除它已保存的指针,如果它有的话,并重新保存 p. 通常,资源的生存期管理应该完全由scoped_ptr自己处理,但是在极少数时候,资源需要在scoped_ptr的析构之前释放,或者scoped_ptr要处理它原有资源之外的另外一个资源。这时,就可以用reset,但一定要尽量少用它。(过多地使用它通常表示有设计方面的问题) 这个函数不会抛出异常。
  1. T& operator*() const;
返回一个到被保存指针指向的对象的引用。由于不允许空的引用,所以解引用一个拥有空指针的scoped_ptr将导致未定义行为。如果不能肯定所含指针是否有效,就用函数get替代解引用。这个函数不会抛出异常。
  1. T* operator->() const;
返回保存的指针。如果保存的指针为空,则调用这个函数会导致未定义行为。如果不能肯定指针是否空的,最好使用函数get。这个函数不会抛出异常。
  1. T* get() const;
返回保存的指针。应该小心地使用get,因为它可以直接操作裸指针。但是,get使得你可以测试保存的指针是否为空。这个函数不会抛出异常。get通常在调用那些需要裸指针的函数时使用。
  1. operator unspecified_bool_type() const
返回scoped_ptr是否为非空。返回值的类型是未指明的,但这个类型可被用于Boolean的上下文中。在if语句中最好使用这个类型转换函数,而不要用get去测试scoped_ptr的有效性
  1. void swap(scoped_ptr& b)
交换两个scoped_ptr的内容。这个函数不会抛出异常。

普通函数
  1. template<typename T> void swap(scoped_ptr<T>& a,scoped_ptr<T>& b)
这个函数提供了交换两个scoped pointer的内容的更好的方法。之所以说它更好,是因为 swap(scoped1,scoped2) 可以更广泛地用于很多指针类型,包括裸指针和第三方的智能指针。scoped1.swap(scoped2) 则只能用于它的定义所在的智能指针,而不能用于裸指针。

用法

scoped_ptr的用法与普通的指针没什么区别;最大的差别在于你不必再记得在指针上调用delete,还有复制是不允许的。典型的指针操作(operator* 和 operator->)都被重载了,并提供了和裸指针一样的语法。用scoped_ptr和用裸指针一样快,也没有大小上的增加,因此它们可以广泛使用。使用boost::scoped_ptr时,包含头文件"boost/scoped_ptr.hpp". 在声明一个scoped_ptr时,用被指物的类型来指定类模板的参数。例如,以下是一个包含std::string指针的scoped_ptr:
  1. boost::scoped_ptr<std::string> p(new std::string("Hello"));
当scoped_ptr被销毁时,它对它所拥有的指针调用delete 。

不需要手工删除

让我们看一个程序,它使用scoped_ptr来管理std::string指针。注意这里没有对delete的调用,因为scoped_ptr是一个自动变量,它会在离开作用域时被销毁。

#include "boost/scoped_ptr.hpp"
#include <string>
#include <iostream>

int main() {
  {
  boost::scoped_ptr<std::string> 
  p(new std::string("Use scoped_ptr often."));

  // 打印字符串的值
  if (p)
    std::cout << *p << '\n';
    
  // 获取字符串的大小
  size_t i=p->size();

  // 给字符串赋新值
  *p="Acts just like a pointer";
  
  } // 这里p被销毁,并删除std::string 
}


这段代码中有几个地方值得注明一下。首先,scoped_ptr可以测试其有效性,就象一个普通指针那样,因为它提供了隐式转换到一个可用于布尔表达式的类型的方法。其次,可以象使用裸指针那样调用被指物的成员函数,因为重载了operator->. 第三,也可以和裸指针一样解引用scoped_ptr,这归功于operator*的重载。这些特性正是scoped_ptr和其它智能指针的用处所在,因为它们和裸指针的不同之处在于对生存期管理的语义上,而不在于语法上。

和auto_ptr几乎一样

scoped_ptr 与 auto_ptr间的区别主要在于对拥有权的处理。auto_ptr在复制时会从源auto_ptr自动交出拥有权,而scoped_ptr则不允许被复制。看看下面这段程序,它把scoped_ptr 和 auto_ptr放在一起,你可以清楚地看到它们有什么不同。

void scoped_vs_auto() {

  using boost::scoped_ptr;
  using std::auto_ptr;

  scoped_ptr<std::string> p_scoped(new std::string("Hello"));
  auto_ptr<std::string> p_auto(new std::string("Hello"));

  p_scoped->size();
  p_auto->size();

  scoped_ptr<std::string> p_another_scoped=p_scoped;
  auto_ptr<std::string> p_another_auto=p_auto;

  p_another_auto->size();
  (*p_auto).size();
}


这个例子不能通过编译,因为scoped_ptr不能被复制构造或被赋值。auto_ptr既可以复制构造也可以赋值,但这们同时也意味着它把所有权从p_auto 转移给了 p_another_auto, 在赋值后p_auto将只剩下一个空指针。这可能会导致令人不快的惊讶,就象你试图把auto_ptr放入容器内时所发生的那样。如果我们删掉对p_another_scoped的赋值,程序就可以编译了,但它的运行结果是不可预测的,因为它解引用了p_auto里的空指针(*p_auto).

由于scoped_ptr::get会返回一个裸指针,所以就有可能对scoped_ptr做一些有害的事情,其中有两件是你尤其要避免的。第一,不要删除这个裸指针。因为它会在scoped_ptr被销毁时再一次被删除。第二,不要把这个裸指针保存到另一个scoped_ptr (或其它任何的智能指针)里。因为这样也会两次删除这个指针,每个scoped_ptr一次。简单地说,尽量少用get, 除非你要使用那些要求你传送裸指针的遗留代码!

scoped_ptr 和Pimpl用法

scoped_ptr可以很好地用于许多以前使用裸指针或auto_ptr的地方,如在实现pimpl用法时。pimpl 用法背后的思想是把客户与所有关于类的私有部分的知识分隔开。由于客户是依赖于类的头文件的,头文件中的任何变化都会影响客户,即使仅是对私有节或保护节的修改。pimpl用法隐藏了这些细节,方法是将私有数据和函数放入一个单独的类中,并保存在一个实现文件中,然后在头文件中对这个类进行前向声明并保存一个指向该实现类的指针。类的构造函数分配这个pimpl类,而析构函数则释放它。这样可以消除头文件与实现细节的相关性。我们来构造一个实现pimpl 用法的类,然后用智能指针让它更为安全。

// pimpl_sample.hpp

#if !defined (PIMPL_SAMPLE)
#define PIMPL_SAMPLE

class pimpl_sample {
  struct impl;  // 译者注:原文中这句在class之外,与下文的实现代码有矛盾
  impl* pimpl_;
public:
  pimpl_sample();
  ~pimpl_sample();
  void do_something();
};

#endif


这是pimpl_sample类的接口。struct impl 是一个前向声明,它把所有私有成员和函数放在另一个实现文件中。这样做的效果是使客户与pimpl_sample类的内部细节完全隔离开来。

// pimpl_sample.cpp 

#include "pimpl_sample.hpp"
#include <string>
#include <iostream>

struct pimpl_sample::impl {
  void do_something_() {
    std::cout << s_ << "\n";
  }

  std::string s_;
};

pimpl_sample::pimpl_sample()
  : pimpl_(new impl) {
  pimpl_->s_ = "This is the pimpl idiom";
}

pimpl_sample::~pimpl_sample() {
  delete pimpl_;
}

void pimpl_sample::do_something() {
  pimpl_->do_something_();
}


看起来很完美,但并不是的。这个实现不是异常安全的!原因是pimpl_sample的构造函数有可能在pimpl被构造后抛出一个异常。在构造函数中抛出异常意味着已构造的对象并不存在,因此在栈展开时将不会调用它的析构函数。这样就意味着分配给pimpl_指针的内存将泄漏。然而,有一样简单的解决方法:用scoped_ptr来解救!

class pimpl_sample {
  struct impl;
  boost::scoped_ptr<impl> pimpl_;
  ...
};


让scoped_ptr来处理隐藏类impl的生存期管理,并从析构函数中去掉对impl的删除(它不再需要,这要感谢scoped_ptr),这样就做完了。但是,你必须记住要手工定义析构函数;原因是在编译器生成隐式析构函数时,类impl还是不完整的,所以它的析构函数不能被调用。如果你用auto_ptr来保存impl, 你可以编译,但也还是有这个问题,但如果用scoped_ptr, 你将收到一个错误提示。

要注意的是,如果你使用scoped_ptr作为一个类的成员,你就必须手工定义这个类的复制构造函数和赋值操作符。原因是scoped_ptr是不能复制的,因此聚集了它的类也变得不能复制了。

最后一点值得注意的是,如果pimpl实例可以安全地被多个封装类(在这里是pimpl_sample)的实例所共享,那么用boost::shared_ptr来管理pimpl的生存期才是正确的选择。用shared_ptr比用scoped_ptr的优势在于,不需要手工去定义复制构造函数和赋值操作符,而且可以定义空的析构函数,shared_ptr被设计为可以正确地用于未完成的类。

scoped_ptr 不同于 const auto_ptr

留心的读者可能已经注意到auto_ptr可以几乎象scoped_ptr一样地工作,只要把auto_ptr声明为const:

  1. const auto_ptr<A> no_transfer_of_ownership(new A);
它们很接近,但不是一样。最大的区别在于scoped_ptr可以被reset, 在需要时可以删除并替换被指物。而对于const auto_ptr这是不可能的。另一个小一点的区别是,它们的名字不同:尽管const auto_ptr意思上和scoped_ptr一样,但它更冗长,也更不明显。当你的词典里有了scoped_ptr,你就应该使用它,因为它可以更清楚地表明你的意图。如果你想说一个资源是要被限制在作用域里的,并且不应该有办法可以放弃它的所有权,你就应该用 boost::scoped_ptr.

总结

使用裸指针来写异常安全和无错误的代码是很复杂的。使用智能指针来自动地把动态分配对象的生存期限制在一个明确的范围之内,是解决这种问题的一个有效方法,并且提高了代码的可读性、可维护性和质量。scoped_ptr 明确地表示被指物不能被共享和转移。正如你所看到的,std::auto_ptr可以从另一个auto_ptr那里窃取被指物,那怕是无意的,这被认为是auto_ptr的最大缺点。正是这个缺点使得scoped_ptr成为auto_ptr最好的补充。当一个动态分配的对象被传送给scoped_ptr, 它就成为了这个对象的唯一的拥有者。因为scoped_ptr几乎总是以自动变量或数据成员来分配的,因此它可以在离开作用域时正确地销毁对象,从而在执行流由于返回语句或异常抛出而离开作用域时,也总能释放它所管理的内存。

在以下情况时使用 scoped_ptr :
  • 在可能有异常抛出的作用域里使用指针
  • 函数里有几条控制路径
  • 动态分配对象的生存期应被限制于特定的作用域内
  • 异常安全非常重要时(总应如此!)

scoped_array

头文件: "boost/scoped_array.hpp"

需要动态分配数组时,通常最好用std::vector来实现,但是有两种情形看起来用数组更适合: 一种是为了优化,用vector多少有一些额外的内存和速度开销;另一种是为了某种原因,要求数组的大小必须是固定的。动态分配的数组会遇到与普通指针一样的危险,并且还多了一个(也是最常见的一个),那就是错误调用delete操作符而不是delete[]操作符来释放数组。我曾经在你想象不到的地方见到过这个错误,那也是它常被用到的地方,就是在你自己实现的容器类里!scoped_array 为数组做了scoped_ptr为单个对象指针所做的事情:它负责释放内存。区别只在于scoped_array 是用delete[] 操作符来做这件事的。

scoped_array是一个单独的类而不是scoped_ptr的一个特化,其原因是,因为不可能用元编程技术来区分指向单个对象的指针和指向数组的指针。不管如何努力,也没有人能发现一种可靠的方法,因为数组太容易退化为指针了,这使得没有类型信息可以表示它们是指向数组的。结果,只能由你来负责,使用scoped_array而不是scoped_ptr,就如你必须用delete[]操作符而不是用delete操作符一样。这样的好处是scoped_array 负责为你处理释放内存的事情,而你则告诉scoped_array 我们要处理的是数组,而不是裸指针。

scoped_array与scoped_ptr非常相似,不同的是它提供了operator[] 来模仿一个裸数组。

scoped_array 是比普通的动态分配数组更好用。它处理了动态分配数组的生存期管理问题,就如scoped_ptr管理对象指针的生存期一样。但是记住,多数情况下应该使用std::vector,它更灵活、更强大。只有当你需要确保数组的大小是固定的时候,才使用scoped_array 来替代 std::vector.


shared_ptr

头文件: "boost/shared_ptr.hpp"

几乎所有稍微复杂点的程序都需要某种形式的引用计数智能指针。这些智能指针让我们不再需要为了管理被两个或多个对象共享的对象的生存期而编写复杂的逻辑。当引用计数降为零,没有对象再需要这个共享的对象时,这个对象就自动被销毁了。引用计数智能指针可以分为插入式(intrusive)和非插入式(non-intrusive)两类。前者要求它所管理的类提供明确的函数或数据成员用于管理引用计数。这意味着在类的设计时就必须预见到它将与一个插入式的引用计数智能指针一起工作,或者重新设计它。非插入式的引用计数智能指针对它所管理的类没有任何要求。引用计数智能指针拥有与它所存指针有关的内存的所有权。没有智能指针的帮助,对象的共享会存在问题,必须有人负负责删除共享的内存。谁负责?什么时候删除?没有智能指针,你必须在管理的内存之外增加生存期的管理,这意味着在各个拥有者之间存在更强的依赖关系。换言之,没有了重用性并增加了复杂性。

被管理的类可能拥有一些特性使得它更应该与引用计数智能指针一起使用。例如,它的复制操作很昂贵,或者它所代表的有些东西必须被多个实例共享,这些特性都值得去共享所有权。还有一种情形是共享的资源没有一个明确的拥有者。使用引用计数智能指针可以在需要访问共享资源的对象之间共享资源的所有权。引用计数智能指针还让你可以把对象指针存入标准库的容器中而不会有泄漏的风险,特别是在面对异常或要从容器中删除元素的时候。如果你把指针放入容器,你就可以获得多态的好处,可以提高性能(如果复制的代价很高的话),还可以通过把相同的对象放入多个辅助容器来进行特定的查找。

在你决定使用引用计数智能指针后,你应该选择插入式的还是非插入式的?非插入式智能指针几乎总是更好的选择,由于它们的通用性、不需要修改已有代码,以及灵活性。你可以对你不能或不想修改的类使用非插入式的引用计数智能指针。而把一个类修改为使用插入式引用计数智能指针的常见方法是从一个引用计数基类派生。这种修改可能比你想象的更昂贵。至少,它增加了相关性并降低了重用性。它还增加了对象的大小,这在一些特定环境中可能会限制其可用性。

shared_ptr 可以从一个裸指针、另一个shared_ptr、一个std::auto_ptr、或者一个boost::weak_ptr构造。还可以传递第二个参数给shared_ptr的构造函数,它被称为删除器(deleter)。删除器稍后会被调用,来处理共享资源的释放。这对于管理那些不是用new分配也不是用delete释放的资源时非常有用(稍后将看到创建客户化删除器的例子)。shared_ptr被创建后,它就可象普通指针一样使用了,除了一点,它不能被显式地删除。

以下是shared_ptr的部分摘要;最重要的成员和相关普通函数被列出,随后是简单的讨论。

namespace boost {

  template<typename T> class shared_ptr {
  public:
    template <class Y> explicit shared_ptr(Y* p);
    template <class Y,class D> shared_ptr(Y* p,D d);

    ~shared_ptr();

    shared_ptr(const shared_ptr & r);
    template <class Y> explicit 
      shared_ptr(const weak_ptr<Y>& r);
    template <class Y> explicit shared_ptr(std::auto_ptr<Y>& r);

    shared_ptr& operator=(const shared_ptr& r);

    void reset(); 
  
    T& operator*() const;
    T* operator->() const;
    T* get() const;

    bool unique() const;
    long use_count() const;

    operator unspecified_bool_type() const;  //译注:原文是unspecified-bool-type(),有误

    void swap(shared_ptr<T>& b);
  };

  template <class T,class U>
    shared_ptr<T> static_pointer_cast(const shared_ptr<U>& r);
}


成员函数

  1. template <class Y> explicit shared_ptr(Y* p);
这个构造函数获得给定指针p的所有权。参数 p 必须是指向 Y 的有效指针。构造后引用计数设为1。唯一从这个构造函数抛出的异常是std::bad_alloc (仅在一种很罕见的情况下发生,即不能获得引用计数器所需的自由空间)。
  1. template <class Y,class D> shared_ptr(Y* p,D d);
这个构造函数带有两个参数。第一个是shared_ptr将要获得所有权的那个资源,第二个是shared_ptr被销毁时负责释放资源的一个对象,被保存的资源将以d(p)的形式传给那个对象。因此p的值是否有效取决于d。如果引用计数器不能分配成功,shared_ptr抛出一个类型为std::bad_alloc的异常。
  1. shared_ptr(const shared_ptr& r);
r中保存的资源被新构造的shared_ptr所共享,引用计数加一。这个构造函数不会抛出异常。
  1. template <class Y> explicit shared_ptr(const weak_ptr<Y>& r);
从一个weak_ptr (本章稍后会介绍)构造shared_ptr。这使得weak_ptr的使用具有线程安全性,因为指向weak_ptr参数的共享资源的引用计数将会自增(weak_ptr不影响共享资源的引用计数)。如果weak_ptr为空 (r.use_count()==0), shared_ptr 抛出一个类型为bad_weak_ptr的异常。
  1. template <typename Y> shared_ptr(std::auto_ptr<Y>& r);
复制代码
这个构造函数从一个auto_ptr获取r中保存的指针的所有权,方法是保存指针的一份拷贝并对auto_ptr调用release。构造后的引用计数为1。而r当然就变为空的。如果引用计数器不能分配成功,则抛出 std::bad_alloc 。
  1. ~shared_ptr();
复制代码
shared_ptr析构函数对引用计数减一。如果计数为零,则保存的指针被删除。删除指针的方法是调用operator delete 或者,如果给定了一个执行删除操作的客户化删除器对象,就把保存的指针作为唯一参数调用这个对象。析构函数不会抛出异常。
  1. shared_ptr& operator=(const shared_ptr& r);  
复制代码
赋值操作共享r中的资源,并停止对原有资源的共享。赋值操作不会抛出异常。
  1. void reset();
复制代码
reset函数用于停止对保存指针的所有权的共享。共享资源的引用计数减一。
  1. T& operator*() const;
复制代码
这个操作符返回对已存指针所指向的对象的一个引用。如果指针为空,调用operator* 会导致未定义行为。这个操作符不会抛出异常。
  1. T* operator->() const;
复制代码
这个操作符返回保存的指针。这个操作符与operator*一起使得智能指针看起来象普通指针。这个操作符不会抛出异常。
  1. T* get() const;
复制代码
get函数是当保存的指针有可能为空时(这时 operator* 和 operator-> 都会导致未定义行为)获取它的最好办法。注意,你也可以使用隐式布尔类型转换来测试 shared_ptr 是否包含有效指针。这个函数不会抛出异常。
  1. bool unique() const;
复制代码
这个函数在shared_ptr是它所保存指针的唯一拥有者时返回 true ;否则返回 false。 unique 不会抛出异常。
  1. long use_count() const;
复制代码
use_count 函数返回指针的引用计数。它在调试的时候特别有用,因为它可以在程序执行的关键点获得引用计数的快照。小心地使用它,因为在某些可能的shared_ptr实现中,计算引用计数可能是昂贵的,甚至是不行的。这个函数不会抛出异常。
  1. operator unspecified-bool-type() const;
复制代码
这是个到unspecified-bool-type类型的隐式转换函数,它可以在Boolean上下文中测试一个智能指针。如果shared_ptr保存着一个有效的指针,返回值为True;否则为false。注意,转换函数返回的类型是不确定的。把返回类型当成bool用会导致一些荒谬的操作,所以典型的实现采用了safe bool idiom,它很好地确保了只有可适用的Boolean测试可以使用。这个函数不会抛出异常。
  1. void swap(shared_ptr<T>& b);
复制代码
这可以很方便地交换两个shared_ptr。swap 函数交换保存的指针(以及它们的引用计数)。这个函数不会抛出异常。

普通函数
  1. template <typename T,typename U>
  2.   shared_ptr<T> static_pointer_cast(const shared_ptr<U>& r);
复制代码
要对保存在shared_ptr里的指针执行static_cast,我们可以取出指针然后强制转换它,但我们不能把它存到另一个shared_ptr里;新的 shared_ptr 会认为它是第一个管理这些资源的。解决的方法是用 static_pointer_cast. 使用这个函数可以确保被指物的引用计数保持正确。static_pointer_cast 不会抛出异常。

用法

使用shared_ptr解决的主要问题是知道删除一个被多个客户共享的资源的正确时机。下面是一个简单易懂的例子,有两个类 A 和 B, 它们共享一个int实例。使用 boost::shared_ptr, 你需要必须包含 "boost/shared_ptr.hpp".
  1. #include "boost/shared_ptr.hpp"
  2. #include <cassert>

  3. class A {
  4.   boost::shared_ptr<int> no_;
  5. public:
  6.   A(boost::shared_ptr<int> no) : no_(no) {}
  7.   void value(int i) {
  8.     *no_=i;
  9.   }
  10. };

  11. class B {
  12.   boost::shared_ptr<int> no_;
  13. public:
  14.   B(boost::shared_ptr<int> no) : no_(no) {}
  15.   int value() const {
  16.     return *no_;
  17.   }
  18. };

  19. int main() {
  20.     boost::shared_ptr<int> temp(new int(14));
  21.     A a(temp);
  22.     B b(temp);
  23.     a.value(28);
  24.     assert(b.value()==28);
  25. }
复制代码
类 A 和 B都保存了一个 shared_ptr<int>. 在创建 A 和 B的实例时,shared_ptr temp 被传送到它们的构造函数。这意味着共有三个 shared_ptr:a, b, 和 temp,它们都引向同一个int实例。如果我们用指针来实现对一个的共享,A 和 B 必须能够在某个时间指出这个int要被删除。在这个例子中,直到main的结束,引用计数为3,当所有 shared_ptr离开了作用域,计数将达到0,而最后一个智能指针将负责删除共享的 int.

回顾Pimpl用法

前一节展示了使用scoped_ptr的pimpl 用法,如果使用这种用法的类是不允许复制的,那么scoped_ptr在保存pimpl的动态分配实例时它工作得很好。但是这并不适合于所有想从pimpl用法中获益的类型(注意,你还可以用 scoped_ptr,但必须手工实现复制构造函数和赋值操作符)。对于那些可以处理共享的实现细节的类,应该用 shared_ptr。当pimpl的所有权被传递给一个 shared_ptr, 复制和赋值操作都是免费的。你可以回忆起,当使用 scoped_ptr 去处理pimpl类的生存期时,对封装类的复制是不允许的,因为 scoped_ptr是不可复制的。这意味着要使这些类支持复制和赋值,你必须手工定义复制构造函数和赋值操作符。当使用 shared_ptr 去处理pimpl类的生存期时,就不再需要用户自己定义复制构造函数了。注意,这时pimpl实例是被该类的多个对象所共享,因此如果规则是每个pimpl实例只能被类的一个实例使用,你还是要手工编写复制构造函数。解决的方法和我们在scoped_ptr那看到的很相似,只是把scoped_ptr换成了shared_ptr。

shared_ptr 与标准库容器

把对象直接存入容器中有时会有些麻烦。以值的方式保存对象意味着使用者将获得容器中的元素的拷贝,对于那些复制是一种昂贵的操作的类型来说可能会有性能的问题。此外,有些容器,特别是 std::vector, 当你加入元素时可能会复制所有元素,这更加重了性能的问题。最后,传值的语义意味着没有多态的行为。如果你需要在容器中存放多态的对象而且你不想切割它们,你必须用指针。如果你用裸指针,维护元素的完整性会非常复杂。从容器中删除元素时,你必须知道容器的使用者是否还在引用那些要删除的元素,不用担心多个使用者使用同一个元素。这些问题都可以用shared_ptr来解决。

下面是如何把共享指针存入标准库容器的例子。
  1. #include "boost/shared_ptr.hpp"
  2. #include <vector>
  3. #include <iostream>

  4. class A {
  5. public:
  6.   virtual void sing()=0;
  7. protected:
  8.   virtual ~A() {};
  9. };

  10. class B : public A {
  11. public:
  12.   virtual void sing() {
  13.     std::cout << "Do re mi fa so la";
  14.   }
  15. };

  16. boost::shared_ptr<A> createA() {
  17.   boost::shared_ptr<A> p(new B());
  18.   return p;
  19. }

  20. int main() {
  21.   typedef std::vector<boost::shared_ptr<A> > container_type;
  22.   typedef container_type::iterator iterator;

  23.   container_type container;
  24.   for (int i=0;i<10;++i) {
  25.     container.push_back(createA());
  26.   }

  27.   std::cout << "The choir is gathered: \n";
  28.   iterator end=container.end();
  29.   for (iterator it=container.begin();it!=end;++it) {
  30.     (*it)->sing();
  31.   }
  32. }
复制代码
这里有两个类, A 和 B, 各有一个虚拟成员函数 sing. B 从 A公有继承而来,并且如你所见,工厂函数 createA 返回一个动态分配的B的实例,包装在shared_ptr<A>里。在 main里, 一个包含shared_ptr<A>的 std::vector 被放入10个元素,最后对每个元素调用sing。如果我们用裸指针作为元素,那些对象需要被手工删除。而在这个例子里,删除是自动的,因为在vector的生存期中,每个shared_ptr的引用计数都保持为1;当 vector 被销毁,所有引用计数器都将变为零,所有对象都被删除。有趣的是,即使 A 的析构函数没有声明为 virtual, shared_ptr 也会正确调用 B的析构函数!

上面的例子示范了一个强有力的技术,它涉及A里面的protected析构函数。因为函数 createA 返回的是 shared_ptr<A>, 因此不可能对shared_ptr::get返回的指针调用 delete 。这意味着如果为了向某个需要裸指针的函数传送裸指针而从shared_ptr中取出裸指针的话,它不会由于意外地被删除而导致灾难。那么,又是如何允许 shared_ptr 删除它的对象的呢? 这是因为指针指向的真正类型是 B; 而B的析构函数不是protected的。这是非常有用的方法,用于给shared_ptr中的对象增加额外的安全性。

shared_ptr 与其它资源

有时你会发现你要把shared_ptr用于某个特别的类型,它需要其它清除操作而不是简单的 delete. shared_ptr可以通过客户化删除器来支持这种需要。那些处理象 FILE*这样的操作系统句柄的资源通常要使用象fclose这样的操作来释放。要在shared_ptr里使用 FILE* ,我们要定义一个类来负责释放相应的资源。
  1. class FileCloser {
  2. public:
  3.    void operator()(FILE* file) {
  4.     std::cout << "The FileCloser has been called with a FILE*, "
  5.       "which will now be closed.\n";
  6.     if (file!=0)
  7.       fclose(file);
  8.   }
  9. };
复制代码
这是一个函数对象,我们用它来确保在资源要释放时调用 fclose 。下面是使用FileCloser类的示例程序。
  1. int main() {
  2.   std::cout <<
  3.     "shared_ptr example with a custom deallocator.\n";
  4.   {
  5.     FILE* f=fopen("test.txt","r");
  6.     if (f==0) {
  7.       std::cout << "Unable to open file\n";
  8.       throw "Unable to open file";
  9.     }

  10.     boost::shared_ptr<FILE>
  11.       my_shared_file(f, FileCloser());

  12.     // 定位文件指针
  13.     fseek(my_shared_file.get(),42,SEEK_SET);
  14.   }
  15.   std::cout << "By now, the FILE has been closed!\n";
  16. }
复制代码
注意,在访问资源时,我们需要对shared_ptr使用 &* 用法, get, 或 get_pointer。(请注意最好使用 &*. 另两个选择不太清晰) 这个例子还可以更简单,如果我们在释放资源时只需要调用一个单参数函数的话,就根本不需要创建一个客户化删除器类型。上面的例子可以重写如下:
  1. {
  2.   FILE* f=fopen("test.txt","r");
  3.   if (f==0) {
  4.     std::cout << "Unable to open file\n";
  5.     throw file_exception();
  6.   }
  7.   
  8.   boost::shared_ptr<FILE> my_shared_file(f,&fclose);

  9.   // 定位文件指针
  10.   fseek(&*my_shared_file,42,SEEK_SET);
  11. }
  12. std::cout << "By now, the FILE* has been closed!\n";
复制代码
定制删除器在处理需要特殊释放程序的资源时非常有用。由于删除器不是 shared_ptr 类型的一部分,所以使用者不需要知道关于智能指针所拥有的资源的任何信息(当然除了如何使用它!)。例如,你可以使用对象池,定制删除器只需简单地把对象返还到池中。或者,一个 singleton 对象应该使用一个什么都不做的删除器。

使用定制删除器的安全性

我们已经看到对基类使用 protected 析构函数有助于增加使用shared_ptr的类的安全性。另一个达到同样安全级别的方法是,声明析构函数为 protected (或 private) 并使用一个定制删除器来负责销毁对象。这个定制删除器必须是它要删除的类的友元,这样它才可以工作。封装这个删除器的好方法是把它实现为私有的嵌套类,如下例所示:
  1. #include "boost/shared_ptr.hpp"
  2. #include <iostream>

  3. class A {
  4.   class deleter {
  5.     public:
  6.       void operator()(A* p) {
  7.         delete p;
  8.       }
  9.   };
  10.   friend class deleter;
  11. public:

  12.   virtual void sing() {
  13.     std::cout << "Lalalalalalalalalalala";
  14.   }

  15.   static boost::shared_ptr<A> createA() {
  16.     boost::shared_ptr<A> p(new A(),A::deleter());
  17.     return p;
  18.   }

  19. protected:
  20.   virtual ~A() {};
  21. };

  22. int main() {
  23.   boost::shared_ptr<A> p=A::createA();
  24. }
复制代码
注意,我们在这里不能使用普通函数来作为 shared_ptr<A> 的工厂函数,因为嵌套的删除器是A私有的。使用这个方法,用户不可能在栈上创建 A的对象,也不可能对A的指针调用 delete 。

从this创建shared_ptr

有时候,需要从this获得 shared_ptr ,即是说,你希望你的类被shared_ptr所管理,你需要把"自身"转换为shared_ptr的方法。看起来不可能?好的,解决方案来自于我们即将讨论的另一个智能指针boost::weak_ptr. weak_ptr 是 shared_ptr的一个观察者;它只是安静地坐着并看着它们,但不会影响引用计数。通过存储一个指向this的 weak_ptr 作为类的成员,就可以在需要的时候获得一个指向this的 shared_ptr。为了你可以不必编写代码来保存一个指向this的 weak_ptr,接着又从weak_ptr获shared_ptr得,Boost.Smart_ptr 为这个任务提供了一个助手类,称为 enable_shared_from_this. 只要简单地让你的类公有地派生自 enable_shared_from_this,然后在需要访问管理this的shared_ptr时,使用函数 shared_from_this 就行了。下面的例子示范了如何使用 enable_shared_from_this :
  1. #include "boost/shared_ptr.hpp"
  2. #include "boost/enable_shared_from_this.hpp"

  3. class A;

  4. void do_stuff(boost::shared_ptr<A> p) {
  5.   ...
  6. }

  7. class A : public boost::enable_shared_from_this<A> {
  8. public:
  9.   void call_do_stuff() {
  10.     do_stuff(shared_from_this());
  11.   }
  12. };

  13. int main() {
  14.   boost::shared_ptr<A> p(new A());
  15.   p->call_do_stuff();
  16. }
复制代码
这个例子还示范了你要用shared_ptr管理this的情形。类 A 有一个成员函数 call_do_stuff 需要调用一个普通函数 do_stuff, 这个普通函数需要一个类型为 boost:: shared_ptr<A>的参数。现在,在 A::call_do_stuff里, this 不过是一个 A指针, 但由于 A 派生自 enable_shared_from_this, 调用 shared_from_this 将返回我们所要的 shared_ptr 。在enable_shared_from_this的成员函数 shared_from_this里,内部存储的 weak_ptr 被转换为 shared_ptr, 从而增加了相应的引用计数,以确保相应的对象不会被删除。

总结

引用计数智能指针是非常重要的工具。Boost的 shared_ptr 提供了坚固而灵活的解决方案,它已被广泛用于多种环境下。需要在使用者之间共享对象是常见的,而且通常没有办法通知使用者何时删除对象是安全的。shared_ptr 让使用者无需知道也在使用共享对象的其它对象,并让它们无需担心在没有对象引用时的资源释放。这对于Boost的智能指针类而言是最重要的。你会看到Boost.Smart_ptr中还有其它的智能指针,但这一个肯定是你最想要的。通过使用定制删除器,几乎所有资源类型都可以存入 shared_ptr。这使得shared_ptr 成为处理资源管理的通用类,而不仅仅是处理动态分配对象。与裸指针相比,shared_ptr会有一点点额外的空间代价。我还没有发现由于这些代价太大而需要另外寻找一个解决方案的情形。不要去创建你自己的引用计数智能指针类。没有比使用 shared_ptr智能指针更好的了。
在以下情况时使用 shared_ptr :
  • 当有多个使用者使用同一个对象,而没有一个明显的拥有者时
  • 当要把指针存入标准库容器时
  • 当要传送对象到库或从库获取对象,而没有明确的所有权时
  • 当管理一些需要特殊清除方式的资源时

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值