auto_ptr是目前C++标准中唯一的一个智能指针(smart pointer),主要是用来自动管理指针所指向的内存资源。资源管理是程序设计中非常重要的一部分。资源(resource)是计算机中很宽泛的一个概念,用来表示程序中数量有限,用完就必须归还的东西,比如常见的互斥锁(mutex lock)、文件指针、Win32中的画刷(brush)……,其中内存(memory)是最常见的一种资源,也是我们最先接触,使用最多的一种资源,因此它的地位至关重要。它的地位到底重要到什么程度?对此有两种截然不同的理念:
1.内存资源是如此的重要,以至于我们不能把它们交给计算机来管理,而必须由程序员来管理。
2.内存资源是如此的重要,以至于我们不能把它们交给程序员来管理,而必须由计算机来管理。
Java、C#、Eiffel秉承了第二种理念,把内存资源交给计算机管理,避免了程序员在内存管理上易犯的大量错误,从整体上提高了开发的效率,但是是以损失一定执行时间上的效率为代价的。因此这些语言在实时系统、操作系统、语言编译器……等一些对时间要求比较严格的领域中运用的很少。
C语言秉承了第一种理念,C++也随之继承了这种理念,从而也就把内存资源管理交给了程序员,对于高段C程序员,当然是给了他们更多的灵活性,可以编制出非常精美的艺术品,但是对于初学者和不那么高段的C程序员,内存管理却是麻烦的根源,带给他们更多的不是灵活性,而是挥之不去的连环噩梦。比如内存泄漏(memory leak)、野指针(wild pointer)等导致的一些极难察觉的bug,最后的调试除错可能会让我们觉得世界末日到了。【注1】
注1:不是有一种玩笑说法吗?真正的程序员用C(或C++),我想,难度颇高的,由程序员本人负责的内存管理可能是支持这个观点的一个重要理由。:)
但是在C++中有了另外一个管理内存(甚至资源)的选择,那就是智能指针(smart pointer),资源获取即初始化(resource acquisition is initialization)理念的具体实现。
【注2】
注2:在C++的准标准Boost库中,引入了几个新的智能指针scoped_ptr、scoped_array、shared_ptr、shared_array、weak_ptr,相对于auto_ptr有它们的许多好处,感兴趣的读者可以到www.boost.org去看一看。Andrei Alexandrescu在他的Loki库中也专门用Policy设计实现了一个可以扩展的SmartPtr模板,里面用到的一些技术还是很有价值的,可以到http://sourceforge.net/projects/loki-lib/下载Loki库阅读。
标准中auto_ptr的引入主要就是为了解决内存资源管理这个让人难以驯服的怪兽。不过在解决一个问题的同时,也会带来一些新的问题,auto_ptr本身有拥有权(ownership)的转移(transfer)问题,而且它本身不支持“值(value)语义”概念【注3】,因此不能用在STL容器里面作为元素使用,在比较新的编译器中,如果这样使用的话,编译器会阻止你。但是在稍微老一点的编译器中使用的话,很可能会无风无浪的编译通过,在执行的过程中,那么我恭喜你,其时你正在一个很长的雷区裸奔,能够毫发无损通过的概率那就只有天知道了。:)
注3:关于这些概念,可以参考我写的《范式的二维平面》。
auto_ptr本身的正确使用在很多书中有详细的讲解和示例,而且我们也可以直接阅读auto_ptr的源代码获得更直观的感受。所以对于它的使用我不想再浪费时间和笔墨,在auto_ptr目前实现中【注4】,我们会看到一个奇怪的类模板auto_ptr_ref,第一次我在阅读《The C++ Standard Library》的时候,看到讲解auto_ptr的时候提到auto_ptr_ref就百思不得其解,说实话,这本书是写得非常清晰易懂的,不过我觉得在auto_ptr这个地方花的笔墨比较吝啬,没有完全把问题讲清楚。而且我看的很多书、文章上也并没有详细讲解这个auto_ptr_ref问题,今天我想来对此深入探讨一下,希望能够抛砖引玉。
注4:auto_ptr的早期实现中有一些bug,后来Nicolai M. Josuttis 对此认真修正了,并作出了一个实现。读者可以到http://www.josuttis.com/libbook/util/autoptr.hpp.html查看。读者最好对照代码看文章。其中关于成员函数模板,我并没有讲解,很多书上都有,主要是为了解决指针之间的转化,特别是对于多态指针。
auto_ptr_ref就像它的名字一样,把一个值转化为引用(reference),具体的说,也就是把一个右值(rvalue)转化为一个左值(lvalue)。【注5】我们可能会很奇怪,C++什么时候还需要用到这种功能?很不幸,为了保证auto_ptr的正确行为和一定的安全性,需要用到这个功能。
注5:到底什么是左值,什么是右值?有许多让人混淆,并不明晰的概念表述,我会在下一篇文章中表明我的观点。
我们都知道,auto_ptr在进行复制操作(assignment operator and copy constructor)的时候,资源的拥有权(ownership)会发生转移,也即原来的auto_ptr所指向的内存资源转给了新的auto_ptr,本身已经为空。所以说auto_ptr不支持“值语义”,即被复制的值不会改变。一个例子可能更能说明问题:
auto_ptr<int> ap1(new int(9));
auto_ptr<int> ap2(ap1);// ap1失去拥有权,现在指向空,ap2现在获得指9的//内存资源
ap1 = ap2;//ap2失去拥有权,现在指向空,ap1现在获得指向9的内存资源
我们在观察auto_ptr的assignment operator、copy constructor的实现时,也能够发现它的参数是auto_ptr& rhs,而不是auto_ptr const& rhs【注6】。也就是说,auto_ptr进行复制操作的时候,它的引数(argument)必须是一个可以改变的左值(事实是这个值必定被改变)。
注6:这种写法你可能不习惯,其实就是const auto_ptr& rhs,我为什么要用这种写法,可以参考我写的《C之诡谲(下)》,就知道我并不是为了标新立异。:)
我们最常见到的,复制操作的参数类型都是引用到常量(reference to const),这正好是为了避免改变传进来的引数(argument)。由于不会改变被引用的值,所以C++标准规定:常量引用可以引用右值。比如下列代码都是合法的:
int const& ri = 60;//60是右值
list<int> const& rl = list<int>();//list<int>()是右值
int fun(){…}; int const& rf = fun();//fun()是右值
但是一般引用(非常量引用)是绝对不可以引用右值的。比如下列代码都是非法的:
int& ri = 60;
list<int>& rl = list<int>();
int fun(){…}; int& rf = fun();
在我们前面谈到的auto_ptr,它的复制操作的参数类型恰好是非常量引用。所以对于下面的情况它就不能正确处理。
auto_ptr<int> ap1 = auto_ptr<int>(new int(8));//等号右边的是一个临时右值
auto_ptr<int> fun()//一个生成auto_ptr<int>的source函数
{
return auto_ptr<int>(new int(8));
}
auto_ptr<int> ap2 ( fun() );//调用fun()生成的auto_ptr<int>是右值
而这种情况不但合法,也是很常见的,我们不能拒绝这种用法,怎么办?天才的C++库设计者已经为我们想到了这一点,auto_ptr_ref的引入就是为了解决这个问题。仔细观察最下面的auto_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());
}
这就是关键所在了。对于一个auto_ptr右值,不可能为它调用正常的赋值运算符函数和复制构造函数,举个例子说明,对于语句
auto_ptr<int> ap1 = auto_ptr<int>(new int(8));
首先生成临时对象右值auto_ptr<int>(new int(8)),然后使用转型函数模板
template<class Y> operator auto_ptr_ref<Y>() throw()
{ return auto_ptr_ref<Y>(release());};
由auto_ptr<int>(new int(8)),首先调用成员函数release(),然后由获取的原始指针生成另外一个临时对象右值auto_ptr_ref<int>(一个指向动态存储区中8的指针)。这个时候我们再看一个构造函数
auto_ptr(auto_ptr_ref<T> rhs) throw()
: ap(rhs.yp) { }
它的参数类型不是引用,采用的是传值(by value),所以可以接受右值,这时候,调用auto_ptr_ref<int>默认生成的复制构造函数(copy constructor),用上面最后得到的那个auto_ptr_ref<int>临时对象右值作为引数,生成了rhs,接着auto_ptr_ref<int>临时对象右值自动析构结束生命,后面的ap(rhs.yp)完成最后的工作。
对这个用法感兴趣的同仁,可以参考SGI的STL源码 :)