当前位置:文档之家› [学习C++]内存管理

[学习C++]内存管理

c++中涉及到的内存的管理问题可以归结为两方面:正确地得到它和有效地使用它。

好的程序员会理解这两个问题为什么要以这样的顺序列出。

因为执行得再快、体积再小的程序如果它不按你所想象地那样去执行,那也一点用处都没有。

“正确地得到”的意思是正确地调用内存分配和释放程序;而“有效地使用”是指写特定版本的内存分配和释放程序。

这里,“正确地得到”显得更重要一些。

然而说到正确性,c++其实从c继承了一个很严重的头疼病,那就是内存泄露隐患。

虚拟内存是个很好的发明,但虚拟内存也是有限的,并不是每个人都可以最先抢到它。

在c中,只要用malloc分配的内存没有用free返回,就会产生内存泄露。

在c++中,肇事者的名字换成了new和delete,但情况基本上是一样的。

当然,因为有了析构函数的出现,情况稍有改善,因为析构函数为所有将被摧毁的对象提供了一个方便的调用delete的场所。

但这同时又带来了更多的烦恼,因为new 和delete是隐式地调用构造函数和析构函数的。

而且,因为可以在类内和类外自定义new和delete操作符,这又带来了复杂性,增加了出错的机会。

下面的条款(还有条款m8)将告诉你如何避免产生那些普遍发生的问题。

5:对应的new和delete要采用相同的形式下面的语句有什么错?string *stringarray = new string[100];...delete stringarray;一切好象都井然有序——一个new对应着一个delete——然而却隐藏着很大的错误:程序的运行情况将是不可预测的。

至少,stringarray指向的100个string 对象中的99个不会被正确地摧毁,因为他们的析构函数永远不会被调用。

用new的时候会发生两件事。

首先,内存被分配(通过operator new 函数,详见条款7-10和条款m8),然后,为被分配的内存调用一个或多个构造函数。

用delete的时候,也有两件事发生:首先,为将被释放的内存调用一个或多个析构函数,然后,释放内存(通过operator delete 函数,详见条款8和m8)。

对于 delete来说会有这样一个重要的问题:内存中有多少个对象要被删除?答案决定了将有多少个析构函数会被调用。

这个问题简单来说就是:要被删除的指针指向的是单个对象呢,还是对象数组?这只有你来告诉delete。

如果你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组:string *stringptr1 = new string;string *stringptr2 = new string[100];...delete stringptr1;// 删除一个对象delete [] stringptr2;// 删除对象数组如果你在stringptr1前加了"[]"会怎样呢?答案是:那将是不可预测的;如果你没在stringptr2前没加上"[]"又会怎样呢?答案也是:不可预测。

而且对于象int这样的固定类型来说,结果也是不可预测的,即使这样的类型没有析构函数。

所以,解决这类问题的规则很简单:如果你调用new时用了[],调用delete 时也要用[]。

如果调用new时没有用[],那调用delete时也不要用[]。

在写一个包含指针数据成员,并且提供多个构造函数的类时,牢记这一规则尤其重要。

因为这样的话,你就必须在所有初始化指针成员的构造函数里采用相同的new的形式。

否则,析构函数里将采用什么形式的delete呢?关于这一话题的进一步阐述,参见条款11。

这个规则对喜欢用typedef的人来说也很重要,因为写typedef的程序员必须告诉别人,用new创建了一个typedef定义的类型的对象后,该用什么形式的delete来删除。

举例如下:typedef string addresslines[4]; //一个人的地址,共4行,每行一个string//因为addresslines是个数组,使用new: string *pal = new addresslines; // 注意"new addresslines"返回string*, 和// "new string[4]"返回的一样delete时必须以数组形式与之对应:delete pal;// 错误!delete [] pal;// 正确为了避免混乱,最好杜绝对数组类型用typedefs。

这其实很容易,因为标准c++库(见条款49)包含有stirng和vector模板,使用他们将会使对数组的需求减少到几乎零。

举例来说,addresslines可以定义为一个字符串(string)的向量(vector),即addresslines可定义为vector<string>类型。

6:析构函数里对指针成员调用delete大多数情况下,执行动态内存分配的的类都在构造函数里用new分配内存,然后在析构函数里用delete释放内存。

最初写这个类的时候当然不难做,你会记得最后对在所有构造函数里分配了内存的所有成员使用delete。

然而,这个类经过维护、升级后,情况就会变得困难了,因为对类的代码进行修改的程序员不一定就是最早写这个类的人。

而增加一个指针成员意味着几乎都要进行下面的工作:·在每个构造函数里对指针进行初始化。

对于一些构造函数,如果没有内存要分配给指针的话,指针要被初始化为0(即空指针)。

·删除现有的内存,通过赋值操作符分配给指针新的内存。

·在析构函数里删除指针。

