【条款1-1-1 】 良好布局的目标是准确表现代码的逻辑结构,始终如一地表现代码的逻辑结构和改善代码可读性。
第二节 单条语句的布局
【条款1-2-1 】变量定义前后必须加空行。
示例:如下例子不符合规范。
UINT nTotal = 0;
UINT nNumber = 0;
for (UINT i = 0; i < MAX_ITEMS; i++)
{
nTotal = GetMoney();
nNumber = GetNumber();
... // other program code
}
应如下书写:
//注意此处要空行
UINT nTotal = 0;
UINT nNumber = 0;
//注意此处要空行
for (UINT i = 0; i < MAX_ITEMS; i++)
{
nTotal = GetMoney();
nNumber = GetNumber();
... // other program code
}
【条款1-2-2-3 】在布尔比较表达式中常量应放左边
if (NEDPR_MORE_PACKETS == nExtractPacketResult
|| NEDPR_JUST_A_PACKET == nExtractPacketResult)
【条款1-2-4 】尽量以const取代#define
#define USER_NUMBER_MAX 1024
替换为:
Const UINT USER_NUMBER_MAX = 1024;
【条款1-2-5 】具名常量和枚举常量应该用大写
范例:
Const UINT USER_NUMBER_MAX = 1024;
enum SSIDE_SESSION_TYEP
{
SSIDE_ST_CTRL = 1,
SSIDE_ST_DATA = 2
};
【条款1-2-6 】较长的语句(>80字符)要分成多行书写,长表达式要在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进行适当的缩进,使排版整齐,语句可读。
示例:
bResult = (NEDPR_MORE_PACKETS == nExtractPacketResult
|| NEDPR_JUST_A_PACKET == nExtractPacketResult);
【条款1-9 】循环、判断等语句中若有较长的表达式或语句,则要进行适应的划分,长表达式要在低优先级操作符处划分新行,操作符放在新行之首。
示例:
if ((nTaskNumber < MAX_ACT_TASK_NUMBER)
&& (IsValidStatItem (nStatItem)))
{
... // program code
}
for (i = 0, j = 0; (i < ArrBufferKeyword[nWordIndex].nWordLength)
&& (j < objNewKeyword.nWordLength); i++, j++)
{
... // program code
}
for (i = 0, j = 0;
(i < FIRST_WORD_LENGTH) && (j < SECOND_WORD_LENGTH);
i++, j++)
{
... // program code
}
【条款1-2-7 】一行只写一条语句。
不符合规范示例1。
objObjObjRect.nLength= 0; objObjRect.nNWidth = 0;
应如下书写
objObjRect.nLength= 0;
objObjRect.nNWidth = 0;
不符合规范示例2,C++没有定义表达式的运算顺序,也没有定义子程序参数的求值顺序,所有编译器在第一个参数的或前或后来计算第二个参数n+2,结果是不确定的,不同的编译器有不同的结果
PrintMessage(++n, n + 2);
应如下书写
++n;
PrintMessage(n, n + 2);
不符合规范示例3
void Strcpy(char* pszS1,char* pszS2)
{
while(*++pszS1= *++pszS2);
}
应如下书写
void Strcpy(char* pszS1,char* pszS2)
{
do
{
++pszS1;
++pszS2;
*pszS1 = *pszS2;
}while(0 != *pszS1 );
}
【条款1-2-8 】对齐只使用空格键,不使用TAB键。
说明:以免用不同的编辑器阅读程序时,因TAB键所设置的空格数目不同而造成程序布局不整齐,不要使用BC作为编辑器合版本,因为BC会自动将8个空格变为一个TAB键,因此使用BC合入的版本大多会将缩进变乱。
【条款1-2-9 】在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格。
说明:采用这种松散方式编写代码的目的是使代码更加清晰。
由于留空格所产生的清晰性是相对的,所以,在已经非常清晰的语句中没有必要再留空格,如果语句已足够清晰则括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C/C++语言中括号已经是最清晰的标志了。
在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格。
示例:
(1) 逗号、分号只在后面加空格。
int a, b, c;
(2)比较操作符, 赋值操作符"="、 “+=”,算术操作符"+"、"%",逻辑操作符"&&"、"&",位域操作符"<<"、"^"等双目操作符的前后加空格。
if (nCurrentTime >= MAX_TIME_VALUE)
a = b + c;
a *= 2;
a = b ^ 2;
(3)"!"、"~"、"++"、"–"、"&"(地址运算符)等单目操作符前后不加空格。
*p = 'a'; // 内容操作"*"与内容之间
flag = !isEmpty; // 非操作"!"与内容之间
p = &mem; // 地址操作"&" 与内容之间
i++; // "++","--"与内容之间
(4)"->"、"."前后不加空格。
p->id = pid; // "->"指针前后不加空格
(5) if、for、while、switch等与后面的括号间应加空格,使if等关键字更为突出、明显。
if (a >= b && c > d)
第三节 控制块的布局
【条款1-3-1 】程序块要采用缩进风格编写,缩进的空格数为4个。
说明:对于由开发工具自动生成的代码可以有不一致。
【条款1-3-2 】if、for、do、while、case、switch、default等语句自占一行,且if、for、do、while等语句的执行语句部分无论多少都要加括号{}。
示例:如下例子不符合规范。
if (NULL == pUserCR) return;
应如下书写:
if NULL == pUserCR)
{
return;
}
【条款1-3-3 】程序块的分界符(如C/C++语言的大括号‘{’和‘}’)应各独占一行并且位于同一列,同时与引用它们的语句左对齐。在函数体的开始、类的定义、结构的定义、枚举的定义以及if、for、do、while、switch、case语句中的程序都要采用如上的缩进方式。
示例:如下例子不符合规范。
for (...) {
... // program code
}
if (...)
{
... // program code
}
void ExampleFunction( void )
{
... // program code
}
应如下书写。
for (...)
{
... // program code
}
if (...)
{
... // program code
}
void ExampleFunction( void )
{
... // program code
}
【条款1-3-4 】相对独立的程序块之间必须加空行
CMonitorInfo objMonitorInfo;
GetMonitorInfo(&objMonitorInfo);
ShowMoitorInfo(&objMonitorInfo)
//注意此处要空行
CDiskInfo obDiskInfo;
GetDiskInfo(&obDiskInfo);
ShowDiskInfo(&obDiskInfo);
第四节 子程序的布局
【条款1-4-1 】一个函数体的代码行数应该推荐限制在80行内。
【条款1-4-2 】如果函数头太长需要分行,那么每个参数单独占一行。
示例:如下例子不符合规范:
BOOL OnPacketIoManagerEventHandle(PACKET_IO_MANAGER_EVENT nEvent, SEVERITY_LEVEL nSeverity, LPCTSTR lpTips)
应如下书写:
BOOL OnPacketIoManagerEventHandle(
PACKET_IO_MANAGER_EVENT nEvent
,SEVERITY_LEVEL nSeverity
,LPCTSTR lpTips
)
【条款1-4-3 】函数或过程的开始、结构的定义及循环、判断等语句中的代码都要采用缩进风格,case语句下的情况处理语句也要遵从语句缩进要求。
第五节 类的布局
【条款1-5-1 】类接口的布局
在布局类接口时,一般应将类成员按如下的顺序排列:
说明类及其完整用法的头部注释。
构造函数与析构函数
Public 子程序
Protected 子程序
Private子程序
【条款1-5-2 】类实现的布局
类实现通常应按如下的顺序排列:
概述类所在文件之内容的头部注释。
类数据
Public 子程序
Protected 子程序
Private子程序
第六节 文件布局
通常一个文件应只有一个类;文件名称应与类名有关;在文件中清晰的分隔各子程序
【条款1-6-1 】类实现的布局
C++源文件内容的典型顺序是:
文件的描述性注释
#include文件行
在多个类里使用的常量定义(如果文件里有多个类)
在多类里面使用的枚举(如果文件里有多个类)
宏函数定义
在多个类里使用的类型定义(如果文件里有多个类)
导入的全局变量和函数
导出的全局变量和函数
本文件使用的私用的变量和函数
各个类,包括各个类中的常量定义,枚举以及类型定义
第二章 注释
第一节 基本规则
【条款2-1-1 】最佳的注释量,约每十行语句有一个注释。
说明:注释的原则是有助于对程序的阅读理解,注释语言必须准确、易懂、简洁。
【条款2-2-2 】避免在注释中使用缩写,特别是非常用缩写。
说明:在使用缩写时或之前,应对缩写进行必要的说明。
【条款2-2-3 】注释的内容要清楚、明了,含义准确,防止注释二义性。
说明:错误的注释不但无益反而有害。
【条款2-2-4 】边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
【条款2-2-5 】注释应与其描述的代码相近,对代码的注释应放在其上方或右方(对单条语句的注释)相邻位置,不可放在下面,如放于上方则需与其上面的代码用空行隔开。
示例:如下例子不符合规范。
例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
应如下书写
// get replicate sub system index and net indicator
repssn_ind = ssn_data[index].repssn_index;
repssn_ni = ssn_data[index].ni;
【条款2-2-6 】对于所有有物理含义的变量、常量,如果其命名不是充分自注释的,在声明时都必须加以注释,说明其物理含义。变量、常量、宏的注释应放在其右方。
示例:
#define ACT_TASK_NUMBER_MAX 1000 ///< active statistic task number
【条款2-2-7 】数据结构声明(包括数组、结构、类、枚举等),如果其命名不是充分自注释的,必须加以注释。对数据结构的注释应放在其上方相邻位置,不可放在下面;对结构中的每个域的注释放在此域的右方。
示例:可按如下形式说明枚举/数据/联合结构。
/// @brief sccp interface with sccp user primitive message name
enum SCCP_USER_PRIMITIVE
{
SNP_UNITDATA_IND, ///< sccp notify sccp user unit data come
SNP_NOTICE_IND, ///< sccp notify user the No.7 network can not \n transmission this message
SNP_UNITDATA_REQ, ///< sccp user's unit data transmission request
};
【条款2-2-8 】全局变量要有较详细的注释,包括对其功能、取值范围、哪些函数或过程存取它以及存取时注意事项等的说明。
示例:
// The ErrorCode when SCCP translate
// Global Title failure, as follows // 变量作用、含义
// 0 - SUCCESS 1 - GT Table error
// 2 - GT error Others - no use // 变量取值范围
// only function SCCPTranslate() in
// this modual can modify it, and other
// module can visit it through call
// the function GetGTTransErrorCode() // 使用方法
BYTE g_nGTTranErrorCode;
【条款2-2-9 】将注释与其上面的代码用空行隔开。
示例:如下例子,显得代码过于紧凑。
// code one comments
program code one
// code two comments
program code two
应如下书写
// code one comments
program code one
// code two comments
program code two
【条款2-2-10 】注释与所描述内容进行同样的缩排。
说明:可使程序排版整齐,并方便注释的阅读与理解。
示例:如下例子,排版不整齐,阅读稍感不方便。
void ExampleFunction( void )
{
// code one comments
CodeBlock One
// code two comments
CodeBlock Two
}
应改为如下布局。
void ExampleFunction( void )
{
// code one comments
CodeBlock One
// code two comments
CodeBlock Two
}
【条款2-2-11 】分支语句(条件分支、循环语句等)必须编写注释。
说明:这些语句往往是程序实现某一特定功能的关键,对于维护人员来说,良好的注释帮助更好的理解程序,有时甚至优于看设计文档。
分支语句例子:
// 如果加载驱动失败或者返回错误0x80070420(0x80070420错误是驱动已经加载)
// 就立即返回
if(0x80070420 != hr && FAILED(hr))
{
m_strErrMsg.Format(_T("Faile to start the '%s' minifilter driver (error code = %x)")
,FILE_FILEENCRYPTION_DRIVER,hr);
ATLTRACE(_T("%s\r\n"),m_strErrMsg);
return FALSE;
}
循环语句例子:
//扫描被打开文件的数组
STRING_ARRAY_ITR itr = m_arrOpenedFiles.begin();
for (; itr != m_arrOpenedFiles.end(); itr++)
{
//查找指定的文件,如果发现就立即返回
if ((*itr).Find(lpFileName))
{
refFilepath = (*itr);
bResult = TRUE;
break;
}
}
【条款2-2-12 】对于switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。
说明:这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。
示例(注意斜体加粗部分):
switch(ch)
{
case CMD_FWD:
{
ProcessFwd();
if (...)
{
...
break;
}
else
{
ProcessCFW_B(); // now jump into case CMD_A
}
}
case CMD_A:
ProcessA();
break;
default:
}
【条款2-2-13 】switch语句的每一个分支需要有注释,放在右边。
例子:
// 根据不同的规则分配不同的缓冲区大小
switch (type)
{
case EFT_EXT_FILTER: //文件扩张名称过滤器
mem_size = sizeof(encrypt_file_filters_data_buffer) + sizeof(ext_filter_t) * filters;
break;
case EFT_PROCESS_FILTER: //进程过滤器
assert(EFT_PROCESS_FILTER == type);
mem_size = sizeof(encrypt_file_filters_data_buffer) + sizeof(process_filter_t) * filters;
break;
case EFT_DIR_FILTER: //目录过滤器
assert(EFT_DIR_FILTER == type);
mem_size = sizeof(encrypt_file_filters_data_buffer) + sizeof(dir_filter_t) * filters;
break;
default:
assert(0);
}
【条款2-2-14 】避免在一行代码或表达式的中间插入注释。
说明:除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差。
【条款2-2-15 】在代码的功能、意图层次上进行注释,提供有用、额外的信息。
说明:注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。
示例:如下注释意义不大。
// if receive_flag is TRUE
if (receive_flag)
而如下的注释则给出了额外有用的信息。
// if mtp receive a message from links
if (receive_flag)
【条款2-2-16 】注释格式尽量统一,建议使用“//”。
【条款2-2-17 】注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达。
说明:注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。
【条款2-2-18 】如果出现超长的程序块, 无法一屏显示,请在在程序块的结束行右方加注释标记,以表明某程序块的结束。
说明:当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。
示例:参见如下例子。
if (...)
{
// program code
while (index < MAX_INDEX)
{
// program code
} // end of while (index < MAX_INDEX) 指明该条while语句结束
} // end of if (...) // 指明是哪条if语句结束
第二节 注释方法
【条款2-2-1 】文件头(包含头文件.h和实现文件.c,.cpp)必须注释,其它文件若有必要也要注释,注释内容包含文件名称、简要说明、详细说明(可选)、作者、版本号、日期等,参见下面的格式。
/
/// COPYRIGHT NOTIC
/// Copy right (c) ${year}, Ax3soft Co., Ltd.
/// All rights reserved.
///
/// @file $URL$
/// @brief
///
///
// $Id$
// $LastChangedDate$
// $LastChangedRevision$
// $LastChangedBy$
/
【条款2-2-2 】命名空间也需要注释,包含简要说明和详细说明(可选),参见下面的格式:
/// @brief 简要说明
///
/// 详细说明至少两行,并且与简要说明之间留一行空格
Namespace test
{
}
【条款2-2-3 】类的定义也需要注释,包含简要说明和详细说明(可选),参见下面的格式:
//*******************************************************************
/// @brief 简要说明
///
/// 详细说明至少两行,并且与简要说明之间留一行空格
//*******************************************************************
Class CTest
{
public:
UINT m_nMode; ///< 测试方法
}
【条款2-2-3 】结构定义也需要注释,包含简要说明和详细说明(可选),参见下面的格式:
/// @brief 数据包头
///
/// 专用的数据包头,用于可靠的数据通信
typedef struct _sax2_packet_header
{
U32 nMagic; ///< 包头识别码'0xFFFEFDFC'
U32 nID; ///< 消息ID
U8 nVersion; ///< 版本号,当前是1
U8 nFlags; ///< 标志, 前4位表示版本,紧接着4位是
U32 nChecksum; ///< 校验和,采用与TCP相同计算方法,需要计算的数据包括包头和载荷数据
U32 nPayloadLen; ///< 携带的数据长度
U16 nPaddingLen; ///< 填充数据长度的长度
}SAX2_PACKET_HEADER;
【条款2-2-4 】枚举常量定义也需要注释,包含简要说明和详细说明(可选),参见下面的格式:
/// 服务器状态枚举定义
///
/// 该枚举定义了服务器事务对象可能处于的状态类型\n
enum NET_TRANSACTION_STATUS
{
SS_INVALID = 0, ///< 枚举,无效状态
SS_INITIALIZED, ///< 枚举,已经成功初始化
SS_RUNING, ///< 枚举,正在运行状态
SS_STOP, ///< 枚举,停止状态
};
【条款2-2-5 】函数头部应进行注释,列出:函数的目的/功能、输入参数、输出参数、返回值、使用注意,作者等信息。
/// @brief Abrief function description
//----------------------------------------------------------------
// @routine
/// @brief A brief function description.
///
/// A detailed description, it
/// should be 2 lines at least.
/// @param 可带描述选项【_in |_out |_inout】
/// @return 返回值说明
/// @note (可选项@warning @see )
/// @author
//---------------------------------------------------------------
【条款2-2-6 】类成员变量、结构成员、枚举常量成员、全局常量或变量,都在其右边使用 "///<"进行注释,全局变量还要遵守条款2-2-8
全局常量例子:
const DWORD TIME_SCAN_WORKING = 1000 * 60; ///< 工作时间扫描时钟
TIPS: 该注释中使用了doxygen工具提供的命令,目的是用该工具生成代码文档,详细的doxygen命令请参考附录。
第三节 注释的种类
注释代码的作用可以分为六种,对于完工的代码只允许有三种类型的注释,如下:
1、重复代码 (禁止)
用不同的文件把代码的工作有描述一遍。它除了增加读者阅读量外,没有其它好处。
2、解释代码 (禁止)
解释性注释通常用于解释复杂的、有巧的、敏感的代码快。通常是因为代码含混不清才使用它。使用它的代码正是需要改进的代码。
3、代码标记 (禁止)
标记性注释提醒开发者某处的工作没有完成或用户报告了某个代码错误。
4、概述代码 (允许)
将若干行代码的意思用一两句话说出来。
5、代码意图说明 (允许)
目的性注释用来指明一段代码的意图,它指出要解决的问题,而非解决问题的方法。比如:
—get current employee information这就是一条意图性注释,然而Update employeeRecord object则是有关解决方案的概述性注释。
传达代码无法表述的信息 (允许)
这类信息包含版权信息、保密信息、版本号等信息
第三章 命名
第1节 基本规则
【条款3-1-1 】标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解.
示例:如下单词的缩写能够被大家基本认可。
temp 可缩写为 tmp ;
flag 可缩写为 flg ;
statistic 可缩写为 stat ;
increment 可缩写为 inc ;
message 可缩写为 msg ;
【条款3-1-2 】避免使用具有相似含义的名字
如果你能够交换两个变量的名字而不妨碍对程序的理解,那么就需要为这两个标识符重新命名了,例如:fileNumber和fileIndex
【条款3-1-3 】避免使用具有不同含义却有相似名字的标识符
例如:clientRecs和clientReps,它们之间只有一个字符的差异,应该采用至少有两个不同字符的名字,或者把不同指出放在名字的开始或结尾处。clientRecords和clientReports比原来好。
【条款3-1-3 】避免使用发音相进的名字
例如: wrap和rap
【条款3-1-4 】命名规范必须与所使用的系统风格保持一致,并在同一项目中统一,比如采用UNIX的全小写加下划线的风格或大小写混排的方式,不要使用大小写与下划线混排的方式,用作特殊标识如标识成员变量或全局变量的m_和g_,其后加上大小写混排的方式是允许的。
示例: Add_User不允许,add_user、AddUser、m_AddUser允许
【条款3-1-5 】禁止在标识符中出现数字
示例:如下命名,使人产生疑惑。
#define _EXAMPLE_0_TEST_
#define _EXAMPLE_1_TEST_
void set_sls00( BYTE sls );
应改为有意义的单词命名
#define _EXAMPLE_UNIT_TEST_
#define _EXAMPLE_ASSERT_TEST_
void set_udt_msg_sls( BYTE sls );
【条款3-1-6 】在同一软件产品内,应规划好接口部分标识符(变量、结构、函数及常量)的命名,防止编译、链接时产生冲突。
说明:对接口部分的标识符应该有更严格限制,防止冲突。如可规定接口部分的变量与常量之前加上“模块”标识等。
【条款3-1-7 】用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
说明:下面是一些在软件中常用的反义词组。
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
示例:
int min_sum;
int max_sum;
int add_user( BYTE *user_name );
int delete_user( BYTE *user_name );
【条款3-1-8 】除了编译开关/头文件等特殊应用,应避免使用_EXAMPLE_TEST_之类以下划线开始和结尾的定义。
【条款3-1-9 】标识符的最佳长度是10到16个字符。
这项条款强调是,如果你检查自己写的代码时发现了很多更短的名字,那么你需要认真检查,确保这些名字含义足够清晰。
【条款3-1-10 】标识符禁止采用中英文混合的方式
不规范的例子:show_ci_pan_xin_xi()
改为:show_disk_info()
【条款3-1-11 】标识符中禁止出现错误的英文单词
【条款3-1-12 】命名中若使用特殊约定或缩写,则要有注释说明。
说明:应该在源文件的开始之处,对文件中所使用的缩写或约定,特别是特殊的缩写,进行必要的注释说明。
【条款3-1-13 】自己特有的命名风格,要自始至终保持一致,不可来回变化。
说明:个人的命名风格,在符合所在项目组或产品组的命名规则的前提下,才可使用。(即命名规则中没有规定到的地方才可有个人命名风格)。
【条款3-1-14 】如果变量或常量中包含有限定词,比如:Total,Sum,Average,Max,Min等,记住把它们加到名字的后面。
例如:RevenueTotal(总收入), RevenueAverage(平均收入)等。
例外:Num限定此的位置已经是约定俗成,在前面表示总数,在后面表示下标,比如:CustomerNum表示当前员工的序号,NumCustomers表示员工的总数。由于这样使用会带来麻烦,因此最好的办法就是不用,而使用Total表示总数,Index表示下标.
第四节 名称缩写
【条款3-2-1 】谨慎的使用缩写
【条款3-2-2 】缩写的一般原则:
1、 使用标准的缩写(列在字典中的那些缩写)
2、 去掉所有的非前置原音。(screen变为scrn, apple变为appl)
3、 去掉虚词and,or,the等
4、 使用名字中的每一个重要单词,最多不超过三个。
5、 去掉无用的后最—ing,en等。
6、 保留每个音节中最引人注意的发音。
7、 取保不要改变标识符的含义。
【条款3-2-2 】不要用从每个单词中删除一个字符的方式来缩写,要么把删除不止一个字符,要么就把单词写完整。
【条款3-2-3 】缩写要一致
例如:要么全用Num,要么全用NO,不要两个都用
【条款3-2-4 】创建你能读出来的名称
例如:用xPos而不用xPstn
【条款3-2-4 】避免使用容易看错或者读错的字符组合
例如:为了表示B的结尾,ENDB要比BEND好
【条款3-2-5 】避免使用令人误解的名字和缩写
要确保名字的含义是明确的,例如:FALSE常用用作TRUE的反义词,如果用它作为”Fig and Almond Season”的缩写就很糟糕。
第五节 常量命名规则
【条款3-3-1 】具名常量需要大写,单词之间用下画线分割
范例:
const DWORD TIME_SCAN_QQ_MSG = 2 * 1000;
【条款3-3-2 】枚举常量需要大写,单词之间用下画线分割,并且要有统一的前缀
范例:
enum SSIDE_SESSION_TYEP
{
SSIDE_ST_CTRL = 1,
SSIDE_ST_DATA = 2
};
【条款3-3-3 】常量名称应该表明常量的含义,而不是常量所具有的数值。
不规范代码示例:
Const UINT FIVE = 5;
Const UINT BAKERS_DOZEN = 1024;
正确代码示例:
Const UINT CYCLES_NEEDED = 6;
Const UINT BAKERS_MAX = 1024;
第六节 变量命名规则
【条款3-4-1 】一个合格的变量名称,要能充分体现其三要素:类型、作用域和意义,即见名知意。
【条款3-4-2 】对于变量命名,禁止取单个字符(如i、j、k…),但i、j、k作局部循环变量是允许的。
【条款3-4-3】如果循环出现嵌套,禁止使用i、j、k作为循环变量,目的防止下标串话和提高可读性。
不规范代码示例:
for(int i = 0; i < m_nCustomerTotal;i++)
{
for(int j = 0; j < m_nTaskTotal;j++)
{
}
}
正确代码示例:
for(int nCustomerIndex = 0; nCustomerIndex < m_nCustomerTotal;nCustomerIndex++)
{
for(int nTaskIndex = 0; nTaskIndex < m_nTaskTotal;nTaskIndex++)
{
}
}
【条款3-4-4】状态变量中不应该包含单词”flag”,因为我们从中丝毫看不出该标记是做什么的,为了清晰起见,我们应该使用枚举类型、具名常量来对其赋值,并且与这些值作比较:
不规范代码示例:
if (flag)...
if (statusFlag & 0x0F)...
if (printFlag == 0) ....
if (computerFlag == 0) ...
flag = 0x01;
statusFlag = 0x80;
printFlag = 16;
computerFlag = 0;
正确代码示例:
if (bDataReady)...
if (nCharacterType & PRINTABLE_CHAR)...
if (nReportType == REPORT_TYPE_ANUAL) ....
if (bRecalcNeeded == false) ...
bDataReady = false;
nCharacterType = CONTROL_CHARACTER;
nReportType = REPORT_TYPE_ANUAL;
nRecalcNeeded = false;
【条款3-4-5】禁止在函数中定义所谓的临时变量。
所谓的临时变量,它们常常被赋予temp,x或者其他一些模糊且缺乏描述性的名称,通常临时变量是一个信号,表明程序员还没有完全把问题搞清楚,而且,由于这些变量被正式的赋予了一种“临时”状态,成员会倾向于比其它变量更为随意的对待这些变量,从而增加了出错的可能。
【条款3-4-6】禁止使用否定的布儿变量,不推荐为布儿变量填加”is“前缀。
不规范代码示例:
BOOL bIsFound = FALSE;
BOOL bNotFound = FALSE;
正确代码示例:
BOOL bFound = FALSE;
【条款3-4-7】VC中变量的名称通常有三部分组成:作用域前缀+类型前缀+变量名称,局部变量和参数将省略了作用域前缀, 作用域前缀和类型前缀之间由下划""分隔,请看下面列子:
例子:
UINT m_nIP;
BOOL bResult = TRUE;
作用域前缀一览表
前缀 说明 例子
无 局部变量
m 类的成员变量(member) Int m_width
ms_ 类的静态成员变量(static member) static int ms_initValue;
s_ 静态变量(static) static int s_initValue;
g_ 外部全局变量(global) int g_howManyPeople;
sg_ 静态全局变量(static global)
gg_ 进程间共享的共享数据段全局变量(global global)
类型前缀一览表
数据类型 前缀 例子
Array arr arrText
BOOL b
UINT n
int i
short n
long l
WORD w
DWORD dw
float f
char c
char* psz
TCHAR* psz
LPCTSTR lpsz
CString str
COLORREF cr
LPLOGPALETTE lp
POINT pt
CPoint pt
HANDLE h
HGLOBAL h
double d
说明:
1.如果是枚举类型的变量或不在上表中的数据型变量,前缀采用"n";
2.如果是引用类型的参数,前缀采用"ref";
3.常规的类和结构体变量,前缀采用"obj";
4.窗口类型的变量,前缀采用"wnd";
第七节 函数命名规则
【条款3-7-1 】描述子程序所做的所有事情。
【条款3-7-2 】避免使用无意义的、模糊或表述不清的动词.
比如HandleCalculation(), PerformService(),OutputUser,ProcessInput和DealWithOutput就不能说明子程序是干什么的。
【条款3-7-3 】要对返回值有有所描述,如果函数有返回值,函数名称应该针对其返回值进行。
比如GetColor()就是不错的名称。
【条款3-7-4 】给过程起名时使用强烈语气的动词加宾语的形式,
如:PrintDocument()
第八节 联合体名规则
【条款3-8-1】联合语句生成的类型名由全大写字母组成,单词间通过下划线来界定,如:
typedef union unIPRange
{
ULONGLONG nIPPair;
struct
{
DWORD nIP1;
DWORD nIP2;
}IPS;
}IP_RANGE;
第九节 结构体名规则
【条款3-8-1】结构语句生成的类型名由全大写字母组成,单词间通过下划线来界定,如:
typedef struct tagDataHeader
{
U32 nID;
U32 nPayloadLen;
}DATA_HEADER;
第十节 类命名规则
第四章 编码
第一节 数据类型
【条款4-1-1】用具名常量代替程序中的神秘数字。
示例:如下的程序可读性差。
if (Trunk[index].trunk_state == 0)
{
Trunk[index].trunk_state = 2;
... // program code
}
应改为如下形式。
#define TRUNK_IDLE 0
#define TRUNK_BUSY 2
if (Trunk[index].trunk_state == TRUNK_IDLE)
{
Trunk[index].trunk_state = TRUNK_BUSY;
... // program code
}
一条很好的经验法则是:程序主体中仅能出现的数字是0或1。
【条款4-1-2】预防除零。
每次使用除法的时候都要检查分母是否可能为0;
【条款4-1-3】使用显示的类型转化
y = x + (float)i;
【条款4-1-4】避免混合类型比较,必须显示的转换后才能比较,具体怎么转换要根据实际情况
不规范例子:
int x = 0;
UINT y = 0;
…..
if (x == y)
{
}
范例子:
int x = 0;
UINT y = 0;
…..
if (x == (int )y)
{
}
【条款4-1-5】检查整数除法
整数除法的结果还是整数,所以当你执行10*(7/10)的计算时,其结果是0,而不是你所期待的结果。对此问题最简单的补救方法就是重新安排表达式的顺序。如安排成:(10*7)/10,结果就准确了。
【条款4-1-6】避免数量级相差巨大的数之间的加减运算
使用数量级相差巨大的数进行加减运算时,有可能不能得到你希望的结果,如果不可避免要使用这种运算,就先对这些数进行排序,然后从最小值开始把它们加起来,这样做虽然不能消除舍入问题,但是能使这一问题的影响减少到最低限度。
【条款4-1-7】避免等量判断
很多应该相等的浮点数值并不一定相等。这里的根本问题是,用两种不同的方法来求同一数值,结果不一定总得到同一个值。
因此应该找一种替代方案,一种有效的方法是先确定可接受的精度范围,然后用布尔函数判断数值是否足够接近。
错误例子:
double nNominal = 1.0;
double nSum = 0.0;
for (int i = 0; i < 10; i++)
{
nSum += 0.1;
}
if (nNominal == nSum)
{
}
应该改为:
double nNominal = 1.0;
double nSum = 0.0;
for (int i = 0; i < 10; i++)
{
nSum += 0.1;
}
if (abs(nNominal - nSum) < 0.00001)
{
}
【条款4-1-8】处理舍入误差问题
这里有处理舍入误差的常见方案
& 换一种精度更高的变量类型
& 换用二进制编码的十进制变量,即BCD模式。
& 把浮点变量变成整型变量。采用这种方法,需要自己处理数字的小数部分。
如果语言中有内置的处理敏感数值的类型,如VB种的Currency等等,就使用它。
【条款4-1-9】用具名常量来加以取代神秘字符和字符串
不规范例子:
if ( 0x1B == chInputChar)
应改为:
if ( ESCAPE == chInputChar)
【条款4-1-10】把C风格字符串长度声明为CONSTANT + 1
列如:
char szName[NAME_LENGTH_MAX + 1] = {0};
【条款4-1-11】复杂的条件判断,建议用布尔变量对程序加以文挡说明
目的不明的布尔判断
if( (nElementIndex < 0) || ( MAX_ELEMENTS < nElementIndex ) || ( nElementIndex == nLastElementIndex))
{
...
}
目的明确的布尔判断
const bool bFinished = ( (nElementIndex < 0) || ( MAX_ELEMENTS < nElementIndex ) );
const bool bRepeatedEntry = ( nElementIndex == nLastElementIndex);
if(bFinished || bRepeatedEntry)
{
...
}
【条款4-1-12】建议定义出枚举的第一项和最后一项,以便用于循环边界,并且把第一项留作非法值
例如:
enum Country
{
Country_InvalidFirst = 0 //非法值
Country_First = 1 //第一项
Country_China = 1
Country_England = 2
Country_France = 3
Country_Germany = 4
Country_India = 5
Country_Japan = 6
Country_Usa = 7
Country_Last = 7 //最后一项
};
注意:给枚举元素明确赋值而带来的失误。
【条款4-1-13】慎重重新定义预定义类型
【条款4-1-14】尽可能坚持谁分配谁释放的原则使用指针
在子程序中分配的指针,就在该子程序中释放;在类构造函数中分配的指针,就在该类的析构函数中释放。
【条款4-1-15】指针在使用前要检查合法性
包括地址是否合法,以及指向的数据是否合法
【条款4-1-16】采用标记字段来检查损毁的内存
例如:
typedef struct tagClientInfo
{
#ifdef _DEBUG
int nTag; //将其设为0Xcc
#endif
CString strName;
UINT nPhone;
}CLIENT_INFO;
if (0xCC == pClientInfo->nTag)
{
}
【条款4-1-17】用额外的指针变量来提高代码的可读性
晦涩的例子:
void InsertLink(Node* pCurrentNode,Node* pNewNode)
{
pNewNode->next = pCurrentNode->Next;
pNewNode->prev = pCurrentNode;
if (NULL != pCurrentNode->Next)
{
pCurrentNode->Next->prev = pNewNode; //很晦涩
}
pCurrentNode->next = pNewNode;
}
修改后的例子:
void InsertLink(Node* pCurrentNode,Node* pNewNode)
{
Node* pMidNode = pCurrentNode->Next;
pMidNode = pCurrentNode->Next;
pNewNode->prev = pCurrentNode;
if (NULL != pMidNode)
{
pMidNode->prev = pNewNode;
}
pCurrentNode->next = pNewNode;
}
【条款4-1-18】在删除指针之前检查指针是否合法,删除把它们设为空。
例如:
ASSERT(NULL != pNode);
delete pNode;
pNode = NULL;
【条款4-1-19】禁止局部变量与全局变量重名,或者同一子程序内出现重名的情况
不规范的例子1:
int nYear = 0;
void Function()
{
int nYear = 0; // 重名
}
不规范的例子2:
void Function()
{
int nYear = 0;
if(...)
{
int nYear = 0;// 重名
}
}
【条款4-1-20】使用访问器子程序来取代全局数据
例如:
int g_nYear = 0;
int GetYear() { return g_nYear; }
void SetYear(int nYear) { g_nYear = nYear;}
【条款4-1-21】结构的功能要单一,是针对一种事务的抽象。
说明:设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。
示例:如下结构不太清晰、合理。
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;
【条款4-1-22】不要设计面面俱到、非常灵活的数据结构。
说明:面面俱到、灵活的数据结构反而容易引起误解和操作困难。
【条款4-1-23】不同结构间的关系不要过于复杂。
说明:若两个结构间关系较复杂、密切,那么应合为一个结构。
示例:如下两个结构的构造不合理。
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;
【条款4-1-24】结构中元素的个数应适中。若结构中元素个数过多可考虑依据某种原则把元素组成不同的子结构,以减少原结构中元素的个数。
说明:增加结构的可理解性、可操作性和可维护性。
示例:假如认为如上的_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];
unsigned char city[15];
unsigned char tel;
} PERSON_ADDRESS;
typedef struct PERSON_STRU
{
PERSON_BASE_INFO person_base;
PERSON_ADDRESS person_addr;
} PERSON;
【条款4-1-25】仔细设计结构中元素的布局与排列顺序,使结构容易理解、节省占用空间,并减少引起误用现象。
说明:合理排列结构中元素顺序,可节省空间并增加可理解性。
示例:如下结构中的位域排列,将占较大空间,可读性也稍差。
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;
【条款4-1-25】尽量减少没有必要的数据类型默认转换与强制转换
【条款4-1-26】使用严格形式定义的、可移植的数据类型,尽量不要使用与具体硬件或软件环境关系密切的变量
说明:使用标准的数据类型,有利于程序的移植。
示例:如下例子(在DOS下BC3.1环境中),在移植时可能产生问题。
void main()
{
register int index; // 寄存器变量
_AX = 0x4000; // _AX是BC3.1提供的寄存器“伪变量”
... // program code
}
第二节 变量
【条款4-2-1】每行只申明一个数据
不规范的例子:
int nRow,nCol;
应改为:
int nRow = 0;
int nCol = 0;
【条款4-2-2】尽最大的可能在声明变量的时候初始化它,能用const则用之。
例如:
UINT nCurrentTime = 0;
const UINT COLOR = GetColor();
注意:局部常量建议用大写
【条款4-2-3】最小化变量的跨度。
例子1:
b = 0;
c = 0;
a = 0;
a = b + c;
例子2:
a = 0;
c = 0;
b = 0;
a = b + c;
如果从变量b的角度来考虑,例子2比例子1好,因为前者的跨度是2,后者是0
【条款4-2-4】最小化变量的存活时间
不规范的例子:
STDMETHODIMP GetDevicePropertyEntry(SSIDE_FIREWALL_TYPE newVal
,IIDEPropertyEntry** pVal)
{
CString strID;
if(NULL == pVal)
{
return E_POINTER;
}
switch(newVal)
{
….Other code
}
return S_OK;
}
改为
STDMETHODIMP GetDevicePropertyEntry(SSIDE_FIREWALL_TYPE newVal
,IIDEPropertyEntry** pVal)
{
if(NULL == pVal)
{
return E_POINTER;
}
CString strID;
switch(newVal)
{
….Other code
}
return S_OK;
}
关于变量strID,修改后的代码生存周期变短了
【条款4-2-5】开始采用最严格的可见性,然后根据需要扩展变量的作用域
Tips: 关于变量的作用域、跨度和存活时间
用域(score):可以看作一种衡量变量知名度的方法(作者诙谐的比喻),也称可见性(visibility),指变量在程序内可见和可引用的范围;
跨度(span):衡量一个变量的不同引用点的靠近程度,即两个引用点之间相隔的语句数;
存活时间(live time):指一个变量存在期间(即从变量声明到最后引用该变量的那条语句,跟作用域有点出入)所跨越的语句总数;
攻击窗口(window of vulnerability):指介于同一变量多个引用点之间的代码。
光是描述不够直观,给出截图:
很明显我们从三个不同的图中看到了不同的描述,即长、短两个修饰词。很明显是第三个图的代码质量是最高的,可能需要说明一下作者区分出这几个名词的意图才能说明情况。
1、从第二个图来看,当变量跨度过大时,攻击窗口也就越大。此时如果我们在变量的两个引用点之间的代码进行了某个操作,而这个操作可能会不小心或者不适当地修改了该变量的值,那么在下一个引用点中所用到的变量值将会是错误的。这也就是为什么这一范围内的代码被称为攻击窗口的原因。
为了减少这一类 bug 的发生,写代码时应该尽量局部化变量引用,将对某个变量的使用集中在一起,以缩小攻击窗口范围,也就是缩小变量的跨度。此外,在一个相对较集中的代码范围内使用某个变量,使得我们在阅读、复查代码时所关注的范围更小。任何人(包括写代码时)都不喜欢在读代码时目光不得不跳来跳去的搜索某个变量最近被赋值为什么了吧?
2、变量跨度这一概念一定是存在于变量存活时间的范围内的,那么同样的道理,缩短了存活时间也能够减小攻击窗口带来的威胁。短的变量存活时间除了使代码更具可读性,还可以使得我们在修改代码时的关注范围更小,有利于重构的进行。
譬如有时需要把原来顺序执行的代码改为循环结构时,存活时间短的变量就可以使得我们不容易忽略掉那些离循环结构太远的初始化代码,循环次数控制变量经常出错很多就是源于这一问题。
【条款4-2-6】为变量指定单一用途
坏例子:这里temp用到两个地方了。
temp = Sqrt( b*b - 4*a*c );
root[0] = ( -b + temp ) / ( 2*a );
root[1] = ( -b - temp ) / ( 2*a );
...
temp = root[0];
root[0] = root[1];
root[1] = temp;
修改后:不仅变量用于单一用途,而且可读性大大提高。
discriminant = Sqrt( b*b - 4*a*c );
root[0] = ( -b + discriminant ) / ( 2*a );
root[1] = ( -b - discriminant ) / ( 2*a );
...
oldRoot = root[0];
root[0] = root[1];
root[1] = oldRoot;
【条款4-2-7】对于不被改变的变量用const修饰
假如变量nNameLength 在接下来的代码中不被修改,那么下面的例子不规范:
unsigned int nNameLength = strlen(lpName);
改为:
const unsigned int nNameLength = strlen(lpName);
【条款4-2-8】慎用auto类型的变量
因为auto变量会降低程序的可读性,因此仅限于定义扫描容器是的叠代变量模板函数中无法确定类型的变量.
例子1:
auto itr = m_mapLMMConsumptionInfo.begin();
for (; m_mapLMMConsumptionInfo.end() != itr; itr++)
{
itr->second->SetEntireProjectNode(this);
}
例子2:
template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty)
{
return x*y;
}
第三节 编写直性代码
【条款4-3-1 】对于顺序相关的语句,可以用参数明确显示依赖关系
不明确的例子(前三句就看不出明显的先后顺序):
ComputeMarketingExpense();
ComputeSalesExpense();
CompusteTravelExpese();
DisplayExpensesSummary();
改为下面稍微好一点
ComputeMarketingExpense(pExpenseData);
ComputeSalesExpense(pExpenseData);
CompusteTravelExpese(pExpenseData);
DisplayExpensesSummary(pExpenseData);
【条款4-3-2 】对于顺序相关的语句,用注释对不清晰的依赖关系进行说明
可以为上面的例子填加注释,如下:
// 下面紧接着的四行代码的顺序不能改变,
ComputeMarketingExpense(pExpenseData);
ComputeSalesExpense(pExpenseData);
CompusteTravelExpese(pExpenseData);
DisplayExpensesSummary(pExpenseData);
【条款4-3-3 】对于顺序相关的语句,如果代码非常重要,可以用状态变量以及错误处理代码或断言来对依赖关系进行说明
请看下面的例如,类成员函数Initial,Start和Stop有严格的依赖关系,因此我们使用了状态变量m_nStatus:
class CNetServiceImpl
{
public:
CNetServiceImpl(void)
: m_nStatus(SS_INVALID)
{
}
public:
BOOL Initial()
{
m_nStatus = SS_INITIALIZED;
return (INVALID_SOCKET != m_pServiceSocket);
}
BOOL Start()
{
ATLASSERT(SS_INITIALIZED == m_nStatus || SS_STOP == m_nStatus);
if (SS_INITIALIZED != m_nStatus && SS_STOP != m_nStatus)
{
return FALSE;
}
}
void Stop()
{
ATLASSERT(SS_RUNING == m_nStatus);
if (SS_RUNING != m_nStatus )
{
return;
}
m_nStatus = SS_STOP;
}
private:
NET_TRANSACTION_STATUS m_nStatus; ///< 服务器状态
};
【条款4-3-4 】对于顺序无关的语句,使代码易于自上而下阅读
示例:如下例子不符合规范
CMarketingData objMarketingData;
CSalesData objSalesData;
objSalesData.AccountQuererly();
objMarketingData.AccountQuererly();
objMarketingData.Print();
objSalesData.Print();
应如下书写:
CMarketingData objMarketingData;
objMarketingData.AccountQuererly();
objMarketingData.Print();
CSalesData objSalesData;
objSalesData.AccountQuererly();
objSalesData.Print();
第四节 条件语句
【条款4-4-1 】把正常情况的代码放在if后面
不规范的例子:
if (NULL == pNode)
{
return FALSE;
}
else
{
pNode->CaleData();
pNode->Display();
}
修改为如下:
if (NULL != pNode)
{
pNode->CaleData();
pNode->Display();
}
else
{
return FALSE;
}
【条款4-4-2 】if子句后面应该跟一个有意义的语句
不规范的例子:
if (NULL == pNode)
{
;
}
else
{
pNode->CaleData();
pNode->Display();
}
应该修改为
if (NULL != pNode)
{
pNode->CaleData();
pNode->Display();
}
【条款4-4-3 】尽可能编写eles子句,那怕是一个空语句,它起码可以表明我们已经考虑这种情况
例如:
// if color is valid
if ( COLOR_MIN <= color && color <= COLOR_MAX)
{
// do something
...
}
else
{
// else color is invalid
// screen not written to -- safely ignore command
}
【条款4-4-4 】利用布尔函数简化复杂的检测
比较复杂的例子:
if (' ' == chCurrent
|| ',' == chCurrent
|| '.' == chCurrent
|| '!' == chCurrent)
{
nType = TYPE_PUNCTUATION;
}
修改如下:
if (IsPunctuation(chCurrent) )
{
nType = TYPE_PUNCTUATION;
}
【条款4-4-5 】为case语句选择最有效的排列顺序
1、 按字符或数字排列各种情况
2、 把正常的情况放在最前面
3、 按执行频率排列case字句
【条款4-4-6 】简化每种情况对应的操作
不规范的例子:
switch (pPacket->GetPacketType())
{
case MT_QUERY_ONLINE:
{
// 许多代码,比如大于5行
…
}
break;
case MT_LOGIN:
{
// 许多代码,比如大于5行
…
}
break;
}
应该为:
switch (pPacket->GetPacketType())
{
case MT_QUERY_ONLINE:
bResult = OnQueryOnline(pEndpoint, pPacket);
break;
case MT_LOGIN:
bResult = OnLogin(pEndpoint, pPacket);
break;
}
【条款4-4-7 】把default子句用于检查真正的默认情况
假设目前用户的输入只能是a,b,c三种情况,我们可能写出下面不规范的代码,这样我们将失去case语句标号的说明功能,也丧失了default字句检测错误的能力,如果有一天用户的输入增加后,这个代码修改起来也比较困难。
switch(chInput)
{
case 'a'
break;
case 'b':
break;
default: // 把c作为了默认情况
}
【条款4-4-8 】把default子句用于检测错误
把上面的代码修改为:
switch(chInput)
{
case 'a'
break;
case 'b':
break;
case 'c':
break;
default:
DispalyError("无效的输入!");
}
第五节 循环语句
【条款4-5-1 】禁止带半循环的while循环语句
不规范的代码例子:
score = 0;
GetNextRating(&ratingIncrement);
rating = rating + ratingIncrement;
while (score < targetScore)
{
GetNextScore(&ScoreIncrement);
score= score+ ScoreIncrement;
GetNextRating(&ratingIncrement); // 重复代码
rating = rating + ratingIncrement;
}
应该修改为:
score = 0;
while (true)
{
GetNextScore(&ScoreIncrement);
score= score+ ScoreIncrement;
if (!((score < targetScore) && (ScoreIncrement != 0)))
{
break;
}
GetNextRating(&ratingIncrement);
rating = rating + ratingIncrement;
}
【条款4-5-2 】禁止在for循环里面通过直接修改下标来结束循环,这种情况应该该用while循环
不规范的例子
for (int i = 0; i < 100; ++i)
{
if ( ... )
i = 100;
// ...
}
应该改为:
int i = 0;
while(i < 100)
{
if(...)
{
break;
}
…
}
【条款4-5-3 】禁止把while循环体填充到for循环头中
不规范的例子,会让人误以为是nRecordCount在控制循环终止:
// 从文件中阅读所有的记录
for (objFile.MoveToStart(),nRecordCount = 0;!objFile.EndOfFile();nRecordCount++)
{
objFile.GetRecord();
}
应该改为:
nRecordCount = 0;
objFile.MoveToStart();
while (!objFile.EndOfFile())
{
objFile.GetRecord();
nRecordCount++
}
【条款4-5-4 】避免空循环
不规范的例子:
while ( (ch = getchar()) != EOF)
{
;
}
应该修改为:
do
{
ch = getchar();
} while (ch != EOF);
【条款4-5-5 】循环内务(用于控制循环)要么放在循环头,要么放在循环尾
规范例子:
nNameCount = 0;
nTotalLength = 0;
while(!objFile.EndOfFile())
{
// do the work of loop
objFile >> strInputString;
arrNames[nNameCount] = strInputString;
...
// prepare for the next pass through the loop -- housekeeping
nNameCount++l;
nTotalLength += nTotalLength + nTotalLength.GetLength();
}
【条款4-5-6 】一个循环只做一件事
【条款4-5-7 】避免出现依赖于循环下标最终取值的代码
不规范的例子:
for (recordCount = 0; recordCount < MAX_RECORDS; ++recordCount)
{
if ( entry[recordCount] == testValue )
{
break;
}
}
return (recordCount < MAX_RECORDS);
应该修改为:
bool bFound = false;
for (recordCount = 0; recordCount < MAX_RECORDS; ++recordCount)
{
if (entry[recordCount] == testValue)
{
bFound = true;
break;
}
}
【条款4-5-8 】把循环嵌套限制在3层以内
【条款4-5-9 】尽量把退出循环的条件写在一起
第六节 不常见的控制结构
【条款4-6-1 】如果能够增加可读性,就用return。
不推荐在函数的正文中使用return,返回函数的期望值,最好是由最后一个return返回,仅推荐两种情况下使用。
第一种:如果参数不符要求,立即返回,示例:
if(NULL == pVal)
{
return E_POINTER;
}
第二种情况,用防卫子句来简化复杂的错误处理,如果代码中必须要在执行正文之前做大量的错误条件检测,就可能导致代码的缩进层次过深,并且遮蔽正常情况的执行路径,如下所示。
if (objFile.ValidName())
{
if ( objFile.Open())
{
if ( objEncryption.Valid())
{
// some other codes
...
}
}
}
修改为,可以看到代码很整洁:
if (!objFile.ValidName())
{
return;
}
if ( !objFile.Open())
{
return;
}
if ( !objEncryption.Valid())
{
return;
}
// some other codes
…
【条款4-6-2 】不推荐在循环语句中使用return
不规范示例:
while(...)
{
if(...)
{
return;
}
}
修改为:
while(...)
{
if(...)
{
break;
}
}
return;
【条款4-6-3 】把递归限制在一个子程序里面
函数A调用函数B, B调用函数C, C调用函数A非常危险很难被检查。
不规范的示例:
void C()
{
A();
}
void B()
{
C();
}
void A()
{
B();
}
【条款4-6-4 】慎用goto语句,最后不要使用,
第七节 宏
【条款4-7-1 】用宏定义表达式时,要使用完备的括号。
示例:如下定义的宏都存在一定的风险。
#define OBJOBJRECTANGLE_AREA( a, b ) a * b
#define OBJOBJRECTANGLE_AREA( a, b ) (a * b)
#define OBJOBJRECTANGLE_AREA( a, b ) (a) * (b)
正确的定义应为:
#define OBJOBJRECTANGLE_AREA( a, b ) ((a) * (b))
【条款4-7-2 】将宏所定义的多条表达式放在大括号中。
示例:下面的语句只有宏的第一条表达式被执行。为了说明问题,for语句的书写稍不符规范。
#define INTI_OBJOBJRECT_VALUE( a, b )\
a = 0;\
b = 0;
for (index = 0; index < OBJOBJRECT_TOTAL_NUM; index++)
INTI_OBJOBJRECT_VALUE( objObjRect.a, objObjRect.b );
正确的用法应为:
#define INTI_OBJOBJRECT_VALUE( a, b )\
{\
a = 0;\
b = 0;\
}
for (index = 0; index < OBJOBJRECT_TOTAL_NUM; index++)
{
INTI_OBJOBJRECT_VALUE( objObjRect[index].a, objObjRect[index].b );
}
【条款4-7-2 】使用宏时,不允许参数发生变化。
示例:如下用法可能导致错误。
#define SQUARE( a ) ((a) * (a))
int a = 5;
int b;
b = SQUARE( a++ ); // 结果:a = 7,即执行了两次增1。
正确的用法是:
b = SQUARE( a );
a++; // 结果:a = 6,即只执行了一次增1。
第八节 函数
【条款4-8-1 】函数的参数个数不能超过7个
【条款4-8-2 】函数的参数应该按 “输入-修改-输出”的顺序排列
例如:
void PrintPageNumber(int nPageNumber,StatusType &refStatus);
【条款4-8-3 】如果几个函数都使用了类似的参数,应该让这些参数的排列顺序保持一致
【条款4-8-4 】不要把函数的参数用作工作变量
示例:下函数的实现不太好。
void sum_data( unsigned int num, int *data, int *sum )
{
unsigned int count;
*sum = 0;
for (count = 0; count < num; count++)
{
*sum += data[count]; // sum成了工作变量,不太好。
}
}
若改为如下,则更好些。
void sum_data( unsigned int num, int *data, int *sum )
{
unsigned int count ;
int sum_temp;
sum_temp = 0;
for (count = 0; count < num; count ++)
{
sum_temp += data[count];
}
*sum = sum_temp;
}
【条款4-8-5 】对所调用函数的错误返回码要仔细、全面地处理。
【条款4-8-6 】明确函数功能,精确(而不是近似)地实现函数设计。
【条款4-8-7 】编写可重入函数时,应注意局部变量的使用(如编写C/C++语言的可重入函数时,应使用auto即缺省态局部变量或寄存器变量)。
说明:编写C/C++语言的可重入函数时,不应使用static局部变量,否则必须经过特殊处理,才能使函数具有可重入性。
【条款4-8-8 】编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。
说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。
示例:假设Exam是int型全局变量,函数Squre_Exam返回Exam平方值。那么如下函数不具有可重入性。
unsigned int example( int para )
{
unsigned int temp;
Exam = para; // (**)
temp = Square_Exam( );
return temp;
}
此函数若被多个进程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的进程可能正好被激活,那么当新激活的进程执行到此函数时,将使Exam赋与另一个不同的para值,所以当控制重新回到“temp = Square_Exam( )”后,计算出的temp很可能不是预想中的结果。此函数应如下改进。
unsigned int example( int para )
{
unsigned int temp;
[申请信号量操作] // 若申请不到“信号量”,说明另外的进程正处于
Exam = para; // 给Exam赋值并计算其平方过程中(即正在使用此
temp = Square_Exam( ); // 信号),本进程必须等待其释放信号后,才可继
[释放信号量操作] // 续执行。若申请到信号,则可继续执行,但其
// 它进程必须等待本进程释放信号量后,才能再使
// 用本信号。
return temp;
}
【条款4-8-9 】一个函数仅完成一件功能。
【条款4-8-10 】为简单功能编写函数。
说明:虽然为仅用一两行就可完成的功能去编函数好象没有必要,但用函数可使功能明确化,增加程序可读性,亦可方便维护、测试。
示例:如下语句的功能不很明显。
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);
【条款4-8-11 】不要设计多用途面面俱到的函数。
说明:多功能集于一身的函数,很可能使函数的理解、测试、维护等变得困难。
【条款4-8-12 】函数的功能应该是可以预测的,也就是只要输入数据相同就应产生同样的输出。
说明:带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C/C++语言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测,然而,当某函数的返回值为指针类型时,则必须是STATIC的局部变量的地址作为返回值,若为AUTO类,则返回为错针。
示例:如下函数,其返回值(即功能)是不可预测的。
unsigned int integer_sum( unsigned int base )
{
unsigned int index;
static unsigned int sum = 0; // 注意,是static类型的。
// 若改为auto类型,则函数即变为可预测。
for (index = 1; index <= base; index++)
{
sum += index;
}
return sum;
}
【条款4-8-13 】尽量不要编写依赖于其他函数内部实现的函数。
说明:此条为函数独立性的基本要求。由于目前大部分高级语言都是结构化的,所以通过具体语言的语法要求与编译器功能,基本就可以防止这种情况发生。但在汇编语言中,由于其灵活性,很可能使函数出现这种情况。
示例:如下是在DOS下TASM的汇编程序例子。过程Print_Msg的实现依赖于Input_Msg的具体实现,这种程序是非结构化的,难以维护、修改。
... // 程序代码
proc Print_Msg // 过程(函数)Print_Msg
... // 程序代码
jmp LABEL
... // 程序代码
endp
proc Input_Msg // 过程(函数)Input_Msg
... // 程序代码
LABEL:
... // 程序代码
endp
【条款4-8-14 】不使用的参数从接口中去掉。
说明:目的减少函数间接口的复杂度。
【条款4-8-15 】非调度函数应减少或防止控制参数,尽量只使用数据参数。
说明:本建议目的是防止函数间的控制耦合。调度函数是指根据输入的消息类型或控制命令,来启动相应的功能实体(即函数或过程),而本身并不完成具体功能。控制参数是指改变函数功能行为的参数,即函数要根据此参数来决定具体怎样工作。非调度函数的控制参数增加了函数间的控制耦合,很可能使函数间的耦合度增大,并使函数的功能不唯一。
示例:如下函数构造不太合理。
int add_sub( int a, int b, unsigned char add_sub_flg )
{
if (add_sub_flg == INTEGER_ADD)
{
return (a + b);
}
else
{
return (a b);
}
}
不如分为如下两个函数清晰。
int add( int a, int b )
{
return (a + b);
}
int sub( int a, int b )
{
return (a b);
}
【条款4-8-16 】检查函数所有参数输入的有效性,特别是指针。
如果是非共有函数的指针参数需要断言,也要检测有效性,否则只需要断言即可.
例子1: SaveName是共有方法,应该如下检查参数
void SaveName(const char* lpName)
{
ASSET(NULL != lpName);
if(NULL != lpName)
{
write(pFile, lpName);
}
}
例子2: SaveName是非共有方法,应该如下检查参数
void SaveName(const char* lpName)
{
ASSET(NULL != lpName);
write(pFile, lpName);
}
【条款4-8-17 】检查函数所有非参数输入的有效性,如数据文件、公共变量等。
说明:函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入之前,应进行必要的检查。
【条款4-8-18 】函数的返回值要清楚、明了,让使用者不容易忽视错误情况。
说明:函数的每种出错返回值的意义要清晰、明了、准确,防止使用者误用、理解错误或忽视错误返回码。
【条款4-8-19 】除非必要,最好不要把与函数返回值类型不同的变量,以编译系统默认的转换方式或强制的转换方式作为返回值返回。
【条款4-8-20 】在调用函数填写参数时,应尽量减少没有必要的默认数据类型转换或强制数据类型转换。
说明:因为数据类型转换或多或少存在危险。
【条款4-8-21 】避免函数中不必要语句,防止程序中的垃圾代码。
说明:程序中的垃圾代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦。
【条款4-8-22 】防止把没有关联的语句放到一个函数中。
说明:防止函数或过程内出现随机内聚。随机内聚是指将没有关联或关联很弱的语句放到同一个函数或过程中。随机内聚给函数或过程的维护、测试及以后的升级等造成了不便,同时也使函数或过程的功能不明确。使用随机内聚函数,常常容易出现在一种应用场合需要改进此函数,而另一种应用场合又不允许这种改进,从而陷入困境。
在编程时,经常遇到在不同函数中使用相同的代码,许多开发人员都愿把这些代码提出来,并构成一个新函数。若这些代码关联较大并且是完成一个功能的,那么这种构造是合理的,否则这种构造将产生随机内聚的函数。
示例:如下函数就是一种随机内聚。
void Init_Var( void )
{
ObjObjRect.nLength= 0;
ObjObjRect.nNWidth = 0; /* 初始化矩形的长与宽 */
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标 */
}
矩形的长、宽与点的坐标基本没有任何关系,故以上函数是随机内聚。
应如下分为两个函数:
void Init_ObjObjRect( void )
{
ObjObjRect.nLength= 0;
ObjObjRect.nNWidth = 0; /* 初始化矩形的长与宽 */
}
void Init_Point( void )
{
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标 */
}
【条款4-8-23 】如果多段代码重复做同一件事情,那么在函数的划分上可能存在问题。
说明:若此段代码各语句之间有实质性关联并且是完成同一件功能的,那么可考虑把此段代码构造成一个新的函数。
【条款4-8-24 】功能不明确较小的函数,特别是仅有一个上级函数调用它时,应考虑把它合并到上级函数中,而不必单独存在。
说明:模块中函数划分的过多,一般会使函数间的接口变得复杂。所以过小的函数,特别是扇入很低的或功能不明确的函数,不值得单独存在。
【条款4-8-25 】设计高扇入、合理扇出(小于7)的函数。
说明:扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。
扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,如总是1,表明函数的调用层次可能过多,这样不利程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。函数较合理的扇出(调度函数除外)通常是3-5。扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。
扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。
较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。
【条款4-8-26 】改进模块中函数的结构,降低函数间的耦合度,并提高函数的独立性以及代码可读性、效率和可维护性。优化函数结构时,要遵守以下原则:
(1)不能影响模块功能的实现。
(2)仔细考查模块或函数出错处理及模块的性能要求并进行完善。
(3)通过分解或合并函数来改进软件结构。
(4)考查函数的规模,过大的要进行分解。
(5)降低函数间接口的复杂度。
(6)不同层次的函数调用要有较合理的扇入、扇出。
(7)函数功能应可预测。
(8)提高函数内聚。(单一功能的函数内聚最高)
说明:对初步划分后的函数结构应进行改进、优化,使之更为合理。
【条款4-8-27 】在多任务操作系统的环境下编程,要注意函数可重入性的构造。
说明:可重入性是指函数可以被多个任务进程调用。在多任务操作系统中,函数是否具有可重入性是非常重要的,因为这是多个进程可以共用此函数的必要条件。另外,编译器是否提供可重入函数库,与它所服务的操作系统有关,只有操作系统是多任务时,编译器才有可能提供可重入函数库。如DOS下BC和MSC等就不具备可重入函数库,因为DOS是单用户单任务操作系统。
【条款4-8-28 】对于提供了返回值的函数,在引用时最好使用其返回值。
返回值应该命名为:前缀+Result
【条款4-8-29 】当一个过程(函数)中对较长变量(一般是结构的成员)有较多引用时,可以用一个意义相当的宏代替。
说明:这样可以增加编程效率和程序的可读性。
示例:在某过程中较多引用TheReceiveBuffer[FirstSocket].byDataPtr,
则可以通过以下宏定义来代替:
【条款4-8-30 】函数的复杂度不要超过10
如何度量程序的复杂度
《代码大全》458页中有提到一个方法可以帮助开发者度量代码的复杂度,此方法最先由 Tom McCabe 提出,通过计算子程序中“决策点”的数量来衡量复杂度,步骤如下:
从 1 开始,一直往下通过程序,一旦遇到以下关键字,或者其同类的词,就加1;(if、while、for、foreach、and、or、&&、||)给 case 语句中的每一种情况加 1;
举个例子:
if ((status && done) || (notDone && (numLines >= maxLines)))
{
// code …
}
在这段代码中,从 1 算起,遇到 if 得 2,&& 得 3,|| 得4,&& 得 5。加起来,这段代码里总共包含了 5 个决策
【条款4-8-31 】用const修饰不被改变的指针参数
不规范的例子:
void SaveName(char* lpName)
{
write(pFile, lpName);
}
应该修改为:
void SaveName(const char* lpName)
{
write(pFile, lpName);
}
7 可测性
¹ 7-1:在同一项目组或产品组内,要有一套统一的为集成测试与系统联调准备的调测开关及相应打印函数,并且要有详细的说明。
说明:本规则是针对项目组或产品组的。
¹ 7-2:在同一项目组或产品组内,调测打印出的信息串的格式要有统一的形式。信息串中至少要有所在模块名(或源文件名)及行号。
说明:统一的调测信息格式便于集成测试。
¹ 7-3:编程的同时要为单元测试选择恰当的测试点,并仔细构造测试代码、测试用例,同时给出明确的注释说明。测试代码部分应作为(模块中的)一个子模块,以方便测试代码在模块中的安装与拆卸(通过调测开关)。
说明:为单元测试而准备。
¹ 7-4:在进行集成测试/系统联调之前,要构造好测试环境、测试项目及测试用例,同时仔细分析并优化测试用例,以提高测试效率。
说明:好的测试用例应尽可能模拟出程序所遇到的边界值、各种复杂环境及一些极端情况等。
¹ 7-5:使用断言来发现软件问题,提高代码可测性。
说明:断言是对某种假设条件进行检查(可理解为若条件成立则无动作,否则应报告),它可以快速发现并定位软件问题,同时对系统错误进行自动报警。断言可以对在系统中隐藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性。实际应用时,可根据具体情况灵活地设计断言。
示例:下面是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__ )
#else // 若不使用断言测试
#define EXAM_ASSERT(condition) NULL
#endif /* end of ASSERT *
/
¹ 7-6:用断言来检查程序正常运行时不应发生但在调测时有可能发生的非法情况。
¹ 7-7:不能用断言来检查最终产品肯定会出现且必须处理的错误情况。
说明:断言是用来处理不应该发生的错误情况的,对于可能会发生的且必须处理的情况要写防错程序,而不是断言。如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。
¹ 7-8:对较复杂的断言加上明确的注释。
说明:为复杂的断言加注释,可澄清断言含义并减少不必要的误用。
¹ 7-9:用断言确认函数的参数。
示例:假设某函数参数中有一个指针,那么使用指针前可对它检查,如下。
int exam_fun( unsigned char *str )
{
EXAM_ASSERT( str != NULL ); // 用断言检查“假设指针不为空”这个条件
... //other program code
}
¹ 7-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
}
¹ 7-11:用断言对程序开发环境(OS/Compiler/Hardware)的假设进行检查。
说明:程序运行时所需的软硬件环境及配置要求,不能用断言来检查,而必须由一段专门代码处理。用断言仅可对程序开发环境中的假设及所配置的某版本软硬件是否具有某种功能的假设进行检查。如某网卡是否在系统运行环境中配置了,应由程序中正式代码来检查;而此网卡是否具有某设想的功能,则可由断言来检查。
对编译器提供的功能及特性假设可用断言检查,原因是软件最终产品(即运行代码或机器码)与编译器已没有任何直接关系,即软件运行过程中(注意不是编译过程中)不会也不应该对编译器的功能提出任何需求。
示例:用断言检查编译器的int型数据占用的内存空间是否为2,如下。
EXAM_ASSERT( sizeof( int ) == 2 );
¹ 7-12:正式软件产品中应把断言及其它调测代码去掉(即把有关的调测开关关掉)。
说明:加快软件运行速度。
¹ 7-13:在软件系统中设置与取消有关测试手段,不能对软件实现的功能等产生影响。
说明:即有测试代码的软件和关掉测试代码的软件,在功能行为上应一致。
¹ 7-14:用调测开关来切换软件的DEBUG版和正式版,而不要同时存在正式版本和DEBUG版本的不同源文件,以减少维护的难度。
¹ 7-15:软件的DEBUG版本和发行版本应该统一维护,不允许分家,并且要时刻注意保证两个版本在实现功能上的一致性。
½ 7-1:在编写代码之前,应预先设计好程序调试与测试的方法和手段,并设计好各种调测开关及相应测试代码如打印函数等。
说明:程序的调试与测试是软件生存周期中很重要的一个阶段,如何对软件进行较全面、高率的测试并尽可能地找出软件中的错误就成为很关键的问题。因此在编写源代码之前,除了要有一套比较完善的测试计划外,还应设计出一系列代码测试手段,为单元测试、集成测试及系统联调提供方便。
½ 7-2:调测开关应分为不同级别和类型。
说明:调测开关的设置及分类应从以下几方面考虑:针对模块或系统某部分代码的调测;针对模块或系统某功能的调测;出于某种其它目的,如对性能、容量等的测试。这样做便于软件功能的调测,并且便于模块的单元测试、系统联调等。
½ 7-3:编写防错程序,然后在处理错误之后可用断言宣布发生错误。
示例:假如某模块收到通信链路上的消息,则应对消息的合法性进行检查,若消息类别不是通信协议中规定的,则应进行出错处理,之后可用断言报告,如下例。
#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;
}
}
8 程序效率
¹ 8-1:编程时要经常注意代码的效率。
说明:代码效率分为全局效率、局部效率、时间效率及空间效率。全局效率是站在整个系统的角度上的系统效率;局部效率是站在模块或函数角度上的效率;时间效率是程序处理输入任务所需的时间长短;空间效率是程序所需内存空间,如机器代码空间大小、数据空间大小、栈空间大小等。
¹ 8-2:在保证软件系统的正确性、稳定性、可读性及可测性的前提下,提高代码效率。
说明:不能一味地追求代码效率,而对软件的正确性、稳定性、可读性及可测性造成影响。
¹ 8-3:局部效率应为全局效率服务,不能因为提高局部效率而对全局效率造成影响。
¹ 8-4:通过对系统数据结构的划分与组织的改进,以及对程序算法的优化来提高空间效率。
说明:这种方式是解决软件空间效率的根本办法。
示例:如下记录学生学习成绩的结构不合理。
typedef unsigned char BYTE;
typedef unsigned short WORD;
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;
¹ 8-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 */
½ 8-1:仔细分析有关算法,并进行优化。
½ 8-2:仔细考查、分析系统及模块处理输入(如事务、消息等)的方式,并加以改进。
½ 8-3:对模块中函数的划分及组织方式进行分析、优化,改进模块中函数的组织结构,提高程序效率。
说明:软件系统的效率主要与算法、处理任务方式、系统功能及函数结构有很大关系,仅在代码上下功夫一般不能解决根本问题。
½ 8-4:编程时,要随时留心代码效率;优化代码时,要考虑周全。
½ 8-5:不应花过多的时间拼命地提高调用不很频繁的函数代码效率。
说明:对代码优化可提高效率,但若考虑不周很有可能引起严重后果。
½ 8-6:要仔细地构造或直接用汇编编写调用频繁或性能要求极高的函数。
说明:只有对编译系统产生机器码的方式以及硬件系统较为熟悉时,才可使用汇编嵌入方式。嵌入汇编可提高时间及空间效率,但也存在一定风险。
½ 8-7:在保证程序质量的前提下,通过压缩代码量、去掉不必要代码以及减少不必要的局部和全局变量,来提高空间效率。
说明:这种方式对提高空间效率可起到一定作用,但往往不能解决根本问题。
½ 8-8:在多重循环中,应将最忙的循环放在最内层。
说明:减少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];
}
}
½ 8-9:尽量减少循环嵌套层次。
½ 8-10:避免循环体内含判断语句,应将循环语句置于判断语句的代码块之中。
说明:目的是减少判断次数。循环体中的判断语句是否可以移到循环体外,要视程序的具体情况而言,一般情况,与循环变量无关的判断语句可以移到循环体外,而有关的则不可以。
示例:如下代码效率稍低。
for (ind = 0; ind < MAX_OBJOBJRECT_NUMBER; ind++)
{
if (data_type == OBJOBJRECT_AREA)
{
area_sum += objObjRect_area[ind];
}
else
{
objObjRect_length_sum += objObjRect[ind].length;
objObjRect_nNWidth_sum += objObjRect[ind].nNWidth;
}
}
因为判断语句与循环变量无关,故可如下改进,以减少判断次数。
if (data_type == OBJOBJRECT_AREA)
{
for (ind = 0; ind < MAX_OBJOBJRECT_NUMBER; ind++)
{
area_sum += objObjRect_area[ind];
}
}
else
{
for (ind = 0; ind < MAX_OBJOBJRECT_NUMBER; ind++)
{
objObjRect_length_sum += objObjRect[ind].length;
objObjRect_nNWidth_sum += objObjRect[ind].nNWidth;
}
}
½ 8-11:尽量用乘法或其它方法代替除法,特别是浮点运算中的除法。
说明:浮点运算除法要占用较多CPU资源。
示例:如下表达式运算可能要占较多CPU资源。
#define PAI 3.1416
radius = circle_nLength/ (2 * PAI);
应如下把浮点除法改为浮点乘法。
#define PAI_RECIPROCAL (1 / 3.1416 ) // 编译器编译时,将生成具体浮点数
radius = circle_nLength* PAI_RECIPROCAL / 2;
½ 8-12:不要一味追求紧凑的代码。
说明:因为紧凑的代码并不代表高效的机器码。
9 质量保证
¹ 9-1:在软件设计过程中构筑软件质量。
¹ 9-2:代码质量保证优先原则
(1)正确性,指程序要实现设计要求的功能。
(2)稳定性、安全性,指程序稳定、可靠、安全。
(3)可测试性,指程序要具有良好的可测试性。
(4)规范/可读性,指程序书写风格、命名规则等要符合规范。
(5)全局效率,指软件系统的整体效率。
(6)局部效率,指某个模块/子模块/函数的本身效率。
(7)个人表达方式/个人方便性,指个人编程习惯。
¹ 9-3:只引用属于自己的存贮空间。
说明:若模块封装的较好,那么一般不会发生非法引用他人的空间。
¹ 9-4:防止引用已经释放的内存空间。
说明:在实际编程过程中,稍不留心就会出现在一个模块中释放了某个内存块(如C语言指针),而另一模块在随后的某个时刻又使用了它。要防止这种情况发生。
¹ 9-5:过程/函数中分配的内存,在过程/函数退出之前要释放。
¹ 9-6:过程/函数中申请的(为打开文件而使用的)文件句柄,在过程/函数退出之前要关闭。
说明:分配的内存不释放以及文件句柄不关闭,是较常见的错误,而且稍不注意就有可能发生。这类错误往往会引起很严重后果,且难以定位。
示例:下函数在退出之前,没有把分配的内存释放。
typedef unsigned char BYTE;
int ExampleFunction( 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 nLengtherror */
if (gt_len > MAX_GT_LENGTH)
{
return GT_LENGTH_ERROR; // 忘了释放gt_buf
}
... // other program code
}
应改为如下。
int ExampleFunction( BYTE gt_len, BYTE *gt_code )
{
BYTE *gt_buf;
gt_buf = (BYTE * ) malloc ( MAX_GT_NLENGTH);
... // program code, include check gt_buf if or not NULL.
/* global title nLengtherror */
if (gt_len > MAX_GT_LENGTH)
{
free( gt_buf ); // 退出之前释放gt_buf
return GT_LENGTH_ERROR;
}
... // other program code
}
¹ 9-7:防止内存操作越界。
说明:内存操作主要是指对数组、指针、内存地址等的操作。内存操作越界是软件系统主要错误之一,后果往往非常严重,所以当我们进行这些操作时一定要仔细小心。
示例:假设某软件系统最多可由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;
}
}
¹ 9-8:认真处理程序所能遇到的各种出错情况。
¹ 9-9:系统运行之初,要初始化有关变量及运行环境,防止未经初始化的变量被引用。
¹ 9-10:系统运行之初,要对加载到系统中的数据进行一致性检查。
说明:使用不一致的数据,容易使系统进入混乱状态和不可知状态。
¹ 9-11:严禁随意更改其它模块或系统的有关设置和配置。
说明:编程时,不能随心所欲地更改不属于自己模块的有关设置如常量、数组的大小等。
¹ 9-12:不能随意改变与其它模块的接口。
¹ 9-13:充分了解系统的接口之后,再使用系统提供的功能。
示例:在B型机的各模块与操作系统的接口函数中,有一个要由各模块负责编写的初始化过程,此过程在软件系统加载完成后,由操作系统发送的初始化消息来调度。因此就涉及到初始化消息的类型与消息发送的顺序问题,特别是消息顺序,若没搞清楚就开始编程,很容易引起严重后果。以下示例引自B型曾出现过的实际代码,其中使用了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变量的)其它方式替代,或者是否可以取消此判断语句。
¹ 9-14:编程时,要防止差1错误。
说明:此类错误一般是由于把“<=”误写成“<”或“>=”误写成“>”等造成的,由此引起的后果,很多情况下是很严重的,所以编程时,一定要在这些地方小心。当编完程序后,应对这些操作符进行彻底检查。
¹ 9-15:要时刻注意易混淆的操作符。当编完程序后,应从头至尾检查一遍这些操作符,以防止拼写错误。
说明:形式相近的操作符最容易引起误用,如C/C++中的“=”与“==”、“|”与“||”、“&”与“&&”等,若拼写错了,编译器不一定能够检查出来。
示例:如把“&”写成“&&”,或反之。
ret_flg = (pmsg->ret_flg & RETURN_MASK);
被写为:
ret_flg = (pmsg->ret_flg && RETURN_MASK);
rpt_flg = (VALID_TASK_NO( nTaskNumber ) && DATA_NOT_ZERO( stat_data ));
被写为:
rpt_flg = (VALID_TASK_NO( nTaskNumber ) & DATA_NOT_ZERO( stat_data ));
¹ 9-16:有可能的话,if语句尽量加上else分支,对没有else分支的语句要小心对待;switch语句必须有default分支。
¹ 9-17:Unix下,多线程的中的子线程退出必需采用主动退出方式,即子线程应return出口。
¹ 9-18:不要滥用goto语句。
说明:goto语句会破坏程序的结构性,所以除非确实需要,最好不使用goto语句。
½ 9-1:不使用与硬件或操作系统关系很大的语句,而使用建议的标准语句,以提高软件的可移植性和可重用性。
½ 9-2:除非为了满足特殊需求,避免使用嵌入式汇编。
说明:程序中嵌入式汇编,一般都对可移植性有较大的影响。
½ 9-3:精心地构造、划分子模块,并按“接口”部分及“内核”部分合理地组织子模块,以提高“内核”部分的可移植性和可重用性。
说明:对不同产品中的某个功能相同的模块,若能做到其内核部分完全或基本一致,那么无论对产品的测试、维护,还是对以后产品的升级都会有很大帮助。
½ 9-4:精心构造算法,并对其性能、效率进行测试。
½ 9-5:对较关键的算法最好使用其它算法来确认。
½ 9-6:时刻注意表达式是否会上溢、下溢。
示例:如下程序将造成变量下溢。
unsigned char size ;
while (size-- >= 0) // 将出现下溢
{
... // program code
}
当size等于0时,再减1不会小于0,而是0xFF,故程序是一个死循环。应如下修改。
char size; // 从unsigned char 改为char
while (size-- >= 0)
{
... // program code
}
½ 9-7:使用变量时要注意其边界值的情况。
示例:如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;
½ 9-8:留心程序机器码大小(如指令空间大小、数据空间大小、堆栈空间大小等)是否超出系统有关限制。
½ 9-9:为用户提供良好的接口界面,使用户能较充分地了解系统内部运行状态及有关系统出错情况。
½ 9-10:系统应具有一定的容错能力,对一些错误事件(如用户误操作等)能进行自动补救。
½ 9-11:对一些具有危险性的操作代码(如写硬盘、删数据等)要仔细考虑,防止对数据、硬件等的安全构成危害,以提高系统的安全性。
½ 9-12:使用第三方提供的软件开发工具包或控件时,要注意以下几点:
(1)充分了解应用接口、使用环境及使用时注意事项。
(2)不能过分相信其正确性。
(3)除非必要,不要使用不熟悉的第三方工具包与控件。
说明:使用工具包与控件,可加快程序开发速度,节省时间,但使用之前一定对它有较充分的了解,同时第三方工具包与控件也有可能存在问题。
½ 9-13:资源文件(多语言版本支持),如果资源是对语言敏感的,应让该资源与源代码文件脱离,具体方法有下面几种:使用单独的资源文件、DLL文件或其它单独的描述文件(如数据库格式)
10 代码编辑、编译、审查
¹ 10-1:打开编译器的所有告警开关对程序进行编译。
¹ 10-2:在产品软件(项目组)中,要统一编译开关选项。
¹ 10-3:通过代码走读及审查方式对代码进行检查。
说明:代码走读主要是对程序的编程风格如注释、命名等以及编程时易出错的内容进行检查,可由开发人员自己或开发人员交叉的方式进行;代码审查主要是对程序实现的功能及程序的稳定性、安全性、可靠性等进行检查及评审,可通过自审、交叉审核或指定部门抽查等方式进行。
¹ 10-4:测试部测试产品之前,应对代码进行抽查及评审。
½ 10-1:编写代码时要注意随时保存,并定期备份,防止由于断电、硬盘损坏等原因造成代码丢失。
½ 10-2:同产品软件(项目组)内,最好使用相同的编辑器,并使用相同的设置选项。
说明:同一项目组最好采用相同的智能语言编辑器,如Muiti Editor,Visual Editor等,并设计、使用一套缩进宏及注释宏等,将缩进等问题交由编辑器处理。
½ 10-3:要小心地使用编辑器提供的块拷贝功能编程。
说明:当某段代码与另一段代码的处理功能相似时,许多开发人员都用编辑器提供的块拷贝功能来完成这段代码的编写。由于程序功能相近,故所使用的变量、采用的表达式等在功能及命名上可能都很相近,所以使用块拷贝时要注意,除了修改相应的程序外,一定要把使用的每个变量仔细查看一遍,以改成正确的。不应指望编译器能查出所有这种错误,比如当使用的是全局变量时,就有可能使某种错误隐藏下来。
½ 10-4:合理地设计软件系统目录,方便开发人员使用。
说明:方便、合理的软件系统目录,可提高工作效率。目录构造的原则是方便有关源程序的存储、查询、编译、链接等工作,同时目录中还应具有工作目录----所有的编译、链接等工作应在此目录中进行,工具目录----有关文件编辑器、文件查找等工具可存放在此目录中。
½ 10-5:某些语句经编译后产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息。
说明:在Borland C/C++中,可用“#pragma warn”来关掉或打开某些告警。
示例:
#pragma warn -rvl // 关闭告警
int examples_fun( void )
{
// 程序,但无return语句。
}
#pragma warn +rvl // 打开告警
编译函数examples_fun时本应产生“函数应有返回值”告警,但由于关掉了此告警信息显示,所以编译时将不会产生此告警提示。
½ 10-6:使用代码检查工具(如C语言用PC-Lint)对源程序检查。
½ 10-7:使用软件工具(如 LogiSCOPE)进行代码审查。
第五章 附录