收藏 分销(赏)

编程注意事项、常见错误与修正方法.doc

上传人:仙人****88 文档编号:11326863 上传时间:2025-07-17 格式:DOC 页数:20 大小:168KB 下载积分:10 金币
下载 相关 举报
编程注意事项、常见错误与修正方法.doc_第1页
第1页 / 共20页
编程注意事项、常见错误与修正方法.doc_第2页
第2页 / 共20页


点击查看更多>>
资源描述
编程注意事项、常见错误与修正方法 南瑞科技股份有限公司电网分公司 各开发部和代码质量组 日期 版本 说明 作者 <2009-12-7> <1.0> 第一稿 彭晖/王谨/侯勇 v 前言 4 v 内存、指针、数组的操作 5 v 内存操作 5 v 指针使用的注意事项 6 指针没有初始化 6 指针的分配和释放 7 CORBA和M语言内存分配中的注意思想 7 特别注意malloc(0) 8 释放内存后将指针置为NULL 8 避免使用释放掉的内存 8 避免连续释放同一块内存 8 避免赋值操作符两边都是动态分配过内存的指针 8 避免“隐藏的”数组越界和内存越界 9 二重指针的使用 10 避免把程序中的指针存放在共享内存里面 10 v 系统资源使用不当 11 v 文件句柄的使用 11 v IPC的使用; 12 v 线程的使用 12 v 接口和头文件的使用 13 v 接口不可以随便改动 13 v 接口中的结构体和类不可以改动,如有改动,需要重新编译 13 v 检查函数入参的合法性 13 v 检查函数调用的返回值 13 v 函数有多个return的情况要特别留意 14 v 多线程下常见的错误 15 v 对于全局变量、静态变量等可能引起竞争的资源,要加锁 15 v 错误的同步 15 v 错误的顺序假设 15 v CORBA类对象的使用 16 v 变量使用 16 v 变量的命名应体现变量的作用 16 v 采用统一的变量命名风格 16 v 避免全局变量、成员变量与局部变量、函数声明参数等同名 16 v 变量在使用前必须初始化 17 v 宏定义与常量避免使用比较“平常”的名字 18 v 头文件与CPP文件 18 v 文件命名应能体现文件所实现的功能 18 v 头文件使用#ifndef 18 v 避免同一头文件被多次引用 18 v 尽量避免在头文件中做函数实现 18 v 文件编码与排版 19 v 文件以空白行结束 19 v 尽量使用unix格式 19 v 使用统一的缩进风格 19 v 前言 为了能够帮助开发部新员工掌握正确的编程方法,同时也是为了方便各个开发部内部的交流。代码质量组整理了OPEN-3000和D-5000开发工作的一些注意思想与常见的一些错误及其修正方法,希望能够通过正反面的素材来让大家辨别好与不好的编程方法,并尽可能在自己的开发工作中予以留意,进而提升开发的质量,提高开发工作的效率。 v 内存、指针、数组的操作 v 内存操作 内存操作失误是编程过程中最为常见的错误之一,内存错误表现各异,为Debug带了很多困难;常见的错误有内存越界访问,内存分配错误,指针没有初始化等,引起的现象有进程的Core Dump,数据被改写等。下面对常见的内存错误举例并分析。 堆上内存、数组越界 进程在运行过程中使用的内存一般从堆上动态分配或者从函数栈上静态分配。所谓动态是指调用libc的库函数malloc 或者C++的操作符 new从进程堆上分配内存,静态分配是指编译器在编译过程中就由编译器计算好大小,在函数栈上分配; ... char * ptr = (char*) malloc( 10); strcpy( ptr , "abcdefghigklmnopqrst"); // Error; ... ... 这是一个很明显的内存越界错误,建议使用安全的字符串操作函数族: int const STRING_LEN = 10; char * ptr = (char*) malloc(STRING_LEN); strncpy( ptr , "abcdefghigklmnopqrst" , STRING_LEN); valgrind检测过这样错误; 程序中不要出现字面常量,这些字面常量利用符号常量或者宏替代,安全,且容易维护; 栈上内存、数组越界 类似于进程堆上的数组越界,函数栈上的数组也存在着同样的问题: int const STRING_LEN = 10; void TestStackArray(int arg1 , int arg2) { int field_1 = 0; int field_2 = 0; char array[10]; // ---> char array[STRING_LEN]; strcpy(array , "abcdefghigklm"); // ---> strncpy(array , "abc ..." , STRING_LEN); } parasoft添加了规则,做出警告 函数栈上的内存益处可能会覆盖field_2, field_1的值,更严重的情况的下,会破坏函数的调用链,造成难以察觉的错误:可能返回时core、可能出错在别的函数中; valgrind等动态检查工具截获malloc,free等libc库函数,从而达到检测动态内存操作的错误,因此,利用valgrind不能检查出函数栈上的内存操作错误。 字符串内存分配上越界 字符串内存分配上很容易碰到,这种错误非常隐蔽,它往往会影响栈上其他变量的数值,譬如如下一段代码: char str1[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 = new char[ MAX_LEN]; ... delete p; ... 这个例子中有两个显著的错误: 分配内存不一定能成功,可能会失败,要做出判断; delete 和 new不匹配; 修正为: char * p = NULL; try { p = new char[MAX_LEN]; } catch(Exception &e) { return ; } ... delete [] p; CORBA和M语言内存分配中的注意思想 另外需要注意CORBA的MARSHAL与M语言的M_CODE内部使用的分配方式是new[]。举例如下: char* p = NULL; int psize = 0; M_CODE(rsp_read, p, psize); *responseBuffer = (char*)malloc(psize+sizeof(int)+sizeof(unsigned char)); memcpy(*responseBuffer, &g_endian, sizeof(unsigned char)); memcpy(*responseBuffer+sizeof(unsigned char), &ret_code, sizeof(int))memcpy(*responseBuffer + sizeof(int) + sizeof(unsigned char), p, psize); delete[] p;//不能使用free(p); parasoft检测出过这样的错误 特别注意malloc(0) Tru 64, AIX, HP-UX等操作系统上的编译器malloc(0)为NULL,在Solaris 10系统Sun Studio 10编译器及Linux g++上malloc(0)不为NULL。 释放内存后将指针置为NULL 举例: if(p) { delete p; p = NULL; } 可以避免再次使用被释放的内存。 避免使用释放掉的内存 本质上说,系统会在堆上维护一个动态内存链表,如果被释放,就意味着该块内存可以继续被分配给其他部分,如果内存被释放后再访问,就可能覆盖其他部分的信息,这是一种严重的错误。 避免连续释放同一块内存 举例: char* p = (char*)malloc(10); char* q = p; //… free(p); free(q);//二次释放 避免赋值操作符两边都是动态分配过内存的指针 举例 int main() { char*x = (char*)malloc(20); char *y = (char*)malloc(20); x=y; free(x); free(y); return 0; } x动态分配的内存泄露。 避免“隐藏的”数组越界和内存越界 案例:连续两次使用TableGet接口带来的问题 以前OPEN-3000 rtdb_server的一段程序,由连续两次使用TableGet接口带来的问题 大家注意一下程序中是否有连续两次调用TableGet接口的地方,有没有考虑到两次TableGet返回值不一样的情况? TableGet接口返回当前表记录个数,因此两次调用可能存在返回值不同的情况,如下面的程序就会出现问题: … Record_num = tableop.TableGet(buff); Vector<T> v1(record_num); … Ret_code = tableop.TableGet(buff2); Char* buf_ptr = buff2.GetBufPtr(); Int buf_size = buff2.GetLength(); … For(int I = 0; I < ret_code; ++i) { If(v1[i]…) { Char* tmp_buf_ptr = buf_ptr + i*record_size; … } … } For循环中ret_code如果比v1.size(),即record_num大,就会出现vi[i]访问越界,同样如果将循环结束条件改为I < record_num, 会出现buf_ptr访问越界,这种情况会给程序带来未知的后果。 其实这个问题应该说是两次访问表个数不一致导致的,上面说的连续两次使用TableGet是一种情况,还可能出现其他情况,比如: Tableop.GetTablePara(field_num, record_num, record_size); … Ret_code = tableop.TableGet(buff); … 这段程序中通过调用GetTablePara得到的记录个数与TableGet的返回值也有可能不同。 这种两次调用实时库接口得到的记录个数不一致的情况是无法避免的,只能大家在使用时尽量小心,尽量不要用两次得到的记录个数,更不能“默认”两次得到的记录个数是相同的,如果确实有这样的需求,尽量判断一下两次得到的记录个数是否一致再做操作。 二重指针的使用 动态库应该提供释放内存的接口,用来释放在动态库分配的内存。 例如实时库中的CCommon::Free(void* pointer) void CCommon::Free(void* pointer) { free(pointer); } 举例: CTableOp table_op; ret_code = table_op.Open(app_no, table_no,context_no); if(ret_code != DB_OK) { TRACE("query by network, error open application %d table %d\n", app_no,table_no); return ret_code; } char* buf_ptr = NULL; int buf_size = 0; std::vector<MEMBER_DEFINITION> vec_offset; ret_code = table_op.SqlGet(str_buf, &buf_ptr, buf_size, vec_offset); //return record number if( ret_code <= 0 ) { TRACE("query data from RTDBMS by sql select, error\n"); return ret_code; } CCommon::Free(buf_ptr); 避免把程序中的指针存放在共享内存里面 在D-5000的开发过程中,科东的同志曾经犯过这样一个错误,譬如如下一段代码: struct MEM_SHARE_STRUCT { ...... char *inner_ptr; ...... }; int main() { ...... struct MEM_SHARE_STRUCT *shm_ptr = shmget(......); shm_ptr->inner_ptr = new char[size]; ...... delete []shm_ptr->inner_ptr; shmdt(shm_ptr); } 当时的现象是如果只是启动一个使用这块共享内存的程序没有什么问题,但是从启动第二个开始,相关程序就莫名奇妙开始CORE。 【思考与启示】: 对于内存错误,表现各异,往往coredump的语句并不是真实出错的地方,利用动态检测工具更容易查出这样的错误。 利用工具,要知道其原理,才能对其效能做出正确的评价,才能在正确的场合使用工具。 v 系统资源使用不当 v 文件句柄的使用 对于操作系统来讲,文件句柄是异常重要的资源,很多设备都通过文件句柄访问,例如文件、目录、socket、管道等。文件描述符表的大小对进程来说是有限的资源,用完需要归还。 例如: . ... while(1) { int fd = open(pathname , mode); ... } 这个例子有2个显著错误: 没有判断fd的值; 没有关闭文件描述符; 修正为: while(1) { int fd = open(pathname , mode); if(fd < 0) { error(); break; } ... close(fd); } v IPC的使用; 同文件句柄一样,共享内存,消息队列,信号灯也是有限系统资源,使用时要注意及时回收,同时可以检索系统手册,修改默认值。 v 线程的使用 多线程是现在编程的趋势,随着SMP、多核处理器的普及,更多的系统将架构在多线程的基础上。了解和熟悉多线程编程,对开发高性能、高可用性系统至关重要。 尽量使用Pthread 线程库,不要使用与特定操作系统邦定的线程库。这样做保证了程序的可移植性。 线程泄露 线程泄露是一个更为隐晦的问题。没有设置为detach的线程在执行完后会保留资源,造成资源的泄露。对于Linux libc2.5来讲,每个线程的线程栈为10M, 如果不及时回收资源,会造成内存的泄漏。 例如: while(1) { pthread_t tid = 0; int ret_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 检查函数入参的合法性 在过去的一些系统中,部分程序不考虑入参的合法性,往往给系统的稳定性带来潜在的危害,譬如如下一段代码就按照这个要求进行了编码: int CTableOp::TableGetByKey(const char* key_ptr, char* buf_ptr, const int buf_size) { int ret_code = DB_OK; if( !key_ptr || !buf_ptr || (buf_size <= 0) || !m_OdbTablePtr ) { ret_code = DB_ERROR; TRACE("parameter error!\n"); return DBE_PARA; } ……. } v 检查函数调用的返回值 譬如以前一个很早期OPEN-2000版本的通用计算程序calserver,曾经出现过如下一段代码: …… TableOp RecClass;//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) return 0; Val = YmRec.val; Factor_val = YmRec.factor; RetCode = GetHardKwhProc(&KwhBufPtr[loop], Val, Factor_val, buffer_temp); …… 由于没有对标红语句的返回值做判断,导致部分计算结果错误。正确的代码如下: …… TableOp RecClass; int RetCode; ……. RetCode = RecClass.TableGet((char* )&(KwhBufPtr[loop].ym_id.yxyc_id), (char* )&YmRec, (int)sizeof(ym_info_scada)); if(RetCode<0) { printf("Error: %d TableOpen error ! \n", YM_INFO_NO); return -1; } if(YmRec.status == STATE_YC_LOCK) return 0; 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 错误的同步 在多线程的环境中,只有明确语义的锁是可以信赖的,不要做出任何的假设,例如: int lock = 0; thread 1: thread 2: if (lock) if(lock) { { lock = 1; lock = 1; .... .... lock = 0; } } 这是一个很显著的错误;一定要用pthread_mutex_t等有语义保证的机制进行同步; ++ , -- 等语句也不是原子的,不能用其作为同步的基础; v 错误的顺序假设 在多线程环境下,在没有使用同步语义的情况下,也不能假设线程的执行顺序。 例如: int ret = pthread_create(&tid_1, hook , NULL , NULL); ret = phtread_create(&tid_2 , hook , NULL , NULL); 我们只是建立了两个线程,至于哪个线程会先被调度执行,何时被调度执行操作系统并不能严格保证; v CORBA类对象的使用 说明:CORBA类对象的初始化要放在主线程中,因为ORB的初始化必须要求在主线程中进行。 【思考与启示】: 多线程的程序是难以调试和跟踪的,做好设计和良好的编码是写好多线程程序的基础。前期没有做好设计和分析,后期去调多线程的程序,是非常痛苦的事情。 v 变量使用 v 变量的命名应体现变量的作用 举例: int app_no; int tab_no; v 采用统一的变量命名风格 举例: int app_no; int tab_no; 避免如下使用: int app_no; int nTableNo; v 避免全局变量、成员变量与局部变量、函数声明参数等同名 出现这种问题会大大增加调试的困难。 举例: class CWhere { public: CWhere(); ~CWhere(); bool GetWhere(std::vector<bool>& vec_where, const char* where_str, const std::vector<FIELD_STRU>& vec_sql); void Show(); int ExpressSql(char * where_str , std::vector<SQLIDX_STRU> & vec_sqlidx, std::vector<IDX4SEARCH_STRU> & idx_vec); private: FOR_WHERE::CWhereSyntaxAnalyser* sql_translate; }; bool CWhere::GetWhere(std::vector<bool>& vec_where, const char* where_str, const std::vector<FIELD_STRU>& vec_sql) { char err_msg[100]; bool bool_val; bool ret_val; // printf("GetWhere:where_str = %s\n", where_str); float real_const_tab[MAX_REALCONSTTABLE_LEN]; //实常量表 int real_const_tab_len; char *string_tab[MAX_STRINGTABLE_LEN]; //字符串表 int string_tab_len; CODE_TABLE_STRU code_tab[MAX_CODETABLE_LEN]; //中间代码表 int code_tab_len; CWhereSyntaxAnalyser sql_translate; //语法分析器 CInterpreter sql_itprt; //解释器 ret_val = sql_translate.Initialize(where_str,vec_sql); //...... return true; } v 变量在使用前必须初始化 尽量不要使用系统默认值,特别注意结构、类成员变量。譬如ALPHA机变量的默认值是0,但是SUN上面是一个随机数,这样一些程序在ALPHA上面运行没有问题,但是在SUN上面运行却有问题。 v 宏定义与常量避免使用比较“平常”的名字 D5000系统开发初期多次遇到我们定义的宏与科东定义的宏冲突的问题。 v 头文件与CPP文件 v 文件命名应能体现文件所实现的功能 v 头文件使用#ifndef 如果该头文件被多次引用时不会出问题。 举例: #ifndef __ODB_TABLEOP_H__ #define __ODB_TABLEOP_H__ …… #endif v 避免同一头文件被多次引用 一些头文件可能已经包含了另外的头文件,被包含的头文件不需要再include。 举例: odb_tableop.h包含了odb_define.h,应用程序如果包含了odb_tableop.h就不要再包含odb_define.h。 v 尽量避免在头文件中做函数实现 如果头文件有函数实现,又没有使用#ifndef,又被多次引用,会导致编译报错。 举例: class CTest { CTest(){}; ~CTest(){}; int m_Num; int GetNum() { return m_Num; } }; v 文件编码与排版 v 文件以空白行结束 否则会有编译警告。 v 尽量使用unix格式 目的是避免文件在unix系统上出现^M。 Unix格式文件在用UltraEdit打开时报是否转换为DOS格式选“否”,在使用FTP工具上传时选ASC格式。 v 使用统一的缩进风格 使用Tab键,缩进4格,避免使用空格键。这样做可以增加程序的易读性、容易适应各种编辑器而且美观。使用Tab键而不是使用空格键还可以缩小文件大小。
展开阅读全文

开通  VIP会员、SVIP会员  优惠大
下载10份以上建议开通VIP会员
下载20份以上建议开通SVIP会员


开通VIP      成为共赢上传

当前位置:首页 > 包罗万象 > 大杂烩

移动网页_全站_页脚广告1

关于我们      便捷服务       自信AI       AI导航        抽奖活动

©2010-2026 宁波自信网络信息技术有限公司  版权所有

客服电话:0574-28810668  投诉电话:18658249818

gongan.png浙公网安备33021202000488号   

icp.png浙ICP备2021020529号-1  |  浙B2-20240490  

关注我们 :微信公众号    抖音    微博    LOFTER 

客服