65.9K
CodeProject 正在变化。 阅读更多。
Home

使用智能指针增强您的代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (116投票s)

2004年9月27日

CPOL

13分钟阅读

viewsIcon

1140515

downloadIcon

2526

对 Boost 库提供的智能指针的初学者介绍。

目录

智能指针可以极大地简化 C++ 开发。最主要的是,它们提供了类似于 C# 或 VB 等更严格的语言的自动内存管理,但它们能做的远不止这些。

我已经知道智能指针,但为什么要使用 Boost?

什么是智能指针?

名字应该已经说明了一切

智能指针是一个 C++ 对象,它表现得像一个指针,但当它不再需要时,还会额外删除它所指向的对象。

“不再需要”很难界定,因为 C++ 中的资源管理非常复杂。不同的智能指针实现覆盖了最常见的情况。当然,也可以实现除了删除对象之外的其他任务,但这些应用超出了本教程的范围。

许多库都提供具有不同优缺点和劣势的智能指针实现。这里的示例使用了 BOOST 库,一个高质量的开源模板库,其中许多提交都已考虑纳入下一个 C++ 标准。

Boost 提供了以下智能指针实现

shared_ptr<T> 指向 T 的指针,使用引用计数来确定何时不再需要该对象。shared_ptr 是 boost 提供的通用、用途最广泛的智能指针。
scoped_ptr<T> 在超出作用域时自动删除的指针。不允许赋值,但与“裸”指针相比没有性能损失
intrusive_ptr<T> 另一种引用计数指针。它比 shared_ptr 性能更好,但要求类型 T 提供自己的引用计数机制。
weak_ptr<T> 弱指针,与 shared_ptr 结合使用以避免循环引用
shared_array<T> 类似于 shared_ptr,但访问语法用于 T 的数组
scoped_array<T> 类似于 scoped_ptr,但访问语法用于 T 的数组

让我们从最简单的开始

第一个:boost::scoped_ptr<T>

scoped_ptr 是 boost 提供的最简单的智能指针。它保证在指针超出作用域时自动删除。

关于示例的说明

示例使用了一个辅助类 CSample,该类在构造、赋值或销毁时打印诊断消息。仍然值得用调试器单步执行。示例包含了 boost 所需的部分,因此无需额外下载 - 但请阅读下面的 boost 安装说明。

以下示例使用 scoped_ptr 进行自动销毁

使用普通指针 使用 scoped_ptr


void Sample1_Plain()
{
  CSample * pSample(new CSample);

  if (!pSample->Query() )
  // just some function...
  {
    delete pSample;
    return;
  }

  pSample->Use();
  delete pSample;

}
#include "boost/smart_ptr.h"

void Sample1_ScopedPtr()
{
  boost::scoped_ptr<CSample> 
             samplePtr(new CSample);

  if (!samplePtr->Query() )
  // just some function...
    return;    




  samplePtr->Use();

}

使用“普通”指针,我们必须记住在每次退出函数的地方删除它。这尤其令人厌烦(并且很容易忘记),尤其是在使用异常时。第二个示例使用 scoped_ptr 完成相同的任务。当函数返回时,它会自动删除指针,即使在发生异常的情况下也是如此(“裸指针”示例甚至没有涵盖这一点!)。

优点显而易见:在一个更复杂的函数中,很容易忘记删除对象。scoped_ptr 会为您完成。另外,在解引用 NULL 指针时,会在调试模式下收到断言。

用于 自动删除局部对象或类成员1,延迟实例化,实现 PIMPL 和 RAII(见下文)
不适用于 STL 容器中的元素,指向同一对象的多个指针
性能 scoped_ptr 对“纯粹”指针的开销增加很小(如果有的话),它执行
  1. 出于此目的,使用 scoped_ptr 比(易于误用且更复杂)的 std::auto_ptr 更具表达力:使用 scoped_ptr,您表明不打算或不允许所有权转移。

引用计数指针

引用计数指针 跟踪有多少指针指向一个对象,当指向对象的最后一个指针被销毁时,它也会删除该对象本身。

Boost 提供的“普通”引用计数指针是 shared_ptr(名称表明多个指针可以共享同一对象)。让我们看几个例子

void Sample2_Shared()
{
  // (A) create a new CSample instance with one reference
  boost::shared_ptr<CSample> mySample(new CSample); 
  printf("The Sample now has %i references\n", mySample.use_count()); // should be 1

  // (B) assign a second pointer to it:
  boost::shared_ptr<CSample> mySample2 = mySample; // should be 2 refs by now
  printf("The Sample now has %i references\n", mySample.use_count());

  // (C) set the first pointer to NULL
  mySample.reset(); 
  printf("The Sample now has %i references\n", mySample2.use_count());  // 1

  // the object allocated in (1) is deleted automatically
  // when mySample2 goes out of scope
}

