九天雁翎的博客
如果你想在软件业获得成功,就使用你知道的最强大的语言,用它解决你知道的最难的问题,并且等待竞争对手的经理做出自甘平庸的选择。 -- Paul Graham

C++可怜的内存管理机制漫谈及奇怪补救auto_ptr介绍

       一直以来C++遵循着一种哲学式的美学设计。很重要的一条就是不为你不需要的付出代价。就我目前所知,整个C++仅仅只在虚函数和多重继承中违背了这条原则。很多非常有用的东西都因为这条原理而否定了。因此C++才能保持着一条定律,那就是只要程序员足够厉害,C++可以足够的快,因为程序员几乎掌握着一切可以用来优化的东西。其中,资源回收系统就是因为这样被否决了。以前一直不觉得怎么样,因为C++社群的舆论导向,甚至有目前看来几乎自虐的想法,那就是不因为其他语言容易学而C++难学就说C++不好,还有,C++给了你完全的控制,自由的世界,自然要付出代价!

      当然,很多代价我都可以接受,我也一直接受着小心翼翼的newdelete。无数次,我看到各种类型各种层次的C++书籍,语重心长地告诉我,要避免内存泄露,无数次看到书中指出看似平常的代码中也可能存在的内存泄露问题。今天看《C++ STL》介绍auto_prt的时候,又一次碰到了这个问题。看到书中作者提出的各种灾难后果的时候,心中竟然像看到鬼故事一样的心悸,眼前似乎如看到核武器爆炸一样的恐怖,一时竟然有心惊肉跳的感觉。突然觉得这样下去,不知道什么时候会心脏病发作。

       什么,你笑我连个delete都忘记?假如加上个delete就万无一失那我就什么都不怕了。类似如下代码:

int f()

{

       classA *ptr = new classA;

       switch()

       case:

              return 0;

       case:

              ...   //do some thing

       delete ptr;

}

       一个需要返回的函数碰到分支返回是很正常的,你必须要保证每个分支返回前都必须先delete,而不是放在最后。假如说这种情况还可以通过小心弥补的话。那么看下面的代码:

void f()

{

       classA *ptr = new classA;

       ...   //do some thing

       delete ptr;

}

       即便这样,你也是不能保证内存不泄漏的。计算你不在中途返回,一旦中途出现异常,此函数就会自动终止,内存一样泄漏。如之奈何?

继续小心?保证中途不抛出异常,当然在很长的程序中这是让你痛苦的,不过也不是不能做到。但是看下面的代码:

void f()

{

       classA *ptr = new classA;

       ...   //do some thing,throw()

       g();  //call g()

       delete ptr;

}

       在一个程序中,一个函数调用另一个函数不少见吧,这样你就必须保证g()也不抛出异常,不然效果一样,内存泄漏了!什么?你也去查看g()的代码,保证没有异常抛出?那g()调用了h()呢?如此下去,我只能告诉你,C++中根本就不该加入异常,因为你没有机会使用。

       说了这么多,你能体会到我对内存泄漏的恐惧之情了吗?还没有?继续:

void f()

{

       classA *ptrA = new classA;

       classB *ptrB = new classB;

       delete ptrA;

       delete ptrB;

}

       我如此害怕出问题,我连用都不敢用,赶紧把刚才new出来的家伙都delete了,很遗憾,这还是有可能内存泄漏!当第二条new抛出异常的话,你什么都delete不了了,哪怕你使用程序从来不用异常机制,很遗憾得告诉你,异常机制是深入C++骨髓的,就算你不用,它自己还是时不时会使用的。暂时这么多吧,体会我的感受了吧。。。。。。。。。。。。

       因为C是我学习第一个语言,尔后接着我学习了名字与其比较像的进阶语言,一直没有改过,虽然后来知道,C++不过名字取得比较好,比较像C语言的继承者而已,JAVA其实也是由C演化来的,并且对面向对象的控制比C++好很多,也容易学很多,还有就是现代的程序语言大多都有C的影子。我也从来没有移情别恋,改用JAVA或哪怕更进一步似乎类似C++继承者的C#了。想起偶尔使用的微软托管代码,一个一个在用惯了C++的我看来潇洒无比的new,一个一个无所顾忌的new,真是一下被震撼了,想起自己每次用到new时的心惊胆颤,一时语塞。当然,我不愿放弃对系统的控制,我不愿放弃对内存的操作,但是就一定要接受这样的事实吗?我并不是每次都需要为一个程序进行多么高深的优化啊,特别是对此程序内存中的操作。因为我要掌握住任何对象的生存期,因为我不愿为我不需要用到new时付出任何代价,我就要在我用到new时付出这样惨重的代价。甚至我们不得不用上如auto_ptr这样有着特性的对象来避免内存的泄漏。我不得不说,auto_ptr的特性真的是可以称得上“特”性了。有人会讽刺我,要想那么轻松就直接去用C#算了。从C++转移到C#阵营的又怎么是少数啊,有人甚至说C++正成为边缘化语言,成为学术化语言,C++社群似乎也以C++的难学自豪,以钻研华而不实只能用来演示的技术为爱好。还好,已经听说在C++ 0X标准中已经考虑加入一个可选的自动废料收集了。。。。。。。。哪怕C++ 0X不做其他任何改变,我也非常期待C++ 0X的时代的到来。

