编程注意事项常见错误与修正方法.docx
《编程注意事项常见错误与修正方法.docx》由会员分享,可在线阅读,更多相关《编程注意事项常见错误与修正方法.docx(23页珍藏版)》请在冰点文库上搜索。
编程注意事项常见错误与修正方法
编程注意事项、常见错误与修正方法
南瑞科技股份有限公司电网分公司
各开发部和代码质量组
日期
版本
说明
作者
<2009-12-7>
<1.0>
第一稿
彭晖/王谨/侯勇
前言4
内存、指针、数组的操作5
内存操作5
指针使用的注意事项6
指针没有初始化6
指针的分配和释放7
CORBA和M语言内存分配中的注意思想7
特别注意malloc(0)8
释放内存后将指针置为NULL8
避免使用释放掉的内存8
避免连续释放同一块内存8
避免赋值操作符两边都是动态分配过内存的指针8
避免“隐藏的”数组越界和内存越界9
二重指针的使用10
避免把程序中的指针存放在共享内存里面10
系统资源使用不当11
文件句柄的使用11
IPC的使用;12
线程的使用12
接口和头文件的使用13
接口不可以随便改动13
接口中的结构体和类不可以改动,如有改动,需要重新编译13
检查函数入参的合法性13
检查函数调用的返回值13
函数有多个return的情况要特别留意14
多线程下常见的错误15
对于全局变量、静态变量等可能引起竞争的资源,要加锁15
错误的同步15
错误的顺序假设15
CORBA类对象的使用16
变量使用16
变量的命名应体现变量的作用16
采用统一的变量命名风格16
避免全局变量、成员变量与局部变量、函数声明参数等同名16
变量在使用前必须初始化17
宏定义与常量避免使用比较“平常”的名字18
头文件与CPP文件18
文件命名应能体现文件所实现的功能18
头文件使用#ifndef18
避免同一头文件被多次引用18
尽量避免在头文件中做函数实现18
文件编码与排版19
文件以空白行结束19
尽量使用unix格式19
使用统一的缩进风格19
v
前言
为了能够帮助开发部新员工掌握正确的编程方法,同时也是为了方便各个开发部内部的交流。
代码质量组整理了OPEN-3000和D-5000开发工作的一些注意思想与常见的一些错误及其修正方法,希望能够通过正反面的素材来让大家辨别好与不好的编程方法,并尽可能在自己的开发工作中予以留意,进而提升开发的质量,提高开发工作的效率。
v内存、指针、数组的操作
v内存操作
内存操作失误是编程过程中最为常见的错误之一,内存错误表现各异,为Debug带了很多困难;常见的错误有内存越界访问,内存分配错误,指针没有初始化等,引起的现象有进程的CoreDump,数据被改写等。
下面对常见的内存错误举例并分析。
堆上内存、数组越界
进程在运行过程中使用的内存一般从堆上动态分配或者从函数栈上静态分配。
所谓动态是指调用libc的库函数malloc或者C++的操作符new从进程堆上分配内存,静态分配是指编译器在编译过程中就由编译器计算好大小,在函数栈上分配;
...
char*ptr=(char*)malloc(10);
strcpy(ptr,"abcdefghigklmnopqrst");//Error;
...
...
这是一个很明显的内存越界错误,建议使用安全的字符串操作函数族:
intconstSTRING_LEN=10;
char*ptr=(char*)malloc(STRING_LEN);
strncpy(ptr,"abcdefghigklmnopqrst",STRING_LEN);
valgrind检测过这样错误;
程序中不要出现字面常量,这些字面常量利用符号常量或者宏替代,安全,且容易维护;
栈上内存、数组越界
类似于进程堆上的数组越界,函数栈上的数组也存在着同样的问题:
intconstSTRING_LEN=10;
voidTestStackArray(intarg1,intarg2)
{
intfield_1=0;
intfield_2=0;
chararray[10];//--->chararray[STRING_LEN];
strcpy(array,"abcdefghigklm");//--->strncpy(array,"abc...",STRING_LEN);
}parasoft添加了规则,做出警告
函数栈上的内存益处可能会覆盖field_2,field_1的值,更严重的情况的下,会破坏函数的调用链,造成难以察觉的错误:
可能返回时core、可能出错在别的函数中;
valgrind等动态检查工具截获malloc,free等libc库函数,从而达到检测动态内存操作的错误,因此,利用valgrind不能检查出函数栈上的内存操作错误。
字符串内存分配上越界
字符串内存分配上很容易碰到,这种错误非常隐蔽,它往往会影响栈上其他变量的数值,譬如如下一段代码:
charstr1[10]="abcdefg";
char*str3;
......
str3=malloc(strlen(str1));//错误,没有考虑字符串后面的’\0’。
正确写法应该是str3=malloc(strlen(str1)+1);
strcpy(str3,str1);
......
free(str3);//不要忘记。
......
【思考与启示】:
字面常量是一种“不好”的编程习惯,会带来程序的难以维护,用宏和符号常量替代。
v指针使用的注意事项
指针使用常见的错误有指针没有初始化、重复释放、对无效指针操作等。
指针没有初始化
对变量没有进行初始化是比较常见的编程错误,对指针没有出示化就使用更会导致各种各样的问题,例如:
例一:
...
char*ptr;//Error
strncpy(ptr,"abc...",STRING_LEN);
.valgrind检测出过这样的错误
这是一个很明显的错误,ptr没有分配内存就对其进行操作,但是会造成各异的结果:
1.进程coredump在strncpy处;
2.破坏了其他地方的数据;
3.进程coredump在其他地方,而真正的根源却在这里;
例二:
...
char*ptr;//--->char*ptr=NULL;
ptr=(char*)realloc(ptr,NEW_SIZE);
valgrind检测出过这样的错误
这个例子有一个隐含的错误,就是指针没有初始化,realloc函数会分配新的内存,同时释放掉老的内存,这是一个隐含的bug。
指针的分配和释放
free,delete的指针一定要与分配方式匹配。
free一个由new操作符分配的内存后果是未知的。
对于c++和动态库来讲,内存的管理更为严格:
C++有各种new和delete操作符,甚至可以重载,使用一定要匹配。
例如:
...
char*p=newchar[MAX_LEN];
...
deletep;
...
这个例子中有两个显著的错误:
分配内存不一定能成功,可能会失败,要做出判断;
delete和new不匹配;
修正为:
char*p=NULL;
try
{
p=newchar[MAX_LEN];
}
catch(Exception&e)
{
return;
}
...
delete[]p;
CORBA和M语言内存分配中的注意思想
另外需要注意CORBA的MARSHAL与M语言的M_CODE内部使用的分配方式是new[]。
举例如下:
char*p=NULL;
intpsize=0;
M_CODE(rsp_read,p,psize);
*responseBuffer=(char*)malloc(psize+sizeof(int)+sizeof(unsignedchar));
memcpy(*responseBuffer,&g_endian,sizeof(unsignedchar));
memcpy(*responseBuffer+sizeof(unsignedchar),&ret_code,sizeof(int))memcpy(*responseBuffer+sizeof(int)+sizeof(unsignedchar),p,psize);
delete[]p;//不能使用free(p);
parasoft检测出过这样的错误
特别注意malloc(0)
Tru64,AIX,HP-UX等操作系统上的编译器malloc(0)为NULL,在Solaris10系统SunStudio10编译器及Linuxg++上malloc(0)不为NULL。
释放内存后将指针置为NULL
举例:
if(p)
{
deletep;
p=NULL;
}
可以避免再次使用被释放的内存。
避免使用释放掉的内存
本质上说,系统会在堆上维护一个动态内存链表,如果被释放,就意味着该块内存可以继续被分配给其他部分,如果内存被释放后再访问,就可能覆盖其他部分的信息,这是一种严重的错误。
避免连续释放同一块内存
举例:
char*p=(char*)malloc(10);
char*q=p;
//…
free(p);
free(q);//二次释放
避免赋值操作符两边都是动态分配过内存的指针
举例
intmain()
{
char*x=(char*)malloc(20);
char*y=(char*)malloc(20);
x=y;
free(x);
free(y);
return0;
}
x动态分配的内存泄露。
避免“隐藏的”数组越界和内存越界
案例:
连续两次使用TableGet接口带来的问题
以前OPEN-3000rtdb_server的一段程序,由连续两次使用TableGet接口带来的问题
大家注意一下程序中是否有连续两次调用TableGet接口的地方,有没有考虑到两次TableGet返回值不一样的情况?
TableGet接口返回当前表记录个数,因此两次调用可能存在返回值不同的情况,如下面的程序就会出现问题:
…
Record_num=tableop.TableGet(buff);
Vectorv1(record_num);
…
Ret_code=tableop.TableGet(buff2);
Char*buf_ptr=buff2.GetBufPtr();
Intbuf_size=buff2.GetLength();
…
For(intI=0;I{
If(v1[i]…)
{
Char*tmp_buf_ptr=buf_ptr+i*record_size;
…
}
…
}
For循环中ret_code如果比v1.size(),即record_num大,就会出现vi[i]访问越界,同样如果将循环结束条件改为I其实这个问题应该说是两次访问表个数不一致导致的,上面说的连续两次使用TableGet是一种情况,还可能出现其他情况,比如:
Tableop.GetTablePara(field_num,record_num,record_size);
…
Ret_code=tableop.TableGet(buff);
…
这段程序中通过调用GetTablePara得到的记录个数与TableGet的返回值也有可能不同。
这种两次调用实时库接口得到的记录个数不一致的情况是无法避免的,只能大家在使用时尽量小心,尽量不要用两次得到的记录个数,更不能“默认”两次得到的记录个数是相同的,如果确实有这样的需求,尽量判断一下两次得到的记录个数是否一致再做操作。
二重指针的使用
动态库应该提供释放内存的接口,用来释放在动态库分配的内存。
例如实时库中的CCommon:
:
Free(void*pointer)
voidCCommon:
:
Free(void*pointer)
{
free(pointer);
}
举例:
CTableOptable_op;
ret_code=table_op.Open(app_no,table_no,context_no);
if(ret_code!
=DB_OK)
{
TRACE("querybynetwork,erroropenapplication%dtable%d\n",app_no,table_no);
returnret_code;
}
char*buf_ptr=NULL;
intbuf_size=0;
std:
:
vectorvec_offset;
ret_code=table_op.SqlGet(str_buf,&buf_ptr,buf_size,vec_offset);//returnrecordnumber
if(ret_code<=0)
{
TRACE("querydatafromRTDBMSbysqlselect,error\n");
returnret_code;
}
CCommon:
:
Free(buf_ptr);
避免把程序中的指针存放在共享内存里面
在D-5000的开发过程中,科东的同志曾经犯过这样一个错误,譬如如下一段代码:
structMEM_SHARE_STRUCT
{
......
char*inner_ptr;
......
};
intmain()
{
......
structMEM_SHARE_STRUCT*shm_ptr=shmget(......);
shm_ptr->inner_ptr=newchar[size];
......
delete[]shm_ptr->inner_ptr;
shmdt(shm_ptr);
}
当时的现象是如果只是启动一个使用这块共享内存的程序没有什么问题,但是从启动第二个开始,相关程序就莫名奇妙开始CORE。
【思考与启示】:
对于内存错误,表现各异,往往coredump的语句并不是真实出错的地方,利用动态检测工具更容易查出这样的错误。
利用工具,要知道其原理,才能对其效能做出正确的评价,才能在正确的场合使用工具。
v系统资源使用不当
v文件句柄的使用
对于操作系统来讲,文件句柄是异常重要的资源,很多设备都通过文件句柄访问,例如文件、目录、socket、管道等。
文件描述符表的大小对进程来说是有限的资源,用完需要归还。
例如:
....
while
(1)
{
intfd=open(pathname,mode);
...
}
这个例子有2个显著错误:
没有判断fd的值;
没有关闭文件描述符;
修正为:
while
(1)
{
intfd=open(pathname,mode);
if(fd<0)
{
error();
break;
}
...
close(fd);
}
vIPC的使用;
同文件句柄一样,共享内存,消息队列,信号灯也是有限系统资源,使用时要注意及时回收,同时可以检索系统手册,修改默认值。
v线程的使用
多线程是现在编程的趋势,随着SMP、多核处理器的普及,更多的系统将架构在多线程的基础上。
了解和熟悉多线程编程,对开发高性能、高可用性系统至关重要。
尽量使用Pthread线程库,不要使用与特定操作系统邦定的线程库。
这样做保证了程序的可移植性。
线程泄露
线程泄露是一个更为隐晦的问题。
没有设置为detach的线程在执行完后会保留资源,造成资源的泄露。
对于Linuxlibc2.5来讲,每个线程的线程栈为10M,如果不及时回收资源,会造成内存的泄漏。
例如:
while
(1)
{
pthread_ttid=0;
intret_code=pthread_create(&tid,hook,NULL,NULL);
if(0==ret_code)
{
ret_code=pthread_detach(tid);//需要分离线程,以及时回收资源
}
}
曾经查出过rtdb_server线程的泄漏,内存高达4G,360余个线程
v接口和头文件的使用
v接口不可以随便改动
接口是模块间的契约,是一个模块向外部说明其内部服务的界面,修改接口可能导致函数无法连接或者运行的错误;
v接口中的结构体和类不可以改动,如有改动,需要重新编译
表结构的改动也可能导致这种问题,譬如曾经在某一个现场,转发表的转发序号域从short改成了int,但是对应的程序还是把转发序号当成short来处理,导致整个转发运行不对。
很多隐晦的bug都是由于接口中的结构体增加了成员引起的,gdb很容易解决这样的问题
v检查函数入参的合法性
在过去的一些系统中,部分程序不考虑入参的合法性,往往给系统的稳定性带来潜在的危害,譬如如下一段代码就按照这个要求进行了编码:
intCTableOp:
:
TableGetByKey(constchar*key_ptr,char*buf_ptr,constintbuf_size)
{
intret_code=DB_OK;
if(!
key_ptr||!
buf_ptr||(buf_size<=0)||!
m_OdbTablePtr)
{
ret_code=DB_ERROR;
TRACE("parametererror!
\n");
returnDBE_PARA;
}
…….
}
v检查函数调用的返回值
譬如以前一个很早期OPEN-2000版本的通用计算程序calserver,曾经出现过如下一段代码:
……
TableOpRecClass;//OPEN-2000的TableOp类似于3000和5000的CTableOp
…….
RetCode=RecClass.TableGet((char*)&(KwhBufPtr[loop].ym_id.yxyc_id),(char*)&YmRec,(int)sizeof(ym_info_scada));//没有判断返回值。
if(YmRec.status==STATE_YC_LOCK)
return0;
Val=YmRec.val;
Factor_val=YmRec.factor;
RetCode=GetHardKwhProc(&KwhBufPtr[loop],Val,Factor_val,buffer_temp);
……
由于没有对标红语句的返回值做判断,导致部分计算结果错误。
正确的代码如下:
……
TableOpRecClass;
intRetCode;
…….
RetCode=RecClass.TableGet((char*)&(KwhBufPtr[loop].ym_id.yxyc_id),(char*)&YmRec,(int)sizeof(ym_info_scada));
if(RetCode<0)
{
printf("Error:
%dTableOpenerror!
\n",YM_INFO_NO);
return-1;
}
if(YmRec.status==STATE_YC_LOCK)
return0;
Val=YmRec.val;
Factor_val=YmRec.factor;
RetCode=GetHardKwhProc(&KwhBufPtr[loop],Val,Factor_val,buffer_temp);
……
v函数有多个return的情况要特别留意
这种情况最容易出现资源泄漏的情况,尤其是内存泄漏。
在以前的旧系统中,最初的原始代码在return前往往能够正确处理资源释放的情况,但是经过一段时间的维护,函数中增加了新的return语句,往往在新的return的地方会遗漏一些资源释放的工作。
推荐通过智能指针解决这样的问题。
譬如CBuffer这样的类,这样的类在析构的时候,自动释放资源,而不用管函数里面有多少出口。
【思考与启示】:
接口是程序模块协同工作的基础,修改接口就等于修改契约,是接口提供者和接口使用者双方的事情,不能一方单独修改而不通知另一方,轻则程序无法编译或者连接,严重时产生隐晦的错误,难以排查,这种错误我们也是经常遇到,每次都要耗费很长的时间去分析。
v多线程下常见的错误
v对于全局变量、静态变量等可能引起竞争的资源,要加锁
说明:
若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。
可使用mutex,信号量等方式处理。
v错误的同步
在多线程的环境中,只有明确语义的锁是可以信赖的,不要做出任何的假设,例如:
intlock=0;
thread1:
thread2:
if(lock)if(lock)
{{
lock=1;lock=1;
....
....
lock=0;
}}
这是一个很显著的错误;一定要用pthread_mutex_t等有语义保证的机制进行同步;
++,--等语句也不是原子的,不能用其作为同步的基础;
v错误的顺序假设
在多线程环境下,在没有使用同步语义的情况下,也不能假设线程的执行顺序。
例如:
intret=pthread_create(&tid_1,hook,NULL,NULL);
ret=phtread_create(&tid_2,hook,NULL,NULL);
我们只是建立了两个线程,至于哪个线程会先被调度执行,何时被调度执行操作系统并不能严格保证;
vCORBA类对象的使用
说明:
CORBA类对象的初始化要放在主线程中,因为ORB的初始化必须要求在主线程中进行。
【思考与启示】:
多线程的程序是难以调试和跟踪的,做好设计和良好的编码是写好多线程程序的基础。
前期没有做好设计和分析,后期去调多线程的程序,是非常痛苦的事情。
v变量使用
v变量的命名应体现变量的作用
举例:
intapp_no;
inttab_no;
v采用统一的变量命