使用智能指针增强您的代码
对 Boost 库提供的智能指针的初学者介绍。
目录
智能指针可以极大地简化 C++ 开发。最主要的是,它们提供了类似于 C# 或 VB 等更严格的语言的自动内存管理,但它们能做的远不止这些。
- 什么是智能指针?
- 第一个:boost::scoped_ptr<T>
- 引用计数指针
- 示例:在容器中使用 shared_ptr
- 你绝对需要知道什么才能正确使用 boost 智能指针
- 循环引用
- 使用 weak_ptr 打破循环
- intrusive_ptr - 轻量级共享指针
- scoped_array 和 shared_array
- 安装 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 对“纯粹”指针的开销增加很小(如果有的话),它执行 |
- 出于此目的,使用
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_ptr
,mySample
。看起来是这样的
然后,我们将其分配给第二个指针 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 *
(例如,因为T
是U
的基类),则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()
,我们将失去与这两个对象的所有“联系”。但这样一来,每个对象都只剩下一个引用,而共享指针看不到任何理由删除其中任何一个!最多这是一种内存泄漏;最坏的情况下,对象会持有更关键的资源,但这些资源没有被正确释放。
这个问题无法通过“更好”的共享指针实现来解决(或者至少只能以不可接受的开销和限制来解决)。所以你必须打破那个循环。有两种方法
- 在释放对其的最后一个引用之前手动打破循环
- 当
Dad
的生命周期已知会超过Child
的生命周期时,Child
可以使用指向Dad
的普通(裸)指针。 - 使用
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
提供了一个有趣的权衡:它提供了“尽可能轻量级”的引用计数指针,前提是对象本身实现了引用计数。当您设计自己的类来与智能指针一起使用时,这其实并不算糟;将引用计数嵌入类本身很容易,可以减少内存占用并提高性能。
要将类型 T
与 intrusive_ptr
一起使用,您需要定义两个函数:intrusive_ptr_add_ref
和 intrusive_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_ptr
和 shared_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 头文件为 min
和 max
定义了宏,因此,这些函数在(原始)STL 实现中缺失。某些 Windows 库(如 MFC)依赖于 min
/max
的存在。然而,Boost 期望 min
和 max
在 std::
命名空间中。更糟糕的是,没有可行的 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;
这个解决方案(与其他任何解决方案一样)也并非没有问题,但它在我需要的所有情况下都有效,而且只需要在一个地方放置它。
资源
信息不足?更多问题?
- boost 用户邮件列表
有关 boost 的问题?那就是您应该去的地方。
Code Project 上的文章
- Andrew Walker 的 Boost 入门
- Jim D'Agostino 的使用 Boost 设计健壮的对象,一篇非常有趣的文章:它似乎涵盖了太多的主题,但它擅长展示不同的工具、机制、范式、库等如何协同工作。
请注意:虽然我很乐意收到(几乎)任何反馈,但请不要在这里询问与 boost 相关的问题。简单来说,boost 专家不太可能在这里找到您的问题(而我只是一个 boost 新手)。当然,如果您对本文或示例项目有任何疑问、投诉或建议,都欢迎您提出。
历史
- 2004 年 9 月 5 日:初始版本
- 2004 年 9 月 27 日:发布到 CodeProject
- 2004 年 9 月 29 日:小幅修正