正文
智能指针能够使 C++ 的开发简单化,主要是它能够像其它限制性语言(如 C# 、 VB )自动管理内存的释放,而且能够做更多的事情。
1、 什么是智能指针
智能指针是一种像指针的 C++ 对象,但它能够在对象不使用的时候自己销毁掉。
我们知道在 C++ 中的对象不再使用是很难定义的,因此 C++ 中的资源管理是很复杂的。各种智能指针能够操作不同的情况。当然,智能指针能够在任务结束的时候删除对象,除了在程序之外。
许多库都提供了智能指针的操作,但都有自己的优点和缺点。 Boost 库是一个高质量的开源的 C++ 模板库,很多人都考虑将其加入下一个 C++ 标准库的版本中。
Boost 提供了下面几种智能指针:
shared_ptr<T> | 本指针中有一个引用指针记数器,表示类型 T 的对象是否已经不再使用。 shared_ptr 是 Boost 中提供普通的智能指针,大多数地方都使用 shared_ptr 。 |
scoped_ptr<T> | 当离开作用域能够自动释放的指针。因为它是不传递所有权的。事实上它明确禁止任何想要这样做的企图!这在你需要确保指针任何时候只有一个拥有者时的任何一种情境下都是非常重要的。 |
intrusive_ptr<T> | 比 shared_ptr 更好的智能指针,但是需要类型 T 提供自己的指针使用引用记数机制。 |
weak_ptr<T> | 一个弱指针,帮助 shared_ptr 避免循环引用。 |
shared_array<T> | 和 shared_ptr 类似,用来处理数组的。 |
scoped_array<T> | 和 scoped_ptr 类似,用类处理数组的。 |
下面让我们看一个简单的例子:
2、 首先介绍: 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 指针能够帮助我们处理那些容易忘记释放的对象。也因此在调试模式下如果使用了空指针,就会出现一个断言。
优点 | 自动释放本地对象和成员变量 [1] ,延迟实例化,操作 PIMPL 和 RAII (看下面) |
缺点 | 在 STL 容器里,多个指针操作一个对象的时候需要注意。 |
性能 | 使用 scoped_ptr 指针,会增加一个普通指针。 |
3、 引用指针计数器
引用指针计数器记录有多少个引用指针指向同一个对象,如果最后一个引用指针被销毁的时候,那么就销毁对象本身。
shared_ptr 就是 Boost 中普通的引用指针计数器,它表示可以有多个指针指向同一个对象,看下面的例子:
void Sample2_Shared() { // (A) 创建 Csample 类的一个实例和一个引用。 boost::shared_ptr<CSample> mySample( new CSample); printf("The Sample now has %i references\n", mySample.use_count()); // The Sample now has 1 references // (B) 付第二个指针给它。 boost::shared_ptr<CSample> mySample2 = mySample; // 现在是两个引用指针。 printf("The Sample now has %i references\n", mySample.use_count()); // (C) 设置第一个指针为空。 mySample.reset(); printf("The Sample now has %i references\n", mySample2.use_count()); // 一个引用 // 当 mySample2 离开作用域的时候,对象只有一个引用的时候自动被删除。 } |
在( A )中在堆栈重创建了 CSample 类的一个实例,并且分配了一个 shared_ptr 指针。对象 mySample 入下图所示:
然后我们分配了第二个指针 mySample2 ,现在有两个指针访问同一个数据。
我们重置第一个指针(将 mySample 设置为空 ),程序中仍然有一个 Csample 实例, mySample2 有一个引用指针。
只要当最有一个引用指针 mySample2 退出了它的作用域之外, Csample 这个实例才被销毁。
当然,并不仅限于单个 Csample 这个实例,或者是两个指针,一个函数,下面是用 shared_ptr 的实例:
· 用作容器中。
· 用在PIMPL的惯用手法 (the pointer-to-implementation idiom )。
· RAII (Resource-Acquisition-Is-Initialization)的惯用手法中。
· 执行分割接口。
注意:如果你没有听说过 PIMPL (a.k.a. handle/body) 和 RAII ,可以找一个好的 C++ 书,在 C++ 中处于重要的内容,一般 C++ 程序员都应该知道(不过我就是第一次看到这个写法)。智能指针只是一中方便的他们的方法,本文中不讨论他们的内容。
PIMPL :如果必须包容一个可能抛异常的子对象,但仍然不想从你自己的构造函数中抛出异常,考虑使用被叫做 Handle Class 或 Pimpl 的方法(“ Pimpl ”个双关语: pImpl 或“ pointer to implementation ”)
4、 主要特点
boost::shared_ptr 有一些重要的特征必须建立在其它操作之上。
· shared_ptr<T> 作用在一个未知类型上
当声明或定义一个 shared_ptr<T> , T 可能是一个未知的类型。例如你仅仅在前面声明了 class T ,但并没有定义 class T 。当我们要释放这个指针的时候我们需要知道这个 T 具体是一个声明类型。
· shared_ptr<T> 作用在任意类型上
在这里本质上不需要制定 T 的类型(如从一个基类继承下来的)
· shared_ptr<T> 支持自己定义释放对象的操作
如果你的类中自己写了释放方法,也可以使用。具体参照 Boost 文档。
· 强制转换
如果你定义了一个 U* 能够强制转换到 T* (因为 T 是 U 的基类),那么 shared_ptr<U> 也能够强制转换到 shared_ptr<T> 。
· shared_ptr 是线程安全的
(这种设计的选择超过它的优点,在多线程情况下是非常必要的)
· 已经作为一种惯例,用在很多平台上,被证明和认同的。
5、 例子:在容器中使用 shared_ptr
许多容器类,包括 STL ,都需要拷贝操作(例如,我们插入一个存在的元素到 list,vector, 或者 container 。)当拷贝操作是非常销毁资源的时候(这些操作时必须的),典型的操作就是使用容器指针。
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")) ); |
当 vector 被销毁的时候,这个元素自动被销毁了。当然,除非有另一个智能指针引用了它,则还本能被销毁。让我们看 Sample3 中的使用:
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 } |
6、 使用 Boost 中的智能指针,什么是正确的使用方法
使用智能指针的一些操作会产生错误(突出的事那些不可用的引用计数器,一些对象太容易释放,或者根本释放不掉)。 Boost 增强了这种安全性,处理了所有潜在存在的危险,所以我们要遵循以下几条规则使我们的代码更加安全。
下面几条规则是你应该必须遵守的:
规则一:赋值和保存 —— 对于智能指针来说,赋值是立即创建一个实例,并且保存在那里。现在智能指针拥有一个对象,你不能手动释放它,或者取走它,这将帮助你避免意外地释放了一个对象,但你还在引用它,或者结束一个不可用的引用计数器。
规则二:_ptr<T> 不是T* —— 恰当地说,不能盲目地将一个 T* 和一个智能指针类型 T 相互转换。意思是:
· 当创建一个智能指针的时候需要明确写出 __ptr<T> myPtr<new T> 。
· 不能将 T* 赋值给一个智能指针。
· 不能写 ptr = NULL ,应该使用 ptr.reset() 。
· 重新找回原始指针,使用 ptr.get() ,不必释放这个指针,智能指针会去释放、重置、赋值。使用 get() 仅仅通过函数指针来获取原始指针。
· 不能通过 T* 指向函数指针来代表一个 __ptr<T> ,需要明确构造一个智能指针,或者说将一个原始指针的所有权给一个指针指针。(见规则三)
· 这是一种特殊的方法来认定这个智能指针拥有的原始指针。不过在 Boost:smart pointer programming techniques 举例说明了许多通用的情况。
规则三:非循环引用 —— 如果有两个对象引用,而他们彼此都通过一个一个引用指针计数器,那么它们不能释放, Boost 提供了 weak_ptr 来打破这种循环引用(下面介绍)。
规则四:非临时的 share_ptr —— 不能够造一个临时的 share_ptr 来指向它们的函数,应该命名一个局部变量来实现。(这可以使处理以外更安全, Boost share_ptr best practices 有详细解说)。
7、 循环引用
引用计数器是一种便利的资源管理机制,它有一个基本回收机制。但循环引用不能够自动回收,计算机很难检测到。一个最简单的例子,如下:
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() ,那么我们两个对象都会失去联系。但这种正确的离开这个引用,共享的指针看上去没有理由去释放那两个对象,我们不能够再访问那两个对象,但那两个对象的确还存在,这是一种非常严重的内存泄露。如果拥有更多的这种对象,那么将由更多的临界资源不能正常释放。
如果不能解决好共享智能指针的这种操作,这将是一个严重的问题(至少是我们不可接受的)。因此我们需要打破这种循环引用,下面有三种方法:
A、 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
B、 当 Dad 的生存期超过 Child 的生存期的时候, Child 需要一个普通指针指向 Dad 。
C、 使用 boost::weak_ptr 打破这种循环引用。
方法 A 和 B 并不是一个完美的解决方案,但是可以在不使用 weak_ptr 的情况下让我们使用智能指针,让我们看看 weak_ptr 的详细情况。
8、 使用 weak_ptr 跳出循环
强引用和弱引用的比较:
一个强引用当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。 boost::share_ptr 就是强引用。相对而言,弱引用当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。
boost::weak_ptr<T> 是执行弱引用的智能指针。当你需要它的时候就可以使用一个强(共享)指针指向它(当对象被释放的时候,它为空),当然这个强指针在使用完毕应该立即释放掉,在上面的例子中我们能够修改它为弱指针。
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 } }; |
9、 Intrusive_ptr ——轻量级共享智能指针
shared_ptr 比普通指针提供了更完善的功能。有一个小小的代价,那就是一个共享指针比普通指针占用更多的空间,每一个对象都有一个共享指针,这个指针有引用计数器以便于释放。但对于大多数实际情况,这些都是可以忽略不计的。
intrusive_ptr 提供了一个折中的解决方案。它提供了一个轻量级的引用计数器,但必须对象本身已经有了一个对象引用计数器。这并不是坏的想法,当你自己的设计的类中实现智能指针相同的工作,那么一定已经定义了一个引用计数器,这样只需要更少的内存,而且可以提高执行性能。
如果你要使用 intrusive_ptr 指向类型 T ,那么你就需要定义两个函数: 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 |
这是一个最简单的(非线程安全)实现操作。但作为一种通用的操作,如果提供一种基类来完成这种操作或许很有使用价值,也许在其他地方会介绍到。
10、 scoped_array 和 shared_array
scoped_array 和 shared_array 和上面讲的 基本上相同,只不过 他们 是指向数组的。就像使用指针操作一样使用 operator new[] ,他们都重载了 operator new[] 。注意他们并不初始化分配长度。
11、 Boost 的安装
从 www.boost.org 上下载最新版本的 boost ,然后解压缩到你指定的目录里,解压缩后的文件目录如下:
Boost\ boost 的源文件和头文件。
Doc\ HTML 格式的文档。
Lib\ 库文件(不是必需的)
… 其他文件(“ more\ ”里有其他资料)
添加目录到我们自己的 IDE 里:
VC6 :在菜单 Tools/Options , Directories tab, "Show Directories for... Include files",
VC7 : 在菜单 Tools/Options, Projects/VC++ directories, "Show Directories for... Include files".
Boost 的头文件都在 boost\ 子目录里,例如本文档例子中有 #include "boost/smart_ptr.hpp" 。所以任何人当读到年的源文件的时候就立刻知道你用到了 boost 中的智能指针。
12、 关于本文档中的例子
本文档中的例子里有一个子目录 boost\ 仅仅包含了本例子中使用到的一些头文件,仅仅是为了你编译这个例子,如果你需要下载完整的 boost 或者获取更多的资源请到 www.boost.org 。
13、 VC6 中 min/max 的灾难
当在 VC 中使用 boost 库,或者其他库的时候会有一些小的问题。
在 Windows 的头文件中已经定义了 min 和 max 宏,所以在 STL 中的这两个函数就调用不到了,例如在 MFC 中就是这样,但是在 Boost 中,都是使用的 std:: 命名空间下的函数,使用 Windows 的函数不能够接受不同类型的参数在模板中使用,但是许多库都依赖这些。
虽然 Boost 尽量处理这些问题,但有时候遇到这样的问题的时候就需要在自己的代码中加入像下面的代码在第一个 #include 前加入 #define _NOMINMAX 。
#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; |
这样操作并不是在任何时候都需要,而只有我们碰到使用了就需要加入这段代码。
14、 资源
获取更多的信息,或者有问题可以查找如下资源:
· Boost home page
· Download Boost
· Smart pointer overview
· Boost users mailing list
· Boost中的智能指针 ( 撰文 Bjorn Karlsson 翻译 曾毅)