auto_ptr解析
auto_ptr是当前C++标准库中提供的一种智能指针,或许相对于boost库提供的一系列眼花缭乱的智能指针,这个不怎么智能的智能指针难免会黯然失色。诚然,auto_ptr有这样那样的不如人意,以至于程序员必须像使用”裸“指针那样非常小心地使用它才能保证不出错,以至于它甚至无法适用于同是标准库中的那么多的容器和一些算法,但即使如此,我们仍然不能否认这个小小的auto_ptr所蕴含的价值与理念。
auto_ptr的出现,主要是为了解决“被异常抛出时发生资源泄漏”的问题,即如果我们让资源在局部对象构造时分配,在局部对象析构时释放。这样即使在函数执行过程时发生异常而退出,也会因为异常能保证局部对象被析构从而保证资源被释放。auto_ptr就是基于这个理念而设计,这最早出现在C++之父Bjarne Stroustrup的两本巨著TC++PL和D&E中,其主题为"resource acquisition is initialization"(raii,资源获取即初始化),然后又在Scott Meyer的《More Effective C++》中相关章节的推动下,被加入了C++标准库。
下面我就列出auto_ptr的源代码,并详细讲解每一部分。因为标准库中的代码要考虑不同编译器支持标准的不同而插入了不少预编译判断,而且命名可读性不是很强(即使是侯捷老师推荐的SGI版本的stl,可读性也不尽如人意),这里我用了Nicolai M. Josuttis(《The C++ standard library》作者)写的一个auto_ptr的版本,并做了少许格式上的修改以易于分析阅读:
namespace std {
// auxiliary type to enable copies and assignments (now global)
template<class Y>
struct auto_ptr_ref {
Y* yp;
auto_ptr_ref (Y* rhs):yp(rhs) {
}
};
template<class T>
class auto_ptr {
private:
T* ap; // refers to the actual owned object (if any)
public:
typedef T element_type;
// 构造函数
explicit auto_ptr (T* ptr = 0) throw()
: ap(ptr) {
}
// 析构函数
~auto_ptr() throw() {
delete ap;
}
// 拷贝构造函数
auto_ptr (auto_ptr& rhs) throw()
: ap(rhs.release()) {
}
template<class Y>
auto_ptr (auto_ptr<Y>& rhs) throw()
: ap(rhs.release()) {
}
// 赋值操作符
auto_ptr& operator= (auto_ptr& rhs) throw() {
reset(rhs.release());
return *this;
}
template<class Y>
auto_ptr& operator= (auto_ptr<Y>& rhs) throw() {
reset(rhs.release());
return *this;
}
// value access
T* get() const throw() {
return ap;
}
T& operator*() const throw() {
return *ap;
}
T* operator->() const throw() {
return ap;
}
// release ownership
T* release() throw() {
T* tmp(ap);
ap = 0;
return tmp;
}
// reset value
void reset (T* ptr=0) throw() {
if (ap != ptr) {
delete ap;
ap = ptr;
}
}
/* special conversions with auxiliary type to enable copies and assignments*/
auto_ptr(auto_ptr_ref<T> rhs) throw()
: ap(rhs.yp) {
}
auto_ptr& operator= (auto_ptr_ref<T> rhs) throw() { // new
reset(rhs.yp);
return *this;
}
template<class Y>
operator auto_ptr_ref<Y>() throw() {
return auto_ptr_ref<Y>(release());
}
template<class Y>
operator auto_ptr<Y>() throw() {
return auto_ptr<Y>(release());
}
};
}
1 构造函数与析构函数
备注:C++支持两种初始化变量的方式:复制初始化(copy-initialization)和直接初始化(dierct-initialization);复制初始化语法用等号,直接初始化则是把初始化式放在括号中(《c++ primer 4th》p42):
int ival(1024); //直接初始化
int ival = 1024; //复制初始化
auto_ptr<>不允许使用一般指针惯用的初始化方式,你必须直接初始化(《stl标准程序库》p40):
std::auto_ptr<ClassA> ptr1(new ClassA); //OK
std::auto_ptr<ClassA> ptr2 = new ClassA; //ERROR
只有auto_ptr可以拿来当做另外一个auto_ptr的初始值,普通指针是不行的(《stl标准程序库》p41):
std::auto_ptr<ClassA> ptr;
ptr = new ClassA; //ERROR
ptr = std::auto_ptr<ClassA>(new ClassA); //ok delete old object and own new
在使用auto_ptr时,有几点需要注意的地方:
1) auto_ptr是这样一种指针,它是“它所指向的对象”的拥有者,当身为对象拥有者的auto_ptr被摧毁时,该对象也将遭到摧毁。auto_ptr要求一个对象只能有一个拥有者,绝对不应该出现多个auto_ptr同时拥有一个对象的情况,像这样:
int* p = new int(0);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2(p);
因为ap1与ap2都认为指针p是归它管的,在析构时都试图删除p,两次删除同一个对象的行为在C++标准中是未定义的。所以我们必须防止这样使用auto_ptr。
2) 并不存在针对array而设计的auto_ptr,考虑下面这种用法:
int* pa = new int[10];
auto_ptr<int> ap(pa);
因为auto_ptr的析构函数中删除指针用的是delete,而不是delete [],所以我们不应该用auto_ptr来管理一个数组指针。
3) 构造函数的explicit关键词有效阻止从一个“裸”指针隐式转换成auto_ptr类型。
4) 因为C++保证删除一个空指针是安全的,所以我们没有必要把析构函数写成:
~auto_ptr() throw()
{
if(ap) delete ap;
}
2 拷贝构造与赋值
与引用计数型智能指针不同的,auto_ptr要求其对“裸”指针的完全占有性。也就是说一个”裸“指针不能同时被两个以上的auto_ptr所拥有。那么,在拷贝构造或赋值操作时,我们必须作特殊的处理来保证这个特性。auto_ptr的做法是“所有权转移”,即拷贝或赋值的源对象将失去对“裸”指针的所有权,所以,与一般拷贝构造函数、赋值函数不同,auto_ptr的拷贝构造函数、赋值函数的参数为引用而不是常引用(const reference).当然,一个auto_ptr也不能同时拥有两个以上的“裸”指针,所以,拷贝或赋值的目标对象将先释放其原来所拥有的对象。下面就auto_ptr拥有权转移的情况举例进行说明:
拷贝构造函数:
std::auto_ptr<ClassA> ptr1(new ClassA); //initialize an auto_ptr with a new object
std::auto_ptr<ClassA> ptr2(ptr1); //copy th auto_ptr
//transfers ownership from ptr1 to ptr2
在第一个语句中,ptr1拥有那个new出来的对象。在第二个语句中,拥有权从ptr1转移到ptr2。此后ptr2就拥有了那个new出来的对象,而ptr1不再拥有它。这样,对象就只会被delet一次--在ptr2销毁的时候。
赋值操作符(赋值函数):
std::auto_ptr<ClassA> ptr1(new ClassA); //initialize an auto_ptr with a new object
std::auto_ptr<ClassA> ptr2(new ClassA); //create another auto_ptr
ptr2 = ptr1; //assign the auto_ptr
//delete object owned by ptr2
//transfers ownership from ptr1 to ptr2
在这里,赋值动作将拥有权从ptr1转移到ptr2,于是,ptr2拥有了先前被ptr1所拥有的那个对象。如果ptr2被赋值前正拥有另外一个对象,赋值动作发生时会调用delete,将该对象删除。
这里的注意点是:
1) 因为一个auto_ptr被拷贝或被赋值后,其已经失去对原对象的所有权,这个时候,对这个auto_ptr的提领(dereference)操作是不安全的。如下:
int* p = new int(0);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2 = ap1;
cout<<*ap1; //错误,此时ap1只剩一个null指针在手了
这种情况较为隐蔽的情形出现在将auto_ptr作为函数参数按值传递,因为在函数调用过程中在函数的作用域中会产生一个局部对象来接收传入的auto_ptr(拷贝构造),这样,传入的实参auto_ptr就失去了其对原对象的所有权,而该对象会在函数退出时被局部auto_ptr删除。如下:
void f(auto_ptr<int> ap){cout<<*ap;}
auto_ptr<int> ap1(new int(0));
f(ap1);
cout<<*ap1; //错误,经过f(ap1)函数调用,ap1已经不再拥有任何对象了。
因为这种情况太隐蔽,太容易出错了,所以auto_ptr作为函数参数按值传递是一定要避免的。或许大家会想到用auto_ptr的指针或引用作为函数参数或许可以,但是仔细想想,我们并不知道在函数中对传入的auto_ptr做了什么,如果当中某些操作使其失去了对对象的所有权,那么这还是可能会导致致命的执行期错误。也许,用const reference的形式来传递auto_ptr会是一个不错的选择。
2)我们可以看到拷贝构造函数提供了一个成员模板,使得可通过型别自动转换,构造出合适的auto_ptr。例如,根据一个派生类的对象,构造出一个基类对象的auto_ptr。同样,赋值操作符(赋值函数)也提供了一个成员模板,使得可通过型别自动转换,赋值给合适的auto_ptr。例如,将一个派生类的对象,赋值给一个基类对象的auto_ptr。
class base{};
class derived: public base{};
那么下列代码就可以通过,实现从auto_ptr<derived>到auto_ptr<base>的隐式转换,因为derived*可以转换成base*类型
auto_ptr<base> apbase = auto_ptr<derived>(new derived);
3) auto_ptr不满足stl容器对元素的要求。
auto_ptr并不满足stl标准容器对元素的最基本要求。因为在拷贝和赋值动作之后,原本的auto_ptr和新产生的auto_ptr并不相等。在拷贝和赋值过后,原来的auto_ptr会交出拥有权,而不是拷贝给新的auto_ptr。因此绝对不要将auto_ptr作为标准容器的元素。
3 提领操作(dereference)
提领操作有两个操作,一个是返回其所拥有的对象的引用,另一个是则实现了通过auto_ptr调用其所拥有的对象的成员。如:
struct A{ … }
auto_ptr<A> apa(new A);
(*apa).f();
apa->f();
当然, 我们首先要确保这个智能指针确实拥有某个对象,否则,这个操作的行为即对空指针的提领是未定义的。
4 辅助函数
1) get用来显式的返回auto_ptr所拥有的对象指针。我们可以发现,标准库提供的auto_ptr既不提供从“裸”指针到auto_ptr的隐式转换(构造函数为explicit),也不提供从auto_ptr到“裸”指针的隐式转换,从使用上来讲可能不那么的灵活,考虑到其所带来的安全性还是值得的。
2) release,用来转移所有权。
3) reset,用来接收所有权,如果接收所有权的auto_ptr如果已经拥有某对象,必须先释放该对象。
5 特殊转换
auto_ptr中剩余的部分(辅助型别auto_ptr_ref及其相关函数)涉及非常精致的技巧,使我们得以拷贝和赋值non-const auto_ptrs,却不能拷贝和赋值const auto_ptr(更加详细的说明,参考《stl标准模板库》P55)。
6 auto_ptr运用实例(《STL标准模板库》p47):
下面的一个例子展示了auto_ptr转移拥有权的行为:
#include <iostream>
#include <memory>
using namespace std;
/* define output operator for auto_ptr
* - print object value or NULL
*/
template <class T>
ostream& operator<< (ostream& strm, const auto_ptr<T>& p)//参数p是常量引用,所以不发生拥有权转移
{
// does p own an object ?
if (p.get() == NULL) {
strm << "NULL"; // NO: print NULL
}
else {
strm << *p; // YES: print the object
}
return strm;
}
int main()
{
auto_ptr<int> p(new int(42));
auto_ptr<int> q;
cout << "after initialization:" << endl;
cout << " p: " << p << endl;
cout << " q: " << q << endl;
q = p;
cout << "after assigning auto pointers:" << endl;
cout << " p: " << p << endl;
cout << " q: " << q << endl;
*q += 13; // change value of the object q owns
p = q;
cout << "after change and reassignment:" << endl;
cout << " p: " << p << endl;
cout << " q: " << q << endl;
}
输出结果为:
after initialization:
p: 42
q: NULL
after assigning auto pointers:
p: NULL
q: 42
after change and reassignment:
p: 55
q: NULL
参考资料:
http://www.cppblog.com/SmartPtr/archive/2008/01/22/27549.html
《STL标准程序库》P38-58
附“Google C++编程风格指南”关于智能指针是说明:
如果确实需要使用智能指针的话,scoped_ptr完全可以胜任。在非常特殊的情况下,例如对STL容器中对象,你应该只使用std::tr1::shared_ptr,任何情况下都不要使用auto_ptr。
“智能”指针看上去是指针,其实是附加了语义的对象。以scoped_ptr为例,scoped_ptr被销毁时,删除了它所指向的对象。shared_ptr也是如此,而且,shared_ptr实现了引用计数(reference-counting),从而只有当它所指向的最后一个对象被销毁时,指针才会被删除。
一般来说,我们倾向于设计对象隶属明确的代码,最明确的对象隶属是根本不使用指针,直接将对象作为一个域(field)或局部变量使用。另一种极端是引用计数指针不属于任何对象,这样设计的问题是容易导致循环引用或其他导致对象无法删除的诡异条件,而且在每一次拷贝或赋值时连原子操作都会很慢。
虽然不推荐这么做,但有些时候,引用计数指针是最简单有效的解决方案。