05-01:定义全局变量时必须仔细分析,明确其含义、作用、取值范围及与其它全局变量间的关系
说明:全局变量关系到程序的结构框架,对于全局变量的理解关系到对整个程序能否正确理解,所以在对全局变量声明的同时,应对其含义、作用及取值范围进行详细地注释说明,若有必要还应说明与其它变量的关系
05-02:明确全局变量与操作此全局变量的函数或过程的关系
说明:全局变量与函数的关系包括:创建、修改及访问。明确过程操作变量的关系后,将有利于程序的进一步优化、单元测试、系统联调以及代码维护等。这种关系的说明可在注释或文档中描述。
05-03:一个变量有且只有一个功能,不能把一个变量用作多种用途
说明:一个变量只用来表示一个特定功能,不能把一个变量作多种用途,即同一变量取值不同时,其代表的意义也不同。
05-04:循环语句与判断语句中,不允许对其它变量进行计算与赋值
说明:循环语句只完成循环控制功能,if语句只完成逻辑判断功能,不能完成计算赋值功能。
正例:
do
{
[处理语句]
cInput = GetChar();
} while (cInput == 0);
反例:
do
{
[处理语句]
} while (cInput = GetChar());
05-05:宏定义中如果包含表达式或变量,表达式和变量必须用小括号括起来
正例:
#define HANDLE(A, B) (( A ) / ( B ))
反例:
#define HANDLE(A, B) (A / B)
05-06:使用宏定义多行语句时, 必须使用 { } 把这些语句括起来
说明:在宏定义中,对多行语句使用大括号,可以避免可能发生的错误。在C++中使用内联函数代替多行宏定义语句。
05-07:尽量构造仅有一个模块或函数可以修改、创建的全局变量,而其余有关模块或函数只能访问
05-08:对于全局变量通过统一的函数访问
05-09:尽量使用const说明常量数据,对于宏定义的常数,必须指出其类型
正例:
const int MAX_COUNT = 1000;
#define MAX_COUNT (int)1000
反例:
#define MAX_COUNT 1000
05-10: 最好不要在语句块内声明局部变量
05-11:结构和联合必须被类型化
正例:
typedef struct
{
char acName[NAME_SIZE];
WORD wScore;
} T_Student;
T_Student *ptStudent;
反例:
struct student
{
char acName[NAME_SIZE];
WORD wScore;
} *ptStudent;
05-12:使用严格形式定义的、可移植的数据类型,尽量不要使用与具体硬件或软件环境关系密切的变量
说明:使用统一的自定义数据类型,有利于程序的移植。
自定义数据类型 类型说明 类型定义(以Win32为例)
VOID 空类型 void
BOOLEAN 逻辑类型 (TRUE或FALSE) unsigned char
BYTE/ UCHAR 无符号 8 位整数 unsigned char
CHAR 有符号 8 位整数 signed char
WORD16/ WORD 无符号 16 位整数 unsigned short
SWORD16/SHORT 有符号 16 位整数 signed short
WORD32/DWORD 无符号 32 位整数 unsigned int
SWORD32/INT/LONG 有符号 32 位整数 signed int
FP32/FLOAT 32 位单精度符点数 float
FP64/DOUBLE 64 位双精度符点数 ouble
05-13:结构是针对一种事务的抽象,功能要单一,不要设计面面俱到的数据结构
说明:设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。
05-14:不同结构间的关系要尽量简单,若两个结构间关系较复杂、密切,那么应合为一个结构
05-15:结构中元素的个数应适中。若结构中元素个数过多可考虑依据某种原则把元素组成不同的子结构,以减少原结构中元素的个数
05-16:仔细设计结构中元素的布局与排列顺序,使结构容易理解、节省占用空间,并减少引起误用现象,对于结构中未用的位明确地给予保留
说明:合理排列结构中元素顺序,可节省空间并增加可理解性。
正例:如下形式,不仅可节省字节空间,可读性也变好了。
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;
05-17:结构的设计要尽量考虑向前兼容和以后的版本升级,并为某些未来可能的应用保留余地(如预留一些空间等)
05-18:注意具体语言及编译器处理不同数据类型的原则及有关细节
05-19:合理地设计数据并使用自定义数据类型,尽量减少没有必要的数据类型默认转换与强制转换
05-20:当声明数据结构时,必须考虑机器的字节顺序、使用的位域及字节对齐等问题
05-21:结构定义时, 尽量做到 pack 1,2,4,8 无关
06-01:运算符的优先级与结合律表
运算符 结合律
( ) [ ] -> . 从左至右
! ~ ++ -- (类型) sizeof + - * & 从右至左
* / % 从左至右
+ - 从左至右
<< >> 从左至右
< <= > >= 从左至右
== != 从左至右
& 从左至右
^ 从左至右
| 从左至右
&& 从左至右
|| 从右至左
?: 从右至左
= += -= *= /= %= &= ^= |= <<= >>= 从左至右
06-02:一条语句只完成一个功能
06-03:在表达式中使用括号,使表达式的运算顺序更清晰
说明:由于将运算符的优先级与结合律熟记是比较困难的,为了防止产生歧义并提高可读性,即使不加括号时运算顺序不会改变,也应当用括号确定表达式的操作顺序。
正例:
if (((iYear % 4 == 0) && (iYear % 100 != 0)) || (iYear % 400 == 0))
反例:
if (iYear % 4 == 0 && iYear % 100 != 0 || iYear % 400 == 0)
06-04:避免表达式中的附加功能,不要编写太复杂的复合表达式
说明:带附加功能的表达式难于阅读和维护,它们常常导致错误。对于一个好的编译器,下面两种情况效果是一样的。
06-05:不可将布尔变量和逻辑表达式直接与TRUE、FALSE或者1、0进行比较
说明:TURE和FALSE的定义值是和语言环境相关的,且可能会被重定义的。
正例:
设bFlag 是布尔类型的变量
if (bFlag) // 表示flag为真
if (!bFlag) // 表示flag为假
反例:
设bFlag 是布尔类型的变量
if (bFlag == TRUE)
if (bFlag == 1)
if (bFlag == FALSE)
if (bFlag == 0)
06-06:在条件判断语句中,当整型变量与0 比较时,不可模仿布尔变量的风格,应当将整型变量用“==”或“!=”直接与0比较
正例:
if (iValue == 0)
if (iValue != 0)
反例:
if (iValue) // 会让人误解 iValue是布尔变量
if (!iValue)
06-07:不可将浮点变量用“==”或“!=”与任何数字比较
说明:无论是float还是double类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该转化成“>=”或“<=”形式。
正例:
if ((fResult >= -EPSINON) && (fResult <= EPSINON))
反例:
if (fResult == 0.0) // 隐含错误的比较
其中EPSINON是允许的误差(即精度)。
06-08:应当将指针变量用“==”或“!=”与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)
06-09: 在switch语句中,每一个case分支必须使用break结尾,最后一个分支必须是default分支
06-10: 尽量避免在for 循环体内修改循环变量,防止for 循环失去控制
06-11:循环嵌套次数不大于3次
06-12:do while语句和while语句仅使用一个条件
06-13:当switch语句的分支比较多时,采用数据驱动方式
06-14:如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面
说明:下面两个示例中,反例比正例多执行了NUM -1次逻辑判断。并且由于前者总要进行逻辑判断,使得编译器不能对循环进行优化处理,降低了效率。如果NUM非常大,最好采用正例的写法,可以提高效率。
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();
}
}
06-15:for语句的循环控制变量的取值采用“半开半闭区间”写法
06-16:在进行“==”比较时,将常量或常数放在“==”号的左边
说明:可以采用这种方式,让编译器去发现错误。
正例:
if (NULL == pTail)
if (0 == iSum)
示例中有意把p和NULL颠倒。编译器认为 if (pTail = NULL) 是合法的,但是会指出 if (NULL = pTail)是错误的,因为NULL不能被赋值
07-01:如果函数没有参数,则用void填充
说明:函数在说明的时候,可以省略参数名。但是为了提高代码的可读性,要求不能省略。
正例:
void SetValue(int iWidth, int iHeight);
float GetValue(void);
反例:
void SetValue(int, int);
float GetValue();
07-02:如果参数是指针,且仅作输入用,则应在类型前加const
说明:防止该指针在函数体内被意外修改。
正例:
int GetStrLen(const char *pcString);
07-03:当结构变量作为参数时,应传送结构的指针而不传送整个结构体,并且不得修改结构中的元素,用作输出时除外
说明:一个函数被调用的时候,形参会被一个个压入被调函数的堆栈中,在函数调用结束以后再弹出。一个结构所包含的变量往往比较多,直接以一个结构为参数,压栈出栈的内容就会太多,不但占用堆栈空间,而且影响代码执行效率,如果使用不当还可能导致堆栈的溢出。如果使用结构的指针作为参数,因为指针的长度是固定不变的,结构的大小就不会影响代码执行的效率,也不会过多地占用堆栈空间
07-04:避免函数有太多的参数,参数个数尽量控制在5个以内
说明:如果参数太多,在使用时容易将参数类型或顺序搞错,而且调用的时候也不方便。如果参数的确比较多,而且输入的参数相互之间的关系比较紧密,不妨把这些参数定义成一个结构,然后把结构的指针当成参数输入
07-05:参数的顺序要合理
说明:参数的顺序要遵循程序员的习惯。如输入参数放在前面,输出参数放在后面等。
正例:
int RelRadioChan(const T_RelRadioChanReq *ptReq, T_RelRadioChanAck *ptAck);
07-06:尽量不要使用类型和数目不确定的参数
07-07:避免使用BOOLEAN参数
说明:一方面因为BOOLEAN参数值无意义,TRUE/FALSE的含义是非常模糊的,在调用时很难知道该参数到底传达的是什么意思;其次BOOLEAN参数值不利于扩充
07-08:不要省略返回值的类型,如果函数没有返回值,那么应声明为void类型
07-09:对于有返回值的函数,每一个分支都必须有返回值
说明:为了保证对被调用函数返回值的判断,有返回值的函数中的每一个退出点都需要有返回值
07-10:如果返回值表示函数运行是否正常,规定0为正常退出,不同非0值标识不同异常退出。避免使用TRUE或FALSE作为返回值
07-11:函数体的规模不能太大,尽量控制在200行代码之内
07-12:为简单功能编写函数
07-13:必须对所调用函数的错误返回值进行处理
07-14:减少函数本身或函数间的递归调用
说明:递归调用特别是函数间的递归调用(如A->B->C->A),影响程序的可理解性;递归调用一般都占用较多的系统资源(如栈空间);递归调用对程序的测试有一定影响。故除非为某些算法或功能的实现方便,应减少没必要的递归调用
08-01:为保证代码的可靠性,编程时请遵循如下基本原则,优先级递减:
正确性,指程序要实现设计要求的功能。
稳定性、安全性,指程序稳定、可靠、安全。
可测试性,指程序要方便测试。
规范/可读性,指程序书写风格、命名规则等要符合规范。
全局效率,指软件系统的整体效率。
局部效率,指某个模块/子模块/函数的本身效率。
个人表达方式/个人方便性,指个人编程习惯。
08-02:在程序编制之前,必须了解编译系统的内存分配方式,特别是编译系统对不同类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等
08-03:防止内存操作越界
08-04:须对动态申请的内存做有效性检查,并进行初始化;动态内存的释放必须和分配成对以防止内存泄漏,释放后内存指针置为NULL
08-05:不使用realloc( )
08-06:变量在使用前应初始化,防止未经初始化的变量被引用
08-07:由于内存总量是有限的,软件系统各模块应约束自己的代码,尽量少占用系统内存
08-08:在通信程序中,为了保证高可靠性,一般不使用内存的动态分配
08-09:在往一个内存区连续赋值之前(memset,memcpy…),应确保内存区的大小能够容纳所赋的数据
08-10:尽量使用memmove( )代替memcpy( )
在源、目的内存区域发生重叠的情况下,如果使用memcpy可能导致重叠区的数据被覆盖
08-11:C++程序中,分配内存使用new和delete,而不使用malloc和free
08-12:指针类型变量必须初始化为NULL
08-13:指针不要进行复杂的逻辑或算术操作
08-14:如果指针类型明确不会改变,应该强制为const类型的指针,以加强编译器的检查
08-15:减少指针和数据类型的强制类型转化
08-16:移位操作一定要确定类型
说明: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;
08-17:对变量进行赋值时,必须对其值进行合法性检查,防止越界等现象发生
08-18:类中的属性应声明为private,用公有的函数访问
08-19: 在编写派生类的赋值函数时,注意不要忘记对基类的成员变量重新赋值
说明:除非在派生类中调用基类的赋值函数,否则基类变量不会自动被赋值。
正例:
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)返回本对象的引用
}
08-20:构造函数应完成简单有效的功能,不应完成复杂的运算和大量的内存管理
08-21:正确处理拷贝构造函数与赋值函数
说明:由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数有些轻视。如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。
反例:
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改成私有数据,用赋* 值函数进行赋值
*/
….
}
08-22:过程/函数中申请的(为打开文件而使用的)文件句柄,在过程/函数退出之前要关闭,除非要把这个句柄传递给其它函数使用
08-23:派生类的构造函数应在其初始化表里调用基类的构造函数
08-24:类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,非内部数据类型的成员对象应当采用第一中方式初始化,以获得更高的效率
08-25:编写可重入函数时,若使用全局变量,则应通过信号量(即P、V操作)等手段对其加以保护
08-26:不要在栈中分配类的实例,也不要生成全局类实例
08-27:通过继承来共享相同的属性和行为。基类的析构函数应该声明为virtual
08-28:减少友元函数的使用
09-01:在同一项目组或产品组内,为准备集成测试和系统联调,要有一套统一的调测开关及相应信息输出函数,并且要有详细的说明。统一的调试接口和输出函数由模块设计和测试人员根据项目特性统一制订,由项目系统人员统一纳入系统设计中
09-02:在同一个项目组或产品组内,调测打印出的信息串要有统一的格式。信息串中应当包含所在的模块名(或源文件名)及行号等信息
09-03:在编写代码之前,应预先设计好程序调试与测试的方法和手段,并设计好各种调测开关及相应测试代码(如打印函数等)
09-04:在同一项目组或产品组内,可以统一由模块设计和测试人员开发调试信息接收平台,统一对软件调试信息进行分析
09-05:设计人员在编程的同时要完成调试信息输出接口函数,但是测试点的选择可以由模块测试人员根据需要合理选择,测试点的选择可以根据测试用例而定,不同的测试用例选择不同的测试点
09-06:调测开关应分为不同级别和类型
09-07:在进行集成测试和系统联调之前,要构造好测试环境、测试项目及测试用例,同时仔细分析并优化测试用例,以提高测试效率
09-08:程序的编译开关应该设置为最高优先级,并且编译选项不要选择优化
09-09:在设计时考虑以下常见发现错误的方法
说明: 以下发现错误的方法为可以为编写可测试性代码提供思路:
• 使用所有数据建立假设
• 求精发现错误的测试用例
• 通过不同的方法再生错误
• 产生更多的数据以生成更多的假设
• 使用否定测试结果
• 提出尽可能多的假设
• 缩小可疑代码区
• 检查最近作过修改的代码
• 扩展可疑代码区
• 逐步集成
• 怀疑以前出过错的子程序
• 耐心检查
• 为迅速的草率的调试设定最大时间
• 检查一般错误
• 使用交谈调试法
• 中断对问题的思考
09-10: 在设计时考虑以下常见改正错误的方法
说明:以下改正错误的方法可以为编写可测试性代码提供思路:
• 理解问题的实质
• 理解整个程序
• 确诊错误
• 放松情绪
• 保存初始源代码
• 修改错误而不是修改症状
• 仅为某种原因修改代码
• 一次作一个修改
• 检查你的工作,验证修改
• 寻找相似错误
09-11:程序开发人员对自己模块内的函数必须通过有效的方法进行测试,保证所有代码都执行到
10-01:整个软件系统应该采用统一的断言。如果系统不提供断言,则应该自己构造一个统一的断言供编程时使用
说明:整个软件系统提供一个统一的断言函数,如Assert(exp), 同时可提供不同的宏进行定义(可根据具体情况灵活设计),如:
(1)#define ASSERT_EXIT_M 中断当前程序执行,打印中断发生的文件、行号,该宏一般在单调时使用。
(2)#define ASSERT_CONTINUE_M 打印程序发生错误或异常的文件,行号,继续进行后续的操作,该宏一般在联调时使用。
(3)#define ASSERT_OK_M 空操作,程序发生错误情况时,继续进行,可以通过适当的方式通知后台的监控或统计程序,该宏一般在RELEASE版本中使用。
10-02:使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的
说明:断言是用来处理不应该发生的错误情况的,对于可能会发生的且必须处理的情况要写防错程序,而不是断言。如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。C++中可使用异常处理机制来捕捉错误
10-03:指向指针的指针及更多级的指针必须逐级检查
说明:对指针逐级检查,有利于给错误准确定位。
正例:
Assert ( (ptStru != NULL)
&& (ptStru->ptForward != NULL)
&& (ptStru->ptForward->ptBackward != NULL));
反例:
Assert (ptStru->ptForward->ptBackward != NULL);
10-04:对较复杂的断言加上明确的注释
10-05:用断言保证没有定义的特性或功能不被使用
10-06:用调测开关来切换软件的DEBUG版和RELEASE版,而不要同时存在RELEASE版本和DEBUG版本的不同源文件,以减少维护的难度
10-07:正式软件产品中应把断言及其它调测代码去掉(即把有关的调测开关关掉)
10-08:在软件系统中设置与取消有关测试手段,不能对软件实现的功能等产生影响
10-09:用断言来检查程序正常运行时不应发生但在调测时有可能发生的非法情况
10-10:用断言对程序开发环境(OS/Compiler/Hardware)的假设进行检查
10-11:尽可能模拟出各种程序出错状态,测试软件对出错状态的处理
10-12:编写错误处理程序,然后在处理错误之后可用断言宣布发生错误
10-13:使用断言检查函数输入参数的有效性、合法性
说明:检查函数的输入参数是否合法,如输入参数为指针,则可用断言检查该指针是否为空,如输入参数为索引,则检查索引是否在值域范围内。
10-14:对所有具有返回值的接口函数的返回结果进行断言检查
10-15:程序中应该显式地处理异常
10-16:尽量少用自定义的异常
10-17:异常处理的try{}中不要加入过多语句
说明:适用于C++。为方便调试,增加程序可读性,一个try{}中应该只处理一条可产生异常的语句。在异常发生时,可明确得知发生异常的语句。如果多条可能出现异常的语句出现在一起,应该嵌套在不同的Try-catch语句中。