返回列表 发帖

[转帖]怎样使你的代码更稳定

正文 怎么使你的代码更稳定(指针篇). (版权所有,如果要转载or出版请和作者亲自联系,谢谢) 一. 说在开头之前: 首先,我要说明一下,我已经工作了.其次我的水平不高,在我们公司水平比我高的人非常多.再次我很少参与那些BBS的讨论.主要是没有时间和这个精力.文章主要是从我工作中来的. 在这个文章中提到的代码主要是C和C++的Code.不是其他的语言. 二. 开始: 代码的稳定主要是两个方面的稳定.一个是代码逻辑上的稳定,一个是代码本身的稳定. 代码逻辑包含了软件的设计,代码结构设计,还有出错处理系统三个部分.这三个每一个部分都是一个很大的方面,就不在这里说明了,以后有空到话,会写写看看.这里主要说明代码本身的稳定性. 代码本身的稳定性是一个程序员的基本功力的考验.当主管分配给你任务的时候,先不管完成要多少时间,但是完成之后不要出错.这个是最最基本的工作要求了.一个软件不管设计上面是如何的烂,但是只是从代码的角度来看至少这个软件能运行,不会因为一些错误的代码而是整个程序完全当掉. 毕竟软件功能的完成,还是要靠能运行的代码来实现的. 如果Code有问题,软件功能实现了也是没有用的. 三 .错误代码-1: 指针 代码是否稳定与否,很大程度上是看代码中指针(Pointer)运用是否合理.程序中如果出现了一个不可捉摸的错误,很有可能是在操作非法指针. 非法指针主要包含的一些操作是: 指针没有初始化(UnInitialize),空指针(NULL Pointer),被损坏的指针(Bad Pointer),越界和指针强制转换错误. 这些错误一旦在Code中存在,Code的当机指数就会直线上升. 1.指针没有初始化 如果一个指针没有初始化就被使用,那Code”可能” 会出错. 这种错误一般不应该产生. 毕竟定义任何变量,不管是否要使用,首先进行变量的初始化. 这个是编程的基本习惯之一.使用一个没有初始化的变量基本上会导致Code逻辑上的问题(非指针变量)和当机(指针变量)两种情况. 比如以下的代码 #include #include void main() { DWORD dwCount; if (dwCount>10000) printf("Error\n"); } 编译没有问题,只是有个Warning而已.Link通过.运行这样的代码,那个”Error”的字串能不能输出,那就天知道了.如果是指针代码,那就当了,比如下面的代码 #include #include void main() { char *Temp; DWORD dwCount; dwCount = GetCount(); if (dwCount>10000) { *Temp = ';a';; printf("Error\n"); } printf("%s\n",Temp); } 这个代码编译之后, 只要运行程序就会当.实际上只要在定义指针Temp之后,加上Temp = NULL.就不会当了. 想避免这个错误,其实很简单.做过一段时间之后,养成一个好的编程习惯就不太会出这种问题了.而且编译的时候一般都会提示有一个Warning. 比如上面的Code在编译的时候就会显示: “warning C4700: local variable ';Temp'; used without having been initialized”. 看到这个错误还不去修改,那就没有话说了. 程序当了活该. 但是像有些情况,连这个Warning都不会报告,但是还是属于没有初始化指针错误 char *Temp; Temp = new char[20]; if (Temp!=NULL) printf("%s\n",Temp); 上面代码里面,定义了一个指针,并分配了内存,但是没有作内存初始化就进行操作,结果往往是不可预知的. 当然上面代码运行的时候程序是不会出错,只不过是输出一些没有意义的字符而已. 这个主要和操作系统有关系,在Windows平台下面,如果定义之后没有初始化,就会自动给一个初始值0XCC. 比如定义 DWORD dwTest, TCHAR szTest[2] 如果没有去用它,dwTest = 0XCCCCCCCC , szTest的Buffer也是被一堆的0XCC填充满. 但是这种情况只是限于Windows 平台. 所以,在你可以操作没有初始化的值,但是他们的内容都是0XCC, 反正一个变量的长度有多长,系统就会填充多长的0XCC. 这点要注意. 但是仅限于非指针的情况.如果是指针的话,如果没有初始化,那么它指向的地址是 0XCCCCCCCC. 但是Windows系统在X86上面, 0x7fffffff以上的地址都是不可访问的.所以,如果你想那没有初始化的指针去访问什么东西的话,下场是可想而知的. 2. 空指针错误 空指针错误是更加进一步错误了. 一般来说如果在Code中定义了一个指针,也赋值为NULL了,但是没有分配内存给它,就对它进行内存操作.那程序运行的时候100%死. 一般这种情况主要在操作的时候没有检查指针是否为NULL造成.这个只要在代码中随时检查一个指针是否为NULL. if (Temp!=NULL) printf("%s\n",Temp); 但是在有些情况之下如果一个不注意还是会出现操作空指针的错误.比如Code先定义了一个指针,分配了20Byte 的内存. 分配完内存之后要检查一下内存分配有没有成功,如果不成功就要报错,并停止运行接下来的Code,返回. 虽然Windows环境之下内存用完的可能性不是很大 ,但是在某些系统中就算系统内存很大,还是会出现内存不够的错误的. Temp = new char[20]; if (Temp==NULL) printf("Error\n"); 接下来如果在使用过程中发现前面分配的20Bytes不够了,需要重新分配内存.这个时候就要万分的小心了. delete[] Temp; 在delete之后,Temp的内存是被回收了(对应的内存空间被填充了一堆的0xEE.),如果在这个时候运行if (Temp==NULL).抱歉,Temp不等于NULL. 如果接下来的Code中有操作Temp指针的代码,要小心了. Temp指针不等于NULL,但是它指向的内存已经被回收. 所以在delete 操作之后,就要把Temp = NULL. 不过delete的操作不是真正的回收内存,它只是把要回收的内存的填充为0xEE,至于什么时候回收,那就有点说不清楚了.这个和windows的内存回收机制有关系.如果内存还没有真正的被回收,那么去访问那段地址的话,最多是值不对,不会引起太大的错误. 如果内存空间真的被回收了. 那么访问那段地址会使程序当掉. 如果去访问没有内存空间的地址段,是会引起系统错误的. 如果企图让指针指向的地址位过低,比如64K以下的内存段.那么系统会出现错误,就像你访问了一个NULL指针一样的效果. 程序当掉.比如下面的代码 DWORD *dwTest ; dwTest = (DWORD*)0x0000ffff; 运行的时候就会出错. 空指针错误和前面的指针没有初始化一样,如果有良好的编程习惯就不会轻易出错. 小心就可以了. 3. 坏指针,越界和指针的强制转换 坏指针,越界和指针的强制转换往往是混在一起产生的. 如果出现一个指针的内存结构被破坏了(被其他的代码以非正常的手段写过了.),那就会叫这个指针为坏指针.坏指针的产生情况非常复杂.主要有,操作没有初始化的指针,越界操作内存,代码逻辑上的错误.指针强制转换越界. 由于这几项的关连性太强. 所以会作为一个整体来说明. 在使用memcpy, strcpy之类的函数的之前,最好作一个被Copy对象的内存越界检查. 以确保内存Copy操作不会引起越界问题. 比如 memcpy(lpTarget,lpSource,size_t) 就要做三个检查,第一lpTarget是否合法,是不是为NULL,有没有指向其他内存区域. 第二,lpSource是不是合法. 第三 lpSource可操作内存的长度是否小于Size_t,lpTarget的内存长度是否小于Size_t. 这三个检查一个都不能忽略.虽然不见的一定要在代码中做检查,但是使用的时候心里要有数,不能没有搞清楚,就去使用. 一旦出现内存操作越界的问题.这个程序100%会死掉. 比如Cstring类的内部保存字符串指针的地址是一直在变化的,这个时候就要小心操作.不要直接使用那个一直在变化的指针. 现在很多代码中都有强制转换指针类型的操作.比如LPBYTE转换成LPVOID.特别是在函数传参数的时候.经常需要做转换,但是那个转换是什么意思,一定要明白.比如转换指针类型之后,操作的内存空间是否会变化. 这个关键是基本数据类型是否熟悉的问题. 比如把一个LPBYTE指针转换成一个 char*,没有问题.两个的长度是相同的.都是一个byte. 如果把LPBYTE转换成一个 LPCTSTR,那就要小心了. 因为根据你程序的编码方式的不同,ANSI 或者Unicode LPCTSTR的长度有时候是1个byte,有时候是2个byte. 或者反过来,定义一个BYTE数组来保存一些个字符.那定义数组的Code要这样写 Temp = new BYTE[20*sizeof(TCHAR)]; 不然的话,有可能会出错. 实际上,最好是Code需要什么样的类型的指针就定义什么样类型的指针.这样就能减少很多的错误. 自定义的函数中如果需要传入一个数组指针,比如上面的Temp指针.只要不是基本数据类型的话,传入指针的同时一定要把数组的长度传入.只是评一个指针类型,是很难判断指针的buffer是多大的.传入指针的buffer的长度在需要把这个指针作为返回值的时候是比较有用的. 这样在函数里面做内存Copy.准备返回的时候,就能判断内存Copy是否会产生越界现象. 另外如果传入一个指针,但是不准备回写,那请你把这个指针定义为const类型.这样在编译的时候就能知道有没有在写这个指针的内存buffer. 注意这个只是一种减少出错的办法,但是不是完美的.毕竟在运行期,代码实际上可以操作任何内存空间的. 四.错误代码-2: 返回局部变量. 指针的错误是综合性的.返回局部变量,这个错误和指针错误也有相关的地方. 函数的返回值一般是1.没有返回值,2.返回简单数据类型 比如LONG DWORD ,BOOL之类的.3返回一个指针. 4. 返回多个值,指针.返回局部变量错误一般是在3,和4两种情况下比较多. 比如下面的Code 就是一个返回局部变量错误. LPCTSTR foo() { char foo[20]; memset(foo,';a';,20); return foo; } void main() { printf("%s\n",foo()); } 函数返回一个指针foo,但是当FOO函数结束的时候,foo指针已经被销毁了.这样main函数拿到的东西是无效的.是一个非法的指针,它指向一块的非法的内存空间.如果想把函数FOO里面的局部变量foo的值拿出来的话,只能是做内存copy. 在运行foo的时候传入一个指针,做完之后,把内存copy出来.就像下面的代码 int foo(LPTSTR lpBuffer,int nBufferLen) { char foo[20]; memset(foo,';a';,20); if (nBufferLen>=20) { lstrcpy(lpBuffer,foo); return 0; } return lstrlen(foo); } void main() { TCHAR Buffer[10]; foo(Buffer,10); printf("%s\n",Buffer); } 虽然准备的Buffer不够,但是也不会内存操作越界. 当进行Buffer Copy的时候,如果Buffer的空间不足.那么一般有两种办法来出来,第一,在函数内部new出来足够的Buffer,到函数外面去delete. 第二种办法,就是上面的code用的办法,如果 Buffer不够,那么就要告诉函数的调用者说”对不起,你准备的Buffer不足,请准备多少多少Buffer来Copy内存. 然后,调用者就会根据第一次调用的结果来准备足够的Buffer,进行第二次的调用.现在就不担心Buffer不足的问题了. 五: 错误代码 – 3 :内存泄漏 内存泄漏往往也是指针操作失误造成的.内存泄漏是比较麻烦的一种错误的指针操作.像前面的指针操作问题,一般都很明确,程序运行会当.而内存泄漏往往运行时不太看的出来,程序能正常运行,但是系统的内存会不停的减少. 这是因为,内存被分配,但是没有回收,导致系统内存总容量的持续减少.但是由于Windows系统本身有内存回收能力,所以当有内存泄漏的程序被关掉的时候,丢失的内存自然会回来. 对于一个桌面应用程序而言,这样的问题有时候没有什么大影响.只要应用程序吃掉的内存不会影响系统的正常运行就好了.在某些的开源的代码里面,有些人的写法就只new,不去delete. 作者的用意应该是想把内存回收工作交给系统来做.但是对于Server类程序,或者要做成Service程序24*7来运行的程序,哪怕是一个bit的内存泄漏,那也是致命的问题. 内存泄漏产生的原因一般是三种情况: 1.分配完内存之后忘了回收.2. 程序Code有问题,造成没有办法回收.3.某些API函数操作不正确,造成内存泄漏. 1. 内存忘记回收,这个是不应该的事情. 分配一段内存之后,用完之后,就一定要回收. 如果不回收,那就造成了内存的泄漏,造成内存泄漏的Code如果被经常调用的话,那内存泄漏的数目就会越来越多的.从而一下整个系统的运行. 比如下面的代码 for (int =0;I<100;I++) { Temp = new BYTE[100]; } 就会产生 100*100Byte的内存泄漏. 2. 在某些时候,因为代码上写的有问题,会导致某些内存想回收都收不回来,比如下面的代码 Temp1 = new BYTE[100]; Temp2 = new BYTE[100]; Temp2 = Temp1; 这样,Temp2的内存就丢掉了,而且永远都找不回了,这个时候Temp2的内存空间想回收都没有办法,而且这种操作还有一个致命的问题,就非法指针,如果 我先delete[]Temp1; 然后再去操作Temp2. Sorry. 指针指向的内存地址非法,程序十有八九会当掉. 所以,当你想把一个Array里面的东西copy到另外一个Array里面的时候,千万不要直接就Temp2 = Temp1,那只是把Temp1的地址赋值给 Temp2. 要去做内存Copy. 当然做内存Copy的时候要小心,这个前面已经说过了. 3. API函数应用不当,在Windows提供API函数里面有一些特殊的API,比如FormatMessage. 如果你给它参数中有FORMAT_MESSAGE_ALLOCATE_BUFFER,它会在函数内部New一块内存Buffer出来.但是这个buffer需要你调用LocalFree来释放. 如果你忘了,那就会产生内存泄漏. 想解决内存泄漏基本上没有什么好的办法,唯一的就是要小心使用内存,规范你的编程习惯. 当然内存泄漏有时候也可以借助工具来诊查. 比如BounderCheck就是一个不错的内存漏洞检测工具,它可以帮助在编码的时候减少代码上的错误.但是仅仅是减少而已,如果想提高代码的质量,最终还是要依靠自身水平的提高.

[转帖]怎样使你的代码更稳定

虽然看不懂,但是 x86 兄的帖子我都看过了,很负责任的个斑竹。
海岸线有你的加入,技术上有了提高。辛苦了!:)

TOP

返回列表 回复 发帖