高质量C++编程指南:指针、数组和内存管理_第1页
高质量C++编程指南:指针、数组和内存管理_第2页
高质量C++编程指南:指针、数组和内存管理_第3页
高质量C++编程指南:指针、数组和内存管理_第4页
高质量C++编程指南:指针、数组和内存管理_第5页
已阅读5页,还剩74页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第4章指针、数组和内存管理

在C++程序中几乎不可能不用到指针、数组、字符串和内存管理。如果程序员不了解指针的本质,不能熟练使用指针的话,那么他就无法动态管理内存,很难想象这样的程序有什么用途。

指针、数组和字符串它们之间既有区别又有联系。许多人很难清楚说出它们之间的关系,因此产生了一些错。4.1指针一、指针的本质

指针是变量,它和我们平常使用的整型变量、字符变量、浮点变量等各种变量没有本质的区别,不同的是它们的类型和值的含义(即解释方式)。在二进制层面,指针的值就是内存单元的地址,而变量又是引用内存单元的值的别名,因此在语言层面指针的值就是变量的地址。例如:inti=100;int*pi=&i;int**ppi=πcout<<"&i:"<<&i<<endl;

//&i:0x0012FF7Ccout<<"pi:"<<pi<<endl;

//pi:0x0012FF7Ccout<<"&pi:"<<&pi<<endl;

//&pi:0x0012FF78cout<<"ppi:"<<ppi<<endl;

//ppi:0x0012FF78cout<<"&ppi:"<<&ppi<<endl;

//&ppi:0x0012FF74cout<<"*ppi:"<<*ppi<<endl;

//

*ppi:0x0012FF7C1000x0012FF7C0x0012FF780x0012FF7C0x0012FF780x0012FF74变量i指针变量pi二重指针变量ppi二、指针的类型及其运算指针的类型为一个类型名和字符“*”的组合。编译器认为这样的一个指针指向的内存单元为4字节。

当一个类型名和字符“*”结合声明变量的时候,就产生一种新的类型,可以利用这种类型来声明一个指针。如:int*pint;int**ppint;//双重指针int***pppint;//三重指针

虽然类型名和“*”的组合是一种指针类型,但是编译器解释的时候,“*”是和其后的变量名结合的。如:int*a,b,c;编译器会理解为:int*a,b,c;只有a是int型的指针。

全局指针变量的默认初始值是NULL。而对于局部指针变量,你必须显式指定其初值,否则其初值是不确定的任意值。

不管指针变量是全局的还是局部的、静态的还是非静态的,应当在声明它的同时初始化它,要么赋予一个有效的地址,要么赋予NULL。指针值是int类型的,那么就可以给它赋予任何整数值,包括负值,如:int*pInt=NULL;pInt=reinterpret_cast<int*>(-0x0024FFE4);//把负数解释为地址cout<<pInt<<endl;//FFDB001Ccout<<reinterpret_cast<unsignedint>(pInt);//4292542492cout<<UINT_MAX<<endl;//42949672954292542492的十六进制表示正好是FFDB001C,因此编译器并不是把指针值解释为signedint,而是解释为unsignedint类型。指针可以参与整数能够参与的任何算术运算,但是除了下列运算以外,其他运算都是没有实际意义的:(1)指针自增(++),表示指向序列中的后一个元素。(2)指针自增(--),表示指向序列中的前一个元素。(3)指针加一个整数i,表示它向后递进了i个元素。(4)指针减一个整数i,表示它向前递进了i个元素。(5)两个同类指针相减,表示计算它们之间元素的个数。(6)指针赋值,把一个指针值指派给另一个指针。(7)指针比较,常用的是==和!=。(8)取地址(&)和反引用(*)。void*型指针不能参与算术运算,只能进行赋值、比较和sizeof()操作。

把&用于指针时,就是在提取指针变量的地址,不能在一个指针前面连续使用多个&,如&&p,因为&p已经不是一个变量了。

不能对字面常量使用&来取其地址,因为字面常理保存在符号表里,没有地址概念

不能对void*类型指针使用*来取其所指向的变量。如:inti=100;void*p=(void*)&i;i=*p;//错误,取无类型指针的值三、指针传递

