新公司的c++的编程规范,学习下.
前 言
软件编程规范的目的是为了统一公司软件编程风格,提高软件源程序的可读性、可靠性和可重用性,提高软件源程序的质量和可维护性,减少软件维护成本,最终提高软件产品生产力。
本规范是针对 C/C++ 语言的编程规则,其它不同编程语言可以参照此规范的基本原则。本规范适用于公司所有产品的软件源程序,同时考虑到不同产品和项目的实际开发特性,本规范分成规则性和建议性两种:对于规则性规范,要求所有软件开发人员严格执行;对于建议性规范,各项目编程人员可以根据实际情况选择执行。本规范的示例都以 C/C++ 语言给出。
本规范的内容包括:基本原则、布局、注释、命名规则、变量常量与结构、表达式与语句、函数与过程、可靠性、可测性、断言与错误处理等。规范最后给出了规范的模板供软件人员参考。
本规范自生效日期起,对以后新编写的和修改的代码有约束力。对以前的代码不要求进行修改。对于由开发工具自动生成的代码可以不约束。
对本规范中所使用的术语解释如下:
规则: 编程时必须遵守的原则。
建议: 编程时必须加以考虑的原则。
说明: 对此规则或建议的必要的解释。
正例: 对此规则或建议给出的正确例子。
反例: 对此规则或建议给出的反面例子。
1 基本原则
【原则 1-1 】首先为人编写程序,其次才是计算机。 |
说明: 这是软件开发的基本要点,软件的生命周期贯穿产品的开发、测试、生产、用户使用、版本升级和后期维护等长期过程,只有易读、易维护的软件代码才具有生命力。
【原则 1-2 】保持代码的简明清晰,避免过分的技巧。 |
说明: 简单是最美。保持代码的简单化是软件工程化的基本要求。不要过分追求技巧,否则会降低程序的可读性。
【原则 1-3 】所有的代码必须遵循 ANSI C 标准。 |
说明: 例如函数的原型声明中,必须包含类型定义。
【原则 1-4 】编程时首先达到正确性,其次考虑效率。 |
说明: 编程首先考虑的是满足正确性、健壮性、可维护性、可移植性等质量因素,最后才考虑程序的效率和资源占用。
【原则 1-5 】保持一致性,尽可能多的使用相同的规则。 |
【原则 1-6 】避免或少用全局变量。 |
说明: 过多地使用全局变量,会将模块间耦合过紧,违反模块化的要求。
【原则 1-7 】禁止使用 GOTO 语句。 |
【原则 1-8 】尽可能复用、修正老的代码。 |
说明: 选择代码完全重建不是一条最优的选择,如果可能,尽量选择可借用的代码,对其修改优化以达到自身要求。
【原则 1-9 】 紧凑的代码并不能保证得到高效、稳定的机器代码。 |
说明: 防止患有“一行清”疾病,即为了把代码尽量写在源代码的一行上,使用一些稀奇古怪的表达式。“多行源代码可能产生效率高的机器代码”。
【原则 1-10 】决不允许同样错误出现两次。 |
说明: 事实上,我们无法做到完全消除错误,但通过不懈的努力,可以减少同样的错误出现的次数。
2. 布局
程序布局的目的是显示出程序良好的逻辑结构,提高程序的准确性、连续性、可读性、可维护性。更重要的是,统一的程序布局和编程风格,有助于提高整个项目的开发质量,提高开发效率,降低开发成本。同时,对于普通程序员来说,养成良好的编程习惯有助于提高自己的编程水平,提高编程效率。因此,统一的、良好的程序布局和编程风格不仅仅是个人主观美学上的或是形式上的问题,而是一个涉及到产品质量,涉及到个人编程能力的提高,必须引起大家重视。
2.1 文件布局
【规则 2-1-1 】 遵循统一的布局顺序来书写头文件。 |
说明: 以下内容 如果某些节不需要,可以忽略。但是其它节要保持该次序。
头文件布局:
文件头(参见第三章“注释”)
#ifndef 文件名 _H (全大写)
#define 文件名 _H
其它条件编译选项
#include (依次为标准库头文件、非标准库头文件)
常量定义
全局宏
全局数据类型
类定义
模板( template )(包括 C++ 中的类模板和函数模板)
extern 声明
全局函数原型
#endif
【规则 2-1-2 】 遵循统一的布局顺序来书写实现文件。 |
说明: 以下内容如果某些节不需要,可以忽略。但是其它节要保持该次序。
实现文件布局:
文件头(参见第三章“注释”)
#include (依次为标准库头文件、非标准库头文件)
常量定义
文件内部使用的宏
文件内部使用的数据类型
全局变量
本地变量(即静态全局变量)
局部函数原型
类的实现
全局函数
局部函数
【规则 2-1-3 】 使用注释块分离上面定义的节。 |
正例:
/ ***********************************************************
* 数据类型定义 *
*********************************************************** /
typedef unsigned char BOOLEAN;
/*************************************************************
* 函数原型 *
************************************************************/
int DoSomething(void);
【规则 2-1-4 】 头文件必须要避免重复包含。 |
说明: 通过宏定义来避免重复包含。
正例:
#ifndef MODULE_H
#define MODULE_H
[ 文件体 ]
#endif
【规则 2-1-5 】 包含标准库头文件用尖括号 < > ,包含非标准库头文件用双引号 “ ” 。 |
正例:
#include <stdio.h>
#include “heads.h”
【规则 2-1-5 】遵循统一的顺序书写类的定义及实现。 |
说明:
类的定义(在定义文件中)按如下顺序书写:
公有属性
公有函数
保护属性
保护函数
私有属性
私有函数
类的实现(在实现文件中)按如下顺序书写:
构造函数
析构函数
公有函数
保护函数
私有函数
2.2 基本格式
【规则 2-2-1 】 程序中一行的代码和注释不能超过 80 列。 |
说明: 包括空格在内不超过 80 列 。
【规则 2-2-2 】 if 、 else 、 else if 、 for 、 while 、 do 等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加 { } 。 |
说明: 这样可以防止书写失误,也易于阅读。
正例:
if (varible1 < varible2)
{
varible1 = varible2;
}
反例: 下面的代码执行语句紧跟 if 的条件之后,而且没有加 {} ,违反规则。
if (varible1 < varible2) varible1 = varible2;
【规则 2-2-3 】定义指针类型的数据, * 应放在变量前。 |
正例:
float *pfBuffer;
反例:
float* pfBuffer;
〖建议 2-2-1 〗源程序中关系较为紧密的代码应尽可能相邻。 |
说明: 这样便于程序阅读和查找。
正例 :
iLength = 10;
iWidth = 5; // 矩形的长与宽关系较密切,放在一起。
strCaption = “Test”;
反例:
iLength = 10;
strCaption = “Test”;
iWidth = 5;
2.3 对齐
【规则 2-3-1 】 禁止使用 TAB 键,必须使用空格进行缩进。缩进为 4 个空格。 |
说明: 消除不同编辑器对 TAB 处理的差异,有的代码编辑器可以设置用空格代替 TAB 键。
【规则 2-3-2 】程序的分界符‘ { ’和‘ } ’应独占一行并且位于同一列,同时与引用它们的语句左对齐。 { } 之内的代码块使用缩进规则对齐。 |
说明: 这样使代码便于阅读,并且方便注释。
do while 语句和结构的类型化时可以例外, while 条件和结构名可与 } 在同一行。
正例:
void Function(int iVar)
{ // 独占一行并与引用语句左对齐。
while (condition)
{
DoSomething(); // 与 { } 缩进 4 格
}
}
反例:
void Function(int iVar){
while (condition){
DoSomething();
}}
【规则 2-3-3 】声明类的时候, public 、 protected 、 private 关键字与分界符 {} 对齐,这些部分的内容要进行缩进。 |
正例:
class CCount
{
public: // 与 { 对齐
CCount (void); // 要进行缩进
~ CCount (void);
int GetCount(void);
void SetCount(int iCount);
private:
int m_iCount;
}
【规则 2-3-4 】结构型的数组、多维的数组如果在定义时初始化,按照数组的矩阵结构分行书写。 |
正例:
int aiNumbers[4][3] =
{
1, 1, 1,
2, 4, 8,
3, 9, 27,
4, 16, 64
}
【规则 2-3-5 】相关的赋值语句等号对齐。 |
正例:
tPDBRes.wHead = 0;
tPDBRes.wTail = wMaxNumOfPDB - 1;
tPDBRes.wFree = wMaxNumOfPDB;
tPDBRes.wAddress = wPDBAddr;
tPDBRes.wSize = wPDBSize;
〖建议 2-3-1 〗在 switch 语句中,每一个 case 分支和 default 要用 { } 括起来, { } 中的内容需要缩进。 |
说明: 使程序可读性更好。
正例 :
switch (iCode)
{
case 1:
{
DoSomething(); // 缩进 4 格
break;
}
case 2:
{ // 每一个 case 分支和 default 要用 {} 括起来
DoOtherThing();
break;
}
… // 其它 case 分支
default:
{
DoNothing();
break;
}
}
2.4 空行空格
【规则 2-4-1 】不同逻辑程序块之间要使用空行分隔。 |
说明: 空行起着分隔程序段落的作用。空行得体(不过多也不过少)将使程序的布局更加清晰。
正例:
void Foo::Hey(void)
{
[Hey 实现代码 ]
}
void Foo::Ack(void)
{
[Ack 实现代码 ]
}
反例:
void Foo::Hey(void)
{
[Hey 实现代码 ]
}
void Foo::Ack(void)
{
[Ack 实现代码 ]
}
// 两个函数的实现是两个逻辑程序块,应该用空行加以分隔。
【规则 2-4-2 】一元操作符如“ ! ”、“ ~ ”、“ ++ ”、“ -- ”、“ * ”、“ & ”(地址运算符)等前后不加空格。象“ []”、“. ”、“-> ” 这类操作符前后不加空格。 |
正例:
!bValue
~iValue
++iCount
*strSource
&fSum
aiNumber[i] = 5;
tBox.dWidth
tBox->dWidth
【规则 2-4-3 】多元运算符和它们的操作数之间至少需要一个空格。 |
正例:
fValue = fOldValue;
fTotal + fValue
iNumber += 2;
【规则 2-4-4 】关键字之后要留空格。 |
说明: if 、 for 、 while 等关键字之后应留一个空格再跟左括号‘ ( ’,以突出关键字。
【规则 2-4-5 】函数名之后不要留空格。 |
说明: 函数名后紧跟左括号‘ ( ’,以与关键字区别。
【规则 2-4-6 】‘ ( ’向后紧跟,‘ ) ’、‘ , ’、‘ ; ’向前紧跟,紧跟处不留空格。‘ , ’之后要留空格。‘ ; ’不是行结束符号时其后要留空格。 |
正例:
例子中的 凵 代表空格。
for 凵 (i 凵 = 凵 0; 凵 i 凵 < 凵 MAX_BSC_NUM; 凵 i++)
{
DoSomething(iWidth, 凵 iHeight);
}
【规则 2-4-7 】注释符与注释内容之间要用一个空格进行分隔。 |
正例:
/* 注释内容 */
// 注释内容
反例:
/* 注释内容 */
// 注释内容
2.5 断行
【规则 2-5-1 】长表达式(超过 80 列)要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。 |
说明: 条件表达式的续行在第一个条件处对齐。
for 循环语句的续行在初始化条件语句处对齐。
函数调用和函数声明的续行在第一个参数处对齐。
赋值语句的续行应在赋值号处对齐。
正例 :
if ((iFormat == CH_A_Format_M)
&& (iOfficeType == CH_BSC_M)) // 条件表达式的续行在第一个条件处对齐
{
DoSomething();
}
for (long_initialization_statement;
long_condiction_statement; // for 循环语句续行在初始化条件语句处对齐
long_update_statement)
{
DoSomething();
}
// 函数声明的续行在第一个参数处对齐
BYTE ReportStatusCheckPara(HWND hWnd,
BYTE ucCallNo,
BYTE ucStatusReportNo);
// 赋值语句的续行应在赋值号处对齐
fTotalBill = fTotalBill + faCustomerPurchases[iID]
+ fSalesTax(faCustomerPurchases[iID]);
【规则 2-5-2 】函数声明时,类型与名称不允许分行书写。 |
正例 :
extern double FAR CalcArea(double dWidth, double dHeight);
反例 :
extern double FAR
CalcArea(double dWidth, double dHeight);
3. 注释
注释有助于理解代码, 有效的注释是指在代码的功能、意图层次上进行注释,提供有用、额外的信息,而不是仅仅代码的表面意义的简单重复。
【规则 3-1 】 C 语言的注释符为“ /* … */ ”。 C++ 语言中,多行注释采用“ /* … */ ”,单行注释采用“ // … ”。 |
【规则 3-2 】一般情况下,源程序有效注释量必须在 20 %以上。 |
说明: 注释的原则是有助于对程序的阅读理解,注释不宜太多也不 能太少,注释语言必须准确、易懂、简洁。有效的注释是指在代码的功能、意图层次上进行注释,提供有用、额外的信息,而不是仅仅代码的表面意义的简单重复。
【规则 3-3 】注释使用中文。 |
说明: 对于 特殊 要求的可以使用英文注释,如工具不支持或国际化版本。
【规则 3-4 】文件头部必须进行注释,包括: .h 文件、 .c 文件、 .cpp 文件、 .inc 文件、 .def 文件、编译说明文件 .cfg 等。 |
说明: 注释必须列出:版权信息、 文件标识 、 内容摘要 、版本号、作者、完成日期、修改信息等。
正例:
下面是文件头部的中文注释 :
/*********************************************************************
* 版权所有 (C)2001, 深圳市中兴通讯股份有限公司。
*
* 文件名称: // 文件名
* 文件标识: // 见配置管理计划书
* 内容摘要: // 简要描述本文件的内容,包括主要模块、函数及其功能的说明
* 其它说明: // 其它内容的说明
* 当前版本: // 输入当前版本
* 作 者: // 输入作者名字及单位
* 完成日期: // 输入完成日期,例: 2000 年 2 月 25 日
*
* 修改记录 1 : // 修改历史记录,包括修改日期、修改者及修改内容
* 修改日期:
* 版 本 号:
* 修 改 人:
* 修改内容:
* 修改记录 2 : …
**********************************************************************/
下面是文件头部的英文注释:
/***********************************************************************
* Copyright (C) 2001, ZTE Corporation.
*
* File Name: // 文件名 (注释对齐)
* File Mark: // 见配置管理计划书
* Description: // 简要描述本文件的内容,完成的主要功能
* Others: // 其它内容的说明
* Version: // 输入当前版本
* Author: // 输入作者名字及单位
* Date: // 输入完成日期,例: 2001-12-12
*
* History 1: // 修改历史记录,包括修改日期、修改者及修改内容
* Date:
* Version:
* Author:
* Modification:
* History 2: …
**********************************************************************/
|
【规则 3-5 】函数头部应进行注释,列出:函数的目的 / 功能、输入参数、输出参数、返回值、 访问和修改的表 、历史信息等。 |
说明: 注释必须列出:函数名称、功能描述、输入参数、输出参数、返 回 值、修改信息等。
正例:
下面是函数头部的中文注释 :
/**********************************************************************
* 函数名称: // 函数名称
* 功能描述: // 函数功能、性能等的描述
* 访问的表: // (可选)被访问的表,此项仅对于有数据库操作的程序
* 修改的表: // (可选)被修改的表,此项仅对于有数据库操作的程序
* 输入参数: // 输入参数说明,包括每个参数的作用、取值说明及参数间关系
* 输出参数: // 对输出参数的说明。
* 返 回 值: // 函数返回值的说明
* 其它说明: // 其它说明
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2002/08/01 V1.0 XXXX XXXX
***********************************************************************/
下面是函数头部的英文注释 :
/**********************************************************************
* Function: // 函数名称(注释对齐)
* Description: // 函数功能、性能等的描述
* Table Accessed: // (可选)被访问的表,此项仅对于有数据库操作的程序
* Table Updated: // (可选)被修改的表,此项仅对于有数据库操作的程序
* Input: // 输入参数说明,包括每个参数的作用、取值说明以及参数间关系
* Output : // 对输出参数的说明
* Return: // 函数返回值的说明
* Others: // 其它说明
* Modify Date Version Author Modification
* -----------------------------------------------
* 20 02/08/01 V1.0 XXXX XXXX
**********************************************************************/
|
【规则 3-6 】包含在 { } 中代码块的结束处应加注释,便于阅读。特别是多分支、多重嵌套的条件语句或循环语句。 |
说明: 此时注释可以用英文,方便查找对应的语句。
正例:
void Main ()
{
if (…)
{
…
while (…)
{
…
} /* end of while (…) */ // 指明该条 while 语句结束
…
} /* end of if (…) */ // 指明是哪条语句结束
} /* end of void main () */ // 指明函数的结束
【规则 3-7 】保证代码和注释的一致性。修改代码同时修改相应的注释,不再有用的注释要删除。 |
【规则 3-8 】注释应与其描述的代码相近,对代码的注释应放在其上方或右方(对单条语句的注释)相邻位置,不可放在下面,如放于上方则需与其上面的代码用空行隔开。 |
说明: 在使用缩写时或之前,应对缩写进行必要的说明。
正例:
如下书写比较结构清晰
/* 获得子系统索引 */
iSubSysIndex = aData[iIndex].iSysIndex;
/* 代码段 1 注释 */
[ 代码段 1 ]
/* 代码段 2 注释 */
[ 代码段 2 ]
反例 1 :
如下例子注释与描述的代码相隔太远。
/* 获得子系统索引 */
iSubSysIndex = aData[iIndex].iSysIndex;
反例 2 :
如下例子注释不应放在所描述的代码下面。
iSubSysIndex = aData[iIndex].iSysIndex;
/* 获得子系统索引 */
反例 3 :
如下例子,显得代码与注释过于紧凑。
/* 代码段 1 注释 */
[ 代码段 1 ]
/* 代码段 2 注释 */
[ 代码段 2 ]
【规则 3-9 】全局变量要有详细的注释,包括对其功能、取值范围、哪些函数或过程存取它以及存取时注意事项等的说明 。 |
正例 :
/*
* 变量作用:(错误状态码)
* 变量范围:例如 0 - SUCCESS 1 - Table error
* 访问说明:(访问的函数以及方法)
*/
BYTE g_ucTranErrorCode;
【规则 3-10 】注释与所描述内容进行同样的缩排。 |
说明: 可使程序排版整齐,并方便注释的阅读与理解。
正例:
如下注释结构比较清晰
int DoSomething(void)
{
/* 代码段 1 注释 */
[ 代码段 1 ]
/* 代码段 2 注释 */
[ 代码段 2 ]
}
反例 :
如下例子,排版不整齐,阅读不方便;
int DoSomething(void)
{
/* 代码段 1 注释 */
[ 代码段 1 ]
/* 代码段 2 注释 */
[ 代码段 2 ]
}
【规则 3-11 】对变量的定义和分支语句(条件分支、循环语句等)必须编写注释。 |
说明: 这些语句往往是程序实现某一特殊功能的关键,对于维护人员来说,良好的注释帮助更好的理解程序,有时甚至优于看设计文档。
〖建议 3-1 〗通过对函数或过程、变量、结构等正确的命名以及合理地组织代码结构,使代码成为自注释的。 |
说明: 清晰准确的函数、变量的命名,可增加代码的可读性,减少不必要的注释。
〖建议 3-2 〗尽量避免在注释中使用缩写,特别是不常用缩写。 |
说明: 在使用缩写时,应对缩写进行必要的说明。
4. 命名规则
好的名字能极大地增加可读性和可维护性。同时,对于一个有上百个人共同完成的大项目来说,统一命名约定也是一项必不可少的内容。 本章仅对程序中的所有标识符(包括变量名、常量名、函数名、类名、结构名、宏定义等)的命名做出约定。
【规则 4-1 】标识符要采用英文单词或其组合,便于记忆和阅读,切忌使用汉语拼音来命名。 |
说明: 标识符应当直观且可以拼读,可望文知意,避免使人产生误解。程序中的英文单词一般不要太复杂,用词应当准确。
【规则 4-2 】标识符只能由 26 个英文字母, 10 个数字,及下划线的一个子集来组成,并 严格禁止使用连续的下划线,下划线也 不能出现在标识符头或结尾( 预编译开关除外) 。 |
说明: 这样做的目的是为了使程序易读。因为 variable_name 和 variable__name 很难区分, 下划线符号‘ _ ’若出现在标识符头或结尾,容易与不带下划线‘ _ ’的标识符混淆。
【规则 4-3 】标识符的命名应当符合“ min-length && max-information ”原则。 |
说明: 较短的单词可通过去掉“元音”形成缩写,较长的单词可取单词的头几个字母形成缩写,一些单词有大家公认的缩写,常用单词的缩写必须统一。协议中的单词的缩写与协议保持一致。 对于本系统使用的专用缩写应该在某处做统一说明 。
正例: 如下单词的缩写能够被大家基本认可。
temp 可缩写为 tmp ;
flag 可缩写为 flg ;
statistic 可缩写为 stat ;
increment 可缩写为 inc ;
message 可缩写为 msg ;
常用缩写如下:
常用词 | 缩写 |
Argument | Arg |
Buffer | Buf |
Clear | Clr |
Clock | Clk |
Compare | Cmp |
Configuration | Cfg |
Context | Ctx |
Delay | Dly |
Device | Dev |
Disable | Dis |
Display | Disp |
Enable | En |
Error | Err |
Function | Fnct |
Hexadecimal | Hex |
High Priority Task | HPT |
I/O System | IOS |
Initialize | Init |
Mailbox | Mbox |
Manager | Mgr |
Maximum | Max |
Message | Msg |
Minimum | Min |
Multiplex | Mux |
Operating System | OS |
Overflow | Ovf |
Parameter | Param |
Pointer | Ptr |
Previous | Prev |
Priority | Prio |
Read | Rd |
Ready | Rdy |
Register | Reg |
Schedule | Sched |
Semaphore | Sem |
Stack | Stk |
Synchronize | Sync |
Timer | Tmr |
Trigger | Trig |
Write | Wr |
【规则 4-4 】程序中不要出现仅靠大小写区分的相似的标识符。 |
【规则 4-5 】用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。 |
说明: 下面是一些在软件中常用的反义词组。
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
【规则 4-6 】宏、常量名都要使用大写字母 , 用下划线 ‘ _ ’ 分割单词。 预编译开关的定义使用 下划线 ‘ _ ’ 开始。 |
正例 : 如 DISP_BUF_SIZE 、 MIN_VALUE 、 MAX_VALUE 等等。
【规则 4-7 】变量名长度应小于 31 个字符,以保持与 ANSI C 标准一致 。不得取单个字符(如 i 、 j 、 k 等),但是局部循环变量除外。 |
说明: 变量,尤其是局部变量,如果用单个字符表示,很容易出错(如 l 误写成 1 ) , 而编译时又检查不出,则有可能增加排错时间。过长的变量名会增加工作量,会使程序的逻辑流程变得模糊,给修改带来困难,所以应当选择精炼的意义明确的名字,才能简化程序语句,改善对程序功能的理解。
【规则 4-8 】程序中 局部变量 不要与 全局变量重名。 |
说明: 尽管局部变量和全局变量的作用域不同而不会发生语法错误,但容易使人误解。
【规则 4-9 】使用一致的前缀来区分变量的活动范围。 |
说明: 变量活动范围前缀规范如下:
g_ : 全局变量
s_ : 模块内静态变量
空 : 局部变量不加范围前缀
【规则 4-10 】使用一致的小写类型指示符作为前缀来区分变量的类型。 |
说明: 常用变量类型前缀列表如下:
i : int
f : float
d : double
c : char
uc : unsigned char 或 BYTE
l : long
p : pointer
b : BOOL
h : HANDLE
w : unsigned short 或 WORD
dw : DWORD 或 unsigned long
a : 数组,array of TYPE
str : 字符串
t : 结构类型
以上前缀可以进一步组合成新的类型, 在进行组合时,数组和指针类型的前缀指示符必须放在变量类型前缀的首位 。
【规则 4-11 】完整的变量名应由前缀 + 变量名主体组成,变量名的主体应当使用“名词”或者“形容词+名词”,且首字母必须大写。 |
说明: 各种前缀字符可能组合使用,在这种情况下,各前缀顺序为:变量活动范围前缀、变量类型前缀。
正例:
float g_fValue; // 类型为浮点数的全局变量
char *pcOldChar; // 类型为字符指针的局部变量
【规则 4-12 】函数名用大写字母开头的单词组合而成,且应当使用“动词”或者“动词+名词”(动宾词组)。 |
说明: 函数名力求清晰、明了,通过函数名就能够判断函数的主要处理功能。 函数名中不同意义字段之间不要用下划线连接,而要把每个字段的首字母大写以示区分。 函数命名采用大小写字母结合的形式 ,但专有名词不受限制。
【规则 4-13 】结构名、联合名、枚举名由前缀 T_ 开头。 |
【规则 4-14 】事件名由前缀 EV_ 开头。 |
【规则 4-15 】类名采用大小写结合的方法。在构成类名的单词之间不用下划线,类名在开头加上 C ,类的成员变量统一在前面加 m_ 前缀。 |
说明: C++Builder 中的类名在开头加 T 。
正例:
void Object::SetValue(int iWidth, int iHeight)
{
m_iWidth = iWidth;
m_iHeight = iHeight;
}
【建议 4-1 】尽量避免名字中出现数字编号,如 Value1 、 Value2 等,除非逻辑上的确需要编号。 |
【建议 4-2 】标识符前最好不加项目、产品、部门的标识和修饰。 |
说明: 这样做的目的是为了代码的可重用性。
5. 变量、常量与类型
变量、常量和数据类型是程序编写的基础,它们的正确使用直接关系到程序设计的成败,变量包括 全局 变量、局部变量和静态变量,常量包括数据常量、常量类型数据和指针常量,类型包括系统的数据类型和自定义数据类型。本章主要说明变量、常量与类型使用时必须遵循的规则和一些需注意的建议,关于它们的命名,参见命名规则。
5.1 变量与常量
【规则 5-1-1 】定义全局变量时必须仔细分析,明确其含义、作用、取值范围及与其它全局变量间的关系。 |
说明: 全局变量关系到程序的结构框架,对于全局变量的理解关系到对整个程序能否正确理解,所以在对全局变量声明的同时,应对其含义、作用及取值范围进行详细地注释说明,若有必要还应说明与其它变量的关系。
【规则 5-1-2 】明确全局变量与操作此全局变量的函数或过程的关系。 |
说明: 全局变量与函数的关系包括:创建、修改及访问。明确过程操作变量的关系后,将有利于程序的进一步优化、单元测试、系统联调以及代码维护等。这种关系的说明可在注释或文档中描述。
【规则 5-1-3 】一个变量有且只有一个功能,不能把一个变量用作多种用途。 |
说明: 一个变量只用来表示一个特定功能,不能把一个变量作多种用途,即同一变量取值不同时,其代表的意义也不同。
正例 :
WORD DelRelTimeQue(T_TCB *ptTcb )
{
WORD wValue;
WORD wLocate;
wLocate = 3;
wValue = DeleteFromQue(wLocate);
return wValue;
}
反例 :
WORD DelRelTimeQue(T_TCB *ptTcb)
{
WORD wLocate;
wLocate = 3;
wLocate = DeleteFromQue(wLocate); // wLocate 具有两种功能。
return wLocate;
}
【规则 5-1-4 】循环语句与判断语句中,不允许对其它变量进行计算与赋值。 |
说明: 循环语句只完成循环控制功能,if 语句只完成逻辑判断功能,不能完成计算赋值功能。
正例 :
do
{
[ 处理语句]
cInput = GetChar();
} while (cInput == 0);
反例:
do
{
[ 处理语句]
} while (cInput = GetChar());
【规则 5-1-5 】宏定义中如果包含表达式或变量,表达式和变量必须用小括号括起来。 |
说明: 在宏定义中,对表达式和变量使用括号,可以避免可能发生的计算错误。
正例:
#define HANDLE(A, B) (( A ) / ( B ))
反例:
#define HANDLE(A, B) (A / B)
【规则 5-1-6 】使用宏定义多行语句时 , 必须使用 { } 把这些语句括起来。 |
说明: 在宏定义中,对多行语句使用大括号,可以避免可能发生的错误。
〖建议 5-1-1 〗尽量构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量。 |
说明: 减少全局变量操作引起的错误。
正例: 在源文件中,可按如下注释形式说明。
T_Student *g_ptStudent;
变量 关系 函数
g_pStudent 创建 SystemInit(void)
修改 无
访问 StatScore(const T_Student *ptStudent)
PrintRec(const T_Student *ptStudent)
〖建议 5-1-2 〗对于全局变量通过统一的函数访问。 |
说明: 可以避免访问全局变量时引起系统错误。
正例:
T_Student g_tStudent;
T_Student GetStudentValue(void)
{
T_Student tStudentValue;
[ 获取g_tStudent 的访问权]
tStudentValue = g_tStudent;
[ 释放g_tStudent 的访问权]
return tStudentValue;
}
BYTE SetStudentValue(const T_Student *ptStudentValue)
{
BYTE ucIfSuccess;
ucIfSuccess = 0;
[ 获取g_tStudent 的访问权]
g_tStudent = *ptStudentValue ;
[ 释放g_tStudent 的访问权]
return ucIfSuccess;
}
〖建议 5-1-3 〗尽量使用 const 说明常量数据,对于宏定义的常数,必须指出其类型。 |
正例:
const int MAX_COUNT = 1000;
#define MAX_COUNT (int)1000
反例:
#define MAX_COUNT 1000
5.2 类型
【规则 5-2-1 】结构和联合必须被类型化。 |
正例:
typedef struct
{
char acName[NAME_SIZE];
WORD wScore;
} T_Student;
T_Student *ptStudent;
反例:
struct student
{
char acName[NAME_SIZE];
WORD wScore;
} *ptStudent;
〖建议 5-2-1 〗使用严格形式定义的、可移植的数据类型,尽量不要使用与具体硬件或软件环境关系密切的变量。 |
说明: 使用统一的自定义数据类型,有利于程序的移植。
自定义数据类型 | 类型说明 | 类型定义(以 Win32 为例) |
VOID | 空类型 | void |
BOOLEAN | 逻辑类型 (TRUE 或 FALSE) | unsigned char |
BYTE/ UCHAR | 无符号 8 位整数 | unsigned char |
CHAR | 有符号 8 位整数 | signed char |
WORD16/ WORD | 无符号 16 位整数 | unsigned short |
SWORD16 | 有符号 16 位整数 | signed short |
WORD32/DWORD | 无符号 32 位整数 | unsigned int |
SWORD32 | 有符号 32 位整数 | signed int |
FP32 | 32 位单精度符点数 | float |
FP64 | 64 位双精度符点数 | double |
〖建议 5-2-2 〗结构的功能要单一,是针对一种事务的抽象,不要设计面面俱到、非常灵活的数据结构。 |
说明: 设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。
正例 :
typedef struct TeacherStruct
{
BYTE aucName[8];
BYTE ucSex;
}T_Teacher;
typedef struct StudentStruct
{
BYTE ucName[8];
BYTE ucAge;
BYTE ucSex;
WORD wTeacherInd;
}T_Student;
反例 :
如下结构不太清晰、合理。
typedef struct StudentStruct
{
BYTE aucName[8];
BYTE ucAge;
BYTE ucSex;
BYTE aucTeacherName[8];
BYTE ucTeacherSex;
}T_Student;
〖建议 5-2-3 〗不同结构间的关系要尽量简单,若两个结构间关系较复杂、密切,那么应合为一个结构。 |
说明: 两个结构关系复杂时,它们可能反映的是一个事物的不同属性。
由于两个结构都是描述同一事物的,那么不如合成一个结构。
正例 :
typedef struct PersonStruct
{
BYTE aucName[8];
BYTE aucAddr[40];
BYTE ucSex;
BYTE aucCity[15];
BYTE ucTel;
}T_Person;
反例: 如下两个结构的构造不合理。
typedef struct PersonOneStruct
{
BYTE aucName[8];
BYTE aucAddr[40];
BYTE ucSex;
BYTE ucCity[15];
}T_PersonOne;
typedef struct PersonTwoStruct
{
BYTE aucName[8];
BYTE aucAddr[40];
BYTE ucTel;
}T_PersonTwo;
〖建议 5-2-4 〗结构中元素的个数应适中。若结构中元素个数过多可考虑依据某种原则把元素组成不同的子结构,以减少原结构中元素的个数。 |
说明: 增加结构的可理解性、可操作性和可维护性。
正例: 假如认为如上的 _PERSON 结 构元素过多,那么可如下对之划分。
typedef struct PersonBaseInfoStruct
{
BYTE aucName[8];
BYTE ucAge;
BYTE ucSex;
}T_PersonBaseInfo;
typedef struct PersonAddressStruct
{
BYTE aucAddr[40];
BYTE aucCity[15];
BYTE ucTel;
} T_PersonAddress;
typedef struct PersonStruct
{
T_PersonBaseInfo tPersonBase;
T_PersonAddress tPersonAddr;
} T_Person;
〖建议 5-2-5 〗仔细设计结构中元素的布局与排列顺序,使结构容易理解、节省占用空间,并减少引起误用现象,对于结构中未用的位明确地给予保留。 |
说明: 合理排列结构中元素顺序,可节省空间并增加可理解性。
正例: 如下形式,不仅可节省字节空间,可读性也变好了。
typedef struct ExampleStruct
{
BYTE ucValid: 1;
BYTE ucSetFlg: 1;
BYTE ucOther: 6; // 保留位
T_Person tPerson;
}T_Example;
反例: 如下结构中的位域排列,将占较大空间,可读性也稍差。
typedef struct ExampleStruct
{
BYTE ucValid: 1;
T_Person tPerson;
BYTE ucSetFlg: 1;
} T_Example;
〖建议 5-2-6 〗结构的设计要尽量考虑向前兼容和以后的版本升级,并为某些未来可能的应用保留余地(如预留一些空间等)。 |
说明: 软件向前兼容的特性,是软件产品是否成功的重要标志之一。如果要想使产品具有较好的前向兼容,那么在产品设计之初就应为以后版本升级保留一定余地,并且在产品升级时必须考虑前一版本的各种特性。
〖建议 5-2-7 〗注意具体语言及编译器处理不同数据类型的原则及有关细节。 |
说明 : 如在C 语言中,static 局部变量将在内存“ 数据区” 中生成,而非stat 局部变量将在“ 堆栈” 中生成。这些细节对程序质量的保证非常重要。
〖建议 5-2-8 〗合理地设计数据并使用自定义数据类型,尽量减少没有必要的数据类型默认转换与强制转换。 |
〖建议 5-2-9 〗当声明用于分布式环境或不同 CPU 间通信环境的数据结构时,必须考虑机器的字节顺序、使用的位域及字节对齐等问题 。 |
说明: 比如Intel CPU 与68360 CPU ,在处理位域及整数时,其在内存存放的“ 顺序” ,正好相反。
正例: 假如有如下短整数及结构。
WORD wExam;
typedef struct ExamBitStruct
{ /* Intel 68360 */
WORD wA1: 1; /* bit 0 2 */
WORD wA2: 1; /* bit 1 1 */
WORD wA3: 1; /* bit 2 0 */
WORD wOther: 13;
} T_ExamBit;
如下是 Intel CPU 生成短整数及位域的方式。
内存: 0 1 2 ... (从低到高,以字节为单位)
wExam wExam 低字节 wExam 高字节
内存: 0 bit 1 bit 2 bit ... (字节的各 “ 位 ” )
T_ExamBit A1 A2 A3
如下是 68360 CPU 生成短整数及位域的方式。
内存: 0 1 2 ... (从低到高,以字节为单位)
wExam wExam 高字节 wExam 低字节
内存: 0 bit 1 bit 2 bit ... (字节的各 “ 位 ” )
T_ExamBit A3 A2 A1
〖建议 5-2-10 〗结构定义时 , 尽量做到 pack 1 , 2 , 4 , 8 无关。 |
说明: 全局紧缩对齐可能会导致代码效率下降。
6. 表达式与语句
表达式是语句的一部分,它们是不可分割的。表达式和语句虽然看起来比较简单,但使用时隐患比较多。本章归纳了正确使用表达式和 if 、 for 、 while 、 goto 、 switch 等基本语句的一些规则与建议。在写表达式和语句的时候要注意运算符的优先级, C/C++ 语言的运算符有数十个,运算符的优先级与结合律如表 6-1 所示。注意一元运算符 + - * 的优先级高于对应的二元运算符。
表 6-1 运算符的优先级与结合律
优先级 | 运算符 | 结合律 |
从
高
到
低
排
列 | ( ) [ ] -> . | 从左至右 |
! ~ ++ -- (类型) sizeof + - * & | 从右至左
| |
* / % | 从左至右 | |
+ - | 从左至右 | |
<< >> | 从左至右 | |
< <= > >= | 从左至右 | |
== != | 从左至右 | |
& | 从左至右 | |
^ | 从左至右 | |
| | 从左至右 | |
&& | 从左至右 | |
|| | 从右至左 | |
?: | 从右至左 | |
= += -= *= /= %= &= ^= |= <<= >>= | 从左至右 |
【规则 6-1 】一条语句只完成一个功能。 |
说明: 复杂的语句阅读起来,难于理解,并容易隐含错误。变量定义时,一行只定义一个变量。
正例 :
aiAge [i] = i;
i++;
反例:
aiAge[i] = i++; // 一条语句实现了多个功能
正例:
int iHelp;
int iBase;
int iResult;
iHelp = iBase;
iResult = iHelp + GetValue(&iBase);
反例:
int iBase, iResult; // 一行定义多个变量
iResult = iBase + GetValue(&iBase); // 一条语句实现了多个功能
【规则 6-2 】在表达式中使用括号 , 使表达式的运算顺序更清晰。 |
说明: 由于将运算符的优先级与结合律熟记是比较困难的,为了防止产生歧义并提高可读性,即使不加括号时运算顺序不会改变,也应当用括号确定表达式的操作顺序。
正例 :
if (((iYear % 4 == 0 ) && (iYear % 100 != 0)) || (iYear % 400 == 0))
反例:
if (iYear % 4 == 0 && iYear % 100 != 0 || iYear % 400 == 0)
【规则 6-3 】避免表达式中的附加功能,不要编写太复杂的复合表达式。 |
说明: 带附加功能的表达式难于阅读和维护,它们常常导致错误。一个好的编译器下面两种情况产生代码的效果是一样的,不论它是一个大的表达式或是多个连续执行的简单的表达式。
正例 :
aiVar[1] = aiVar[2] + aiVar[3];
aiVar[4]++;
iResult = aiVar[1] + aiVar[4];
aiVar[3]++;
反例 :
iResult = (aiVar[1] = aiVar[2] + aiVar[3]++) + ++aiVar[4] ;
【规则 6-4 】不可将布尔变量直接与 TRUE 、 FALSE 或者 1 、 0 进行比较。 |
正例:
设bFlag 是布尔类型的变量
if (bFlag) // 表示 flag 为真
if (!bFlag) // 表示 flag 为假
反例:
设 bFlag 是布尔类型的变量
if (bFlag == TRUE)
if (bFlag == 1)
if (bFlag == FALSE)
if (bFlag == 0)
【规则 6-5 】在条件判断语句中,当整型变量与 0 比较时,不可模仿布尔变量的风格。 |
说明: 应当将整型变量用“ == ”或“ != ”直接与 0 比较。
正例 :
if (iValue == 0)
if (iValue != 0)
反例 :
if (iValue) // 会让人误解 iValue 是布尔变量
if (!iValue)
【规则 6-6 】不可将浮点变量用“ == ”或“ != ”与任何数字比较。 |
说明: 无论是float 还是double 类型的变量,都有精度限制。所以一定要避免将浮点变量用“== ”或“!= ”与数字比较,应该设法转化成“>= ”或“<= ”形式。
正例 :
if ((fResult >= -EPSINON) && (fResult <= EPSINON))
反例:
if (fResult == 0.0) // 隐含错误的比较
其中 EPSINON 是允许的误差(即精度)。
【规则 6-7 】应当将指针变量用“ == ”或“ != ”与 NULL 比较。 |
说明: 指针变量的零值是“空”(记为NULL )。尽管NULL 的值与0 相同,但是两者意义不同。
正例:
if (pHead == NULL) // pHead 与 NULL 显式比较,强调 pHead 是指针变量
if (pHead != NULL)
反例:
if (pHead == 0) // 容易让人误解 pHead 是整型变量
if (pHead != 0)
或者
if (pHead) // 容易让人误解 pHead 是布尔变量
if (!pHead)
【规则 6-8 】 禁止将逻辑表达式与 TRUE 比较,而应该与 FALSE 比较或直接使用它们不进行比较。 |
说明: 逻辑表达式的结果是布尔类型,根据布尔类型的语义,零值为“假”(记为FALSE ),任何非零值都是“真”(记为TRUE )。TRUE 的值究竟是什幺并没有统一的标准。例如Visual C++ 将TRUE 定义为1 ,而Visual Basic 则将TRUE 定义为-1 。
通常情况下 , 一个逻辑表达式不需要与 TRUE 或 FALSE 进行比较,因为逻辑表达式的值就是 TRUE 或 FALSE 。
反例:
BOOLEAN bFound = FALSE;
if (!bFound == TRUE) // 比较的结果是 FALSE , 因为 !0 不等于 1
...
【规则 6-9 】 在 switch 语句中 , 每一个 case 分支必须使用 break 结尾 , 最后一个分支必须是 default 分支。 |
说明: 避免漏掉break 语句造成程序错误。同时保持程序简洁。
正例 :
switch (iMessage)
{
case SPAN_ON:
{
[ 处理语句 ]
break;
}
case SPAN_OFF:
{
[ 处理语句 ]
break;
}
default:
{
[ 处理语句 ]
}
}
【规则 6-10 】 不可在 for 循环体内修改循环变量,防止 for 循环失去控制。 |
〖建议 6-1 〗 循环嵌套次数不大于 3 次。 |
说明: 保持程序简洁。
〖建议 6-2 〗 do while 语句和 while 语句仅使用一个条件。 |
说明: 保持程序简洁。如果需要判断的条件较多,建议用临时布尔变量先计算是否满足条件。
正例 :
BOOLEAN bCondition;
do
{
……..
bCondition = ((tAp[iPortNo].bStateAcpActivity != PASSIVE)
|| (tAp[iPortNo].bStateLacpActivity != PASSIVE))
&& (abLacpEnabled[iPortNo])
&& (abPortEenabled[iPortNo])
} while (bCondition);
〖建议 6-3 〗当 switch 语句的分支比较多时,采用数据驱动方式。 |
说明: 当switch 语句中case 语句比较多时,会降低效率和程序的结构清晰性。
正例 :
extern void TurnState(void);
extern void SendMessage (void);
…….
void (*StateChange[20])() = {TurnState, SendMessage, NULL, TurnState… };
…
if (StateChange[iState])
{
(*StateChange[iState])();
}
〖建议 6-4 〗 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。 |
正例 :
for (iCol = 0; iCol < 5; iCol++)
{
for (iRow = 0; iRow < 100; iRow++)
{
iSum = iSum + aiDate[iRow][iCol];
}
}
反例 :
for (iRow = 0; iRow < 100; iRow++)
{
for (iCol = 0; iCol < 5; iCol++)
{
iSum = iSum + aiDate[iRow][iCol];
}
}
〖建议 6-5 〗 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。 |
说明: 下面两个示例中,反例的程序比正例多执行了num-1 次逻辑判断。并且由于前者总要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。如果num 非常大,最好采用示例(2) 的写法,可以提高效率。如果N 非常小,两者效率差别并不明显,采用示例(1) 的写法比较好,因为程序更加简洁。
const int NUM = 100000;
正例 :
if (bCondition)
{
for (i = 0; i < NUM; i++)
{
DoSomething();
}
}
else
{
for (i = 0; i < NUM; i++)
{
DoOtherthing();
}
}
反例 :
for (i = 0; i < NUM; i++)
{
if (bCondition)
{
DoSomething();
}
else
{
DoOtherthing();
}
}
〖建议 6-6 〗 for 语句的循环控制变量的取值采用“半开半闭区间”写法。 |
说明: 这样做更能适应c 语言数组的特点,c 语言的下标属于一个“半开半闭区间”。
正例:
int aiScore[NUM];
…
for (i = 0; i < NUM; i++)
{
printf(“%d/n”,aiScore[i])
}
反例 :
int aiScore[NUM];
…
for (i = 0; i <= NUM-1 ; i++)
{
printf(“%d/n”,aiScore[i]);
}
相比之下,正例的写法更加直观,尽管两者的功能是相同的。
〖建议 6-7 〗在进行“ == ”比较时,将常量或常数放在“ == ”号的左边。 |
说明: 可以采用一些技巧,让编译器去发现错误。
正例 :
if (NULL == pTail)
if (0 == iSum)
示例中有意把 p 和 NULL 颠倒。编译器认为 if (pTail = NULL) 是合法的,但是会指出 if (NULL = pTail) 是错误的,因为 NULL 不能被赋值。
7. 函数与过程
函数是 C/C++ 程序的基本功能单元,是构建大厦的基石。如何编写出正确、高效、易维护的函数是软件编码质量控制的关键。一个函数包括函数头,函数名,函数体,参数,返回值。其中函数头的编写参见第三章注释,函数名参见第四章命名规则,本章着重描述作为接口要素的参数和返回值,函数体的实现以及函数相互之间的调用关系。
7.1 参数
【 规则 7-1-1 】 不能省略参数名 , 如果函数没有参数,则用void 填充。 |
说明: 函数在说明的时候,可以省略参数名。但是为了提高代码的可读性,要求不能省略。
正例 :
void SetValue(int iWidth, int iHeight);
float GetValue(void);
反例 :
void SetValue(int, int);
float GetValue();
【 规则 7-1-2 】 如果参数是指针 ,且仅作输入用,则应在类型前加 const 。 |
说明: 防止该指针在函数体内被意外修改。
正例:
int GetStrLen(const char *pcString);
【 规则 7-1-3 】 当结构变量作为参数时,应传送结构的指针而不传送整个结构体,并且不得修改结构中的元素,用作输出时除外。 |
说明: 一个函数被调用的时候,形参会被一个个压入被调函数的堆栈中,在函数调用结束以后再弹出。 一个结构所包含的变量往往比较多 ,直接以一个结构为参数,压栈出栈的内容就会太多,不但占用堆栈空间,而且影响代码执行效率,如果使用不当还可能导致堆栈的溢出。如果使用结构的指针作为参数,因为指针的长度是固定不变的,结构的大小就不会影响代码执行的效率,也不会过多地占用堆栈空间。
〖建议 7-1-1 〗 避免函数有太多的参数 ,参数个数尽量控制在 5 个以内。 |
说明: 如果参数太多,在使用时容易将参数类型或顺序搞错,而且调用的时候也不方便。如果参数的确比较多,而且输入的参数相互之间的关系比较紧密,不妨把这些参数定义成一个结构,然后把结构的指针当成参数输入。
〖建议 7-1-2 〗 参数命名顺序要合理 。 |
说明: 参数的顺序要遵循程序员的习惯。如输入参数放在前面,输出参数放在后面等。
正例:
int RelRadioChan(const T_RelRadioChanReq *ptReq, T_RelRadioChanAck *ptAck);
〖建议 7-1-3 〗尽量不要使用类型和数目不确定的参数。 |
说明: 编译器不对可变参数个数的函数调用作类型检查和参数检查 。 这种风格的函数在编译时丧失了严格的类型安全检查。
〖建议 7-1-4 〗 避免使用 BOOLEAN 参数。 |
说明: 一方面因为 BOOLEAN 参数值无意义, TRUE/FALSE 的含义是非常模糊的,在调用时很难知道该参数到底传达的是什么意思;其次 BOOLEAN 参数值不利于扩充。
7.2 返回值
【 规则 7-2-1 】 不要省略返回值的类型,如果函数没有返回值,那么应声明为 void 类型。 |
说明: C 语言中,凡不加类型说明的函数,一律自动按整型处理。如果不注明类型,容易被误解为 void 类型,产生不必要的麻烦。
C++ 语言有很严格的类型安全检查,不允许上述情况发生。由于 C++ 程序可以调用 C 函数,为了避免混乱,规定任何 C/ C++ 函数都必须有类型。如果函数没有返回值,那么应声明为 void 类型。
【 规则 7-2-2 】 对于有返回值的函数,每一个分支都必须有返回值。 |
说明: 为了保证对被调用函数返回值的判断,有返回值的函数中的每一个退出点都需要有返回值。
〖建议 7-2-1 〗 如果返回值表示函数运行是否正常 ,规定 0 为正常退出,不同非 0 值标识不同异常退出。不要使用 TRUE 或 FALSE 作为返回值。 |
说明: 在需要判断函数是否正常运行的情况下,就需要分析函数的返回值。在 C 语言中,根据布尔类型的语义,零值为“假”(记为 FALSE ),任何非零值都是“真”(记为 TRUE )。 TRUE 的值究竟是什么并没有统一的标准。例如 Visual C++ 将 TRUE 定义为 1 ,而 Visual Basic 则将 TRUE 定义为 -1 。
正例:
int SubFunction(void);
反例:
BOOLEAN SubFunction(void);
7.3 内部实现
函数体的实现并不是随心所欲 ,而是有一定的规矩可循。不但要仔细检查入口参数的有效性和精心设计返回值,还要保证函数的功能单一,具有很高的功能内聚性,尽量减少函数之间的耦合,方便调试和维护。
【 规则 7-3-1 】 对输入参数的正确性和有效性进行检查。 |
说明: 很多程序错误是由非法参数引起的,我们应该充分理解并正确处理来防止此类错误。
【 规则 7-3-2 】 防止将函数的参数作为工作变量 。 |
说明: 将函数的参数作为工作变量,有可能错误地改变参数内容,所以很危险。对必须改变的参数,最好先用局部变量代之,最后再将该局部变量的内容赋给该参数。
正例:
void SumData(int iNum, int *piData, int *piSum )
{
int iCount ;
int iSumTmp; // 存储“和”的临时变量
iSumTmp = 0;
for (iCount = 0; iCount < iNum; iCount++)
{
iSumTmp += piData[iCount];
}
*piSum = iSumTmp;
}
反例:
void SumData(int iNum, int *piData, int *piSum )
{
int iCount;
*piSum = 0;
for (iCount = 0; iCount < iNum; iCount++ )
{
*piSum += piData[iCount]; // piSum 成了工作变量,不好。
}
}
〖建议 7-3-1 〗尽量避免函数带有“记忆”功能。函数的输出应该具有可预测性,即相同的输入应当产生相同的输出。 |
说明: 带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在 C/C++ 语言中,函数的 static 局部变量是函数的“记忆”存储器。建议尽量少用 static 局部变量,除非必需。
〖建议 7-3-2 〗函数的功能要单一,不要设计多用途的函数。 |
说明: 多用途的函数往往通过在输入参数中有一个控制参数,根据不同的控制参数产生不同的功能。这种方式增加了函数之间的控制耦合性,而且在函数调用的时候,调用相同的一个函数却产生不同的效果,降低了代码的可读性,也不利于代码调试和维护。
正例:
以下两个函数功能清晰:
int Add(int iParaOne, int iParaTwo)
{
return (iParaOne + iParaTwo);
}
int Sub(int iParaOne, int iParaTwo)
{
return (iParaOne – iParaTwo);
}
反例:
如果把这两个函数合并在一个函数中,通过控制参数决定结果,不可取。
int AddOrSub(int iParaOne, int iParaTwo, unsigned char ucAddOrSubFlg)
{
if (INTEGER_ADD == ucAddOrSubFlg) // 参数标记为“求和”
{
return (iParaOne + iParaTwo);
}
else
{
return (iParaOne –iParaTwo);
}
}
〖建议 7-3-3 〗函数功能明确,防止把没有关联的语句放到一个函数中。 |
说明: 防止函数或过程内出现随机内聚。随机内聚是指将没有关联或关联很弱的语句放到同一个函数或过程中。随机内聚给函数或过程的维护、测试及以后的升级等造成了不便,同时也使函数或过程的功能不明确。使用随机内聚函数,常常容易出现在一种应用场合需要改进此函数,而另一种应用场合又不允许这种改进,从而陷入困境。
正例:
矩形的长、宽与点的坐标基本没有任何关系,应该在不同的函数中实现。
void InitRect(void)
{
// 初始化矩形的长与宽
tRect.wLength = 0;
tRect.wWidth = 0;
}
void InitPoint(void)
{
// 初始化“点”的坐标
tPoint.wX = 10;
tPoint.wY = 10;
}
反例:
矩形的长、宽与点的坐标基本没有任何关系,故以下函数是随机内聚。
void InitVar(void)
{
// 初始化矩形的长与宽
tRect.wLength = 0;
tRect.wWidth = 0;
// 初始化“点”的坐标
tPoint.wX = 10;
tPoint.wY = 10;
}
〖建议 7-3-4 〗 函数体的规模不能太大 ,尽量控制在 200 行代码之内。 |
说明: 冗长的函数无论从可读性和调试的角度来说都不受欢迎。
〖建议 7-3-5 〗为简单功能编写函数。 |
说明: 虽然为仅用一两行就可完成的功能去编函数好象没有必要,但用函数可使功能明确化,增加程序可读性,亦可方便维护、测试。
正例:
如下显得很清晰。
int Max(int iParaOne, int iParaTwo)
{
int iMaxValue;
iMaxValue = (iParaOne > iParaTwo) ? iParaOne : iParaTwo;
return iMaxValue;
}
反例:
如下语句的功能不很明显。
iMaxValue = (iParaOne > iParaTwo) ? iParaOne : iParaTwo;
7.4 函数调用
【 规则 7-4-1 】 必须对所调用函数的错误返回值进行处理。 |
说明: 函数返回错误,往往是因为输入的参数不合法,或者此时系统已经出现了异常。如果不对错误返回值进行必要的处理,会导致错误的扩大,甚至导致系统的崩溃。
正例:
在程序中定义了一个函数:
int DbAccess(WORD wEventNo, T_InPara *ptInParam, T_OutPara *ptOutParam);
在引用该函数的时候应该如下处理:
int iResult;
iResult = DbAccess(EV_GETRADIOCHANNEL, ptReq, ptAck);
switch (iResult)
{
case NO_CHANNEL: // 无可用无线资源
{
[ 异常处理 ]
break;
}
case CELL_NOTFOUND: // 小区未找到
{
[ 异常处理 ]
break;
}
default:
{
[ 其它处理 ]
}
}
[ 正常处理 ]
反例:
对上面的正例中定义的函数进行如下的处理就不合适。
DbAccess(EV_GETRADIOCHANNEL, ptReq, ptAck);
[ 正常处理 ]
〖建议 7-4-1 〗减少函数本身或函数间的递归调用 。 |
说明: 递归调用特别是函数间的递归调用(如 A->B->C->A ),影响程序的可理解性;递归调用一般都占用较多的系统资源(如栈空间);递归调用对程序的测试有一定影响。故除非为某些算法或功能的实现方便,应减少没必要的递归调用。
对于前台软件为了系统的稳定性和可靠性,往往规定了进程的堆栈大小。如果采用了递归算法,收敛的条件又往往难以确定,很容易使得进程的堆栈溢出,破坏系统的正常运行;另外,由于无法确定递归的次数,降低了系统的稳定性和可靠性。
〖建议 7-4-2 〗设计高扇入、合理扇出的函数。 |
说明: 扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。
扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,如总是 1 ,表明函数的调用层次可能过多,这样不利程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。函数较合理的扇出(调度函数除外)通常是 3-5 。扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。
扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。
较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。
8. 可靠性
为保证代码的可靠性,编程时请遵循如下基本原则,优先级递减:
◆正确性,指程序要实现设计要求的功能。
◆稳定性、安全性,指程序稳定、可靠、安全。
◆可测试性,指程序要具有良好的可测试性。
◆规范 / 可读性,指程序书写风格、命名规则等要符合规范。
◆全局效率,指软件系统的整体效率。
◆局部效率,指某个模块 / 子模块 / 函数的本身效率。
◆个人表达方式 / 个人方便性,指个人编程习惯。
8.1 内存使用
【规则 8-1-1 】程序员在程序编制之前,必须了解编译系统的内存分配方式,特别是编译系统对不同类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等。 |
【规则 8-1-2 】防止内存操作越界,小心边界。 |
说明: 内存操作主要是指对数组、指针、内存地址等的操作,内存操作越界是软件系统主要错误之一,后果往往非常严重,所以当我们进行这些操作时一定要仔细小心。通常数组的越界在边界上。
正例:
const int MAX_USE_NUM = 10 // 用户号为 1-10
unsigned char aucLoginFlg[MAX_USR_NUM]={0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
void ArrayFunction(void)
{
unsigned char ucUserNo;
for (ucUserNo = 0; ucUserNo < MAX_USE_NUM; ucUserNo++)
{
aucLoginFlg[ucUser_No] = ucUserNo;
… …
}
}
反例:
const int MAX_USE_NUM = 10 // 用户号为 1-10
unsigned char aucLoginFlg[MAX_USR_NUM]={0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
void ArrayFunction(void)
{
unsigned char ucUserNo;
for (ucUserNo = 1; ucUserNo < 11; ucUserNo++) // 10 已经越界了
{
aucLoginFlg[User_No] = ucUserNo;
… …
}
}
【规则8-1-3 】 必须对动态申请的内存做有效性检查,并进行初始化;动态内存的释放必须和分配成对以防止内存泄漏,释放后内存指针置为 NULL 。 |
说明: 对嵌入式系统,通常内存是有限的,内存的申请可能会失败,如果不检查就对该指针进行操作,可能出现异常,而起这种异常不是每次都出现,比较难定位。
指针释放后,该指针可能还是指向原有的内存块,可能不是,变成一个野指针,一般用户不会对它再操作,但用户失误情况下对它的操作可能导致程序崩溃。
正例:
MemmoryFunction(void)
{
unsigned char *pucBuffer = NULL;
pucBuffer = GetBuffer(sizeof(DWORD));
if (NULL != pucBuffer) // 申请的内存指针必须进行有效性验证
{
// 申请的内存使用前必须进行初始化
memcpy(pucBuffer, 0xFF, sizeof(DWORD));
}
….
FreeBuffer(pucBuffer); // 申请的内存使用完毕必须释放
pucBuffer = NULL; // 申请的内存释放后指针置为空
…
}
【规则 8-1-4 】不使用 realloc( ) 。 |
说明: 调用 realloc 对一个内存块进行扩展,因此原来的内容发生了存储位置的变化, realloc 函数既要调用 free ,又要调用 malloc 。执行时究竟调用哪个函数,取决于是要缩小还是扩大相应内存块的大小。
【规则 8-1-5 】变量在使用前应初始化,防止未经初始化的变量被引用。 |
说明: 不同的编译系统,定义的变量在初始化前其值是不确定的。有些系统会初始化为 0 ,而有些不是。
〖建议 8-1-1 〗由于内存总量是有限的,软件系统各模块应约束自己的代码,尽量少占用系统内存。 |
〖建议 8-1-2 〗在通信程序中,为了保证高可靠性,一般不使用内存的动态分配。 |
〖建议 8-1-3 〗在往一个内存区连续赋值之前 (memset,memcpy…) ,应确保内存区的大小能够容纳所赋的数据。 |
〖建议8-1-4 〗尽量使用 memmove( ) 代替 memcpy( ) 。 |
〖建议 8-1-5 〗为防止堆栈的溢出,在函数体内的局部变量占用内存不应过多。 |
说明: 当局部变量过多时,造成指针跑飞,这种错误很难检测。
8.2 指针使用
【规则8-2-1 】 指针类型变量必须初始化为 NULL 。 |
【规则 8-2-2 】指针不要进行复杂的逻辑或算术操作。 |
说明: 指针加一的偏移,通常由指针的类型确定,如果通过复杂的逻辑或算术操作, 则指针的位置就很难确定。
【规则 8-2-3 】如果指针类型明确不会改变,应该强制为 const 类型的指针,以加强编译器的检查。 |
说明: 可以防止不必要的类型转换错误。
【规则 8-2-4 】减少指针和数据类型的强制类型转化。 |
【规则 8-2-5 】移位操作一定要确定类型。 |
说明: BYTE 的移位后还是 BYTE ,如将 4 个字节拼成一个 long ,则应先把字节转化成 long .
正例 :
unsigned char ucMove;
unsigned long lMove;
unsigned long lTemp;
ucMove = 0xA3;
lTemp = (unsigned long) ucMove;
lMove = (lTemp << 8) | lTemp; /* 用 4 个字节拼成一个长字 */
lMove = (lMove << 16) | lMove;
反例:
unsigned char ucMove = 0xA3;
unsigned long lMove;
lMove = (ucMove << 8) | ucMove; /* 用 4 个字节拼成一个长字 */
lMove = (lMove << 16) | lMove;
【规则 8-2-6 】向变量传递数据时,必须对其值进行合法性检查,防止越界等现象发生。 |
说明: 对全局变量赋值时,应进行合法性检查,以提高代码的可靠性、稳定性。
8.3 类和函数
说明: 这样可以防止对类属性的误操作。
正例 :
class CCount
{
public:
CCount (void);
~ CCount (void);
int GetCount(void);
void SetCount(int iCount);
private:
int m_iCount;
}
【规则 8-3-2 】在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。 |
说明: 除非在派生类中调用基类的赋值函数,否则基类变量不会自动被赋值。
正例 :
class CBase
{
public:
…
CBase & operate = (const CBase &other); // 类 CBase 的赋值函数
private:
int m_iLength;
int m_iWidth;
int m_iHeigth;
};
class CDerived : public CBase
{
public:
…
CDerived & operate = (const CDerived &other); // 类 CDerived 的赋值函数
private:
int m_iLength;
int m_iWidth;
int m_iHeigth;
};
CDerived & CDerived::operate = (const CDerived &other)
{
if (this == &other) // ( 1 )检查自赋值
{
return *this;
}
CBase::operate =(other); // ( 2 )对基类的数据成员重新赋值
// 因为不能直接操作私有数据成员
// ( 3 )对派生类的数据成员赋值
m_iLength = other.m_iLength;
m_iWidth = other.m_iWidth;
m_iHeigth = other.m_iHeigth;
return *this; // ( 4 )返回本对象的引用
}
【规则 8-3-3 】构造函数应完成简单有效的功能,不应完成复杂的运算和大量的内存管理。 |
说明: 如果该类有相当多的初始化工作,应生成专门的 Init ( void )函数,不能完全在构造函数中进行,因为构造函数没有返回值,不能确定初始化是否成功。
【规则 8-3-4 】不要在栈中分配类的实例,也不要生成全局类实例。 |
说明: 这里所说的类,是带有构造函数的类。在栈中分配类的实例,类的构造函数和析构函数会带来很多麻烦。而全局类实例使得用户不能对该实例进行管理。
正例 :
void MemmoryFunction(…)
{
CMyClass *pMyClass = NULL;
pMyClass = new CMyClass(void); // 动态申请内存
if (pMyClass) // 对申请的指针作有效性检查
{
…
delete pMyClass ; // 内存使用完后应释放
pMyClass = NULL;
…
}
…
}
反例:
void MemmoryFunction(…)
{
CMyClass OneClass; // 在栈分配类的实例可能导致构造函数的失败
OneClass.Param1 = 2; // 访问分配失败的实例成员是违规的
…
} // 在函数返回前,要调用类的析构函数,则又造成析构异常
【规则 8-3-5 】正确处理拷贝构造函数与赋值函数。 |
说明: 由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数有些轻视。如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。
反例:
class CString
{
public:
CString(const char *pStr = NULL); // 普通构造函数
CString(const CString &other); // 拷贝构造函数
~ CString(void); // 析构函数
CString & operate =(const CString &other); // 赋值函数
public:
char *m_pData; // 用于保存字符串
};
CString::CString(const char *pStr)
{
if (pStr == NULL)
{
m_pData = new char[10];
*m_pData = ‘/0’;
}
else
{
int iLength;
iLength = strlen(pStr);
m_pData = new char[iLength + 1];
strcpy(m_pData, pStr);
}
}
CString::~CString(void) // CString 的析构函数
{
delete [] pData; // 由于 pData 是内部数据类型,也可以写成 delete pData;
}
main()
{
CString CStringA(“hello);
CString CStringB(“word”);
CString CstringC = CStringA; // 拷贝构造函数
CStringC = CStringB; // 赋值函数
CStringB.pData = CStringA. pData; // 这将造成三个错误:
/* 1. CStringB.m_pData 原有的内存没被释放,造成内存泄露;
* 2. CStringB.m_pData 和 CStringA.m_pData 指向同一块内存, CStringA 或 *CstringB
* 任何一方变动都会影响另一方
* 3. 对象被析构时, m_pData 被释放了两次。应把 m_pData 改成私有数据 , 用赋 * 值函数进行赋值
*/
….
} |
【规则 8-3-6 】过程 / 函数中申请的(为打开文件而使用的)文件句柄,在过程 / 函数退出之前要关闭,除非要把这个句柄传递给其它函数使用。 |
〖建议 8-3-1 〗编写可重入函数时,若使用全局变量,则应通过信号量(即 P 、 V 操作)等手段对其加以保护。 |
说明: 若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。
正例:
假设g_iExam 是int 型全局变量,函数SqureExam 返回g_iExam 平方值。那么如下函数具有可重入性。
unsigned int Example(int iPara)
{
unsigned int iTemp;
[ 申请信号量操作 ]
g_iExam = iPara;
iTemp = SquareExam( );
[ 释放信号量操作 ]
return iTemp;
}
反例:
如下函数不具有可重入性。
unsigned int Example( int iPara )
{
unsigned int iTemp;
g_iExam = iPara; // 在访问全局变量前没有使用信号量保护
iTemp = SquareExam();
return iTemp;
}
此函数若被多个进程调用的话,其结果可能是未知的,因为当访问全局变量语句刚执行完后,另外一个使用本函数的进程可能正好被激活,那么当新激活的进程执行到此函数时,将使g_iExam 赋与另一个不同的iPara 值,所以当控制重新回到“iTemp = SquareExam()” 后,计算出的iTemp 很可能不是预想中的结果。
9. 可测试性
在设计阶段就必须考虑所编写代码的可测试性,只有提供足够的测试手段才能全面、高效地发现和解决代码中的各类问题。编写的代码是否可测试,是衡量代码质量的最基本的、最重要的尺度之一。
程序设计过程中(或程序编码完毕后),必须编写软件模块测试文档,测试文档的编写规范参见后续规范,主要应包括:设计思路、程序输入、程序输出和数据结构等。测试是设计的一部分。
【 规则 9-1 】 在同一项目组或产品组内,为准备集成测试和系统联调,要有一套统一的调测开关及相应信息输出函数,并且要有详细的说明。统一的调试接口和输出函数由模块设计和测试人员根据项目特性统一制订,由项目系统人员统一纳入系统设计中 |
说明: 本规则是针对项目组或产品组的。
【 规则 9-2 】 在同一个项目组或产品组内,调测打印出的信息串要有统一的格式。信息串中应当包含所在的模块名(或源文件名)及行号等信息。 |
说明: 统一的调测信息格式便于集成测试。
【 规则 9-3 】 在编写代码之前,应预先设计好程序调试与测试的方法和手段,并设计好各种调测开关及相应测试代码(如打印函数等)。 |
说明: 程序的调试与测试是软件生存周期中非常重要的一个阶段,如何对软件进行较全面、高效率的测试并尽可能地找出软件中的错误就成为非常关键的问题。因此在编写源代码之前,除了要有一套比较完善的测试计划外,还应设计出一系列测试代码作为手段,为单元测试、集成测试及系统联调提供方便。
〖建议 9-1 〗在同一项目组或产品组内,可以统一由模块设计和测试人员开发调试信息接收平台,统一对软件调试信息进行分析。 |
说明: 本建议是针对项目组或产品组的。
〖建议 9-2 〗 设计人员在编程的同时要完成调试信息输出接口函数,但是测试点的选择可以由模块测试人员根据需要合理选择,测试点的选择可以根据测试用例而定,不同的测试用例选择不同的测试点。 |
说明: 为模块测试做准备。
〖建议 9-3 〗调测开关应分为不同级别和类型。 |
说明: 调测开关的设置及分类应从以下几方面考虑:针对模块或系统某部分代码的调测;针对模块或系统某功能的调测;出于某种其它目的,如对性能、容量等的测试。这样做便于软件功能的调测,并且便于模块的单元测试、系统联调等。
〖建议 9-4 〗在进行集成测试和系统联调之前,要构造好测试环境、测试项目及测试用例,同时仔细分析并优化测试用例,以提高测试效率。 |
说明: 好的测试用例应尽可能模拟出程序所遇到的边界值、各种复杂环境及一些极端情况等。
〖建议 9-5 〗程序的编译开关应该设置为最高优先级,并且编译选项不要选择优化。 |
说明: 将编译开关置为最高优先级,可以将程序的错误尽量暴露在编译阶段,便于修正程序;将编译选项设置为不优化,是为了避免编译器优化时出错,导致程序运行出错;将编译选项设置为不优化,也更容易在程序出错时对错误进行定位。
〖建议 9-6 〗在设计时考虑以下常见发现错误的方法。 |
说明: 以下发现错误的方法为可以为编写可测试性代码提供思路 :
· 使用所有数据建立假设
· 求精产生错误的测试用例
· 通过不同的方法再生错误
· 产生更多的数据以生成更多的假设
· 使用否定测试结果
· 提出尽可能多的假设
· 缩小可疑代码区
· 检查最近作过修改的代码
· 扩展可疑代码区
· 逐步集成
· 怀疑以前出过错的子程序
· 耐心检查
· 为迅速的草率的调试设定最大时间
· 检查一般错误
· 使用交谈调试法
· 中断对问题的思考
〖建议 9-7 〗在设计时考虑以下常见改正错误的方法。 |
说明: 以下改正错误的方法为可以为编写可测试性代码提供思路 :
· 理解问题的实质
· 理解整个程序
· 确诊错误
· 放松情绪
· 保存初始源代码
· 修改错误而不是修改症状
· 仅为某种原因修改代码
· 一次作一个修改
· 检查你的工作,验证修改
· 寻找相似错误
〖建议 9-8 〗程序开发人员对自己模块内的函数必须通过有效的方法进行测试,保证所有代码都执行到。 |
10. 断言与错误处理
断言是对某种假设条件进行检查(可理解为若条件成立则无动作,否则应报告)。它可以快速发现并定位软件问题,同时对系统错误进行自动报警。断言可以对在系统中隐藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性。在实际应用时,可根据具体情况灵活地设计断言。
说明: 整个软件系统提供一个统一的断言函数,如 Assert(exp) , 同时可提供不同的宏进行定义(可根据具体情况灵活设计),如:
( 1 ) #define ASSERT_EXIT_M 中断当前程序执行,打印中断发生的文件、行号,该宏一般在单调时使用。
( 2 ) #define ASSERT_CONTINUE_M 打印程序发生错误或异常的文件 , 行号 , 继续进行后续的操作,该宏一般在联调时使用。
( 3 ) #define ASSERT_OK_M 空操作,程序发生错误情况时,继续进行,可以通过适当的方式通知后台的监控或统计程序,该宏一般在 RELEASE 版本中使用。
【规则 10-2 】使用断言检查函数输入参数的有效性、合法性。 |
说明: 检查函数的输入参数是否合法,如输入参数为指针,则可用断言检查该指针是否为空,如输入参数为索引,则检查索引是否在值域范围内。
正例:
BYTE StoreCsrMsg(WORD wIndex, T_CMServReq *ptMsgCSR)
{
WORD wStoreIndex;
T_FuncRet tFuncRet;
Assert (wIndex < MAX_DATA_AREA_NUM_A); // 使用断言检查索引
Assert (ptMsgCSR != NULL); // 使用断言检查指针
… // 其它代码
return OK_M;
}
【规则 10-3 】使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。 |
说明: 断言是用来处理不应该发生的错误情况的,对于可能会发生的且必须处理的情况要写防错程序,而不是断言。如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。
【规则 10-4 】指向指针的指针及更多级的指针必须逐级检查。 |
说明: 对指针逐级检查,有利于给错误准确定位。
正例:
Assert ( (ptStru != NULL)
&& (ptStru->ptForward != NULL)
&& (ptStru->ptForward->ptBackward != NULL));
反例:
Assert (ptStru->ptForward->ptBackward != NULL);
【规则 10-5 】对所有具有返回值的接口函数的返回结果进行断言检查。 |
说明: 对接口函数的返回结果进行检查,可以避免程序运行过程中使用不正确的返回值引起错误。
正例:
BYTE HandleTpWaitAssEvent(T_CcuData *ptUdata, BYTE *pucMsg)
{
T_CacAssignFail *ptAssignfail;
T_CccData *ptCdata;
ptAssignfail = (T_CacAssignFail *)pbMsg;
… // 其它代码
ptCdata = GetCallData(ptUdata->waCallindex[0]);
Assert (ptCdata != NULL); // 使用断言对函数的返回结果进行检查
… // 其它代码
return CCNO_M;
}
【规则 10-6 】对较复杂的断言加上明确的注释。 |
说明: 为复杂的断言加注释,可澄清断言含义并减少不必要的误用。
【规则 10-7 】用断言保证没有定义的特性或功能不被使用。 |
说明: 假设某通信模块在设计时,在消息处理接口准备处理“同步消息”和“异步消息”。但当前的版本中的消息处理接口仅实现了处理“异步消息”,且在此版本的正式发行版中,用户层(上层模块)不应产生发送“同步消息”的请求,那么在测试时可用断言检查用户是否发送了“同步消息”。
正例 :
const CHAR ASYN_EVENT = 0;
const CHAR SYN_EVENT = 1;
WORD MsgProcess( T_ExamMessage *ptMsg )
{
CHAR cType; // 消息类型
Assert (ptMsg != NULL); // 用断言检查消息是否为空
cType = GetMsgType (ptMsg);
Assert (cType != SYN_EVENT); // 用断言检查是否是同步消息
... // 其它代码
}
【规则 10-8 】用调测开关来切换软件的 DEBUG 版和 RELEASE 版,而不要同时存在 RELEASE 版本和 DEBUG 版本的不同源文件,以减少维护的难度。 |
说明: DEBUG 版和 RELEASE 版的源文件相同,通过调测开关来进行区分,有利于版本的管理和维护。
【规则 10-9 】正式软件产品中应把断言及其它调测代码去掉(即把有关的调测开关关掉)。 |
说明: 加快软件运行速度。
【规则 10-10 】在软件系统中设置与取消有关测试手段,不能对软件实现的功能等产生影响。 |
说明: 即有测试代码的软件和关掉测试代码的软件,在功能行为上应该一致。
【规则 10-11 】用断言来检查程序正常运行时不应发生但在调测时有可能发生的非法情况。 |
说明: 对 RELEASE 版本不用的测试代码可以通过断言来检查测试代码中的非法情况。
〖建议 10-1 〗用断言对程序开发环境( OS/Compiler/Hardware )的假设进行检查。 |
说明: 程序运行时所需的软硬件环境及配置要求,不能用断言来检查,而必须由一段专门代码处理。用断言仅可对程序开发环境中的假设及所配置的某版本软硬件是否具有某种功能的假设进行检查。如某网卡是否在系统运行环境中配置了,应由程序中正式代码来检查;而此网卡是否具有某设想的功能,则可由断言来检查。
对编译器提供的功能及特性假设可用断言检查,原因是软件最终产品(即运行代码或机器码)与编译器已没有任何直接关系,即软件运行过程中(注意不是编译过程中)不会也不应该对编译器的功能提出任何需求。如用断言检查编译器的 int 型数据占用的内存空间是否为 2 :
Assert (sizeof(int) == 2);
〖建议 10-2 〗尽可能模拟出各种程序出错状态,测试软件对出错状态的处理。 |
说明: “不要让事情很少发生。”需要确定子系统中可能发生哪些事情,并且使它们一定发生和经常发生。如果发现子系统中有极罕见的行为,要干方百计地设法使其重现。
〖建议 10-3 〗编写错误处理程序,然后在处理错误之后可用断言宣布发生错误。 |
说明: 假如某模块收到通信链路上的消息,则应对消息的合法性进行检查,若消息类别不是通信协议中规定的,则应进行出错处理,之后可用断言报告。
正例:
#ifdef _EXAM_ASSERT_TEST_ // 若使用断言测试
/* 注意 : 这个函数不终止和退出程序 t */
VOID AssertReport(CHAR *pcFileName, WORD wLineno)
{
printf(“/n[EXAM]Error Report:%s, ling%u/n”,
pcFileName, wLineno);
}
#define ASSERT_REPORT(condition)
if (condition) // 若条件成立,则无动作
{
NULL;
}
else // 否则报告
{
AssertReport(_FILE_, _LINE_)
}
#else // 若不使用断言测试
#define ASSERT_REPORT(condition) NULL
#endif // 断言结束
WORD MsgHandle(CHAR cMsgname, CHAR *pcMsg)
{
switch(cMsgname)
{
case MSG_ONE:
{
… // 消息 MSG_ONE 处理
retum MSG_HANDLE_SUCCESS;
}
… // 其它合法消息处理
default:
{
… // 消息出错处理
ASSERT_REPORT ( FALSE ) ; // “合法”消息不成立,报告
retum MSG_HANDLE_ERROR;
}
}
}