这篇文章试图说明如何使用auto_ptr和shared_ptr,从而使得动态分配对象的使用和管理更安全,方便。除了一般的使用说明外,更主要是说明它们之间的异同 —— 满足需求的不同和开销上的差异。
文章的多数知识都来源于:
<!--[if !supportLists]-->
1. <!--[endif]-->Exceptional C++(Herb)Item 37 auto_ptr
<!--[if !supportLists]-->
2. <!--[endif]-->Exceptional C++ Style(Herb)和C++ Coding Standard(Herb,Andrei)其中一些关于使用shared_ptr的论述
<!--[if !supportLists]-->
3. <!--[endif]-->GC++和VC++ 8.0 auto_ptr的源代码
<!--[if !supportLists]-->
4. <!--[endif]-->Boost库shared_ptr的源码和文档
auto_ptr和shared_ptr都是智能指针的一种实现,所谓智能指针,多数情况下都是指这样的一些对象:
<!--[if !supportLists]-->
1. <!--[endif]-->内部有一个动态分配对象的指针,拥有该对象的使用权和所有权(独占或共享)。
<!--[if !supportLists]-->
2. <!--[endif]-->重载*和->操作,行为上跟所拥有的对象的指针一致。
<!--[if !supportLists]-->
3. <!--[endif]-->当自身的生命期结束的时候,会做一些跟拥有对象相关的清理动作。
auto_ptr
auto_ptr是现在标准库里面一个轻量级的智能指针的实现,存在于头文件 memory中,之所以说它是轻量级,是因为它只有一个成员变量(拥有对象的指针),相关的调用开销也非常小。
下面的代码来自于VC++ 8.0里面的源码:
里面有个auto_ptr_ref的数据结构,我们可以把它忽略,这个只是内部使用的代理结构,用于一些隐式的const变化,我们客户端代码通常不会直接使用到它。
我们可以看到除了构造函数,拷贝构造函数,赋值函数,析构函数和两个重载操作符(*,->)外,还有get,release和reset三个函数,它们的作用分别是:
<!--[if !supportLists]-->
1. <!--[endif]-->get,获得内部对象的指针
<!--[if !supportLists]-->
2. <!--[endif]-->release,放弃内部对象的所有权,将内部指针置为空
<!--[if !supportLists]-->
3. <!--[endif]-->reset,销毁内部对象并接受新的对象的所有权(如果使用缺省参数的话,也就是没有任何对象的所有权)
下面的例程来自Exceptional C++,Item 37:
//
Example 2: Using an auto_ptr
//
void
g()
...
{
T* pt1 = new T;
// right now, we own the allocated object
// pass ownership to an auto_ptr
auto_ptr<T> pt2( pt1 );
// use the auto_ptr the same way
// we'd use a simple pointer
*pt2 = 12; // same as "*pt1 = 12;"
pt2->SomeFunc(); // same as "pt1->SomeFunc();"
// use get() to see the pointer value
assert( pt1 == pt2.get() );
// use release() to take back ownership
T* pt3 = pt2.release();
// delete the object ourselves, since now
// no auto_ptr owns it any more
delete pt3;
}
//
pt2 doesn't own any pointer, and so won't
//
try to delete it... OK, no double delete
//
Example 3: Using reset()
//
void
h()
...
{
auto_ptr<T> pt( new T(1) );
pt.reset( new T(2) );
// deletes the first T that was
// allocated with "new T(1)"
}
//
finally, pt goes out of scope and
//
the second T is also deleted
从上面的例子来看,auto_ptr的使用很简单,通过构造函数拥有一个动态分配对象的所有权,然后就可以被当作对象指针来使用,当auto_ptr对象被销毁的时候,它也会自动销毁自己拥有所有权的对象(嗯,标准的RAAI做法),release可以用来手动放弃所有权,reset可用于手动销毁内部对象。
但实际上,auto_ptr是一个相当容易被误用并且在实际中常常被误用的类。原因是由于它的对象所有权占用的特性和它非平凡的拷贝行为。
auto_ptr的对象所有权是独占性的!
这决定了不可能有两个auto_ptr对象同时拥有同一动态对象的所有权,从而也导致了auto_ptr的拷贝行为是非对等的,其中伴随着对象所有权的转移。
我们仔细观察auto_ptr的源码就会发现拷贝构造和赋值操作符所接受的参数类型都是非const的引用类型(
auto_ptr
<_Ty>& ),而不是我们一般应该使用的const引用类型,查看源码我们会发现:
auto_ptr(auto_ptr
<
_Ty
>&
_Right) _THROW0()
: _Myptr(_Right.release())
...
{ // construct by assuming pointer from _Right auto_ptr
}
template
<
class
_Other
>
auto_ptr
<
_Ty
>&
operator
=
(auto_ptr
<
_Other
>&
_Right) _THROW0()
...
{ // assign compatible _Right (assume pointer)
reset(_Right.release());
return (*this);
}
拷贝过程中被拷贝的对象(_Right)都会被调用release来放弃所包括的动态对象的所有权,动态对象的所有权被转移了,新的auto_ptr独占了动态对象的所有权。也就是说被拷贝对象在拷贝过程中会被修改,拷贝物与被拷贝物之间是非等价的。这意味着如下的代码是错误的(例程来自 Exceptional C++ Item 37):
//
Example 6: Never try to do work through
//
a non-owning auto_ptr
//
void
f()
...
{
auto_ptr<T> pt1( new T );
auto_ptr<T> pt2;
pt2 = pt1; // now pt2 owns the pointer, and
// pt1 does not
pt1->DoSomething();
// error: following a null pointer
}
同时也不要将auto_ptr放进标准库的容器中,否则在标准库容器无准备的拷贝行为中(标准库容器需要的拷贝行为是等价的),会导致难以发觉的错误。(请参考Exceptional C++ Item 37获得更多信息)
auto_ptr特殊的拷贝行为使得使用它来远距离传递动态对象变成了一件十分危险的行为,在传递的过程中,一不小心就会留下一些实际为空但程序本身却缺少这样认知的auto_ptr对象。
简单的说来,auto_ptr适合用来管理生命周期比较短或者不会被远距离传递的动态对象,使用auto_ptr来管理动态分配对象,最好是局限于某个函数内部或者是某个类的内部。也就是说,动态对象的产生,使用和销毁的全过程是处于一个小的受控的范围,而不会在其中加入一些适应未来时态的扩展。