可以把函数的参数或返回值类型声明为指针类型,此时函数接受的参数或返回值就是地址。而不是指针所指的内存单元值。例如:voidfun(int*p){*p=100;cout<<*p<<endl;//修改p所指内存p=NULL;}intcount=0;fun(&count);cout<<count<<endl;//100也可以使用指针的引用。如:voidallocate(char*&p,intsize){p=newchar[size];}voidtest(void){char*str=NULL;allocate(str,100);strcpy(str,”HelloWorld!”);cout<<str<<endl;delete[]str;}4.2数组一、数组的本质:任何数组,不论是静态声明的还是动态创建的,其所有元素在内存中都是连续存放的,也就是说保存在一大块连续的内存中

数组元素的下标编号从0开始,最后一个元素下标等于元素个数减1,下标必须是整数或整数表达式。

当使用[]来引用数组元素的时候,编译器必须把它转换为同类型的指针表示形式,然后再进行编译。例如:a[3]=100;

//转换为*(a+3)=100cout<<a[3]<<endl;//转换为

cout<<*(a+3)<<endl;

数组名字本身就是一个指针,是一个常量指针,即a等价于int*consta,因此你不能试图修改数组名的值。

任何2个数组之间不能直接赋值,即使是同类型数组,必须按元素逐个的赋值。

编译器在给数组分配内存空间的时候总是以用户指定的元素个数为准,如果初始值个数多于指定的个数则报错。

C++不会对用户访问的数组是否越界进行任何检查,无论是静态的还是动态的。这是因为可以使用整型变量及其表达式作为数组下标,而变量值在编译时是无法确定的,所以语言无法执行静态检查。

理论上讲,C++可以在运行时进行数组的越界访问检查,这是因为数组大小信息保存在程序的某个地方,一般是放在数组第一个元素位置的前面,占用一个int变量字节数,它的地址为a-sizeof(int)。但是如果在每次访问数组元素的时候都要去这个地方取数组的大小并与下标值比较,这会极大增加运行时开销。

另一方面,C++允许通过同类型指针访问数组每一个元素,指针除了它的类型和值外,不带有任何其它信息,又如何确定指针是否越界了呢?二、数组的传递

数组是不能从函数的return语句返回的,但是数组可以作为函数的参数,如:voidfun(const

inta[],intsize){cout<<sizeof(a)<<endl;//是4不是400for(inti=0;i<size;i++)cout<<a[i]<<endl;}intx[100]={0};cout<<sizeof(x)<<endl;//是400fun(x,100);

把数组作为参数传递给函数的时候并非把整个数组的内容传递进去,此时数组退化为一个同类型的指针。即函数fun原型中声明的数组并非真正的数组,编译器把它改写为:voidfun(const

int*consta,intsize)为什么要这样做呢?大概是因为:(1)数组在内存中是连续存放的,因此编译器可以通过地址计算来引用数组中元素。(2)出于性能考虑。如果把整个数组中的元素全部传递进去,不仅需要大量时间来拷贝数据,而且这个拷贝还会占用大量的堆栈空间。

对于多维数组传递,你必须指出除第一维之外的所有维的元素个数,如:voidfun(const

inta[][20],intline){cout<<sizeof(a)<<endl;}//4intx[10][20]={{100}};cout<<sizeof(x)<<endl;//800fun(x,10);对于多维数组,并不像一维数组那样可以简单地转换为同类型指针,而是转换为与其等价的数组指针。例如int

a[m][n]就转换为int(*a)[n],就是说a是指向一维数组的指针,而该一维数组有n个元素。下面4种表达方式是等价的:(1)a[i][j](2)*(a[i]+j)(3)(*(a+i))[j](4)*(*(a+i)+j)

为什么在向函数传递多维数组的时候不需要说明第一维的长度而必须说明其他所有维的大小呢?原因有三:(1)数组元素在内存中按照“行优先”的规则存储。(2)编译器在计算元素的地址时不需要数组第一维的长度,但是需要所有其他维的长度信息。(3)C++不对数组进行越界访问检查,因此对编译器来说不需要知道第一维长度。

数组传递在C++中默认是地址传递。如果你想按值来传递数组,可以把数组封装起来,例如放到struct或class里面作为一个成员,因为结构和类对象默认都是按值传递。4.2字符数组、字符指针和字符串一、字符数组、字符串和“\0”

字符数组就是元素为字符变量的数组,而字符串则是以“\0”(ASCII码为0x00)为结束字符的字符数组。因此字符数组不一定就是字符串。

对于字符串来说,它是变长的,因此无法记录自己的长度,如何来表示字符串的结束呢?它本身没有长度信息,必须用一个字符来标识字符串的结束。这个字符就是“\0”。[提示1]:如果用一个字符串字面常量来初始化一个字符数组,数组的长度至少要比字符串字面常量的长度大1,因为要保存“\0”。如:chararray[]=“Hello”;数组array的元素为{‘H’,’e’,’l’,‘l’,‘o’,‘\0’}[提示2]:对字符数组来说,它并不在乎中间或末尾有没有“\0”结束字符,因为数组知道它自己有多少个元素。但如果字符数组中没有“\0”结束标志,把它当作字符串来用时就会出错,导致内存访问冲突。[提示3]:如果你能够保证总是使用下标来访问数组中的每个元素,那么就没有必要非得在字符数组的结尾放进一个“\0”。charchar_1[]={‘a’,‘b’,‘\0’,‘d’,‘e’};charchar_2[]=“hello”;char*p=“hello”;cout<<sizeof(char_1);//表示数组占5字节

cout<<strlen(char_1);//表示字符串长度为2

cout<<sizeof(char_2);//表示数组占6字节

cout<<strlen(char_2);//表示字符串长度为5

cout<<sizeof(p);//表示指针p占4字节

cout<<strlen(p);//表示字符串长度为5二、字符指针的误区提示1:当使用字符指针来引用一个字符变量时,千万要当心,因为C++默认char*表示字符串。如:charch=‘a’;

//用‘a’初始化变量chchar*pchar=&ch;

//指针指向字符变量cout<<pchar<<endl;

//错把字符当字符串上面输出除a外,还有乱字符。正确应该是:cout<<*pchar<<endl;

三、字符串复制和比较

字符串的复制请使用strcpy或strncpy,不要企图用“=”对字符串进行复制,因为那是字符指针的赋值。

不要使用“==”、“>=”、“!=”符号直接比较两个字符串,字符串的比较应该使用strcmp、strncmp等库函数。

对字符串进行拷贝时,要保证函数结束后目标字符串的结尾有“\0”结束标志,函数strncpy和strncat不会自动在目标字符串结尾追加“\0”。四、引用和指针比较引用”&”是C++新增的概念,下面程序中n是变量m的一个引用,m是被引用物。

intm;

int&n=m;

n相当于m的别名,对n的操作就是对m的操作,所以n既不是m的拷贝,也不是指向m的指针,其实n就是m自己。

引用和指针的比较如下:(1)引用在创建的同时必须初始化,即引用到一个有效的对象,而指针在定义的时候不必初始化,可以在定义后面的任何地方重新赋值。(2)不存在NULL引用,引用必须与合法的存储单元关联,而指针可以使NULL。如果把一个引用初始化为0,如:

constint

rint=0;

其语义并非是把引用初始化为NULL,而是创建一个临时的int对象并用0来初始化它。然后再用它来初始化引用rint。而该临时对象将一直保留到rint销毁的时候才销毁。所以不要用字面常量来初始化引用。(3)引用一旦被初始化为指向一个对象,它就不能被改变为对另一个对象的引用。而指针在任何时候都可以改变为指向另一个对象。(4)引用的创建和销毁并不会调用类的拷贝构造函数。(5)在语言层面,引用的用法和对象一样,在二进制层面,引用一般都是通过指针来实现的,只不过编译器帮我们完成了转换

引用既具有指针的效率,又具有变量使用的方便性和直观性。

如果的确只需要借用一下某个对象的别名,那么就用引用,而不要用指针,以免发生意外。4.3内存管理一、内存分配方式内存分配方式有三种:(1)从静态存储区域分配。内存在程序编译的时候就已经分配好了(即已经编址),这些内存在程序的整个运行期间都存在。如全局变量、静态变量等(2)在堆栈上创建。在函数执行期间,函数内局部变量(包括形参)的存储单元都创建在堆栈上,函数结束时这些存储单元自动释放(堆栈清退)。堆栈内存分配运算内置于处理器的指令集中,效率很高,并且不存在失败的危险,但是分配的内存容量有限。(3)从堆(heap)上分配。亦称动态分配内存。程序在运行期间用malloc或new申请任意数量的内存,程序员自己使用free或delete掌握释放内存的恰当时机。动态分配内存的生存期由程序员自己决定,使用非常灵活,但也最容易产生问题。

一般的原则是:

如果使用堆栈存储和静态存储就能满足应用要求,那么就不要使用动态存储。这是因为使用动态内存需要很可观额外开销(a)应用程序将调用操作系统的内存管理模块中的堆管理器,搜索其中是否有符合要求的空闲的连续字节内存块。特别是在经过多次动态分配后,堆会“千疮百孔”,出现大量的碎片,此时可能需要进行碎片合并,然后才能分配成功,在这种情况下的动态分配需要很长时间。(b)如果动态分配失败,还需要检查返回值或者捕获异常,这也需要额外开销。(c)动态创建的对象可能被删除多次,甚至在删除后还会继续使用,或者根本就不会被删除,于是出现运行时错误,或程序吃内存现象,这些问题并不是小心编程就可以避免的。二、常见的内存错误及其对策 发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。常见的内存错误及其对策如下:1、内存分配未成功,却使用了它。编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理。2、内存分配虽然成功,但是尚未初始化就引用它。犯这种错误主要有两个原因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。

内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。3、内存分配成功并且已经初始化,但操作越过了内存的边界。例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。4、忘记了释放内存,造成内存泄露。含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。动态内存的申请与释放必须配对,程序中new与delete的使用次数一定要相同,否则肯定有错误。5、释放了内存却继续使用它。有四种情况:(1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。(2)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。(3)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。(4)多次释放同一块内存。【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。【规则4】动态内存的申请与释放必须配对,防止内存泄漏。【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。三、指针参数是如何传递内存的?

如果函数的参数是一个指针,不要指望用该指针去申请动态内存。下例中,试图用指针参数申请动态内存,Test函数的语句GetMemory(str,200)并没有使str获得期望的内存,str依旧是NULL,为什么?voidGetMemory(char*p,intnum){ p=newchar[num];}voidTest(void){ char*str=NULL; GetMemory(str,100); //str

仍然为NULLstrcpy(str,“hello”); //运行错误}

毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是_p,编译器使_p=p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用delete释放内存。如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见下例:voidGetMemory2(char**p,intnum){ *p=newchar[num];}voidTest2(void){ char*str=NULL; GetMemory2(&str,100); //注意参数是

&str,而不是strstrcpy(str,"hello"); cout<<str<<endl; delete[]str; }

由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单。char*GetMemory3(intnum){ char*p=newchar[num];returnp;}//返回指针;voidTest3(void){char*str=NULL; str=GetMemory3(100); strcpy(str,"hello"); cout<<str<<endl; delete[]str; }

用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见下例:char*GetString(void){ charp[]="helloworld"; returnp; //编译器将提出警告}voidTest4(void){char*str=NULL;

str=GetString(); //str

的内容是垃圾

cout<<str<<endl;}

用调试器逐步跟踪Test4,发现执行str=GetString语句后str不再是NULL指针,但是str的内容不是“helloworld”而是垃圾.

再看下面的例子:char*GetString2(void){ char*p="helloworld"; returnp;}voidTest5(void){ char*str=NULL; str=GetString2(); cout<<str<<endl;}

函数Test5运行虽然不会出错,但是函数GetString2的设计概念却是错误的.因为GetString2内的“helloworld”是常量字符串,位于静态存储区,它在程序生命期内恒定不变.无论什么时候调用GetString2,它返回的始终是同一个“只读”内存块的地址。不可试图对它进行操作,你可以把返回值改为constchar*。四、free和delete把指针怎么啦?

别看free和delete的名字恶狠狠的(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。用调试器跟踪下例,发现指针p被delete以后其地址仍然不变(非NULL),只是该地址对应的内存是垃圾,p成了“野指针”。如果此时不把p设置为NULL,会让人误以为p是个合法的指针。

char*p=newchar[100];

strcpy(p,“hello”); delete[]p; //p所指的内存被释放,但是p所指的地址仍然不变

if(p!=NULL) //没有起到防错作用

{strcpy(p,“world”);//出错

}如果程序比较长,我们有时记不住p所指的内存是否已经被释放,在继续使用p之前,通常会用语句if(p!=NULL)进行防错处理。很遗憾,此时if语句起不到防错作用,因为即便p不是NULL指针,它也不指向合法的内存块。不等于NULL的指针不一定就是有效的指针,所以我们强调一定不要忘记初始化指针变量(NULL或者有效地址)。五、动态内存会被自动释放吗? 函数体内的局部变量在函数结束时自动消亡。很多人误以为下例是正确的。理由是p是局部的指针变量,它消亡的时候会让它所指的动态内存一起完蛋。这是错觉!voidFunc(void){char*p=(char*)malloc(100);

}//动态内存会自动释放吗?

由于指针变量和其指向的内存空间不是一回事,才导致指针看上去具有一些不易理解的特征:(1)指针消亡了,并不表示它所指的内存会被自动释放。(2)内存被释放了,并不表示指针会消亡或者成了NULL指针.

这表明释放内存并不是一件可以草率对待的事。也许有人不服气,一定要找出可以草率行事的理由:

如果程序终止了运行,一切指针都会消亡,动态内存会被操作系统回收。既然如此,在程序临终前,就可以不必释放内存、不必将指针设置为NULL了。终于可以偷懒而不会发生错误了吧?如果程序长期不结束,那要吃掉多少内存啊!再说如果别人把那段程序取出来用到其它地方怎么办?

六、杜绝“野指针”

“野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。“野指针”的成因主要有两种:(1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如

char*p=NULL; char*str=(char*)malloc(100);(2)指针p被delete之后,没有置为NULL,让人误以为p是个合法的指针.(3)指针操作超越了变量的作用范围,这种情况让人防不胜防,例如:

classA{ public: voidFunc(void){

cout<<“classA”<<endl;}};

voidTest(void){A*p; {Aa; p=&a;}//注意a的生命期

p->Func(); //p是“野指针”

}函数Test在执行语句p->Func()时,对象a已经消失,而p是指向a的,所以p就成了“野指针”。但奇怪的是运行这个程序时居然没有出错,这可能与编译器有关。实际上,根本原因是a虽然退栈了,但仅仅是调用了一下析构函数而已,而析构函数其实没干什么重要的事情,它并没有清除a的内存,所以a仍然好好地在那里放着,只是你无法在程序块外直接访问而已。七.有malloc/free为什么还要new/delete?

malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free

因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete.

注意new/delete不是库函数。下面例子说明malloc/free和new/delete如何实现对象的动态内存管理classObj{public: Obj(void){cout<<“Initialization”<<endl;}~Obj(void){cout<<“Destroy”<<endl;}void Initialize(void){cout<<“Initialization”<<endl;}voidDestroy(void){cout<<“Destroy”<<endl;}};voidUseMallocFree(void){Obj*a=(obj*)malloc(sizeof(obj)); //申请内存

a->Initialize(); //初始化

//… a->Destroy();free(a);}//清除工作,释放内存

voidUseNewDelete(void){ Obj*a=newObj; //申请动态内存并且初始化//… deletea; } //清除并且释放内存类Obj的函数Initialize模拟了构造函数的功能,函数Destroy模拟了析构函数的功能。函数UseMallocFree中,由于malloc/free不能执行构造函数与析构函数,必须调用成员函数Initialize和Destroy来完成初始化与清除工作。函数UseNewDelete则简单得多。

所以我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。

既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。八、内存耗尽怎么办?如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。通常有以下几种方式处理“内存耗尽”问题。(1)判断指针是否为NULL,如果是则马上用return语句终止本函数。例如:voidFunc(void){A*a=newA;if(a==NULL){ return; }…}(2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。例如:voidFunc(void){A*a=newA;if(a==NULL){cout<<“MemoryExhausted”<<endl;exit(1);} …}(3)为new和malloc设置异常处理函数。例如VisualC++可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。详细内容请参考C++使用手册。(4)捕获new抛出的异常,并尝试从中恢复。

上述(1)(2)方式使用最普遍。如果一个函数内有多处需要申请动态内存,那么方式(1)就显得力不从心(释放内存很麻烦),应该用方式(2)来处理。不过在C++中我们提倡使用方式(4)。

很多人不忍心用exit(1),问:“不编写出错处理程序,让操作系统自己解决行不行?”不行。如果发生“内存耗尽”这样的事情,一般说来应用程序已经无药可救。如果不用exit(1)把坏程序杀死,它可能会害死操作系统。道理如同:如果不把歹徒击毙,歹徒在老死之前会犯下更多的罪。

有一个很重要的现象要告诉大家:

对于32位以上的应用程序而言,无论怎样使用malloc与new,几乎不可能导致“内存耗尽”。在WindowsXP下用VisualC++编写测试程序,见下例。voidmain(void){ float*p=NULL;

while(TRUE) { p=newfloat[1000000];

cout<<“eatmemory”<<endl;

if(p==NULL) exit(1); }}

这个程序会无休止地运行下去,根本不会终止。因为32位操作系统支持“虚存”,内存用完了,自动用硬盘空间顶替。只会听到硬盘嘎吱嘎吱地响,WindowXP已经累得对键盘、鼠标毫无反应。

可以得出这么一个结论:对于32位以上的应用程序,“内存耗尽”错误处理程序毫无用处。这下可把Unix和Windows程序员们乐坏了:反正错误处理程序不起作用,我就不写了,省了很多麻烦。

这里不想误导大家,必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。

不过对于那些大型科学计算,比如求解数学难题、气象预报、环境监测等领域,以及那些需要使用大规模递归算法来求解的难题,比如64层汉诺塔问题,很容易出现内存不足,所以不可掉以轻心。九、内存分配使用要点1、malloc/free的使用要点函数malloc的原型如下:

void*malloc(size_tsize);

用malloc申请一块长度为length的整数类型的内存,程序如下:int*p=(int*)malloc(sizeof(int)*length);

我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。

malloc返回值的类型是void*,所以在调用malloc时要显式地进行类型转换,将void*转换成所需要的指针类型。

malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。我们通常记不住int,float等数据类型的变量的确切字节数。例如int变量在16位系统下是2个字节,在32位下是4个字节;而float变量在16位系统下是4个字节,在32位下也是4个字节。最好用以下程序作一次测试:cout<<sizeof(char)<<endl;cout<<sizeof(int)<<endl;cout<<sizeof(unsigned

int)<<endl;cout<<sizeof(long)<<endl;cout<<sizeof(unsignedlong)<<endl;cout<<sizeof(float)<<endl;cout<<sizeof(double)<<endl;cout<<sizeof(void*)<<endl;

在malloc的“()”中使用sizeof运算符是良好的风格,但要当心有时我们会昏了头,写出p=malloc(sizeof(p))这样的程序来。

函数free的原型如下:voidfree(void*memblock);

为什么free函数不象malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。

如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。2、new/delete的使用要点

运算符new使用起来要比函数malloc简单得多,例如:int*p1=(int*)malloc(sizeof(int)*length);int*p2=newint[length];

这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new的语句也可以有多种形式。例如:classObj{public:

Obj(void); //无参数的构造函数

Obj(intx); //带一个参数的构造函数…}voidTest(void){

Obj*a=newObj;

Obj*b=newObj(1); //初值为1 … deletea; deleteb;}

如果用new创建对象数组,那么只能使用对象的无参数构造函数。例如:

Obj*objects=newObj[100];

//创建100个动态对象不能写成: Obj*objects=new

Obj[100](1);//创建100个动态对象的同时赋初值1在用delete释放对象数组时,留意不要丢了符号‘[]’。例如delete[]objects;

//正确的用法deleteobjects;

//错误的用法后者相当于deleteobjects[0],漏掉了另外99个对象。提示:对于内部数据类型(如int、float等)的动态数组p而言,deletep和delete[]p是等价的.但是对于非内部数据类型的动态数组objects而言,deleteobjects和delete[]objects是不等价的。

多次delete一个不等于NULL的指针会导致运行时错误,但是多次delete一个NULL指针没有任何危险。因为delete运算符会首先检查这种情况,并且简单返回。3、new的三种用法:它们是:plainnew,nothrownew以及placementnew。(1)、plainnew

这是普通的new,也是我们常使用的new它们是这样定义的:void*operatornew(std::size_t)throw(std::bad_alloc);voidoperatordelete(void*)throw();

原来C++中的new总是返回NULL表示分配失败,现在plainnew在失败后抛出标准异常std::bad_alloc而不是返回NULL,因此通过检查返回值是否为NULL来判断分配是否成功是徒劳的。plainnew的使用如下:char*GetMemory(unsignedlongsize){char*p=newchar[size];returnp;}voidmain(void){//可能抛出异常try{char*p=GetMemory(1000000);//……delete[]p;}catch(conststd::bad_alloc&ex){cout<<ex.what()<<endl;}}(2)、nothrownew

nothrownew就是不抛出异常的运算符new的形式,nothrownew在失败时返回NULL,所以你使用它就不需要设置异常处理器,而是像过去那样检查返回值是否为NULL即可。nothrownew的定义是:void*operatornew(std::size_t,conststd::nothrow_t&)throw();voidoperatordelete(void*)throw();

nothrownew的另一个参数类型为nothrow_t,它的定义为:

struct

nothrow_t{};

constnothrow_t

nothrow;

nothrow是C++定义的一个nothrow_t的全局const对象。例:

voidfun(unsignedlength){unsignedchar*p=new(nothrow)unsignedchar[length];if(p==NULL)cout<<“allocatefailed!”<<endl;//……delete[]p;}(3)、placementnew/delete

这种形式new允许在一块已经分配成功的内存上重新构造对象或者对象数组。显然这种形式不用担心内存分配失败。它所做的唯一一件事情就是调用对象的构造函数。

它的定义形式为:void*operatornew(std::size_t

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论