C++内存管理详解
1.对应的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类型。
2.析构函数里对指针成员调用delete
大多数情况下,执行动态内存分配的的类都在构造函数里用new分配内存,然后在析构函数里用delete释放内存。最初写这个类的时候当然不难做,你会记得最后对在所有构造函数里分配了内存的所有成员使用delete。
然而,这个类经过维护、升级后,情况就会变得困难了,因为对类的代码进行修改的程序员不一定就是最早写这个类的人。而增加一个指针成员意味着几乎都要进行下面的工作:
·在每个构造函数里对指针进行初始化。对于一些构造函数,假如没有内存要分配给指针的话,指针要被初始化为0(即空指针)。
·删除现有的内存,通过赋值操作符分配给指针新的内存。
·在析构函数里删除指针。假如在构造函数里忘了初始化某个指针,或者在赋值操作的过程中忘了处理它,问题会出现得很快,很明显,所以在实践中这两个问题不会那么折磨你。但是,假如在析构函数里没有删除指针,它不会表现出很明显的外部症状。相反,它可能只是表现为一点微小的内存泄露,并且不断增长,最后吞噬了你的地址空间,导致程序夭折。因为这种情况经常不那么引人注重,所以每增加一个指针成员到类里时一定要记清楚。
另外,删除空指针是安全的(因为它什么也没做)。所以,在写构造函数,赋值操作符,或其他成员函数时,类的每个指针成员要么指向有效的内存,要么就指向空,那在你的析构函数里你就可以只用简单地delete掉他们,而不用担心他们是不是被new过。
当然对本条款的使用也不要绝对。例如,你当然不会用delete去删除一个没有用new来初始化的指针,而且,就象用智能指针对象时不用劳你去删除一样,你也永远不会去删除一个传递给你的指针。换句话说,除非类成员最初用了new,否则是不用在析构函数里用delete的。
说到智能指针,这里介绍一种避免必须删除指针成员的方法,即把这些成员用智能指针对象来代替,比如c++标准库里的auto_ptr。想知道它是如何工作的,看看条款m9和m10。
3.预先预备好内存不够的情况
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头文件(或与它相等价的用到了名字空间的版本,见条款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函数,它在头文件里大致是象下面这样定义的:
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 cant allocate enough memory
void nomorememory()
{
cerr << "unable to satisfy request for memory
";
abort();
}int main()
{
set_new_handler(nomorememory);
int *pbigdataarray = new int[100000000];...
}
假如operator new不能为100,000,000个整数分配空间,nomorememory将会被调用,程序发出一条出错信息后终止。这就比简单地让系统内核产生错误信息来结束程序要好。(顺便考虑一下,假如cerr在写错误信息的过程中要动态分配内存,那将会发生什么...)
operator new不能满足内存分配请求时,new-handler函数不只调用一次,而是不断重复,直至找到足够的内存。实现重复调用的代码在条款8里可以看到,这里我用描述性的的语言来说明:一个设计得好的new-handler函数必须实现下面功能中的一种。
·产生更多的可用内存。这将使operator new下一次分配内存的尝试有可能获得成功。实施这一策略的一个方法是:在程序启动时分配一个大的内存块,然后在第一次调用new-handler时释放。释放时伴随着一些对用户的警告信息,如内存数量太少,下次请求可能会失败,除非又有更多的可用空间。
·安装另一个不同的new-handler函数。假如当前的new-handler函数不能产生更多的可用内存,可能它会知道另一个new- handler函数可以提供更多的资源。这样的话,当前的new-handler可以安装另一个new-handler来取代它(通过调用 set_new_handler)。下一次operator new调用new-handler时,会使
补充:软件开发 , C++ ,