前奏:
Garbage Collection 技术一直颇受注目,并且在 Java 中已经发展成熟,成为内
存管理的一大利器,但它在 C++ 语言中的发展却不顺利,C++ 为了追求运行速度,20 年来
态度坚决地将其排除在标准之外。
为了稍许平复因为没有 Garbage Collection 而引发的 C++ 程序员的怨气,C++
对 Smart Pointer 技术采取了不同的态度。
首先,了解一下智能指针,
该方法使用一个指针类来代表对资源的管理逻辑,并将指向资源的句柄(指针
或引用)通过构造函数传递给该类。当离开当前范围(scope)时,该对象的析构函数一定会
被调用,所以嵌在析构函数中的资源回收的代码也总是会被执行。这种方法的好处在于,由
于将资源回收的逻辑通过特定的类从原代码中剥离出来,自动正确地销毁动态分配的对象,
这会让思路变得更加清晰,同时确保内存不发生泄露。
它的一种通用实现技术是使用引用计数(Reference Count) 。引用计数智能指针,是一
种生命期受管的对象,其内部有一个引用计数器。当内部引用计数为零时,这些对象会自动
销毁自身的智能指针类。每次创建类的新对象时,会初始化指针并将引用计数置为 1 ;当对
象作为另一对象的副本而创建时,它会调用拷贝构造函数拷贝指针并增加与之相应的引用计
数 ;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数 ;如果引用计数
减至 0,则删除对象,并增加右操作数所指对象的引用计数 ;调用析构函数时,构造函数减
少引用计数,直到计数为 0,释放对象空间。
其次,auto_ptr包含在#include <memory>
auto_ptr 可以指向一个以 new 建立的对象,当 auto_ptr 的生命周期结束时,其所指向的
对象之资源也会被自动释放,且不必显式地调用 delete,而对象指针的操作依旧如故。例如:
class A
{
public:
A(){}
~A(){}
void Hello()
{
std::cout<<"Hello Smart Pointer";
}
};
int main()
{
std::auto_ptr<A> pA(new A());
pA->Hello();
return 0;
}
当然,也可以建立一个未指向任何对象的 auto_prt,例如:
std::auto_ptr<int> iPtr;
它就像空指针,未指向任何对象,所以也就不能进行操作,但是可以通过 get() 函数来
判断它是否指向对象的地址:
if(iPtr.get() == 0) // 不指向任何对象
{
iPtr.reset(new int(2011)); // 指向一个对象
}
auto_ptr 还可以使用另一个 auto_ptr 来建立,但是需要十分小心的是,这会造成所有权
的转移,例如:
auto_ptr< string> sPtr1 (new string("Smart Pointer"));
auto_ptr< string> sPtr2 (sPtr1);
if( !sPtr1->empty() )
cout<<*sPtr1<< endl;
当使用 sPtr1 来建立 sPtr2 时,sPtr1 不再对所指向对象的资源释放负责,而是将接力
棒传递到了 sPtr2 的手里,sPtr1 丧失了使用 string 类成员函数的权利,所以在判断 sPtr1-
>empty() 时程序会崩溃。
auto_ptr 的资源维护动作是以 inline 的方式来完成的,在编译时代码会被扩展开来,所
以使用它并不会牺牲效率。虽然 auto_ptr 指针是一个 RAII (Resource Acquisition In Initialization)对象,能够给我们带来很多便利,
但是它的缺点同样不可小觑:
auto_ptr 对象不可作为 STL 容器的元素,所以二者带来的便利不能同时拥有。这一重
大缺陷让 STL 的忠实拥趸们愤怒不已。
auto_ptr 缺少对动态配置而来的数组的支持,如果用它来管理这些数组,结果是可怕
的、不可预期的。
auto_ptr 在被复制的时候会发生所有权转移。
就在 2011 年的 9 月刚刚获得通过的 C++ 新标
准 C++ 11 中废弃了 auto_ptr 指针,取而代之的是两个新的指针类:shared_ptr 和 unique_ptr。
shared_ptr 只是单纯的引用计数指针,unique_ptr 是用来取代 auto_ptr 的。unique_ptr 提供了
auto_ptr 的大部分特性,唯一的例外是 auto_ptr 的不安全、隐性的左值搬移 ;而 unique_ptr
可以存放在 C++0x 提出的那些能察觉搬移动作的容器之中。
在 Boost 中的智能指针共有五种 :scoped_ptr、scoped_array、shared_ptr、shared_array、
weak_ptr,其中最有用的就是 shared_ptr,它采取了引用计数,并且是线程安全的,同时支
持扩展,推荐在大多数情况下使用。
boost::shared_ptr 支持 STL 容器:
typedef boost::shared_ptr<string> CStringPtr;
std::vector< CStringPtr > strVec;
strVec.push_back( CStringPtr(new string("Hello")) );
当 vector 被销毁时,其元素 — 智能指针对象才会被销毁,除非这个对象被其他的智能
指针引用,如下面的代码片段所示:
typedef boost::shared_ptr<string> CStringPtr;
std::vector< CStringPtr > strVec;
strVec.push_back( CStringPtr(new string("Hello")) );
strVec.push_back( CStringPtr(new string("Smart")) );
strVec.push_back( CStringPtr(new string("Pointer")) );
CStringPtr strPtr = strVec[0];
strVec.clear(); //strVec 清空,但是保留了 strPtr 引用的 strVec[0]
cout<<*strPtr<<endl; // strVec[0] 依然有效
Boost 智能指针同样支持数组,boost::scoped_array 和 boost::shared_array 对象指向的是
动态配置的数组。
Boost 的智能指针虽然增强了安全性,处理了潜在的危险,但是我们在使用时还是应该
遵守一定的规则,以确保代码更加鲁棒。
规则 1:Smart_ptr<T> 不同于 T*
Smart_ptr<T> 的真实身份其实是一个对象,一个管理动态配置对象的对象,而 T* 是指
向 T 类型对象的一个指针,所以不能盲目地将一个 T* 和一个智能指针类型 Smart_ptr<T> 相
互转换。
在创建一个智能指针的时候需要明确写出 Smart_ptr<T> tPtr<new T>。
禁止将 T* 赋值给一个智能指针。
不能采用 tPtr = NULL 的方式将 tPtr 置空,应该使用智能指针类的成员函数。
规则 2:不要使用临时的 share_ptr 对象
class A;
bool IsAllReady();
void ProcessObject(boost::shared_ptr< A> pA, bool isReady);
ProcessObject(boost::shared_ptr(new A), IsAllReady());
调用 ProcessObject 函数之前,C++ 编译器必须完成三件事:
(1) 执行 "new A"。
(2) 调用 boost::shared_ptr 的构造函数。
(3) 调用函数 IsAllReady()。
因为函数参数求值顺序的不确定性,如果调用 IsAllReady() 发生在另外两个过程中间,
而它又正好出现了异常,那么 new A 得到的内存返回的指针就会丢失,进而发生内存泄露,
因为返回的指针没有被存入我们期望能阻止资源泄漏的 boost::shared_ptr 上。避免出现这种
问题的方式就是不要使用临时的 share_ptr 对象,改用一个局部变量来实现,在一个独立的语
句中将通过 new 创建出来的对象存入智能指针中:
boost::shared_ptr<A> pA(new A)
ProcessObject(pA, IsAllReady());
如果疏忽了这一点,当异常发生时,可能会引起微妙的资源泄漏。