第 A 行在堆上创建一个新的 CSample 实例,并将指针分配给一个 shared_ptrmySample。看起来是这样的

然后,我们将其分配给第二个指针 mySample2。现在,两个指针访问相同的数据

我们重置第一个指针(相当于裸指针的 p=NULL)。CSample 实例仍然存在,因为 mySample2 持有指向它的引用

只有当最后一个引用 mySample2 超出作用域时,CSample 才会随之销毁

当然,这不限于单个 CSample 实例、两个指针或一个函数。以下是 shared_ptr 的一些用例。

  • 在容器中使用
  • 使用实现指针(PIMPL)的模式
  • 资源获取即初始化(RAII)模式
  • 分离接口与实现

注意:如果您从未听说过 PIMPL(又名 handle/body)或 RAII,请找一本好的 C++ 书籍 - 它们是每个 C++ 程序员都应该了解的重要概念。智能指针只是在某些情况下方便实现它们的几种方法之一 - 在这里讨论它们会超出本文的范围。

重要特性

boost::shared_ptr 的实现有一些重要特性,使其在其他实现中脱颖而出

  • shared_ptr<T> 可以与不完整类型一起工作

    声明或使用 shared_ptr<T> 时,T 可以是“不完整类型”。例如,您只使用 class T; 进行前向声明。但尚未定义 T 的实际样子。只有在您解引用指针时,编译器才需要知道“所有内容”。

  • shared_ptr<T> 可以与任何类型一起工作

    T 几乎**没有**要求(例如继承自基类)。

  • shared_ptr<T> 支持自定义删除器

    因此,您可以存储需要与 delete p 不同清理的对象。有关更多信息,请参阅 boost 文档。

  • 隐式转换

    如果类型 U * 可以隐式转换为 T *(例如,因为 TU 的基类),则 shared_ptr<U> 也可以隐式转换为 shared_ptr<T>

  • shared_ptr 是线程安全的

    (这是一个设计选择,而不是一个优势,然而,它在多线程程序中是必需的,并且开销很低。)

  • 跨平台兼容,经过验证和同行评审,常规的优势。

示例:在容器中使用 shared_ptr

许多容器类,包括 STL 容器,都需要复制操作(例如,将现有元素插入列表、向量或容器时)。然而,当这些复制操作成本高昂(甚至不可用)时,通常的解决方案是使用指针容器

std::vector<CMyLargeClass *> vec;
vec.push_back( new CMyLargeClass("bigString") );

然而,这又将内存管理任务交还给调用者。但是,我们可以使用 shared_ptr

typedef boost::shared_ptr<CMyLargeClass>  CMyLargeClassPtr;
std::vector<CMyLargeClassPtr> vec;
vec.push_back( CMyLargeClassPtr(new CMyLargeClass("bigString")) );

非常相似,但现在,元素在向量被销毁时会自动销毁 - 当然,除非还有另一个智能指针仍然持有引用。让我们看看示例 3

void Sample3_Container()
{
  typedef boost::shared_ptr<CSample> CSamplePtr;

  // (A) create a container of CSample pointers:
  std::vector<CSamplePtr> vec;

  // (B) add three elements
  vec.push_back(CSamplePtr(new CSample));
  vec.push_back(CSamplePtr(new CSample));
  vec.push_back(CSamplePtr(new CSample));

  // (C) "keep" a pointer to the second: 
  CSamplePtr anElement = vec[1];

  // (D) destroy the vector:
  vec.clear();

  // (E) the second element still exists
  anElement->Use();
  printf("done. cleanup is automatic\n");

  // (F) anElement goes out of scope, deleting the last CSample instance
}

你绝对需要知道什么才能正确使用 boost 智能指针

智能指针可能会出现一些问题(最突出的是无效的引用计数,导致对象过早删除或根本不删除)。boost 实现促进了安全性,使所有“潜在危险”的操作都变得显式。因此,只要记住几个规则,您就是安全的。

不过,有几个规则您应该(或必须)遵守

规则 1:分配并持有 - 立即将新构造的实例分配给智能指针,然后一直持有。智能指针现在拥有该对象,您不得手动删除它,也不能将其取回。这有助于避免意外删除仍被智能指针引用的对象,或导致无效的引用计数。

