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 t
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值