C++可怜的内存管理机制中有个奇怪补救措施:标准中唯一的smart point(智能指针)auto_ptrauto_ptr作为标准中智能指针的独苗,实在是太被人呵护和爱戴了。以至于它脾气生得倔强异常,放在C++中怎么看都是各从别的地方突然闯入的异类。为了避免我们心里都被内存泄漏带进可怕的阴影,auto_ptr横空出世了,因为横的利害,我们必须小心的避免它的很多问题,当然,这些问题比起内存泄漏来说,给我们心里造成创伤的机会还是少很多的。

       auto_ptr在《C++ PL》中只有很少的内容,《C++ Primer》中提及也不算太多,《Effective C++》中论及的反而算是比较多了,一直没有太深的了解,所以平时还是用普通的指针比较多,今天在《C++ STL》中才算看到比较详尽的内容,想到以后我的程序似乎应该有所改变了才是,不能老是用*搞指针了,用复杂的对象吧。

       首先,auto_ptr是为了防止内存泄漏而生的,的确,这本来是个好消息,特别是被内存泄漏吓怕了的我来说。auto_ptr是个类似指针的对象,它重载了*,->等操作符。可以和指针进行类似的取值操作。但是它有如下几点要注意:1。它不能用作数组或容器的对象。2。它不能进行一般意义的赋值和复制。3。它的指针算数没有意义。4 。你最好不要用它来传递参数,当不得不用的时候必须用const引用才行。5。同时不能有两个以上的auto_ptr指向同一个值。这以上五点都必须注意,不然《C++ STL 》作者的警告是:“如果你的误用行为没有导致全盘崩溃,你或许会暗自庆幸,而这其实是真正的不幸,因为你或许根本就没有意识到你已经犯了错误。”我看这话怎么看怎么就像恐吓。本来准备大面积用auto_ptr的我,一下子不敢用了。auoto_ptr的最大特点就是所有权概念。并且此所有权只能移交,不能复制。首先,你不能使用惯用的赋值来初始化,只能用函数形式的。

       std::auto_ptr<classA> ptr(new classA);

还好,我们也不是太不习惯。

void f()

{

       std::auto_ptr<classA> ptr1(new classA);

       std::auto_ptr<classA> ptr2(ptr1);

       std::auto_ptr<classA> ptr3;

       ptr3 = ptr2;

}

执行完第二个语句时,ptr1就为NULL了,因为它的所有权都移交给ptr2了。最后ptr2也为空了,因为所有权给ptr3了。这里要注意另外一点,

ptr3 = new classA;

语句是非法的,因为虽然auto_ptr可以用new来初始化,但只能用auto_ptr赋值。

当你对任何已经为NULLauto_ptr做取值运算*的时候,都会导致未定义的操作,可能导致程序的崩溃。没有崩溃?参考上面的警告。

权利的移交其实还算好记,但是请不要用auto_ptr传递参数,因为默认的参数传递方式就是复制,然后会导致你不知道的权利移交。这里有个技巧,当你把一个auto_ptr声明为const的时候,就可以制止一切权利的移交。但是却可以改变其值,类似const指针。

如下例子演示了权利的移交机制和利用const auto_ptr引用传递参数的方法。

 

#include "stdafx.h"

#include <iostream>

#include <memory>

using namespace std;

 

template <class T>

ostream& operator<< (ostream& os,const auto_ptr<T> &p)

{

       if(p.get() == NULL)

              os<<"NULL";

       else

              os<< *p;

       return os;

}

 

int main()

{

       auto_ptr<int> p(new int(34));

       auto_ptr<int> q;

       cout <<"At first:"<<endl;

       cout <<"p: "<<p<<endl;

       cout <<"q: "<<q<<endl;

       q = p;

       cout <<"Later:"<<endl;

       cout <<"p: "<<p<<endl;

       cout <<"q: "<<q<<endl;

       *q = 43;

       p = q;

       auto_ptr<int> h(p);

       cout <<"At last:"<<endl;

       cout <<"p: "<<p<<endl;

       cout <<"q: "<<q<<endl;

       cout <<"h: "<<h<<endl;

       return 0;

}

 

       此例子来自《C++ STL》中的类似例子,你会发现所有的情况下,实际只有一个auto_ptr拥有所有权。

       最后讲讲auto_ptr的作用,因为auto_ptr是一个类,不抛出异常,并且有析构函数释放资源,所以在使用auto_ptr替代普通指针时上述的几种内存泄漏情况都有所缓解。道理在于一旦出现问题,因为auto_ptr是个局部临时对象,那么自然会调用析构函数,哪怕突然抛出异常这点也不例外。所以,当使用auto_ptr后,你可以稍微感受到一点光new而不delete的快感,可惜的是,语法复杂了很多,而且还要注意很多很多问题。当然,已经难能可贵了。因为这些都没有牺牲C++语言的特性,仅仅是一个库。

       假如我有什么不对,请指教。

 

分类:  C++ 
标签:  auto_ptr  C++ 

Posted By 九天雁翎 at 九天雁翎的博客 on 2007年10月12日

分享到:

前一篇: 看《C++ STL》发现的关于异常说明的问题 后一篇: vector成员转换为char输出的六种方法,STL的学习过程乱想