如果在构造函数里忘了初始化某个指针,或者在赋值操作的过程中忘了处理它,问题会出现得很快,很明显,所以在实践中这两个问题不会那么折磨你。

但是,如果在析构函数里没有删除指针,它不会表现出很明显的外部症状。

相反,它可能只是表现为一点微小的内存泄露,并且不断增长,最后吞噬了你的地址空间,导致程序夭折。

因为这种情况经常不那么引人注意,所以每增加一个指针成员到类里时一定要记清楚。

另外,删除空指针是安全的(因为它什么也没做)。

所以,在写构造函数,赋值操作符,或其他成员函数时,类的每个指针成员要么指向有效的内存,要么就指向空,那在你的析构函数里你就可以只用简单地delete掉他们,而不用担心他们是不是被new过。

当然对本条款的使用也不要绝对。

例如,你当然不会用delete去删除一个没有用new来初始化的指针,而且,就象用智能指针对象时不用劳你去删除一样,你也永远不会去删除一个传递给你的指针。

换句话说,除非类成员最初用了new,否则是不用在析构函数里用delete的。

说到智能指针,这里介绍一种避免必须删除指针成员的方法,即把这些成员用智能指针对象来代替,比如c++标准库里的auto_ptr。

想知道它是如何工作的,看看条款m9和m10。

7:预先准备好内存不够的情况operator new在无法完成内存分配请求时会抛出异常(以前的做法一般是返回0,一些旧一点的编译器还这么做。

你愿意的话也可以把你的编译器设置成这样。

关于这个话题我将推迟到本条款的结尾处讨论)。

大家都知道,处理内存不够所产生的异常真可以算得上是个道德上的行为,但实际做起来又会象刀架在脖子上那样痛苦。

所以,你有时会不去管它,也许一直没去管它。

但你心里一定还是深深地隐藏着一种罪恶感:万一new真的产生了异常怎么办?你会很自然地想到处理这种情况的一种方法,即回到以前的老路上去,使用预处理。

例如,c的一种常用的做法是,定义一个类型无关的宏来分配内存并检查分配是否成功。

对于c++来说,这个宏看起来可能象这样:#define new(ptr, type) \try { (ptr) = new type; } \catch (std::bad_alloc&) { assert(0); }(“慢!std::bad_alloc是做什么的?”你会问。

bad_alloc是operator new 不能满足内存分配请求时抛出的异常类型,std是bad_alloc所在的名字空间(见条款28)的名称。

“好!”你会继续问,“assert又有什么用?”如果你看看标准c头文件<assert.h>(或与它相等价的用到了名字空间的版本<cassert>,见条款49),就会发现assert是个宏。

这个宏检查传给它的表达式是否非零,如果不是非零值,就会发出一条出错信息并调用abort。

assert只是在没定义标准宏ndebug的时候,即在调试状态下才这么做。

在产品发布状态下,即定义了ndebug 的时候,assert什么也不做,相当于一条空语句。

所以你只能在调试时才能检查断言(assertion))。

new宏不但有着上面所说的通病,即用assert去检查可能发生在已发布程序里的状态(然而任何时候都可能发生内存不够的情况),同时,它还在c++里有另外一个缺陷:它没有考虑到new有各种各样的使用方式。

例如,想创建类型t对象,一般有三种常见的语法形式,你必须对每种形式可能产生的异常都要进行处理:new t;new t(constructor arguments);new t[size];这里对问题大大进行了简化,因为有人还会自定义(重载)operator new,所以程序里会包含任意个使用new的语法形式。

那么,怎么办?如果想用一个很简单的出错处理方法,可以这么做:当内存分配请求不能满足时,调用你预先指定的一个出错处理函数。

这个方法基于一个常规,即当operator new不能满足请求时,会在抛出异常之前调用客户指定的一个出错处理函数——一般称为new-handler函数。

(operator new实际工作起来要复杂一些,详见条款8)指定出错处理函数时要用到set_new_handler函数,它在头文件<new>里大致是象下面这样定义的:typedef void (*new_handler)();new_handler set_new_handler(new_handler p) throw();可以看到,new_handler是一个自定义的函数指针类型,它指向一个没有输入参数也没有返回值的函数。

set_new_handler则是一个输入并返回new_handler类型的函数。

set_new_handler的输入参数是operator new分配内存失败时要调用的出错处理函数的指针,返回值是set_new_handler没调用之前就已经在起作用的旧的出错处理函数的指针。

可以象下面这样使用set_new_handler:// function to call if operator new can't allocate enough memoryvoid nomorememory(){cerr << "unable to satisfy request for memory\n";abort();}int main(){set_new_handler(nomorememory);int *pbigdataarray = new int[100000000];...}假如operator new不能为100,000,000个整数分配空间,nomorememory将会被调用,程序发出一条出错信息后终止。

相关主题