Q/XJ 许继集团有限公司企业标准 Q/XJ ××××—×××× C/C++语言代码编写规范
(征求意见稿)
××××-××-××发布 ××××-××-××实施 许继集团有限公司发布 Q/XJ ××××—××××
目 次
前 言 ............................................................................. II
1 范围 ................................................................................ 1 2 规范性引用文件 ...................................................................... 1 3 术语和定义 .......................................................................... 1 4 文件结构 ............................................................................ 1 4.1 概述 .............................................................................. 1 4.2 版权和版本的声明 .................................................................. 1 4.3 头文件的结构 ...................................................................... 2 4.4 定义文件的结构 .................................................................... 2 5 程序的版式 .......................................................................... 3 5.1 空行 .............................................................................. 3 5.2 代码行 ............................................................................ 3 5.3 代码行内的空格 .................................................................... 4 5.4 对齐 .............................................................................. 5 5.5 长行拆分 .......................................................................... 6 5.6 注释 .............................................................................. 7 6 命名规则 ........................................................................... 12 6.1 共性规则 ......................................................................... 12 6.2 一般命名规则 ..................................................................... 13 7 其它规则 ........................................................................... 14 7.1 布尔变量与零值比较 ............................................................... 15 7.2 整型变量与零值比较 ............................................................... 15 7.3 浮点变量与零值比较 ............................................................... 15 7.4 指针变量与零值比较 ............................................................... 15 附 录 A (资料性附录) 良好的C/C++编程风格; ........................................ 17 A.1 良好的注释技巧 ................................................................... 17 A.2 表达式和基本语句 ................................................................. 17 A.3 变量、结构与数据类型 ............................................................. 20 A.4 常量 ............................................................................. 25 A.5 函数 ............................................................................. 26 A.6 宏 ............................................................................... 30 A.7 程序效率 ......................................................................... 31 A.8 可测性 ........................................................................... 34 A.9 质量保证 ......................................................................... 38 A.10 代码编辑、编译、审查 ............................................................ 43
I
Q/XJ ××××—××××
前 言
本标准的附录A为资料性附录。
本标准由许继电气股份有限公司提出。 本标准由许昌继电器研究所归口。
本标准起草单位:许继保护及自动化事业部、科研处、北京许继电气有限公司、珠海许继电气有限公司、深圳市许继昌达电网控制设备有限公司。
本标准主要起草人:张新昌、包伟、杨智德、李江林、李旺、王若醒、刘彬、王伟东。
II
Q/XJ ××××—××××
C/C++语言编程规范
1 范围
本标准规定了软件编程中程序文件的结构和编程风格方面的一些规则。
本标准适用于C/C++编程。 2 规范性引用文件
下列文件中的条款通过本标准的引用而成为本标准的条款。凡是注日期的引用文件,其随后所有的修改单(不包括勘误的内容)或修订版均不适用于本标准,然而,鼓励根据本标准达成协议的各方研究是否可使用这些文件的最新版本。凡是不注日期的引用文件,其最新版本适用于本标准。
GB/T 5271.1-2000 信息技术 词汇 第1部分:基本术语 GB/T 5271.7-1986 数据处理词汇 07部分:计算机程序设计 GB/T 5271.15-1986 数据处理词汇 15部分:程序设计语言 3 术语和定义
GB/T 5271.1-2000、GB/T 5271.7-1986和GB/T 5271.15-1986确立的术语和定义适用于本标准。 4 文件结构
4.1 概述
每个C/C++程序应至少包括两类文件。一个文件用于保存程序的声明,称为头文件。另一个文件用于保存程序的实现,称为定义文件。
C/C++程序的头文件以“.h”为后缀,C程序的定义文件以“.c”为后缀,C++程序的定义文件通常以“.cpp”为后缀(也有一些系统以“.cc”或“.cxx”为后缀)。 4.2 版权和版本的声明
/* * Copyright (c) 2001,许继集团有限公司 * All rights reserved. * * 文件名称:filename.h * 摘 要:简要描述本文件的内容 * * 版本号:1.2 作者:作者(修改者) 完成日期:2003年7月20日 * * 版本号:1.1 作者:作者(修改者) 完成日期:2003年5月20日 * 版本号:1.0 作者:作者(修改者) 完成日期:2003年2月20日 */ 图1 版权和版本的声明 版权和版本的声明位于头文件和定义文件的开头(见图1),主要内容有: —— 版权信息; —— 文件名称,摘要;
1
Q/XJ ××××—××××
—— 当前版本号,作者/修改者,完成日期;
—— 版本历史信息。 4.3 头文件的结构 4.3.1 概述
头文件由三部分内容组成: —— 头文件的版权和版本声明(见图1); —— 预处理块; —— 函数和类结构声明等。
若头文件名称为 graphics.h,头文件的结构见图2。
// 版权和版本声明见图1 #ifndef GRAPHICS_H // 防止graphics.h被重复引用 #define GRAPHICS_H #include 4.3.2 为了防止头文件被重复引用,应当用ifndef/define/endif结构产生预处理块。 4.3.3 用 #include 4.3.5 头文件中宜只存放“声明” 而不存放“定义”。 4.3.6 不提倡使用全局变量,不宜在头文件中出现extern int value 这类声明。 4.4 定义文件的结构 定义文件有三部分内容: —— 定义文件的版权和版本声明(见图1); —— 对一些头文件的引用; —— 程序的实现体(包括数据和代码)。 若定义文件的名称为 graphics.cpp,定义文件的结构见图3。 2 Q/XJ ××××—×××× // 版权和版本声明见图1 #include “graphics.h” // 引用头文件 „ // 全局函数的实现体 void Function1(„) { „ } // 类成员函数的实现体 void Box::Draw(„) { „ } 图3 C/C++定义文件的结构 5 程序的版式 5.1 空行 5.1.1 在变量声明之后、每个类声明之后和每个函数定义结束之后应加空行(见图4(左))。 5.1.2 在一个函数体内,逻辑上密切相关的语句之间应不加空行,其它地方应加空行分隔(见图4(右)) 。 // 空行 void Function1(„) { „ } // 空行 void Function2(„) { „ } // 空行 void Function3(„) { „ } // 空行 while (condition) { statement1; // 空行 if (condition) { statement2; } else { statement3; } // 空行 statement4; } 图4 函数之间的空行(左),函数内部的空行(右) 5.2 代码行 5.2.1 一行代码宜只做一件事情,如只定义一个变量,或只写一条语句(见图5)。 5.2.2 if、for、while、do等语句应自占一行,执行语句不应紧跟其后。不论执行语句有多少都应加‘{}’(见图5)。 3 Q/XJ ××××—×××× int width; // 宽度 int height; // 高度 int depth; // 深度 x = a + b; y = c + d; z = e + f; if (width < height) { dosomething(); } for (initialization; condition; update) { dosomething(); } // 空行 other(); int width, height, depth; // 宽度高度深度 X = a + b; y = c + d; z = e + f; if (width < height) dosomething(); for (initialization; condition; update) dosomething(); other(); 图5 风格良好的代码行(左),与风格不良的代码行(右) 5.2.3 尽可能在定义变量的同时初始化该变量。 若变量的引用处与其定义处相隔比较远,变量的初始化易被忘记。若引用了未被初始化的变量,易导致程序错误。 示例 : int width = 10; int height = 10; int depth = 10; // 定义并初绐化width // 定义并初绐化height // 定义并初绐化depth 5.3 代码行内的空格 void Func1(int x, int y, int z); // 良好的风格 void Func1 (int x,int y,int z); // 不良的风格 if (year >= 2000) // 良好的风格 if(year>=2000) // 不良的风格 if ((a>=b) && (c<=d)) // 良好的风格 if(a>=b&&c<=d) // 不良的风格 for (i=0; i<10; i++) // 良好的风格 for(i=0;i<10;i++) // 不良的风格 for (i = 0; I < 10; i ++) // 过多的空格 x = a < b ? a : b; // 良好的风格 x=aFunction(); // 不应写成 b -> Function(); 图6 代码行内的空格 4 Q/XJ ××××—×××× 5.3.1 关键字之后应留空格。如if、for、while等关键字之后应留一个空格再跟左括号‘(’,以突 出关键字。 5.3.2 函数名之后应不留空格,紧跟左括号‘(’,以与关键字区别。 5.3.3 ‘(’后面应不留空格,‘)’、‘,’、‘;’前面应不留空格。 5.3.4 ‘,’之后应留空格,如Function(x, y, z)。若‘;’不是一行的结束符号,其后应留空格,如for (initialization; condition; update)。 5.3.5 赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应加空格。 5.3.6 一元操作符如“!”、“~”、“++”、“--”、“&”(地址运算符)等前后应不加空格。 5.3.7 “[]”、“.”、“->”等操作符前后应不加空格。 注: 对于表达式比较长的for语句和if语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0; i<10; i++)和 if ((a<=b) && (c<=d)) 5.4 对齐 void Function(int x) { „ // program code } if (condition) { „ // program code } else { „ // program code } void Function(int x){ „ // program code } if (condition){ „ // program code } else { „ // program code } for (initialization; condition; update) for (initialization; condition; update){ { „ // program code „ // program code } } While (condition) { „ // program code } 若出现嵌套的{},则使用缩进对齐,如: { „ { „ } „ } while (condition){ „ // program code } 图7 风格良好的对齐(左),与风格不良的对齐(右) 5.4.1 程序块应采用缩进风格编写,缩进的空格数为4个。 5 Q/XJ ××××—×××× 注: 对于由开发工具自动生成的代码可以有不一致。 5.4.2 对齐只使用空格键,不使用TAB键。 注: 以免用不同的编辑器阅读程序时,因TAB键所设置的空格数目不同而造成程序布局不整齐,不应使用BC作为编 辑器合版本,因为BC会自动将8个空格变为一个TAB键,因此使用BC合入的版本大多会将缩进变乱。 5.4.3 程序的分界符‘{’和‘}’应独占一行并且位于同一列,同时与引用它们的语句左对齐。(见 图7) 5.4.4 在函数体的开始、类的定义、结构的定义、枚举的定义以及if、for、do、while、switch、case语句中的程序都要采用如上的缩进方式。(见图8) for (...) for (...) { { ... // program code ... // program code } } if (...) if (...) { { ... // program code ... // program code } } void example_fun( void ) void example_fun( void ) { { ... // program code ... // program code } } 图8 风格良好的分界(左),与风格不良的分界(右) 5.5 长行拆分 5.5.1 代码行最大长度宜控制在120个字符以内。 5.5.2 长表达式宜在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行应进行适当的缩进,使排版整齐,语句可读(见图8)。 if ((very_longer_variable1 >= very_longer_variable12) && (very_longer_variable3 <= very_longer_variable14) && (very_longer_variable5 <= very_longer_variable16)) { dosomething(); } virtual CMatrix CMultiplyMatrix (CMatrix leftMatrix, CMatrix rightMatrix); for (very_longer_initialization; very_longer_condition; very_longer_update) { dosomething(); } 图9 长行的拆分 6 Q/XJ ××××—×××× 5.5.3 若函数或过程中的参数较长,则要进行适当的划分。 n7stat_str_compare((BYTE *) & stat_object, (BYTE *) & (act_task_table[taskno].stat_object), sizeof (_STAT_OBJECT)); n7stat_flash_act_duration( stat_item, frame_id *STAT_TASK_CHECK_NUMBER + index, stat_object ); 图10 参数的拆分 5.5.4 在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格。 注: 采用这种松散方式编写代码的目的是使代码更加清晰。 由于留空格所产生的清晰性是相对的,所以,在已经非常清晰的语句中没有必要再留空格,如果语句已足够清晰则括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C/C++语言中括号已经是最清晰的标志了。 在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格。 5.6 注释 5.6.1 概述 注释分为序言性注释和功能性注释: —— 序言性注释通常置于每一个程序模块的开头部分,给出程序的整体说明,对于理解程序具有 引导作用; —— 功能性注释嵌在源程序体中,用以描述其后的语句或程序段在做什么工作。 5.6.2 注释行的数量不得少于整个源程序的1/10。 5.6.3 单行注释原则上不得超过可视窗口宽度。 5.6.4 若代码本来就是清楚的,则不必加注释。 示例 : i++; // i 加 1,多余的注释 5.6.5 边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释应删除。在存档的某一版本的源代码中不得存在由于调试而留下的大篇注释。 5.6.6 注释应当准确、易懂,防止注释有二义性。 5.6.7 尽量避免在注释中使用缩写,特别是不常用缩写。 5.6.8 注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。(见图11) 5.6.9 在同一函数或模块中的注释应尽量对齐,如下例所示: 示例 : BOOL bReturnCache; HANDLE FileToWrite; DWORD BytesWritten; //是否将Cache中的内容返回客户端 //用来写数据的文件 //写入的数据长度 7 Q/XJ ××××—×××× /* get replicate sub system index and net indicator */ repssn_ind = ssn_data[index].repssn_index; repssn_ni = ssn_data[index].ni; 例1: /* get replicate sub system index and net indicator */ repssn_ind = ssn_data[index].repssn_index; repssn_ni = ssn_data[index].ni; 例2: repssn_ind = ssn_data[index].repssn_index; repssn_ni = ssn_data[index].ni; /* get replicate sub system index and net indicator */ 图11 风格良好的注释(左),与风格不良的注释(右) 5.6.10 当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读(见图12)。 /* * 函数介绍: * 输入参数: * 输出参数: * 返回值 : */ void Function(float x, float y, float z) { … } if (…) { … while (…) { … } // end of while … } // end of if 图12 程序的注释 5.6.11 注释与所描述内容进行同样的缩排。 注: 可使程序排版整齐,并方便注释的阅读与理解。 示例 : 如下例子,排版不整齐,阅读不方便。 void example_fun( void ) { /* code one comments */ CodeBlock One /* code two comments */ CodeBlock Two } 应改为如下布局。 void example_fun( void ) 8 Q/XJ ××××—×××× { /* code one comments */ CodeBlock One /* code two comments */ CodeBlock Two } 5.6.12 将注释与其上面的代码用空行隔开。 示例 : 如下例子,显得代码过于紧凑。 /* code one comments */ program code one /* code two comments */ program code two 应如下书写 /* code one comments */ program code one /* code two comments */ program code two 5.6.13 函数注释(序言性注释) 5.6.13.1 单元文件宜不超过1000行(包括注释), 复杂的组件单元可以不受此限制. 5.6.13.2 每个函数或过程代码必须有注释文字, 每个函数体中,每一小功能段都应有注释。 5.6.13.3 函数定义之前必须有对整个函数的描述,注释格式如下 /***************************************************************************** 【函数名称】 (必需) 【函数功能】 (必需) 【参数】 (若有参数则必需注明) 【全局变量】 (可选) 【返回值】 (若有返回值则必需注明) 【备注】 (可选) 【开发者及日期】 (若与头文件注释中的开发者为不同的人,则必需注明) 【版本】 (若与头文件注释中的版本不同,则必需注明) 【更改记录】 (若有修改,则必需注明) [最后修改] [修改日期] 更改原因概要 [版本] 【使用情况】 (可选) *****************************************************************************/ 注释内容说明: 函数名称:应包括函数名及参数; 参数:在本项中标明是输入参数(用[in]表示)还是输出参数(用[out]表示); 9 Q/XJ ××××—×××× 全局变量:该函数访问的全局变量和成员全局变量; 版本:如1.0; 更改记录:每次改动时增加一行注释,说明更改标识,更改日期,人,版本,更改原因概要; 使用情况:即函数的调用情况,被调用的次数; 示例 : /***************************************************************************** 【函数名称】 int My_GetLastModifedTime(CString strFileName,char*FileTime,long *cl) 【函数功能】 寻找目标文件,从中读取Last-Modified:信息、Contene-Length信息 【参数】 FileName [in] 指向目标文件(从URL而来,需要转换,与目标文件一一对应) FileTime [out] cl 1 [out] 【返回值】 文件存在,并从中取得Last-Modified:信息 -1 else 【备注】 使用了StrStrI函数,所以必须include shlwapi.h文件,且在编译的link项中包括shlwapi.li文件 【开发者及日期】 周慈 2000-02-05 【版本】 1.0 【修改历史】 [最后修改] [2000-03-26] 修改打开文件的模式,从OPEN_ALWAYS改为OPEN_EXISTING [2000-03-28] 返回的时间字符串,最后一位不需要'\\n',而且清除后面的所有空格 1.2 [版本] 【使用情况】 仅在ProxyThread函数中被调用一次 *****************************************************************************/ XXX 获取的Last-Modified:信息 获取的Contene-Length信息 5.6.14 程序注释(功能性注释) 5.6.14.1 变量 对所有有特定含义的变量命名,若其不能充分自注释,则必须注释变量意义;全局变量必须有较详细的注释,包括:功能、取值范围、存取关系、哪些函数或过程存取它以及注意事项等。 示例 : /* active statistic task number */ #define MAX_ACT_TASK_NUMBER 1000 #define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number*/ 10 Q/XJ ××××—×××× 5.6.14.2 数据结构声明 (包括数组、结构、类、枚举等),如果其命名不是充分自注释的,必须加以注释。对数据结构的注释应放在其上方相邻位置,不可放在下面;对结构中的每个域的注释放在此域的右方。 示例 : 可按如下形式说明枚举/数据/联合结构。 /* sccp interface with sccp user primitive message name */ enum SCCP_USER_PRIMITIVE { N_UNITDATA_IND, /* sccp notify sccp user unit data come */ N_NOTICE_IND, /* sccp notify user the No.7 network can not*/ /* transmission this message */ N_UNITDATA_REQ, /* sccp user's unit data transmission*/ /* request */ }; 5.6.14.3 判断 注释应说明被检查对象的意义。 5.6.14.4 分支 对变量的定义和分支语句(条件分支、循环语句等)必须编写注释。 注: 这些语句往往是程序实现某一特定功能的关键,对于维护人员来说,良好的注释帮助更好的理解程序,有时 甚至优于看设计文档。 5.6.14.5 case语句 对于switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。 注: 这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。 示例 : (注意斜体部分): case CMD_UP: ProcessUp(); break; case CMD_DOWN: ProcessDown(); break; case CMD_FWD: ProcessFwd(); if (...) { ... break; } 11 Q/XJ ××××—×××× else { ProcessCFW_B(); // now jump into case CMD_A } case CMD_A: ProcessA(); break; case CMD_B: ProcessB(); break; case CMD_C: ProcessC(); case CMD_D: ProcessD(); break; ... break; 5.6.14.6 调用函数 注释应说明该函数完成的处理。 6 命名规则 6.1 共性规则 6.1.1 标识符应当直观且可以拼读,可望文知意,不必进行“解码”。标识符最好采用英文单词或其组合。 6.1.2 标识符的长度应当符合“min-length && max-information”原则。 6.1.3 命名规则尽量与所采用的操作系统或开发工具的风格保持一致。 6.1.4 程序中不应出现仅靠大小写区分的相似的标识符。 示例 : int x, X; // 变量x 与 X 容易混淆 void foo(int x); // 函数foo 与FOO容易混淆 void FOO(float x); 6.1.5 程序中不应出现标识符完全相同的局部变量和全局变量。 6.1.6 变量的名字应当使用“名词”或者“形容词+名词”。 示例 : float value; float oldValue; float newValue; 6.1.7 全局函数的名字宜使用“动词”或者“动词+名词”(动宾词组)。类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。 12 Q/XJ ××××—×××× 示例 : DrawBox(); // 全局函数 // 类的成员函数 box->Draw(); 示例 : int minValue; int maxValue; int SetValue(…); int GetValue(…); 下面是一些在软件中常用的反义词组。 add / remove begin / end create / destroy insert / delete first / last get / release increment / decrement put / get add / delete lock / unlock open / close min / max old / new start / stop next / previous source / target show / hide send / receive source / destination cut / paste up / down 6.1.8 宜用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。 6.1.9 尽量避免名字中出现数字编号,如Value1,Value2等,除非逻辑上的确需要编号。 6.1.10 在同一软件产品内,应规划好接口部分标识符(变量、结构、函数及常量)的命名,防止编译、链接时产生冲突。 注: 对接口部分的标识符应该有更严格限制,防止冲突。如可规定接口部分的变量与常量之前加上“模块”标识 等。 6.2 一般命名规则 6.2.1 类名和函数名、结构名、typedef的名用大写字母开头的单词组合而成。 示例 : class Node; class LeafNode; // 类名 // 类名 void Draw(void); // 函数名 void SetValue(int value); // 函数名 typedef struct STUDENT; //typedef 6.2.2 变量和参数应用小写字母开头的单词组合而成。 示例 : BOOL flag; int drawMode; 6.2.3 常量应全用大写的字母,并用下划线分割单词。 示例 : const int MAX = 100; const int MAX_LENGTH = 100; 6.2.4 静态变量应加前缀s_(表示static)。 示例 : void Init(…) { 13 Q/XJ ××××—×××× static int s_initValue; // 静态变量 … } 6.2.5 若必须使用全局变量,则全局变量应加前缀g_(表示global)。 示例 : int g_howManyPeople; int g_howMuchMoney; 示例 : void Object::SetValue(int width, int height) { m_width = width; m_height = height; } // 全局变量 // 全局变量 6.2.6 类的数据成员应加前缀m_(表示member),以避免数据成员与成员函数的参数同名。 6.2.7 为了防止某一软件库中的一些标识符和其它软件库中的冲突,可为各种标识符加上能反映软件 性质的前缀。例如三维图形标准OpenGL的所有库函数均以gl开头,所有常量(或宏定义)均以GL开头。 6.2.8 变量名应以小写字母打头,各英文描述单词的首字母分别大写,其他字母一律小写。全局变量用g_,文件级的用m_, 函数内的用nCount,局部变量不加(见表1)。 表1 变量命名示例 变量类型 前缀 全局变量 文件级变量 函数级变量 布尔型 整 型 字符串 对 象(类、结构等) 浮点型 指 针 示例 : _bFind 是不允许出现的变量; 示例 (仅以全局变量为例) g_bFind g_nCount g_strError g_objFont g_dblOffset g_pMainFrame g_b g_n g_str g_obj g_dbl g_p m_b m_n m_str m_obj m_dbl m_p bCount nCount strCount objCount dblCount pCount 6.2.9 不应使用以下划线“_”打头或结尾的标识符。 6.2.10 一个以小写字母开头的名字应当用下划线“_”将其和前缀隔离。 示例 : m_b_find; 6.2.11 名中含多于一个单词时,应将几个单词写到一起,每个随后的单词大写开头。 示例 : m_nLastCount 中应大写L和C; 6.2.12 名字不能太长,必要时可使用缩写名字,缩写规则应统一。 示例 : m_nRemoteSendDataNumber 名字太长,应定义为m_nRSDNum 7 其它规则 14 Q/XJ ××××—×××× 7.1 布尔变量与零值比较 不应将布尔变量直接与TRUE、FALSE或者1、0进行比较。 注: 根据布尔类型的语义,零值为“假”(记为FALSE),任何非零值都是“真”(记为TRUE)。TRUE的值究竟是 什么并没有统一的标准。例如Visual C++ 将TRUE定义为1,而Visual Basic则将TRUE定义为-1。 示例 : 若布尔变量名字为flag,它与零值比较的标准if语句如下: if (flag) // 表示flag为真 if (!flag) // 表示flag为假 其它的用法都属于不良风格,例如: if (flag == TRUE) if (flag == 1 ) if (flag == 0) if (flag == FALSE) 7.2 整型变量与零值比较 应将整型变量用“==”或“!=”直接与0比较。 示例 : 若整型变量的名字为value,它与零值比较的标准if语句如下: if (value == 0) if (value != 0) 不可模仿布尔变量的风格而写成 if (value) if (!value) // 会让人误解 value是布尔变量 7.3 浮点变量与零值比较 不应将浮点变量用“==”或“!=”与任何数字比较。 注: 无论是float还是double类型的变量,都有精度限制。所以应避免将浮点变量用“==”或“!=”与数字比较, 应设法转化成“>=”或“<=”形式。 示例 : 若浮点变量的名字为x,应将 if (x == 0.0) // 隐含错误的比较 转化为 if ((x>=-EPSINON) && (x<=EPSINON)) 其中EPSINON是允许的误差(即精度)。 7.4 指针变量与零值比较 应将指针变量用“==”或“!=”与NULL比较。 注: 指针变量的零值是“空”(记为NULL)。尽管NULL的值与0相同,但是两者意义不同。 示例 : 若指针变量的名字为p,它与零值比较的标准if语句如下: if (p == NULL)// p与NULL显式比较,强调p是指针变量 if (p != NULL) 不应写成 if (p == 0) // 容易让人误解p是整型变量 if (p != 0) 或者 if (p) // 容易让人误解p是布尔变量 15 Q/XJ ××××—×××× if (!p) 16 Q/XJ ××××—×××× 附 录 A (资料性附录) 良好的C/C++编程风格; A.1 良好的注释技巧 A.1.1 避免在一行代码或表达式的中间插入注释。 注: 除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差。 A.1.2 通过对函数或过程、变量、结构等正确的命名以及合理地组织代码的结构,使代码成为自注释的。 注: 清晰准确的函数、变量等的命名,可增加代码可读性,并减少不必要的注释。 A.1.3 在代码的功能、意图层次上进行注释,提供有用、额外的信息。 注: 注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,因此要防止 没必要的重复注释。 示例 : 如下注释意义不大。 /* if receive_flag is TRUE */ if (receive_flag) 而如下的注释则给出了额外有用的信息。 /* if mtp receive a message from links */ if (receive_flag) A.1.4 在程序块的结束行右方加注释标记,以表明某程序块的结束。 注: 当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。 示例 : 参见如下例子。 if (...) { // program code while (index < MAX_INDEX) { // program code } /* end of while (index < MAX_INDEX) */ // 指明该条while语句结束 } /* end of if (...)*/ // 指明是哪条if语句结束 A.1.5 注释格式尽量统一,建议使用“/* „„ */”。 A.1.6 注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达。 注: 注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。 A.2 表达式和基本语句 17 Q/XJ ××××—×××× A.2.1 运算符的优先级 C/C++语言的运算符有数十个,运算符的优先级与结合律如表A.1所示。注意一元运算符 + - * 的优先级高于对应的二元运算符。 表A.1运算符的优先级与结合律 优先级 从 高 到 低 排 列 ( ) [ ] -> . ! ~ ++ -- (类型) sizeof + - * & * / % + - << >> < <= > >= == != & ^ | && || ?: = += -= *= /= %= &= ^= |= <<= >>= 运算符 从左至右 从右至左 从左至右 从左至右 从左至右 从左至右 从左至右 从左至右 从左至右 从左至右 从左至右 从右至左 从右至左 从左至右 结合律 A.2.1.1 若代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。 A.2.2 复合表达式 A.2.2.1 不应编写太复杂的复合表达式。 示例 : i = a >= b && c < d && c + f <= g + h ; // 复合表达式过于复杂 示例 : d = (a = b + c) + r ; 该表达式既求a值又求d值。应该拆分为两个独立的语句: a = b + c; d = a + r; A.2.2.2 不应有多用途的复合表达式。 A.2.2.3 不要把程序中的复合表达式与“真正的数学表达式”混淆。 示例 : // a < b < c是数学表达式而不是程序表达式 if (a < b < c) 并不表示 if ((aA.2.3 循环语句的效率 A.2.3.1 在多重循环中,若有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。 18 Q/XJ ××××—×××× for (row=0; row<100; row++) { for ( col=0; col<5; col++ ) { sum = sum + a[row][col]; } } for (col=0; col<5; col++ ) { for (row=0; row<100; row++) { sum = sum + a[row][col]; } } 图A.1低效率:长循环在最外层(左),高效率:长循环在最内层(右) A.2.3.2 若循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。图A.2(左)的程序比图A.2(右)多执行了N-1次逻辑判断。并且由于前者老要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。若N非常大,最好采用图A.2(右)的写法,可以提高效率。若N非常小,两者效率差别并不明显,采用图A.2(左)的写法比较好,因为程序更加简洁。 for (i=0; i A.2.4.1 不可在for 循环体内修改循环变量,防止for 循环失去控制。 A.2.4.2 建议for语句的循环控制变量的取值采用“半开半闭区间”写法。 图A.3(左)中的x值属于半开半闭区间“0 =< x < N”,起点到终点的间隔为N,循环次数为N。 图A.3(右)中的x值属于闭区间“0 =< x <= N-1”,起点到终点的间隔为N-1,循环次数为N。 相比之下,图A.3(左)的写法更加直观,尽管两者的功能是相同的。 for (int x=0; x A.2.5.1 switch是多分支选择语句,而if语句只有两个分支可供选择。虽然可以用嵌套的if语句来实现多分支选择,但那样的程序冗长难读。这是switch语句存在的理由。 switch语句的基本格式是: switch (variable) 19 Q/XJ ××××—×××× { case value1 : … break; case value2 : … break; … default : … break; } A.2.5.2 每个case语句的结尾不要忘了加break,否则将导致多个分支重叠。若有意使多个分支重叠, 必须在该case语句处理完、下一个case语句前加上明确的注释。这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。 A.2.5.3 不要忘记最后那个default分支。即使程序真的不需要default处理,也应该保留语句 default : break; 这样做并非多此一举,而是为了防止别人误以为你忘了default处理。 A.2.6 goto语句 由于goto语句可以灵活跳转,若不加限制,它的确会破坏结构化设计风格,而且,goto语句经常带来错误或隐患。因此,应少用、慎用goto语句。 A.3 变量、结构与数据类型 A.3.1 变量 A.3.1.1 明确公共变量与操作此公共变量的函数或过程的关系,如访问、修改及创建等。 明确过程操作变量的关系后,将有利于程序的进一步优化、单元测试、系统联调以及代码维护等。这种关系的说明可在注释或文档中描述。 示例 : 在源文件中,可按如下注释形式说明。 RELATION System_Init Input_Rec Print_Rec Stat_Score Student Create Modify Access Access Score Create Modify Access Access, Modify 注: RELATION为操作关系;System_Init、Input_Rec、Print_Rec、Stat_Score为四个不同的函数;Student、Score 为两个全局变量;Create表示创建,Modify表示修改,Access表示访问。其中,函数Input_Rec、Stat_Score都可修改变量Score,故此变量将引起函数间较大的耦合,并可能增加代码测试、维护的难度。 A.3.1.2 当向公共变量传递数据时,应十分小心,防止赋与不合理的值或越界等现象发生。 注: 对公共变量赋值时,若有必要应进行合法性检查,以提高代码的可靠性、稳定性。 A.3.1.3 防止局部变量与公共变量同名。 注: 若使用了较好的命名规则,那么此问题可自动消除。 A.3.1.4 严禁使用未经初始化的变量作为右值。 注: 特别是在C/C++中引用未经赋值的指针,经常会引起系统崩溃。 A.3.1.5 构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的公共变量,防止多个不同模块或函数都可以修改、创建同一公共变量的现象。 注: 降低公共变量耦合度。 A.3.1.6 使用严格形式定义的、可移植的数据类型,尽量不要使用与具体硬件或软件环境关系密切的变量。 注: 使用标准的数据类型,有利于程序的移植。 示例 : 20 Q/XJ ××××—×××× 如下例子(在DOS下BC3.1环境中),在移植时可能产生问题。 void main() { register int index; // 寄存器变量 _AX = 0x4000; // _AX是BC3.1提供的寄存器“伪变量” ... // program code } A.3.2 结构 A.3.2.1 结构的功能应单一,是针对一种事务的抽象。 设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。 示例 : 如下结构不太清晰、合理。 typedef struct STUDENT_STRU { unsigned char name[8]; /* student's name */ unsigned char age; /* student's age */ unsigned char sex; /* student's sex, as follows */ /* 0 - FEMALE; 1 - MALE */ unsigned char teacher_name[8]; /* the student teacher's name */ unisgned char teacher_sex; /* his teacher sex */ } STUDENT; 若改为如下,可能更合理些。 typedef struct TEACHER_STRU { unsigned char name[8]; /* teacher name */ unisgned char sex; /* teacher sex, as follows */ /* 0 - FEMALE; 1 - MALE */ } TEACHER; typedef struct STUDENT_STRU { unsigned char name[8]; /* student's name */ unsigned char age; /* student's age */ unsigned char sex; /* student's sex, as follows */ /* 0 - FEMALE; 1 - MALE */ unsigned int teacher_ind; /* his teacher index */ } STUDENT; A.3.2.2 不要设计面面俱到、非常灵活的数据结构。 面面俱到、灵活的数据结构反而容易引起误解和操作困难。 21 Q/XJ ××××—×××× A.3.2.3 不同结构间的关系不应过于复杂。 若两个结构间关系较复杂、密切,那么应合为一个结构。 示例 : 如下两个结构的构造不合理。 typedef struct PERSON_ONE_STRU { unsigned char name[8]; unsigned char addr[40]; unsigned char sex; unsigned char city[15]; } PERSON_ONE; typedef struct PERSON_TWO_STRU { unsigned char name[8]; unsigned char age; unsigned char tel; } PERSON_TWO; 由于两个结构都是描述同一事物的,那么不如合成一个结构。 typedef struct PERSON_STRU { unsigned char name[8]; unsigned char age; unsigned char sex; unsigned char addr[40]; unsigned char city[15]; unsigned char tel; } PERSON; A.3.2.4 结构中元素的个数应适中。若结构中元素个数过多可考虑依据某种原则把元素组成不同的子结构,以减少原结构中元素的个数。 增加结构的可理解性、可操作性和可维护性。 示例 : 假如认为如上的_PERSON结构元素过多,那么可如下对之划分。 typedef struct PERSON_BASE_INFO_STRU { unsigned char name[8]; unsigned char age; unsigned char sex; } PERSON_BASE_INFO; typedef struct PERSON_ADDRESS_STRU { unsigned char addr[40]; 22 Q/XJ ××××—×××× unsigned char city[15]; unsigned char tel; } PERSON_ADDRESS; typedef struct PERSON_STRU { PERSON_BASE_INFO person_base; PERSON_ADDRESS person_addr; } PERSON; A.3.2.5 仔细设计结构中元素的布局与排列顺序,使结构容易理解、节省占用空间,并减少引起误用 现象。 合理排列结构中元素顺序,可节省空间并增加可理解性。 示例 : 如下结构中的位域排列,将占较大空间,可读性也稍差。 typedef struct EXAMPLE_STRU { unsigned int valid: 1; PERSON person; unsigned int set_flg: 1; } EXAMPLE; 若改成如下形式,不仅可节省1字节空间,可读性也变好了。 typedef struct EXAMPLE_STRU { unsigned int valid: 1; unsigned int set_flg: 1; PERSON person ; } EXAMPLE; A.3.2.6 结构的设计应尽量考虑向前兼容和以后的版本升级,并为某些未来可能的应用保留余地(如预留一些空间等)。 软件向前兼容的特性,是软件产品是否成功的重要标志之一。若要想使产品具有较好的前向兼容,那么在产品设计之初就应为以后版本升级保留一定余地,并且在产品升级时必须考虑前一版本的各种特性。 A.3.2.7 当声明用于分布式环境或不同CPU间通信环境的数据结构时,必须考虑机器的字节顺序、使用的位域及字节对齐等问题 。 比如Intel CPU与68360 CPU,在处理位域及整数时,其在内存存放的“顺序”正好相反。 示例 1: 假如有如下短整数及结构。 unsigned short int exam; typedef struct EXAM_BIT_STRU { /* Intel 68360 */ unsigned int A1: 1; /* bit 0 7 */ unsigned int A2: 1; /* bit 1 6 */ unsigned int A3: 1; /* bit 2 5 */ 23 Q/XJ ××××—×××× } EXAM_BIT; 如下是Intel CPU生成短整数及位域的方式。 内存: 0 1 2 ... (从低到高,以字节为单位) exam exam低字节 exam高字节 内存: 0 bit 1 bit 2 bit ... (字节的各“位”) EXAM_BIT A1 A2 A3 如下是68360 CPU生成短整数及位域的方式。 内存: 0 1 2 ... (从低到高,以字节为单位) exam exam高字节 exam低字节 内存: 7 bit 6 bit 5 bit ... (字节的各“位”) EXAM_BIT A1 A2 A3 注: 在对齐方式下,CPU的运行效率要快得多。 示例 2: 示例 如下所示,当一个long型数(如图中long1)在内存中的位置正好与内存的字边界对齐时,CPU存取这个数只需访问一次内存,而当一个long型数(如图中的long2)在内存中的位置跨越了字边界时,CPU存取这个数就需要多次访问内存,如i960cx访问这样的数需读内存三次(一个BYTE、一个SHORT、一个BYTE,由CPU的微代码执行,对软件透明),所有对齐方式下CPU的运行效率明显快多了。 1 8 16 24 32 ------- ------- ------- ------- | long1 | long1 | long1 | long1 | ------- ------- ------- ------- | | | | long2 | ------- ------- ------- -------- | long2 | long2 | long2 | | ------- ------- ------- -------- | .... A.3.3 数据类型 A.3.3.1 留心具体语言及编译器处理不同数据类型的原则及有关细节。 注: 如在C语言中,static局部变量将在内存“数据区”中生成,而非static局部变量将在“堆栈”中生成。这 些细节对程序质量的保证非常重要。 A.3.3.2 编程时,应注意数据类型的强制转换。 注: 当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周, 就很有可能留下隐患。 A.3.3.3 对编译系统默认的数据类型转换,也应有充分的认识。 示例 : 如下赋值,多数编译器不产生告警,但值的含义还是稍有变化。 char chr; unsigned short int exam; chr = -1; 24 Q/XJ ××××—×××× exam = chr; // 编译器不产生告警,此时exam为0xFFFF。 A.3.3.4 尽量减少没有必要的数据类型默认转换与强制转换。 A.3.3.5 合理地设计数据并使用自定义数据类型,避免数据间进行不必要的类型转换。 A.3.3.6 对自定义数据类型进行恰当命名,使它成为自描述性的,以提高代码可读性。注意其命名方式在同一产品中的统一。 注: 使用自定义类型,可以弥补编程语言提供类型少、信息量不足的缺点,并能使程序清晰、简洁。 示例 : 可参考如下方式声明自定义数据类型。 下面的声明可使数据类型的使用简洁、明了。 typedef unsigned char BYTE; typedef unsigned short WORD; typedef unsigned int DWORD; 下面的声明可使数据类型具有更丰富的含义。 typedef float DISTANCE; typedef float SCORE; A.4 常量 A.4.1 为什么需要常量 A.4.1.1 若不使用常量,直接在程序中填写数字或字符串,将会有什么麻烦? —— 程序的可读性(可理解性)变差。程序员自己会忘记那些数字或字符串是什么意思,用户则 更加不知它们从何处来、表示什么。 —— 在程序的很多地方输入同样的数字或字符串,难保不发生书写错误。 —— 若要修改数字或字符串,则会在很多地方改动,既麻烦又容易出错。 A.4.1.2 尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。 示例 : #define const int MAX 100 MAX = 100; /* C语言的宏常量 */ // C++ 语言的const常量 const float PI = 3.14159; // C++ 语言的const常量 A.4.2 const 与 #define的比较 A.4.2.1 C++ 语言可以用const来定义常量,也可以用 #define来定义常量。但是前者比后者有更多的优点: —— const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对 后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。 —— 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。 A.4.2.2 在C++ 程序中应只使用const常量而不使用宏常量,即用const常量完全取代宏常量。 A.4.2.3 避免使用不易理解的数字,用有意义的标识来替代。涉及物理状态或者含有物理意义的常量,不应直接使用数字,必须用有意义的枚举或宏来代替。 示例 : 如下的程序可读性差。 if (Trunk[index].trunk_state == 0) { 25 Q/XJ ××××—×××× Trunk[index].trunk_state = 1; ... // program code } 应改为如下形式。 #define TRUNK_IDLE 0 #define TRUNK_BUSY 1 if (Trunk[index].trunk_state == TRUNK_IDLE) { Trunk[index].trunk_state = TRUNK_BUSY; ... // program code } A.4.3 常量定义规则 A.4.3.1 需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。 A.4.3.2 若某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。 示例 : const float RADIUS = 100; const float DIAMETER = RADIUS * 2; A.5 函数 A.5.1 函数设计 A.5.1.1 功能不明确较小的函数,特别是仅有一个上级函数调用它时,应考虑把它合并到上级函数中,而不必单独存在。 注: 模块中函数划分的过多,一般会使函数间的接口变得复杂。所以过小的函数,特别是扇入很低的或功能不明 确的函数,不值得单独存在。 A.5.1.2 若多段代码重复做同一件事情,那么在函数的划分上可能存在问题。若此段代码各语句之间有实质性关联并且是完成同一件功能的,那么可考虑把此段代码构造成一个新的函数。 A.5.1.3 函数的功能应单一,不要设计多用途的函数。 A.5.1.4 函数体的规模要小,尽量控制在50行代码之内。 A.5.1.5 尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。 注:带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函 数既不易理解又不利于测试和维护。在C/C++语言中,函数的static局部变量是函数的“记忆”存储器。建议尽量少用static局部变量,除非必需。 A.5.1.6 设计高扇入、合理扇出(小于7)的函数。 注: 扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。 扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,如总是1,表明函数的调用层次可能过多,这样不利程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。函数较合理的扇出(调度函数除外)通常是3-5。扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。 扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。 26 Q/XJ ××××—×××× 较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。 A.5.1.7 减少函数本身或函数间的递归调用。 注: 递归调用特别是函数间的递归调用(如A->B->C->A),影响程序的可理解性;递归调用一般都占用较多的系 统资源(如栈空间);递归调用对程序的测试有一定影响。故除非为某些算法或功能的实现方便,应减少没必要的递归调用。 A.5.1.8 仔细分析模块的功能及性能需求,并进一步细分,同时若有必要画出有关数据流图,据此来 进行模块的函数划分与组织。 注: 函数的划分与组织是模块的实现过程中很关键的步骤,如何划分出合理的函数结构,关系到模块的最终效率 和可维护性、可测性等。根据模块的功能图或/及数据流图映射出函数结构是常用方法之一。 A.5.1.9 改进模块中函数的结构,降低函数间的耦合度,并提高函数的独立性以及代码可读性、效率和可维护性。优化函数结构时,应遵守以下原则: —— 不能影响模块功能的实现。 —— 仔细考查模块或函数出错处理及模块的性能要求并进行完善。 —— 通过分解或合并函数来改进软件结构。 —— 考查函数的规模,过大的应进行分解。 —— 降低函数间接口的复杂度。 —— 不同层次的函数调用应有较合理的扇入、扇出。 —— 函数功能应可预测。 —— 提高函数内聚。(单一功能的函数内聚最高) 注: 对初步划分后的函数结构应进行改进、优化,使之更为合理。 A.5.1.10 在多任务操作系统的环境下编程,应注意函数可重入性的构造。 注: 可重入性是指函数可以被多个任务进程调用。在多任务操作系统中,函数是否具有可重入性是非常重要的, 因为这是多个进程可以共用此函数的必要条件。另外,编译器是否提供可重入函数库,与它所服务的操作系统有关,只有操作系统是多任务时,编译器才有可能提供可重入函数库。如DOS下BC和MSC等就不具备可重入函数库,因为DOS是单用户单任务操作系统。 A.5.2 参数的规则 A.5.2.1 参数的书写应完整,不要贪图省事只写参数的类型而省略参数名字。若函数没有参数,则用void填充。 示例 : void SetValue(int width, int height); // 良好的风格 void SetValue(int, int); float GetValue(void); float GetValue(); 示例 : 编写字符串拷贝函数StringCopy,它有两个参数。若把参数名字起为str1和str2: void StringCopy(char *str1, char *str2); 那么我们很难搞清楚究竟是把str1拷贝到str2中,还是刚好倒过来。 可以把参数名字起得更有意义,如叫strSource和strDestination。这样从名字上就可以看出应该把strSource拷贝到strDestination。 还有一个问题,这两个参数那一个该在前那一个该在后?参数的顺序应遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面。 若将函数声明为: void StringCopy(char *strSource, char *strDestination); 27 // 不良的风格 // 良好的风格 // 不良的风格 A.5.2.2 参数命名应恰当,顺序应合理。 Q/XJ ××××—×××× 别人在使用时可能会不假思索地写成如下形式: char str[20]; StringCopy(str, “Hello World”); // 参数顺序颠倒 A.5.2.3 若参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。 示例 : void StringCopy(char *strDestination,const char *strSource); 若输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构 造和析构过程,从而提高效率。 避免函数有太多的参数,参数个数尽量控制在5个以内。若参数太多,在使用时容易将参数类型或顺序搞错。 A.5.2.4 尽量不要使用类型和数目不确定的参数。 C标准库函数printf是采用不确定参数的典型代表,其原型为: int printf(const chat *format[, argument]„); 这种风格的函数在编译时丧失了严格的类型安全检查。 A.5.2.5 避免使用BOOL参数。 注: 原因有二,其一是BOOL参数值无意义,TURE/FALSE的含义是非常模糊的,在调用时很难知道该参数到底传 达的是什么意思;其二是BOOL参数值不利于扩充。还有NULL也是一个无意义的单词。 A.5.3 返回值的规则 A.5.3.1 不要省略返回值的类型。 C语言中,凡不加类型说明的函数,一律自动按整型处理。这样做不会有什么好处,却容易被误解为void类型。 C++语言有很严格的类型安全检查,不允许上述情况发生。由于C++程序可以调用C函数,为了避免混乱,规定任何C++/ C函数都必须有类型。若函数没有返回值,那么应声明为void类型。 A.5.3.2 函数名字与返回值类型在语义上不可冲突。 违反这条规则的典型代表是C标准库函数getchar。 示例 : char c; c = getchar(); if (c == EOF) „ 按照getchar名字的意思,将变量c声明为char类型是很自然的事情。但不幸的是getchar的确不是char类型,而是int类型,其原型如下: int getchar(void); 由于c是char类型,取值范围是[-128,127],若宏EOF的值在char的取值范围之外,那么if语句将总是失败,这种“危险”人们一般哪里料得到!导致本例错误的责任并不在用户,是函数getchar误导了使用者。 A.5.3.3 不应将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return语句返回。 回顾上例,C标准库函数的设计者为什么要将getchar声明为令人迷糊的int类型呢?他会那么傻吗? 在正常情况下,getchar的确返回单个字符。但如果getchar碰到文件结束标志或发生读错误,它必须返回一个标志EOF。为了区别于正常的字符,只好将EOF定义为负数(通常为负1)。因此函数getchar就成了int类型。 我们在实际工作中,经常会碰到上述令人为难的问题。为了避免出现误解,我们应该将正常值和错误标志分开。即:正常值用输出参数获得,而错误标志用return语句返回。 28 Q/XJ ××××—×××× 函数getchar可以改写成 BOOL GetChar(char *c); 虽然gechar比GetChar灵活,例如 putchar(getchar()); 但是如果getchar用错了,它的灵活性又有什么用呢? A.5.3.4 有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。 示例 : 字符串拷贝函数strcpy的原型: char *strcpy(char *strDest,const char *strSrc); strcpy函数将strSrc拷贝至输出参数strDest中,同时函数的返回值又是strDest。这样做并非多此一举,可以获得如下灵活性: char str[20]; int length = strlen( strcpy(str, “Hello World”) ); A.5.4 函数内部实现的规则 A.5.4.1 在函数体的“入口处”,对参数的有效性进行检查。 很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言”(assert)来防止此类错误。详见A.6.2节“使用断言”。 A.5.4.2 编写可重入函数时,若使用全局变量,则应加以保护; 若全局变量不加以保护,当多个进程调用此函数时,很可能使此变量变为不可知状态。 示例 : 若 Exam是int全局变量,函数Squre_Exam返回Exam平方值,如下函数不具有可重入性。 Unsigned int example ( int para) { unsigned int temp; Exam = para; temp = Square_Exam(); return temp; } 此函数若被多个函数调用, Exam 可能成为未知的。 应改为如下方式: Unsigned int example ( int para) { unsigned int temp; [申请信号量操作] //若申请不到信号量,说明另外的进程正 Exam = para; //处于给Exam赋值并计算其平方过程, temp = Square_Exam(); //即正在使用此信号量,本进程必须等待 [释放信号量操作] //其释放信号后,才可继续执行 } return temp; A.5.4.3 源程序中关系较为紧密的代码应尽可能相邻。 注: 便于程序阅读和查找。 示例 : 以下代码布局不太合理。 29 Q/XJ ××××—×××× rect.length = 10; char_poi = str; rect.width = 5; 若按如下形式书写,可能更清晰一些。 rect.length = 10; rect.width = 5; // 矩形的长与宽关系较密切,放在一起。 char_poi = str; A.5.4.4 不应使用难懂的技巧性很高的语句,除非很有必要时。 注: 高技巧语句不等于高效率的程序,实际上程序的效率关键在于算法。 示例 : 如下表达式,考虑不周就可能出问题,也较难理解。 * stat_poi ++ += 1; * ++ stat_poi += 1; 应分别改为如下。 *stat_poi += 1; stat_poi++; // 此二语句功能相当于“ * stat_poi ++ += 1; ” ++ stat_poi; *stat_poi += 1; // 此二语句功能相当于“ * ++ stat_poi += 1; ” A.5.5 其它建议 A.5.5.1 不仅应检查输入参数的有效性,还应检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。 A.5.5.2 用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。 A.5.5.3 通过对函数或过程、变量、结构等正确的命名以及合理地组织代码的结构,使代码成为自注释的。可增强代码可读性,并减少不必要的注释。 A.6 宏 A.6.1 用宏定义表达式时,应使用完备的括号。 示例 : 如下定义的宏都存在一定的风险。 #define RECTANGLE_AREA( a, b ) a * b #define RECTANGLE_AREA( a, b ) (a * b) #define RECTANGLE_AREA( a, b ) (a) * (b) 正确的定义应为: #define RECTANGLE_AREA( a, b ) ((a) * (b)) A.6.2 将宏所定义的多条表达式放在大括号中。 示例 : 下面的语句只有宏的第一条表达式被执行。为了说明问题,for语句的书写稍不符规范。 #define INTI_RECT_VALUE( a, b ) a = 0; b = 0; 30 Q/XJ ××××—×××× for (index = 0; index < RECT_TOTAL_NUM; index++) INTI_RECT_VALUE( rect.a, rect.b ); 正确的用法应为: #define INTI_RECT_VALUE( a, b ) { a = 0; b = 0; } for (index = 0; index < RECT_TOTAL_NUM; index++) { INTI_RECT_VALUE( rect[index].a, rect[index].b ); } A.6.3 使用宏时,不允许参数发生变化。 示例 : 如下用法可能导致错误。 #define SQUARE( a ) ((a) * (a)) int a = 5; int b; b = SQUARE( a++ ); // 结果:a = 7,即执行了两次增1。 正确的用法是: b = SQUARE( a ); a++; // 结果:a = 6,即只执行了一次增1。 A.7 程序效率 A.7.1 编程时要经常注意代码的效率。 注: 代码效率分为全局效率、局部效率、时间效率及空间效率。全局效率是站在整个系统的角度上的系统效率; 局部效率是站在模块或函数角度上的效率;时间效率是程序处理输入任务所需的时间长短;空间效率是程序所需内存空间,如机器代码空间大小、数据空间大小、栈空间大小等。 A.7.2 在保证软件系统的正确性、稳定性、可读性及可测性的前提下,提高代码效率。 注: 不能一味地追求代码效率,而对软件的正确性、稳定性、可读性及可测性造成影响。 A.7.3 局部效率应为全局效率服务,不能因为提高局部效率而对全局效率造成影响。 A.7.4 通过对系统数据结构的划分与组织的改进,以及对程序算法的优化来提高空间效率。 注: 这种方式是解决软件空间效率的根本办法。 示例 : 如下记录学生学习成绩的结构不合理。 typedef unsigned char BYTE; typedef unsigned short WORD; 31 Q/XJ ××××—×××× typedef struct STUDENT_SCORE_STRU { BYTE name[8]; BYTE age; BYTE sex; BYTE class; BYTE subject; float score; } STUDENT_SCORE; 因为每位学生都有多科学习成绩,故如上结构将占用较大空间。应如下改进(分为两个结构),总的存贮空间将变小,操作也变得更方便。 typedef struct STUDENT_STRU { BYTE name[8]; BYTE age; BYTE sex; BYTE class; } STUDENT; typedef struct STUDENT_SCORE_STRU { WORD student_index; BYTE subject; float score; } STUDENT_SCORE; A.7.5 循环体内工作量最小化。 注: 应仔细考虑循环体内的语句是否可以放在循环体之外,使循环体内工作量最小,从而提高程序的时间效率。 示例: 如下代码效率不高。 for (ind = 0; ind < MAX_ADD_NUMBER; ind++) { sum += ind; back_sum = sum; /* backup sum */ } 语句“back_sum = sum;”完全可以放在for语句之后,如下。 for (ind = 0; ind < MAX_ADD_NUMBER; ind++) { sum += ind; } back_sum = sum; /* backup sum */ 32 Q/XJ ××××—×××× A.7.6 仔细分析有关算法,并进行优化。 A.7.7 仔细考查、分析系统及模块处理输入(如事务、消息等)的方式,并加以改进。 A.7.8 对模块中函数的划分及组织方式进行分析、优化,改进模块中函数的组织结构,提高程序效率。 注: 软件系统的效率主要与算法、处理任务方式、系统功能及函数结构有很大关系,仅在代码上下功夫一般不能 解决根本问题。 A.7.9 编程时,要随时留心代码效率;优化代码时,要考虑周全。 A.7.10 不应花过多的时间拼命地提高调用不很频繁的函数代码效率。 注: 对代码优化可提高效率,但若考虑不周很有可能引起严重后果。 A.7.11 要仔细地构造或直接用汇编编写调用频繁或性能要求极高的函数。 注: 只有对编译系统产生机器码的方式以及硬件系统较为熟悉时,才可使用汇编嵌入方式。嵌入汇编可提高时间 及空间效率,但也存在一定风险。 A.7.12 在保证程序质量的前提下,通过压缩代码量、去掉不必要代码以及减少不必要的局部和全局变量,来提高空间效率。 注: 这种方式对提高空间效率可起到一定作用,但往往不能解决根本问题。 A.7.13 在多重循环中,应将最忙的循环放在最内层。 注: 减少CPU切入循环层的次数。 示例 : 如下代码效率不高。 for (row = 0; row < 100; row++) { for (col = 0; col < 5; col++) { sum += a[row][col]; } } 可以改为如下方式,以提高效率。 for (col = 0; col < 5; col++) { for (row = 0; row < 100; row++) { sum += a[row][col]; } } A.7.14 尽量减少循环嵌套层次。 A.7.15 避免循环体内含判断语句,应将循环语句置于判断语句的代码块之中。 注: 目的是减少判断次数。循环体中的判断语句是否可以移到循环体外,要视程序的具体情况而言,一般情况, 与循环变量无关的判断语句可以移到循环体外,而有关的则不可以。 示例 : 如下代码效率稍低。 for (ind = 0; ind < MAX_RECT_NUMBER; ind++) { if (data_type == RECT_AREA) { 33 Q/XJ ××××—×××× area_sum += rect_area[ind]; } else { rect_length_sum += rect[ind].length; rect_width_sum += rect[ind].width; } } 因为判断语句与循环变量无关,故可如下改进,以减少判断次数。 if (data_type == RECT_AREA) { for (ind = 0; ind < MAX_RECT_NUMBER; ind++) { area_sum += rect_area[ind]; } } else { for (ind = 0; ind < MAX_RECT_NUMBER; ind++) { rect_length_sum += rect[ind].length; rect_width_sum += rect[ind].width; } } A.7.16 尽量用乘法或其它方法代替除法,特别是浮点运算中的除法。 注: 浮点运算除法要占用较多CPU资源。 示例 : 如下表达式运算可能要占较多CPU资源。 #define PAI 3.1416 radius = circle_length / (2 * PAI); 应如下把浮点除法改为浮点乘法。 #define PAI_RECIPROCAL (1 / 3.1416 ) // 编译器编译时,将生成具体浮点数 radius = circle_length * PAI_RECIPROCAL / 2; A.7.17 不要一味追求紧凑的代码。 注: 因为紧凑的代码并不代表高效的机器码。 A.8 可测性 A.8.1 一般规则 A.8.1.1 在同一项目组或产品组内,要有一套统一的为集成测试与系统联调准备的调测开关及相应打印函数,并且要有详细的说明。 注: 本规则是针对项目组或产品组的。 34 Q/XJ ××××—×××× A.8.1.2 在同一项目组或产品组内,调测打印出的信息串的格式要有统一的形式。信息串中至少要有 所在模块名(或源文件名)及行号。 注: 统一的调测信息格式便于集成测试。 A.8.1.3 编程的同时要为单元测试选择恰当的测试点,并仔细构造测试代码、测试用例,同时给出明确的注释说明。测试代码部分应作为(模块中的)一个子模块,以方便测试代码在模块中的安装与拆卸(通过调测开关)。 注: 为单元测试而准备。 A.8.1.4 正式软件产品中应把断言及其它调测代码去掉(即把有关的调测开关关掉)。 注: 加快软件运行速度。 A.8.1.5 在软件系统中设置与取消有关测试手段,不能对软件实现的功能等产生影响。 注: 即有测试代码的软件和关掉测试代码的软件,在功能行为上应一致。 A.8.1.6 用调测开关来切换软件的DEBUG版和正式版,而不要同时存在正式版本和DEBUG版本的不同源文件,以减少维护的难度。软件的DEBUG版本和发行版本应该统一维护,不允许分家,并且要时刻注意保证两个版本在实现功能上的一致性。 A.8.1.7 在编写代码之前,应预先设计好程序调试与测试的方法和手段,并设计好各种调测开关及相应测试代码如打印函数等。 注: 程序的调试与测试是软件生存周期中很重要的一个阶段,如何对软件进行较全面、高率的测试并尽可能地找 出软件中的错误就成为很关键的问题。因此在编写源代码之前,除了要有一套比较完善的测试计划外,还应设计出一系列代码测试手段,为单元测试、集成测试及系统联调提供方便。 A.8.1.8 调测开关应分为不同级别和类型。 注: 调测开关的设置及分类应从以下几方面考虑:针对模块或系统某部分代码的调测;针对模块或系统某功能的 调测;出于某种其它目的,如对性能、容量等的测试。这样做便于软件功能的调测,并且便于模块的单元测试、系统联调等。 A.8.2 使用断言 A.8.2.1 程序一般分为Debug版本和Release版本,Debug版本用于内部调试,Release版本发行给用户使用。 断言assert是仅在Debug版本起作用的宏,它用于检查“不应该”发生的情况。图A.4是一个内存复制函数。在运行过程中,若assert的参数为假,那么程序就会中止(一般地还会出现提示对话,说明在什么地方引发了assert)。 void *memcpy(void *pvTo, const void *pvFrom, size_t size) { assert((pvTo != NULL) && (pvFrom != NULL)); // 使用断言 byte *pbTo = (byte *) pvTo; // 防止改变pvTo的地址 byte *pbFrom = (byte *) pvFrom; // 防止改变pvFrom的地址 while(size -- > 0 ) *pbTo ++ = *pbFrom ++ ; return pvTo; } 图A.4复制不重叠的内存块 assert不是一个仓促拼凑起来的宏。为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。所以assert不是函数,而是宏。程序员可以把assert看成一个在任何系统状态下都可以安全使用的无害测试手段。若程序在assert处终止了,并不是说含有该assert的函数有错误,而是调用者出了差错,assert可以帮助我们找到发生错误的原因。 35 Q/XJ ××××—×××× 很少有比跟踪到程序的断言,却不知道该断言的作用更让人沮丧的事了。你化了很多时间,不是为 了排除错误,而只是为了弄清楚这个错误到底是什么。有的时候,程序员偶尔还会设计出有错误的断言。所以若搞不清楚断言检查的是什么,就很难判断错误是出现在程序中,还是出现在断言中。幸运的是这个问题很好解决,只要加上清晰的注释即可。这本是显而易见的事情,可是很少有程序员这样做。这好比一个人在森林里,看到树上钉着一块“危险”的大牌子。但危险到底是什么?树要倒?有废井?有野兽?除非告诉人们“危险”是什么,否则这个警告牌难以起到积极有效的作用。难以理解的断言常常被程序员忽略,甚至被删除。 A.8.2.2 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。 A.8.2.3 在函数的入口处,使用断言检查参数的有效性(合法性)。 示例 : 若某函数参数中有一个指针,那么使用指针前可对它检查,如下。 int exam_fun( unsigned char *str ) { EXAM_ASSERT( str != NULL ); // 用断言检查“若指针不为空”这个条件 ... //other program code } A.8.2.4 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。 A.8.2.5 一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。 A.8.2.6 使用断言来发现软件问题,提高代码可测性。 注: 断言是对某种若条件进行检查(可理解为若条件成立则无动作,否则应报告),它可以快速发现并定位软件问 题,同时对系统错误进行自动报警。断言可以对在系统中隐藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性。实际应用时,可根据具体情况灵活地设计断言。 示例 : 下面是C语言中的一个断言,用宏来设计的。(其中NULL为0L) #ifdef _EXAM_ASSERT_TEST_ // 若使用断言测试 void exam_assert( char * file_name, unsigned int line_no ) { printf( \"\\n[EXAM]Assert failed: %s, line %u\\n\ file_name, line_no ); abort( ); } #define EXAM_ASSERT( condition ) { if (condition) // 若条件成立,则无动作 NULL; else // 否则报告 exam_assert( __FILE__, __LINE__ ) } 36 Q/XJ ××××—×××× #else // 若不使用断言测试 #define EXAM_ASSERT(condition) NULL #endif /* end of ASSERT */ A.8.2.7 用断言来检查程序正常运行时不应发生但在调测时有可能发生的非法情况。 A.8.2.8 不能用断言来检查最终产品肯定会出现且必须处理的错误情况。 注: 断言是用来处理不应该发生的错误情况的,对于可能会发生的且必须处理的情况要写防错程序,而不是断言。 如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。 A.8.2.9 对较复杂的断言加上明确的注释。 注: 为复杂的断言加注释,可澄清断言含义并减少不必要的误用。 A.8.2.10 用断言保证没有定义的特性或功能不被使用。 示例 : 若某通信模块在设计时,准备提供“无连接”和“连接” 这两种业务。但当前的版本中仅实现了“无连接”业务,且在此版本的正式发行版中,用户(上层模块)不应产生“连接”业务的请求,那么在测试时可用断言检查用户是否使用“连接”业务。如下。 #define EXAM_CONNECTIONLESS 0 // 无连接业务 #define EXAM_CONNECTION 1 // 连接业务 int msg_process( EXAM_MESSAGE *msg ) { unsigned char service; /* message service class */ EXAM_ASSERT( msg != NULL ); service = get_msg_service_class( msg ); EXAM_ASSERT( service != EXAM_CONNECTION ); // 若不使用连接业务 ... //other program code } A.8.2.11 用断言对程序开发环境(OS/Compiler/Hardware)的假设进行检查。 注: 程序运行时所需的软硬件环境及配置要求,不能用断言来检查,而必须由一段专门代码处理。用断言仅可对 程序开发环境中的假设及所配置的某版本软硬件是否具有某种功能的假设进行检查。如某网卡是否在系统运行环境中配置了,应由程序中正式代码来检查;而此网卡是否具有某设想的功能,则可由断言来检查。 对编译器提供的功能及特性假设可用断言检查,原因是软件最终产品(即运行代码或机器码)与编译器已没有任何直接关系,即软件运行过程中(注意不是编译过程中)不会也不应该对编译器的功能提出任何需求。 示例 : 用断言检查编译器的int型数据占用的内存空间是否为2,如下。 EXAM_ASSERT( sizeof( int ) == 2 ); A.8.2.12 编写防错程序,然后在处理错误之后可用断言宣布发生错误。 37 Q/XJ ××××—×××× 示例 : 假如某模块收到通信链路上的消息,则应对消息的合法性进行检查,若消息类别不是通信协议中规定的,则应进行出错处理,之后可用断言报告,如下例。 #ifdef _EXAM_ASSERT_TEST_ // 若使用断言测试 /* Notice: this function does not call 'abort' to exit program */ void assert_report( char * file_name, unsigned int line_no ) { printf( \"\\n[EXAM]Error Report: %s, line %u\\n\ file_name, line_no ); } #define ASSERT_REPORT( condition ) if ( condition ) // 若条件成立,则无动作 NULL; else // 否则报告 assert_report ( __FILE__, __LINE__ ) #else // 若不使用断言测试 #define ASSERT_REPORT( condition ) NULL #endif /* end of ASSERT */ int msg_handle( unsigned char msg_name, unsigned char * msg ) { switch( msg_name ) { case MSG_ONE: ... // 消息MSG_ONE处理 return MSG_HANDLE_SUCCESS; ... // 其它合法消息处理 default: ... // 消息出错处理 ASSERT_REPORT( FALSE ); // “合法”消息不成立,报告 return MSG_HANDLE_ERROR; } } A.9 质量保证 A.9.1 代码质量保证优先原则 —— 正确性,指程序要实现设计要求的功能。 38 Q/XJ ××××—×××× —— 稳定性、安全性,指程序稳定、可靠、安全。 —— 可测试性,指程序要具有良好的可测试性。 —— 规范/可读性,指程序书写风格、命名规则等要符合规范。 —— 全局效率,指软件系统的整体效率。 —— 局部效率,指某个模块/子模块/函数的本身效率。 —— 个人表达方式/个人方便性,指个人编程习惯。 A.9.2 只引用属于自己的存贮空间。 注: 若模块封装的较好,那么一般不会发生非法引用他人的空间。 A.9.3 防止引用已经释放的内存空间。 注: 在实际编程过程中,稍不留心就会出现在一个模块中释放了某个内存块(如C语言指针),而另一模块在随后 的某个时刻又使用了它。要防止这种情况发生。 A.9.4 过程/函数中分配的内存,在过程/函数退出之前要释放。 A.9.5 过程/函数中申请的(为打开文件而使用的)文件句柄,在过程/函数退出之前要关闭。 注: 分配的内存不释放以及文件句柄不关闭,是较常见的错误,而且稍不注意就有可能发生。这类错误往往会引 起很严重后果,且难以定位。 示例 : 下函数在退出之前,没有把分配的内存释放。 typedef unsigned char BYTE; int example_fun( BYTE gt_len, BYTE *gt_code ) { BYTE *gt_buf; gt_buf = (BYTE *) malloc (MAX_GT_LENGTH); ... //program code, include check gt_buf if or not NULL. /* global title length error */ if (gt_len > MAX_GT_LENGTH) { return GT_LENGTH_ERROR; // 忘了释放gt_buf } ... // other program code } 应改为如下。 int example_fun( BYTE gt_len, BYTE *gt_code ) { BYTE *gt_buf; gt_buf = (BYTE * ) malloc ( MAX_GT_LENGTH ); ... // program code, include check gt_buf if or not NULL. /* global title length error */ 39 Q/XJ ××××—×××× if (gt_len > MAX_GT_LENGTH) { free( gt_buf ); // 退出之前释放gt_buf return GT_LENGTH_ERROR; } ... // other program code } A.9.6 防止内存操作越界。 注: 内存操作主要是指对数组、指针、内存地址等的操作。内存操作越界是软件系统主要错误之一,后果往往非 常严重,所以当我们进行这些操作时一定要仔细小心。 示例 : 若某软件系统最多可由10个用户同时使用,用户号为1-10,那么如下程序存在问题。 #define MAX_USR_NUM 10 unsigned char usr_login_flg[MAX_USR_NUM]= \"\"; void set_usr_login_flg( unsigned char usr_no ) { if (!usr_login_flg[usr_no]) { usr_login_flg[usr_no]= TRUE; } } 当usr_no为10时,将使用usr_login_flg越界。可采用如下方式解决。 void set_usr_login_flg( unsigned char usr_no ) { if (!usr_login_flg[usr_no - 1]) { usr_login_flg[usr_no - 1]= TRUE; } } A.9.7 认真处理程序所能遇到的各种出错情况。 A.9.8 系统运行之初,要初始化有关变量及运行环境,防止未经初始化的变量被引用。 A.9.9 系统运行之初,要对加载到系统中的数据进行一致性检查。 注: 使用不一致的数据,容易使系统进入混乱状态和不可知状态。 A.9.10 严禁随意更改其它模块或系统的有关设置和配置。 注: 编程时,不能随心所欲地更改不属于自己模块的有关设置如常量、数组的大小等。 A.9.11 不能随意改变与其它模块的接口。 A.9.12 充分了解系统的接口之后,再使用系统提供的功能。 示例 : 在B型机的各模块与操作系统的接口函数中,有一个要由各模块负责编写的初始化过程,此过程在软件系统加载完成后,由操作系统发送的初始化消息来调度。因此就涉及到初始化消息的类型与消息发送的顺序问题,特别是消息顺序,若没搞清楚就开始编程,很容易引起严重后果。以下示例引自B型曾出现过的实际代码,其中使用了 40 Q/XJ ××××—×××× FID_FETCH_DATA与FID_INITIAL初始化消息类型,注意B型机的系统是在FID_FETCH_DATA之前发送FID_INITIAL的。 MID alarm_module_list[MAX_ALARM_MID]; int FAR SYS_ALARM_proc( FID function_id, int handle ) { _UI i, j; switch ( function_id ) { ... // program code case FID_INITAIL: for (i = 0; i < MAX_ALARM_MID; i++) { if (alarm_module_list[i]== BAM_MODULE // **) || (alarm_module_list[i]== LOCAL_MODULE) { for (j = 0; j < ALARM_CLASS_SUM; j++) { FAR_MALLOC( ... ); } } } ... // program code break; case FID_FETCH_DATA: ... // program code Get_Alarm_Module( ); // 初始化alarm_module_list break; ... // program code } } 由于FID_INITIAL是在FID_FETCH_DATA之前执行的,而初始化alarm_module_list是在FID_FETCH_DATA中进行的,故在FID_INITIAL中(**)处引用alarm_module_list变量时,它还没有被初始化。这是个严重错误。 应如下改正:要么把Get_Alarm_Module函数放在FID_INITIAL中(**)之前;要么就必须考虑(**)处的判断语句是否可以用(不使用alarm_module_list变量的)其它方式替代,或者是否可以取消此判断语句。 41 Q/XJ ××××—×××× A.9.13 编程时,要防止差1错误。要时刻注意易混淆的操作符。当编完程序后,应从头至尾检查一遍 这些操作符,以防止拼写错误。 注: 形式相近的操作符最容易引起误用,如C/C++中的“=”与“==”、“|”与“||”、“<=”与“<”等,若 拼写错了,编译器不一定能够检查出来。由此引起的后果,很多情况下是很严重的,所以编程时,一定要在这些地方小心。当编完程序后,应对这些操作符进行彻底检查。 示例 : 如把“&”写成“&&”,或反之。 ret_flg = (pmsg->ret_flg & RETURN_MASK); 被写为: ret_flg = (pmsg->ret_flg && RETURN_MASK); rpt_flg = (VALID_TASK_NO( taskno ) && DATA_NOT_ZERO( stat_data )); 被写为: rpt_flg = (VALID_TASK_NO( taskno ) & DATA_NOT_ZERO( stat_data )); A.9.14 有可能的话,if语句尽量加上else分支,对没有else分支的语句要小心对待;switch语句必须有default分支。 A.9.15 Unix下,多线程的中的子线程退出必需采用主动退出方式,即子线程应return出口。 A.9.16 不使用与硬件或操作系统关系很大的语句,而使用建议的标准语句,以提高软件的可移植性和可重用性。 A.9.17 除非为了满足特殊需求,避免使用嵌入式汇编。 注: 程序中嵌入式汇编,一般都对可移植性有较大的影响。 A.9.18 精心地构造、划分子模块,并按“接口”部分及“内核”部分合理地组织子模块,以提高“内核”部分的可移植性和可重用性。 注: 对不同产品中的某个功能相同的模块,若能做到其内核部分完全或基本一致,那么无论对产品的测试、维护, 还是对以后产品的升级都会有很大帮助。 A.9.19 精心构造算法,并对其性能、效率进行测试。 A.9.20 对较关键的算法最好使用其它算法来确认。 A.9.21 时刻注意表达式是否会上溢、下溢。 示例 : 如下程序将造成变量下溢。 unsigned char size ; while (size-- >= 0) // 将出现下溢 { ... // program code } 当size等于0时,再减1不会小于0,而是0xFF,故程序是一个死循环。应如下修改。 char size; // 从unsigned char 改为char while (size-- >= 0) { ... // program code } A.9.22 使用变量时要注意其边界值的情况。 示例 : 42 Q/XJ ××××—×××× 如C语言中字符型变量,有效值范围为-128到127。故以下表达式的计算存在一定风险。 char chr = 127; int sum = 200; chr += 1; // 127为chr的边界值,再加1将使chr上溢到-128,而不是128。 sum += chr; // 故sum的结果不是328,而是72。 若chr与sum为同一种类型,或表达式按如下方式书写,可能会好些。 sum = sum + chr + 1; A.9.23 留心程序机器码大小(如指令空间大小、数据空间大小、堆栈空间大小等)是否超出系统有关 限制。 A.9.24 为用户提供良好的接口界面,使用户能较充分地了解系统内部运行状态及有关系统出错情况。 A.9.25 系统应具有一定的容错能力,对一些错误事件(如用户误操作等)能进行自动补救。 A.9.26 对一些具有危险性的操作代码(如写硬盘、删数据等)要仔细考虑,防止对数据、硬件等的安全构成危害,以提高系统的安全性。 A.9.27 使用第三方提供的软件开发工具包或控件时,要注意以下几点: —— 充分了解应用接口、使用环境及使用时注意事项。 —— 不能过分相信其正确性。 —— 除非必要,不要使用不熟悉的第三方工具包与控件。 注: 使用工具包与控件,可加快程序开发速度,节省时间,但使用之前一定对它有较充分的了解,同时第三方工 具包与控件也有可能存在问题。 A.9.28 资源文件(多语言版本支持),若资源是对语言敏感的,应让该资源与源代码文件脱离,具体方法有下面几种:使用单独的资源文件、DLL文件或其它单独的描述文件(如数据库格式)。 A.10 代码编辑、编译、审查 A.10.1 打开编译器的所有告警开关对程序进行编译。 A.10.2 在产品软件(项目组)中,要统一编译开关选项。 A.10.3 通过代码走读及审查方式对代码进行检查。 注: 代码走读主要是对程序的编程风格如注释、命名等以及编程时易出错的内容进行检查,可由开发人员自己或 开发人员交叉的方式进行;代码审查主要是对程序实现的功能及程序的稳定性、安全性、可靠性等进行检查及评审,可通过自审、交叉审核或指定部门抽查等方式进行。 注: 测试部测试产品之前,应对代码进行抽查及评审。 A.10.4 编写代码时要注意随时保存,并定期备份,防止由于断电、硬盘损坏等原因造成代码丢失。 A.10.5 同产品软件(项目组)内,最好使用相同的编辑器,并使用相同的设置选项。 A.10.6 同一项目组最好采用相同的智能语言编辑器,如Muiti Editor,Visual Editor等,并设计、使用一套缩进宏及注释宏等,将缩进等问题交由编辑器处理。 A.10.7 要小心地使用编辑器提供的块拷贝功能编程。 注: 当某段代码与另一段代码的处理功能相似时,许多开发人员都用编辑器提供的块拷贝功能来完成这段代码的 编写。由于程序功能相近,故所使用的变量、采用的表达式等在功能及命名上可能都很相近,所以使用块拷贝时要注意,除了修改相应的程序外,一定要把使用的每个变量仔细查看一遍,以改成正确的。不应指望编译器能查出所有这种错误,比如当使用的是全局变量时,就有可能使某种错误隐藏下来。 A.10.8 合理地设计软件系统目录,方便开发人员使用。 注: 方便、合理的软件系统目录,可提高工作效率。目录构造的原则是方便有关源程序的存储、查询、编译、链 43 Q/XJ ××××—×××× 接等工作,同时目录中还应具有工作目录----所有的编译、链接等工作应在此目录中进行,工具目录----有关文件编辑器、文件查找等工具可存放在此目录中。 A.10.9 某些语句经编译后产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息。 注: 在Borland C/C++中,可用“#pragma warn”来关掉或打开某些告警。 示例 : #pragma warn -rvl // 关闭告警 int examples_fun( void ) { // 程序,但无return语句。 } #pragma warn +rvl // 打开告警 编译函数examples_fun时本应产生“函数应有返回值”告警,但由于关掉了此告警信息显示,所以编译时将不会产生此告警提示。 44 Q/XJ ××××—×××× 附件1: 勘误表 页码 错误位置 错误内容 修正方法 发现者 版次 45 Q/XJ ××××—×××× 附件 2: 意见征集表 意见人 意见描述 改进方法描述 46 因篇幅问题不能全部显示,请点此查看更多更全内容