本文从代码的可维护性(可读、可理解性、可修改性)、代码逻辑与效率、函数(模块)接口、可测试性四个方面阐述了软件编程规范,规范分成规则和建议两种,其中规则部分为强制执行项目,而建议部分则不作强制,可根据习惯取舍。 编码规范 1.排版风格 <规则 1> 程序块采用缩进风格编写,缩进为4个空格位。排版不混合使用空格和TAB键。 <规则2> 在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格。 采用这种松散方式编写代码的目的是使代码更加清晰。例如: (1) 逗号、分号只在后面加空格 printf("%d %d %d" , a, b, c); (2)比较操作符, 赋值操作符"="、 "+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位域操作符"<<"、"^"等双目操作符的前后加空格 if(lCurrentTime >= MAX_TIME_VALUE) a = b + c; a *= 2; a = b ^ 2; (3)"!"、"~"、"++"、"--"、"&"(地址运算符)等单目操作符前后不加空格 *pApple = 'a'; // 内容操作"*"与内容之间 flag = !bIsEmpty; // 非操作"!"与内容之间 p = &cMem; // 地址操作"&" 与内容之间 i++; // "++","--"与内容之间 (4)"->"、"."前后不加空格 p->id = pId; // "->"指针前后不加空格 由于留空格所产生的清晰性是相对的,所以,在已经非常清晰的语句中没有必要再留空格,如最内层的括号内侧(即左括号后面和右括号前面)不要加空格,因为在C/C++语言中括号已经是最清晰的标志了。 另外,在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。 最后,即使留空格,也不要连续留两个以上空格(为了保证缩进和排比留空除外)。 <规则3> 函数体的开始,类的定义,结构的定义,if、for、do、while、switch及case语句中的程序都应采用缩进方式," { " 独占一行并且位于同一列,同时与引用它们的语句左对齐, 示例: for ( ... ) { ... // 程序代码 } if ( ... ) { ... // 程序代码 } void DoExam( void ) { ... // 程序代码 } <规则4> 功能相对独立的程序块之间或for、if、do、while、switch等语句前后应加一空行。 示例: 例一: if ( ! ValidNi( ni ) ) { ... // 程序代码 } nRepssnInd = SsnData[ index ].nRepssnIndex ; nRepssnNi = SsnData[ index ].ni ; 例二: char *pContext; int nIndex; long lCounter; pContext = new (CString); if(pContext == NULL) { return FALSE; } <规则5> if、while、for、case、default、do等语句自占一行。 示例: if( pUserCR == NULL ) { return; } <规则6> 若语句较长(多于80字符),可分成多行写,划分出的新行要进行适应的缩进,使排版整齐,语句可读。 memset(pData->pData + pData->nCount, 0, (m_nMax - pData->nCount) * sizeof(LPVOID)); CNoTrackObject* pValue = (CNoTrackObject*)_afxThreadData->GetThreadValue(m_nSlot); for ( i = 0, j = 0 ; ( i < BufferKeyword[ WordIndex ].nWordLength ) && ( j < NewKeyword.nWordLength ) ; i ++ , j ++ ) { ... // 程序代码 } <规则7> 一行最多写一条语句。 示例: rect.width = 0 ; <规则8> 对结构成员赋值,等号对齐。 示例: rect.top = 0; rect.left = 0; rect.right = 300; rect.bottom = 200; <规则9> #define的各个字段对齐 示例: #define MAX_TASK_NUMBER 100#define LEFT_X 10 #define BOTTOM_Y 400 <规则10> 不同类型的操作符混合使用时,使用括号给出优先级。 如本来是正确的代码: if( year % 4 == 0 || year % 100 != 0 && year % 400 == 0 ) 如果加上括号,则更清晰。 if((year % 4) == 0 || ((year % 100) != 0 && (year % 400) == 0)) 2. 可理解性 1.1 注释 注释的原则是有助于对程序的阅读理解,注释不宜太多也不能太少,太少不利于代码理解,太多则会对阅读产生干扰,因此只在必要的地方才加注释,而且注释要准确、易懂、尽可能简洁。注释量一般控制在30%到50%之间。 <规则1> 程序在必要的地方必须有注释,注释要准确、易懂、简洁。 例如如下注释意义不大。 /* 如果bReceiveFlag 为 TRUE */ if ( bReceiveFlag == TRUE) 而如下的注释则给出了额外有用的信息。 /* 如果mtp 从连接处获得一个消息*/ if ( bReceiveFlag == TURE) <规则2> 注释应与其描述的代码相近,对代码的注释应放在其上方或右方(对单条语句的注释)相邻位置,不可放在下面,如放于上方则需与其上面的代码用空行隔开。 示例: 例子1 /* 获得系统指针和网络指针的副本 */ nRepssnInd = SsnData[ index ].nRepssnIndex ; nRepssnNi = SsnData[ index ].ni ; 例子2 nRepssnInd = SsnData[ index ].nRepssnIndex ; nRepssnNi = SsnData[ index ].ni ; /*获得系统指针和网络指针的副本 */ 应如下书写 /*获得系统指针和网络指针的副本 */ nRepssnInd = SsnData[ index ].nRepssnIndex ; nRepssnNi = SsnData[ index ].ni ; <规则3> 对于所有的常量,变量,数据结构声明(包括数组、结构、类、枚举等),如果其命名不是充分自注释的,在声明时都必须加以注释,说明其含义。 示例: /* 活动任务的数量 */ #define MAX_ACT_TASK_NUMBER 1000 #define MAX_ACT_TASK_NUMBER 1000 /*活动任务的数量 */ /* 带原始用户信息的SCCP接口 */ enum SCCP_USER_PRIMITIVE { N_UNITDATA_IND , /* 向SCCP用户报告单元数据已经到达 */ N_UNITDATA_REQ , /* SCCP用户的单元数据发送请求 */ } ; <规则4> 头文件、源文件的头部,应进行注释。注释必须列出:文件名、作者、目的、功能、修改日志等。 例如: /********************************************* 文件名: 编写者: 编写日期: 简要描述: 修改记录: ********************************************/ 说明:简要描述一项描述本文件的目的和功能等。修改记录是修改日志列表,每条修改记录应包括修改日期、修改者及修改内容简述。 <规则5> 函数头部应进行注释,列出:函数的目的、功能、输入参数、输出参数、修改日志等。 形式如下: /************************************************* 函数名称: 简要描述: // 函数目的、功能等的描述 输入: // 输入参数说明,包括每个参数的作用、取值说明及参数间关系, 输出: // 输出参数的说明, 返回值的说明 修改日志: *************************************************/ 对一些复杂的函数,在注释中最好提供典型用法。 <规则6> 仔细定义并明确公共变量的含义、作用、取值范围及使用方法。 在对变量声明的同时,应对其含义、作用、取值范围及使用方法进行注释说明,同时若有必要还应说明与其它变量的关系。明确公共变量与操作此公共变量的函数或过程的关系,如访问、修改及创建等。 示例: /* SCCP转换时错误代码 */ /* 全局错误代码,含义如下 */ // 变量作用、含义 /* 0 - 成功 1 - GT 表错误 2 -GT 错误 其它值- 未使用 */ // 变量取值范围 <规则7> 对指针进行充分的注释说明,对其作用、含义、使用范围、注意事项等说明清楚。 在对指针变量、特别是比较复杂的指针变量声明时,应对其含义、作用及使用范围进行注释说明,如有必要,还应说明其使用方法、注意事项等。 示例: /* 学生记录列表的头指针 */ /* 当在此模块中创建该列表时,该头指针必须初始化, */ /* 这样可以利用GetListHead()获得这一列表。*/ //指针作用、含义 /* 该指针只在本模块使用,其它模块通过调用GetListHead()获取*/ /* 当使用时必须保证它非空 */ //使用范围、方法 STUDENT_RECORD *pStudentRecHead; <规则8> 对重要代码段的功能、意图进行注释,提供有用的、额外的信息。并在该代码段的结束处加一行注释表示该段代码结束。 示例: /* 可选通道的组合 */ if ((gsmBCIe31->radioChReq >= DUAL_HR_RCR) && (gsmBCIe32->radioChReq >= DUAL_HR_RCR)) { gsmBCIe31->radioChReq = FR_RCR; gsmBCIe32->radioChReq = FR_RCR; } else if ((gsmBCIe31->radioChReq >= DUAL_HR_RCR) && (gsmBCIe32->radioChReq == FR_RCR) ) { gsmBCIe31->radioChReq = FR_RCR; } else if ((gsmBCIe31->radioChReq == FR_RCR) && (gsmBCIe32->radioChReq >= DUAL_HR_RCR)) { gsmBCIe32->radioChReq = FR_RCR; } /* 本块结束 ( 可选通道组合 ) */ <规则9> 在switch语句中,对没有break语句的case分支加上注释说明。 示例: switch(SubT30State) { case TA0: AT(CHANNEL, "AT+FCLASS=1/r", 0); if(T30Status != 0) { return(1); } InitFax(); /* 准备发送传真 */ AT(CHANNEL, "ATD/r",-1); /*发送CNG ,接收 CED 和 HDLC 标志*/ T1_Flg = 1; iResCode = 0; /* 没有 break; */ case TA1: iResCode = GetModemMsg(CHANNEL); break; default: break; } <规则 10> 维护代码时,要更新相应的注释,删除不再有用的注释。 保持代码、注释的一致性,避免产生误解。 1.2 命名
本文列出Visual C++的标识符命名规范。 <规则 1> 标识符缩写 形成缩写的几种技术: 1) 去掉所有的不在词头的元音字母。如screen写成scrn, primtive写成prmv。 2) 使用每个单词的头一个或几个字母。如Channel Activation写成ChanActiv,Release Indication写成RelInd。 3) 使用变量名中每个有典型意义的单词。如Count of Failure写成FailCnt。 4) 去掉无用的单词后缀 ing, ed等。如Paging Request写成PagReq。 5) 使用标准的或惯用的缩写形式(包括协议文件中出现的缩写形式)。如BSIC(Base Station Identification Code)、MAP(Mobile Application Part)。 关于缩写的准则: 1) 缩写应该保持一致性。如Channel不要有时缩写成Chan,有时缩写成Ch。Length有时缩写成Len,有时缩写成len。 2) 在源代码头部加入注解来说明协议相关的、非通用缩写。 3) 标识符的长度不超过32个字符。 <规则2> 变量命名约定 参照匈牙利记法,即 [作用范围域前缀] + [前缀] + 基本类型 + 变量名 其中: 前缀是可选项,以小写字母表示; 基本类型是必选项,以小写字母表示; 变量名是必选项,可多个单词(或缩写)合在一起,每个单词首字母大写。 前缀列表如下: 前缀 意义 举例 g_ Global 全局变量 g_MyVar m_ 类成员变量 或 模块级变量 m_ListBox, m_Size s_ static 静态变量 s_Count h Handle 句柄 hWnd p Pointer 指针 pTheWord lp Long Point 长指针 lpCmd a Array 数组 aErr 基本类型列表如下: 基本类型 意义 举例 b Boolean 布尔 bIsOK by Byte 字节 byNum c Char 字符 cMyChar i或n Intger 整数 nTestNumber u Unsigned integer 无符号整数 uCount ul Unsigned Long 无符号长整数 ulTime w Word 字 wPara dw Double Word 双字 dwPara l Long 长型 lPara f Float 浮点数 fTotal s String 字符串 sTemp sz NULL结束的字符串 szTrees fn Funtion 函数 fnAdd enm 枚举型 enmDays x,y x,y坐标 <规则3> 宏和常量的命名 宏和常量的命名规则:单词的字母全部大写,各单词之间用下划线隔开。命名举例: #define MAX_SLOT_NUM 8 #define EI_ENCR_INFO 0x07 const int MAX_ARRAY <规则4> 结构和结构成员的命名 结构名各单词的字母均为大写,单词间用下划线连接。可用或不用typedef,但是要保持一致,不能有的结构用typedef,有的又不用。如: typedef struct LOCAL_SPC_TABLE_STRU { char cValid; int nSpcCode[MAX_NET_NUM]; } LOCAL_SPC_TABLE ; 结构成员的命名同变量的命名规则。 <规则5> 枚举和枚举成员的命名 枚举名各单词的字母均为大写,单词间用下划线隔开。 枚举成员的命名规则:单词的字母全部大写,各单词之间用下划线隔开;要求各成员的第一个单词相同。命名举例: typdef enum { LAPD_ MDL_ASSIGN_REQ, LAPD_MDL_ASSIGN_IND, LAPD_DL_DATA_REQ, LAPD_DL_DATA_IND, LAPD_DL_UNIT_DATA_REQ, LAPD_DL_UNIT_DATA_IND, } LAPD_PRMV_TYPE; <规则6> 类的命名 前缀 意义 举例 C 类 CMyClass CO COM类 COMMyObjectClass CF COM class factory CFMyClassFactory I COM interface class IMyInterface CImpl COM implementation class CImplMyInterface <规则7> 函数的命名 单词首字母为大写,其余均为小写,单词之间不用下划线。函数名应以一个动词开头。 void PerformSelfTest(void) ; void ProcChanAct(MSG_CHAN_ACTIV *pMsg, UC MsgLen); 1.3 可维护性 <规则1> 在逻辑表达式中使用明确的逻辑判断。 示例:如下逻辑表达式不规范。 1) if ( strlen(strName) ) 2) for ( index = MAX_SSN_NUMBER; index ; index -- ) 3) while ( p && *p ) // 假设p为字符指针 应改为如下: 1) if ( strlen(strName) != 0 ) 2) for ( index = MAX_SSN_NUMBER; index != 0 ; index -- ) 3) while ((p != NULL) && (*p != '/0' )) <规则2> 预编译条件不应分离一完整的语句。 不正确: if (( cond == GLRUN) #ifdef DEBUG || (cond == GLWAIT) #endif ) { } 正确: #ifdef DEBUG if( cond == GLRUN || cond == GLWAIT ) #else if( cond == GLRUN ) #endif { } <规则3> 在宏定义中合并预编译条件。 不正确: #ifdef EXPORT for ( i = 0; i < MAX_MSXRSM; i++ ) #else for ( i = 0; i < MAX_MSRSM; i++ ) #endif 正确: 头文件中: #ifdef EXPORT #define MAX_MS_RSM MAX_MSXRSM #else #define MAX_MS_RSM MAX_MSRSM #endif 源文件中: for( i = 0; i < MAX_MS_RSM; i++ ) <规则4> 使用宏定义表达式时,要使用完备的括号。 如下的宏定义表达式都存在一定的隐患。 #define REC_AREA(a, b) a * b #define REC_AREA(a, b) (a * b) #define REC_AREA(a, b) (a) * (b) 正确的定义为: #define REC_AREA(a, b) ((a) * (b)) <规则5> 宏所定义的多条表达式应放在大括号内。 示例:下面的语句只有宏中的第一条表达式被执行。为了说明问题,for语句的书写稍不符规范。 #define INIT_RECT_VALUE( a, b ) / a = 0 ; / b = 0 ; for ( index = 0 ; index < RECT_TOTAL_NUM ; index ++ ) INIT_RECT_VALUE( rect.a, rect.b ) ; 正确的用法应为: #define INIT_RECT_VALUE( a, b ) / { / a = 0 ; / b = 0 ; / } for ( index = 0 ; index < RECT_TOTAL_NUM ; index ++ ) { INIT_RECT_VALUE( rect[ index ].a, rect[ index ].b ) ; } <规则6> 宏定义不能隐藏重要的细节,避免有return,break等导致程序流程转向的语句。 如下例子是不规范的应用,其中隐藏了程序的执行流程。 #define FOR_ALL for(i = 0; i < SIZE; i++) /* 数组 c 置0 */ FOR_ALL { c[i] = 0; } #define CLOSE_FILE { / fclose(fp_local); / fclose(fp_urban); / return; / } <规则7> 使用宏时,不允许参数发生变化。 下面的例子隐藏了重要的细节,隐含了错误。 #define SQUARE ((x) * (x)) . . . w = SQUARE(++value); 这个引用将被展开称: w = ((++value) * (++value)); 其中value累加了两次,与设计思想不符。正确的用法是: w = SQUARE(x); x++; <规则8> 当if、while、for等语句的程序块为摽諗时,使用搟}敺 拧_ while ( *s++ == *t++ ) ; 以上代码不符合规范,正确的书写方式为: while( *s++ == *t++ ) { /* 无循环体 */ } 或 while( *s++ == *t++ ) { } <规则9> 结构中元素布局合理,一行只定义一个元素。 如下例子不符合规范, typedef struct { _UI left, top, right, bottom; } RECT; 应书写称: typedef struct { _UI left; /* 矩形左侧 x 坐标 */ _UI top; _UI right; _UI bottom; } RECT; <规则10> 枚举值从小到大顺序定义。 <规则11> 包含头文件时,使用撓喽月肪稊,不使用摼 月肪稊。 如下引用: #include "c:/switch/inc/def.inc" 应改为: #include "inc/def.inc" 或 #include "def.inc" <规则12> 不允许使用复杂的操作符组合等。 下面用法不好, iMaxVal = ( (a > b ? a : b) > c ? (a > b ? a : b) : c ); 应该为: iTemp = ( a > b ? a : b); iMaxVal = (iTemp > b ? iTemp : b); 不要把"++"、"--"操作符与其他如"+="、"-="等组合在一起形成复杂奇怪的表达式。如下的表达式那以理解。 *pStatPoi++ += 1; *++pStatPoi += 1; 应分别改为: *pStatPoi += 1; pStatPoi++; 和 ++pStatPoi; *pStatPoi += 1; <规则13> 函数和过程中关系较为紧密的代码尽可能相邻。 如初始化代码应放在一起,不应在中间插入实现其它功能的代码。以下代码不符合规范, for (uiUserNo = 0; uiUserNo < MAX_USER_NO; uiUserNo++) { ...; /* 初始化用户数据 */ } pSamplePointer = NULL; g_uiCurrentUser = 0; /* 设置当前用户索引号 */ 应必为: for (uiUserNo = 0; uiUserNo < MAX_USER_NO; uiUserNo++) { ...; /* 初始化用户数据 */ } g_uiCurrentUser = 0; /* 设置当前用户索引号 */ pSamplePointer = NULL; <规则14> 每个函数的源程序行数原则上应该少于200行。 对于消息分流处理函数,完成的功能统一,但由于消息的种类多,可能超过200行的限制,不属于违反规定。 <规则15> 语句嵌套层次不得超过5层。 嵌套层次太多,增加了代码的复杂度及测试的难度,容易出错,增加代码维护的难度。 <规则16> 用sizeof来确定结构、联合或变量占用的空间。 这样可提高程序的可读性、可维护性,同时也增加了程序的可移植性。 <规则17> 避免相同的代码段在多个地方出现。 当某段代码需在不同的地方重复使用时,应根据代码段的规模大小使用函数调用或宏调用的方式代替。这样,对该代码段的修改就可在一处完成,增强代码的可维护性。 <规则18> 使用强制类型转换。 示例: USER_RECORD *pUser; pUser = (USER_RECORD *) malloc (MAX_USER * sizeof(USER_RECORD)); <规则19> 避免使用 goto 语句。 <规则20> 避免产生摮绦蚪釘(program knots),在循环语句中,尽量避免break、goto的使用。 如下例子: for( i = 0; i < n; i++) { bEof = fscanf( pInputFile, "%d;", &x[i]); if( bEof == EOF ) { break; } nSum += x[i]; } 最好按以下方式书写,避免程序打摻釘: for( i = 0; i < n && bEof= EOF; i++) { bEof = fscanf( pInputFile, "%d;", &x[i]); if( bEof!= EOF ) { nSum += x[i]; } } <规则21> 功能相近的一组常量最好使用枚举来定义。 不推荐定义方式: /* 功能寄存器值 */ #define ERR_DATE 1 /* 日期错误 */ #define ERR_TIME 2 /* 时间错误 */ #define ERR_TASK_NO 3 /* 任务号错误 */ 推荐按如下方式书写: /*功能寄存器值 */ enum ERR_TYPE { ERR_DATE = 1, /*日期错误 */ ERR_TIME = 2, /*时间错误 */ ERR_TASK_NO = 3 /* 任务号错误 */ } <规则22> 每个函数完成单一的功能,不设计多用途面面俱到的函数。 多功能集于一身的函数,很可能使函数的理解、测试、维护等变得困难。 使函数功能明确化,增加程序可读性,亦可方便维护、测试。 <建议1> 循环、判断语句的程序块部分用花括号括起来,即使只有一条语句。 如: if( bCondition == TRUE ) bFlag = YES; 建议按以下方式书写: if( bCondition == TRUE ) { bFlag = YES; } 这样做的好处是便于代码的修改、增删。 <建议2> 一行只声明一个变量。 不推荐的书写方式: void DoSomething(void) { int Amicrtmrs, nRC; int nCode, nStatus; 推荐做法: void DoSomething(void) { int nAmicrtmrs; /* ICR 计时器 */ int nRC; /* 返回码 */ int nCode; /* 访问码 */ int nStatus; /* 处理机状态 */ <建议3> 使用专门的初始化函数对所有的公共变量进行初始化。 <建议4> 使用可移植的数据类型,尽量不要使用与具体硬件或软件环境关系密切的变量。 <建议5> 用明确的函数实现不明确的语句功能 示例:如下语句的功能不很明显。 value = ( a > b ) ? a : b ; 改为如下就很清晰了。 int max( int a, int b ) { return ( ( a > b ) ? a : b ) ; } value = max( a, b ) ; 或改为如下。 #define MAX( a, b ) ( ( ( a ) > ( b ) ) ? ( a ) : ( b ) ) value = MAX( a, b ) ;
1.4. 程序正确性、效率
<规则1> 严禁使用未经初始化的变量。
|