规则 2:_ptr<T> 不是 T * - 更准确地说,T * 和指向 T 类型的智能指针之间没有隐式转换。

这意味着

  • 创建智能指针时,您必须显式编写 ..._ptr<T> myPtr(new T)
  • 您不能将 T * 分配给智能指针
  • 您甚至不能写 ptr=NULL。为此请使用 ptr.reset()
  • 要检索裸指针,请使用 ptr.get()。当然,您不得删除该指针,或在它所属的智能指针被销毁、重置或重新分配后使用它。仅当您必须将指针传递给需要裸指针的函数时,才使用 get()
  • 您不能直接将 T * 传递给需要 _ptr<T> 的函数。您必须显式构造一个智能指针,这也会清楚地表明您将裸指针的所有权转移给智能指针。(另请参阅规则 3。)
  • 没有通用的方法可以找到“持有”给定裸指针的智能指针。但是,boost:智能指针编程技术 说明了许多常见情况的解决方案。

规则 2:无循环引用 - 如果两个对象通过引用计数指针相互引用,则它们永远不会被删除。Boost 提供了 weak_ptr 来打破此类循环(见下文)。

规则 3:无临时 shared_ptr - 不要构造临时 shared_ptr 将其传递给函数,始终使用命名的(局部)变量。(这在异常情况下可以使您的代码安全。有关详细解释,请参阅boost:shared_ptr 最佳实践。)

循环引用

引用计数是一种方便的资源管理机制,但它有一个根本的缺点:循环引用不会自动释放,并且计算机很难检测到。最简单的例子是

struct CDad;
struct CChild;

typedef boost::shared_ptr<CDad>   CDadPtr;
typedef boost::shared_ptr<CChild> CChildPtr;


struct CDad : public CSample
{
   CChildPtr myBoy;
};

struct CChild : public CSample
{
  CDadPtr myDad;
};

// a "thing" that holds a smart pointer to another "thing":

CDadPtr   parent(new CDadPtr); 
CChildPtr child(new CChildPtr);

// deliberately create a circular reference:
parent->myBoy = child; 
child->myDad = dad;


// resetting one ptr...
child.reset();

parent 仍然引用 CDad 对象,该对象本身引用 CChild。整个结构看起来像这样

现在如果我们调用 dad.reset(),我们将失去与这两个对象的所有“联系”。但这样一来,每个对象都只剩下一个引用,而共享指针看不到任何理由删除其中任何一个!最多这是一种内存泄漏;最坏的情况下,对象会持有更关键的资源,但这些资源没有被正确释放。

这个问题无法通过“更好”的共享指针实现来解决(或者至少只能以不可接受的开销和限制来解决)。所以你必须打破那个循环。有两种方法

  1. 在释放对其的最后一个引用之前手动打破循环
  2. Dad 的生命周期已知会超过 Child 的生命周期时,Child 可以使用指向 Dad 的普通(裸)指针。
  3. 使用 boost::weak_ptr 来打破循环。

解决方案 (1) 和 (2) 并不是完美的解决方案,但它们适用于不提供像 boost 那样提供 weak_ptr 的智能指针库。但让我们详细看看 weak_ptr

使用 weak_ptr 打破循环

强引用与弱引用:

强引用会保持被引用对象的生存(即,只要对象存在至少一个强引用,它就不会被删除)。boost::shared_ptr 充当强引用。相比之下,弱引用不会保持对象生存,它只是在对象生存期间引用它。

请注意,在此意义上,裸 C++ 指针是弱引用。但是,如果您只有指针,您就无法检测对象是否仍然存在。

boost::weak_ptr<T> 是一个充当弱引用的智能指针。当您需要时,您可以从中请求一个强(共享)指针。(如果对象已被删除,则可能为 NULL。)当然,强指针应在使用后立即释放。在上面的示例中,我们可以决定将一个指针设为弱指针

struct CBetterChild : public CSample
{
  weak_ptr<CDad> myDad;

  void BringBeer()
  {
    shared_ptr<CDad> strongDad = myDad.lock(); // request a strong pointer
    if (strongDad)                      // is the object still alive?
      strongDad->SetBeer();
    // strongDad is released when it goes out of scope.
    // the object retains the weak pointer
  }
};

更多内容请参见示例 5。

intrusive_ptr - 轻量级共享指针

shared_ptr 提供了比“普通”指针更多的服务。这有一个小代价:共享指针的大小比普通指针大,并且对于共享指针中持有的每个对象,都有一个跟踪对象来保存引用计数和删除器。在大多数情况下,这是可以忽略的。

