scoped_ptr解析

来源:互联网 发布:布鲁斯威利斯 知乎 编辑:程序博客网 时间:2024/05/22 13:06

scoped_ptr解析

《超越C++标准库-Boost库导论》

头文件: "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); 
}

 

成员函数

explicit scoped_ptr(T* p=0)

构造函数,存储p的一份拷贝。注意,p 必须是用operator new分配的,或者是null. 在构造的时候,不要求T必须是一个完整的类型。当指针p是调用某个分配函数的结果而不是直接调用new得到的时候很有用:因为这个类型不必是完整的,只需要类型T的一个前向声明就可以了。这个构造函数不会抛出异常。

~scoped_ptr()

删除指针所指向的对象。类型T在被销毁时必须是一个完整的类型。如果scoped_ptr在它被析构时并没有保存资源,它就什么都不做。这个析构函数不会抛出异常。

void reset(T* p=0);

重置一个 scoped_ptr 就是删除它已保存的指针,如果它有的话,并重新保存p. 通常,资源的生存期管理应该完全由scoped_ptr自己处理,但是在极少数时候,资源需要在scoped_ptr的析构之前释放,或者scoped_ptr要处理它原有资源之外的另外一个资源。这时,就可以用reset,但一定要尽量少用它。(过多地使用它通常表示有设计方面的问题) 这个函数不会抛出异常。

T& operator*() const;

该运算符返回一个智能指针中存储的指针所指向的对象的引用。由于不允许空的引用,所以解引用一个拥有空指针的scoped_ptr将导致未定义行为。如果不能肯定所含指针是否有效,就用函数get替代解引用。这个函数不会抛出异常。

T* operator->() const;

返回智能指针所保存的指针。如果保存的指针为空,则调用这个函数会导致未定义行为。如果不能肯定指针是否空的,最好使用函数get。这个函数不会抛出异常。

T* get() const;

返回保存的指针。应该小心地使用get,因为它可以直接操作裸指针。但是,get使得你可以测试保存的指针是否为空。这个函数不会抛出异常。get通常在调用那些需要裸指针的函数时使用。

operator unspecified_bool_type() const

返回scoped_ptr是否为非空。返回值的类型是未指明的,但这个类型可被用于Boolean的上下文(boolean context)中。在if语句中最好使用这个类型转换函数,而不要用get去测试scoped_ptr的有效性

void swap(scoped_ptr& b)

交换两个scoped_ptr的内容。这个函数不会抛出异常。

普通函数

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:

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用法时。[4]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:

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.

原创粉丝点击