目录
5 变量、结构
5-1 防止同名
5-2 去掉没必要的公共变量
5-3 结构功能单一
5-4 数据结构避免过于灵活、全面
5-5 检查结构元素布局与排列
6 函数、过程
6-1 可重入函数使用局部变量
6-2 可重入函数使用全部变量需要保护
6-3 防止将函数的参数作为工作变量
6-4 函数的规模尽量限制在200行以内
6-5 为简单功能编写函数
6-6 避免设计多参数函数
6-7 注意非调度函数
6-8 动宾组词命名函数
6-9 防止把没有关联的语句放到一个函数中
6-10 设计高扇入、合理扇出(小于7)的函数
6-11 避免使用 BOOL 参数
7 可测性
7-1 使用断言,提高可测性
7-2 断言检查非法情况
7-3 断言避免错误情况
7-4 断言确认函数参数
7-5 断言检查开发环境
7-6 正式版本去掉断言
7-7 断言宣布错误发生
8 程序效率
8-1 局部效率应为全局效率服务
8-2 对数据结构的划分与组织的改进,以及算法优化来提高空间效率
8-3 循环体内工作量最小化
8-4 多重循环中,应将最忙的循环放在最内层
8-5 尽量用乘法或其它方法代替除法,特别是浮点运算中的除法
9 宏
9-1 用宏定义表达式时,要使用完备的括号
9-2 使用宏时,不允许参数发生变化
防止局部变量与公共变量同名。
说明:公共变量是增大模块间耦合的原因之一,故应减少没必要的公共变量以降低模块间的耦合度
结构的功能要单一,是针对一种事务的抽象。
示例:如下结构不太清晰、合理。
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 */
unsigned 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;
不要设计面面俱到、非常灵活的数据结构。
说明:面面俱到、灵活的数据结构反而容易引起误解和操作困难
仔细设计结构中元素的布局与排列顺序,使结构容易理解、节省占用空间,并减少引起误用现象。
示例:如下结构中的位域排列,将占较大空间,可读性也稍差。
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;
编写可重入函数应注意局部变量的使用(如使用auto即缺省态局部变量或寄存器变量)。
说明:编写 C/C++语言的可重入函数时,不应使用 static 局部变量,否则必须经过特殊处理,才能使函数具有可重入性。
编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段加以保护。
说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态
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;
temp = Square_Exam( );
[释放信号量操作]
return temp;
}
说明:将函数的参数作为工作变量,有可能错误地改变参数内容,所以很危险。对必须改
变的参数,最好先用局部变量代之,最后再将该局部变量的内容赋给该参数。
示例:下函数的实现不太好。
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;
}
说明:不包括注释和空格行
说明:虽然为仅用一两行就可完成的功能去编函数好象没有必要,但用函数可使功能明确
化,增加程序可读性,亦可方便维护、测试。
示例:如下语句的功能不很明显。
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);
不使用的参数从接口中去掉,减少函数间接口的复杂度
应减少或防止控制参数,尽量只使用数据参数
目的是防止函数间的控制耦合。调度函数是指根据输入的消息类型或控制命令,来启动相
应的功能实体(即函数或过程),而本身并不完成具体功能。控制参数是指改变函数功能
行为的参数,即函数要根据此参数来决定具体怎样工作。非调度函数的控制参数增加了函
数间的控制耦合,很可能使函数间的耦合度增大,并使函数的功能不唯一。
示例:如下函数构造不太合理。
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);
}
使用动宾词组为执行某操作的函数命名。如果是OOP方法,可只有动词(名词是对象本身)
示例:参照如下方式命名函数。
void print_record( unsigned int rec_ind ) ;
int input_record( void ) ;
unsigned char get_current_color( void ) ;
防止函数或过程内出现随机内聚。随机内聚是指将没有关联或关联很弱的语句放到
同一个函数或过程中
示例:如下函数就是一种随机内聚。
void Init_Var( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的长与宽 */
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标 */
}
//矩形的长、宽与点的坐标基本没有任何关系,故以上函数是随机内聚。
应如下分为两个函数:
void Init_Rect( void )
{
Rect.length = 0;
Rect.width = 0; /* 初始化矩形的长与宽 */
}
void Init_Point( void )
{
Point.x = 10;
Point.y = 10; /* 初始化“点”的坐标 */
}
扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。
扇出过大:
表明函数过分复杂,需要控制和协调过多的下级函数;
一般是由于缺乏中间层次,可适当增加中间层次的函数。
扇出过小:
如总是1,表明函数的调用层次可能过多,这样不利于程序的阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力;
可把下级函数进一步分解为多个函数,或合并到上级函数中;
扇入越大:
表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。
较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。
说明:断言是对某种假设条件进行检查(可理解为若条件成立则无动作,否则应报告),
它可以快速发现并定位软件问题,同时对系统错误进行自动报警。断言可以对在系统中隐
藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的
可测性
示例:下面是 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 */
检查程序正常运行时不应发生但在调测时有可能发生的非法情况
不能用断言来检查最终产品肯定会出现且必须处理的错误情况
断言是用来处理不应该发生的错误情况的,对于可能会发生的且必须处理的情况要写防错
程序,而不是断言
示例:假设某函数参数中有一个指针,那么使用指针前可对它检查,如下。
int exam_fun( unsigned char *str )
{
EXAM_ASSERT( str != NULL ); // 用断言检查“假设指针不为空”这个条件
... //other program code
}
用断言对程序开发环境(OS/Hardware)的假设进行检查
对编译器提供的功能及特性假设可用断言检查,原因是软件最终产品(即运行代码或机器
码)与编译器已没有任何直接关系,即软件运行过程中(注意不是编译过程中)不会也不
应该对编译器的功能提出任何需求
示例:用断言检查编译器的 int 型数据占用的内存空间是否为 2,如下。
EXAM_ASSERT( sizeof( int ) == 2 );
正式软件产品中把断言及其它调测代码去掉
加快软件运行速度
编写防错程序,然后在处理错误之后可用断言宣布发生错误
假如某模块收到通信链路上的消息,则应对消息的合法性进行检查,若消息类别不是通信协议中规定的,则应进行出错处理,之后可用断言报告
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;
}
}
示例:如下记录学生学习成绩的结构不合理。
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;
说明:应仔细考虑循环体内的语句是否可以放在循环体之外,使循环体内工作量最小,从
而提高程序的时间效率
示例:如下代码效率不高。
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 */
说明:减少 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];
}
}
说明:浮点运算除法要占用较多 CPU 资源
示例:如下表达式运算可能要占较多 CPU 资源。
#define PAI 3.1416
radius = circle_length / (2 * PAI);
应如下把浮点除法改为浮点乘法。
#define PAI_RECIPROCAL (1 / 3.1416 ) // 编译器编译时,将生成具体浮点数
radius = circle_length * PAI_RECIPROCAL / 2;
示例:如下定义的宏都存在一定的风险。
#define RECTANGLE_AREA( a, b ) a * b
#define RECTANGLE_AREA( a, b ) (a * b)
#define RECTANGLE_AREA( a, b ) (a) * (b)
正确的定义应为:
#define RECTANGLE_AREA( a, b ) ((a) * (b))
示例:如下用法可能导致错误。
#define SQUARE( a ) ((a) * (a))
int a = 5;
int b;
b = SQUARE( a++ ); // 结果:a = 7,即执行了两次增 1。
正确的用法是:
b = SQUARE( a );
a++; // 结果:a = 6,即只执行了一次增 1