intrusive_ptr 提供了一个有趣的权衡:它提供了“尽可能轻量级”的引用计数指针,前提是对象本身实现了引用计数。当您设计自己的类来与智能指针一起使用时,这其实并不算糟;将引用计数嵌入类本身很容易,可以减少内存占用并提高性能。

要将类型 Tintrusive_ptr 一起使用,您需要定义两个函数:intrusive_ptr_add_refintrusive_ptr_release。以下示例显示了如何为自定义类执行此操作

#include "boost/intrusive_ptr.hpp"

// forward declarations
class CRefCounted;


namespace boost
{
    void intrusive_ptr_add_ref(CRefCounted * p);
    void intrusive_ptr_release(CRefCounted * p);
};

// My Class
class CRefCounted
{
  private:
    long    references;
    friend void ::boost::intrusive_ptr_add_ref(CRefCounted * p);
    friend void ::boost::intrusive_ptr_release(CRefCounted * p);

  public:
    CRefCounted() : references(0) {}   // initialize references to 0
};

// class specific addref/release implementation
// the two function overloads must be in the boost namespace on most compilers:
namespace boost
{
 inline void intrusive_ptr_add_ref(CRefCounted * p)
  {
    // increment reference count of object *p
    ++(p->references);
  }



 inline void intrusive_ptr_release(CRefCounted * p)
  {
   // decrement reference count, and delete object when reference count reaches 0
   if (--(p->references) == 0)
     delete p;
  } 
} // namespace boost

这是最简单(且非线程安全)的实现。然而,这是一个非常常见的模式,所以提供一个公共基类来完成这项任务是有意义的。也许是另一篇文章;)

scoped_array 和 shared_array

它们几乎与 scoped_ptrshared_ptr 相同 - 只是它们表现得像数组指针,即像使用 operator new[] 分配的指针。它们提供了重载的 operator[]。请注意,它们都不会知道初始分配的长度。

安装 Boost

boost.org 下载当前 boost 版本,并将其解压缩到您选择的文件夹。解压缩后的源文件具有以下结构(使用我的文件夹)

boost\ 实际的 boost 源文件/头文件
doc\ 当前版本的文档,HTML 格式
libs\ 库(不需要用于
.... 一些奇怪的小东西(“更多”有一些有趣的东西)

我将此文件夹添加到 IDE 的通用包含目录中

  • 在 VC6 中,这是工具/选项目录选项卡,"显示目录用于...包含文件",
  • 在 VC7 中,这是工具/选项,然后是项目/VC++ 目录,"显示目录用于...包含文件"。

由于实际头文件位于 boost\ 子文件夹中,因此我的源代码包含 #include "boost/smart_ptr.hpp"。所以任何阅读源代码的人都会立即知道您正在使用 boost 智能指针,而不仅仅是任何智能指针。

关于示例项目说明

示例项目包含一个名为 boost\ 的子文件夹,其中包含 boost 所需的 **部分** 头文件。这仅仅是为了让您可以下载并编译示例。您应该真正下载完整且最新的源代码(**现在**!)。

VC6:min/max 的悲剧

VC6 有一个“小”问题,使得开箱即用 boost(和其他库)的使用有些麻烦。

Windows 头文件为 minmax 定义了宏,因此,这些函数在(原始)STL 实现中缺失。某些 Windows 库(如 MFC)依赖于 min/max 的存在。然而,Boost 期望 minmaxstd:: 命名空间中。更糟糕的是,没有可行的 min/max 模板可以接受不同的(隐式可转换的)参数类型,但某些库依赖于此。

boost 尽其所能修复这个问题,但有时您会遇到问题。如果发生这种情况,这是我所做的:在第一个 include 之前放置以下代码

#define _NOMINMAX            // disable windows.h defining min and max as macros
#include "boost/config.hpp"  // include boosts compiler-specific "fixes"
using std::min;              // makle them globally available
using std::max;

这个解决方案(与其他任何解决方案一样)也并非没有问题,但它在我需要的所有情况下都有效,而且只需要在一个地方放置它。

资源

信息不足?更多问题?

Code Project 上的文章

请注意:虽然我很乐意收到(几乎)任何反馈,但请不要在这里询问与 boost 相关的问题。简单来说,boost 专家不太可能在这里找到您的问题(而我只是一个 boost 新手)。当然,如果您对本文或示例项目有任何疑问、投诉或建议,都欢迎您提出。

历史

  • 2004 年 9 月 5 日:初始版本
  • 2004 年 9 月 27 日:发布到 CodeProject
  • 2004 年 9 月 29 日:小幅修正
© . All rights reserved.