1994年,在英国成立了一个叫做汽车工业软件可靠性联合会(The Motor Industry Software Reliability Association,以下简称MISRA)的组织。它是致力于协助汽车厂商开发安全可靠的软件的跨国协会,其成员包括:AB汽车电子、罗孚汽车、宾利汽车、福特汽车、捷豹汽车、路虎公司、Lotus公司、MIRA公司、Ricardo公司、TRW汽车电子、利兹大学和福特VISTEON汽车系统公司。
经过了四年的研究和准备,MISRA于1998年发布了一个针对汽车工业软件安全性的C语言编程规范——《汽车专用软件的C语言编程指南》(Guidelines for the Use of the C Language in Vehicle Based Software),共有127条规则,称为MISRAC:1998。[Page]
C语言并不乏国际标准。国际标准化组织(International Organization of Standardization,简称ISO)的“标准C语言”经历了从C90、C96到C99的变动。但是,嵌入式程序员很难将ISO标准当作编写安全代码的规范。一是因为标准C语言并不是针对代码安全的,也并不是专门为嵌入式应用设计的;二是因为“标准C语言”太庞大了,很难操作。MISRAC:1998规范的产生恰恰弥补了这方面的空白。
参考文献 1 MISRAC:2004, Guidelines for the use of the C language in critical systems. The Motor Industry Software Reliability Association, 2004
2 Harbison III. Samuel P, Steele Jr. Guy L. C语言参考手册. 邱仲潘,等译. 第5版. 北京:机械工业出版社,2003
3 Kernighan. Brian W, Ritchie. Dennis M. C程序设计语言. 徐宝文,等译. 第2版. 北京:机械工业出版社,2001
4 Koenig Andrew. C陷阱与缺陷. 高巍译. 北京:人民邮电出版社,2002
5 McCall Gavin. Introduction to MISRAC:2004, Visteon UK, http://www.MISRAC2.com/
6 Hennell Mike. MISRA CIts role in the bigger picture of critical software development, LDRA. http://www.MISRAC2.com/
7 Hatton Les. The MISRA C Compliance Suite—The next step, Oakwood Computing. http://www.MISRAC2.com/
8 Montgomery Steve. The role of MISRA C in developing automotive software, Ricardo Tarragon. http://www.MISRAC2.com/
各层次缩进的风格采用TAB缩进(TAB宽度原则上使用系统默认值,TC使用8空格宽度,VC使用4空格宽度)。示例:
if (x is true)
{
we do y
}
else
{
if (a > b)
{
...
}
else
{
...
}
}
和:
if (x == y)
{
...
}
else if (x > y)
{
...
}
else
{
....
}
注意,右括号所在的行不应当有其它东西,除非跟随着一个条件判断。也就是do-while语句中的“while”,象这样: do
{
body of do-loop
} while (condition);
在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如-> ),后不应加空格。采用这种松散方式编写代码的目的是使代码更加清晰。
由于留空格所产生的清晰性是相对的,所以,在已经非常清晰的语句中没有必要再留空格,如果语句已足够清晰则括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C/C++语言中括号已经是最清晰的标志了。
在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格。
(1)逗号、分号只在后面加空格。 int a, b, c;
(2)比较操作符, 赋值操作符"="、 "+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位域操作符"<<"、"^"等双目操作符的前后加空格。 if (current_time >= 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)
注释的原则是有助于对程序的阅读理解,在该加的地方都加了,注释不宜太多也不能太少,注释语言必须准确、易懂、简洁。通过对函数或过程、变量、结构等正确的命名以及合理地组织代码的结构,使代码成为自注释的——清晰准确的函数、变量等的命名,可增加代码可读性,并减少不必要的注释——过量的注释则是有害的。
注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。 示例:如下注释意义不大。
/* if receive_flag is TRUE */
if (receive_flag)
而如下的注释则给出了额外有用的信息。
/* if mtp receive a message from links */
if (receive_flag)
示例:可按如下形式说明枚举/数据/联合结构。
/* sccp interface with sccp user primitive message name */
enum SCCP_USER_PRIMITIVE
{
N_UNITDATA_IND, /* sccp notify sccp user unit data come */
N_NOTICE_IND, /* sccp notify user the No.7 network can not */
/* transmission this message */
N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
};
示例:
/* 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_GTTranErrorCode;
(8)注释与所描述内容进行同样的缩排,让程序排版整齐,并方便注释的阅读与理解。
示例:如下例子,排版不整齐,阅读稍感不方便。
void example_fun( void )
{
/* code one comments */
CodeBlock One
/* code two comments */
CodeBlock Two
}
应改为如下布局。
void example_fun( void )
{
/* code one comments */
CodeBlock One
/* code two comments */
CodeBlock Two
}
(9)将注释与其上面的代码用空行隔开。
示例:如下例子,显得代码过于紧凑。
/* code one comments */
program code one
/* code two comments */
program code two
应如下书写
/* code one comments */
program code one
(11)对于switch 语句下的case 语句,如果因为特殊情况需要处理完一个case 后进入下一个case 处理(即上一个case后无break),必须在该case 语句处理完、下一个case 语句前加上明确的注释,以清楚表达程序编写者的意图,有效防止无故遗漏break语句(可避免后期维护人员对此感到迷惑:原程序员是遗漏了break语句还是本来就不应该有)。示例:
case CMD_DOWN:
ProcessDown();
break;
case CMD_FWD:
ProcessFwd();
if (...)
{
...
break;
} else
{
ProcessCFW_B(); // now jump into case CMD_A
}
case CMD_A:
ProcessA();
break;
...
(12)在程序块的结束行右方加注释标记,以表明某程序块的结束。当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。示例:参见如下例子。
if (...)
{
program code
while (index < MAX_INDEX)
{
program code
} /* end of while (index < MAX_INDEX) */ // 指明该条while语句结束
} /* end of if (...)*/ // 指明是哪条if语句结束
(8)用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。 下面是一些在软件中常用的反义词组。
add / remove begin / end create / destroy
insert / delete first / last g et / 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 );
示例:如下函数,其返回值(即功能)是不可预测的。
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;
}
6.3 函数参数
(1)只当你确实需要时才用全局变量,函数间应尽可能使用参数、返回值传递消息。
(2)防止将函数的参数作为工作变量。将函数的参数作为工作变量,有可能错误地改变参数内容,所以很危险。对必须改变的参数,最好先用局部变量代之,最后再将该局部变量的内容赋给该参数。
示例:下函数的实现不太好。
void sum_data( unsigned int num, int *data, int *sum )
{
unsigned int count;
*sum = 0;
while (size-- >= 0) // 将出现下溢
{
... // program code
}
当size等于0时,再减1不会小于0,而是0xFF,故程序是一个死循环。应如下修改。
char size; // 从unsigned char 改为char
while (size-- >= 0)
{
... // program code
}
(19)使用变量时要注意其边界值的情况。
示例:如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;
(20)系统应具有一定的容错能力,对一些错误事件(如用户误操作等)能进行自动补救。
非计算机专业C语言初学者编程规范(学生用)—宏
(1)用宏定义表达式时,要使用完备的括号。
示例:如下定义的宏都存在一定的风险。
#define RECTANGLE_AREA( a, b ) a * b
#define RECTANGLE_AREA( a, b ) (a * b)
#define RECTANGLE_AREA( a, b ) (a) * (b)
正确的定义应为:
#define RECTANGLE_AREA( a, b ) ((a) * (b))
(2)将宏所定义的多条表达式放在大括号中。
示例:下面的语句只有宏的第一条表达式被执行。为了说明问题,for语句的书写稍不符规范。
#define INTI_RECT_VALUE( a, b )\
a = 0;\
b = 0;
for (index = 0; index < RECT_TOTAL_NUM; index++)
INTI_RECT_VALUE( rect.a, rect.b );
正确的用法应为:
#define INTI_RECT_VALUE( a, b )\
{\
a = 0;\
b = 0;\
}
for (index = 0; index < RECT_TOTAL_NUM; index++)
{
INTI_RECT_VALUE( rect[index].a, rect[index].b );
}
(3)使用宏时,不允许参数发生变化。
示例:如下用法可能导致错误。
#define SQUARE( a ) ((a) * (a))
int a = 5;
int b;
b = SQUARE( a++ ); // 结果:a = 7,即执行了两次增1。
正确的用法是:
b = SQUARE( a );
a++; // 结果:a = 6,即只执行了一次增1。
示例:如下例子不符合规范。
例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;
示例:可按如下形式说明枚举/数据/联合结构。
/* sccp interface with sccp user primitive message name */
enum SCCP_USER_PRIMITIVE
{
N_UNITDATA_IND, /* sccp notify sccp user unit data come */
N_NOTICE_IND, /* sccp notify user the No.7 network can not */
/* transmission this message */
N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
};
示例:
/* 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_GTTranErrorCode;
2-10:注释与所描述内容进行同样的缩排。
说明:可使程序排版整齐,并方便注释的阅读与理解。示例:如下例子,排版不整齐,阅读稍感不方便。
void example_fun( void )
{
/* code one comments */
CodeBlock One
/* code two comments */
CodeBlock Two
}
应改为如下布局。
void example_fun( void )
{
/* code one comments */
CodeBlock One
/* code two comments */
CodeBlock Two
}
示例:如下注释意义不大。
/* if receive_flag is TRUE */
if (receive_flag)
而如下的注释则给出了额外有用的信息。
/* if mtp receive a message from links */
if (receive_flag)
2-14:在程序块的结束行右方加注释标记,以表明某程序块的结束。
说明:当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。示例:参见如下例子。
if (...)
{
// program code
while (index < MAX_INDEX)
{
// program code
} /* end of while (index < MAX_INDEX) */ // 指明该条while 语句结束
} /* end of if (...)*/ // 指明是哪条if 语句结束
示例:下面所示的局部变量名的定义方法可以借鉴。
int liv_Width
其变量名解释如下:
l 局部变量(Local) (其它:g 全局变量(Global)...)
i 数据类型(Interger)
v 变量(Variable) (其它:c 常量(Const)...)
Width 变量含义
这样可以防止局部变量与全局变量重名。
说明:下面是一些在软件中常用的反义词组。
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 );
示例:如下函数,其返回值(即功能)是不可预测的。
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;
}
示例:如下定义的宏都存在一定的风险。
#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))
11-2:将宏所定义的多条表达式放在大括号中。
示例:下面的语句只有宏的第一条表达式被执行。为了说明问题,for 语句的书写稍不符规范。
#define INTI_RECT_VALUE( a, b )\
a = 0;\
b = 0;
for (index = 0; index < RECT_TOTAL_NUM; index++)
INTI_RECT_VALUE( rect.a, rect.b );
正确的用法应为:
#define INTI_RECT_VALUE( a, b )\
{\
a = 0;\
b = 0;\
}
for (index = 0; index < RECT_TOTAL_NUM; index++)
{
INTI_RECT_VALUE( rect[index].a, rect[index].b );
}
11-3:使用宏时,不允许参数发生变化。
示例:如下用法可能导致错误。
#define SQUARE( a ) ((a) * (a))
int a = 5;
int b;
b = SQUARE( a++ ); // 结果:a = 7,即执行了两次增1。
正确的用法是:
b = SQUARE( a );
a++; // 结果:a = 6,即只执行了一次增1。
许多C程序员发现在命名变量时使用下划线是很方便的,这可能是因为加下划线会大大加强可读性。例如,下面的两个函数名是相似的,但使用下划线的函数名的可读性更强:
check disk space available(selected disk drive);
CheckDiskSpaceAvailable (Selected Disk Drive);
上例中的第二个函数名使用了骆驼式命名法——见19.5中关于骆驼式的解释。
可以用变量名来指示变量的数据类型吗?
可以。在变量名中指出数据类型已经成为今天的大型复杂系统中普遍使用的一条规则。通常,变量类型由一个或两个字符表示,并且这些字符将作为变量名的前缀。使用这一技术的一种广为人知的命名规则就是匈牙利命名法,它的名称来自于Microsoft公司的程序员CharlesSimonyi。表19.2列出了一些常用的前缀。
表1 9.2一些常用的匈牙利命名法前缀
---------------------------------------------------------------------------------
数据类型 前缀 例子
---------------------------------------------------------------------------------
char c clnChar
int i iReturnValue
long l lNumRecs
string sz szlnputString ( 以零字节结束 )
int array ai aiErrorNumbers
char * psz pszInputString
---------------------------------------------------------------------------------
象Microsoft Windows这样的环境,就大量使用了匈牙利命名法或其派生体。其它一些第四代环境,例如Visual Basic和Access,也采用了匈牙利命名法的一种变体。
在C程序中用空白符隔开可执行语句、函数和注释等,将有助于提高程序的可读性和清晰度。许多时候,只要在语句之间加入空行,就可提高程序的可读性。请看下面的这段代码:
/ * clcpy by GBlansten * /
void clcpy(EMP * e,int rh,int ot)
{ e->grspy= (e->rt * rh)+ (e->rt * ot * 1.5) ;
e->txamt = e->grspy * e->txrt ;
e->ntpy = e->grspy-e->txamt ;
updacctdata (e);
if (e->dd= =false) cutpyck(e) ;
else prtstb (e) ; }
你可以看到,这个函数确实是一团糟。尽管这个函数显然是正确的,但恐怕这个世界上没有一个程序员愿意去维护这样的代码。如果采用本章中的一些命名规则(例如使用下划线,少用一些短而模糊的名字),使用一些大括号技巧,加入一些空白符和注释,那么这个函数将会是下面这个样子:
/********************************************************************************/
Function Name: calc_pay
Parameters: emp -EMPLOYEE pointer that points to employee data
reg-hours -The number of regular hours (<=40) employee
has worked
ot-hours -The number of overtime hours (>40) employee
has worked
Author : Gern Blansten
Date Written: 13 dec 1993
Modification: 04 sep 1994 by Lloyd E. Work
-Rewrote function to make it readable by human beings.
Description: This function calculates an employee's gross bay ,tax
amount, and net pay, and either prints a paycheck for the
employee or (in the case of those who have direct deposit)
prints a paycheck stub.
/*********************************************************************************/
void calc_pay (EMPLOYEE * emp, int reg hours, int or_hours)
{
/ * gross_pay = (employee rate * regular hours)+
(employee rate * overtime hours * 1.5) * /
emp->gross_pay= (emp->rate * reg_hours) +
(emp->rate * ot hours* 1.5);
/ * tax amount=gross_pay * employee's tax rate * /
emp->tax amount=emp->gross_pay * emp->tax-rate ;
/ * net pay=gross pay-tax amount * /
emp->net-pay=emp->gross pay-emp->tax_amount ;
/ * update the accounting data * /
update accounting data(emp);
/ * check for direct deposit * /
if (emp->direct_deposit= =false)
cut_ paycheck(emp); / * print a paycheck * /
else
print_paystub(emp); /* print a paycheck stub * /
}
你可以看到,Lloyd版本(该版本中使用了大量的注释、空行、描述性变量名等)的可读性比糟糕的Gern版本要强得多。你应该尽量在你认为合适的地方使用空白符(和注释符等),这将大大提高程序的可读性一一当然,这可能会延长你的工作时间。
请注意,在这些例子中,函数名都以一个动词开始,以一个名词结束。如果按英语习惯来读这些函数名,你会发现它们其实就是:
print the reports(打印报告)
spawn the utility program(生成实用程序)
exit the system(退出系统)
initialize the disk(初始化磁盘)
使用动词一名词规则(特别是在英语国家)能有效地加强程序的可读性,并且使程序看起来更熟悉。
如果可能的话,你应该避免使用递归函数。例如,前文中的阶乘函数可以写成下面这种形式:
# include <stdio. h>
void main(void) ;
unsigned long calc factorial(unsigned long x);
void main (void)
{
int x=5;
printf("The factorial of %d is %ld. \n" ,x ,calc_factorial (x)) ;
}
unsigned long calc-factorial(unsigned long x)
{
unsigned long factorial;
factorial=x;
while (x>1L)
{
factorial * =--x;
}
return (factorial);
}
这个版本的calc_factorial()函数用一个while循环来计算一个值的阶乘,它不仅比递归版本快得多,而且只占用很小的栈空间。
continue语句用来返回循环的起始处,而break语句用来退出循环。例如,下例中就有一条典型的continue语句:
while(!feof(infile))
{
fread(inbuffer,80,1,infile);/*read in a line from input file*/
if(!strncmpi(inbuffer,"REM",3)) /*check if it is
a comment line*/
continue; /*it's a comment,so jump back to the while()*/
else
parse_line(); /*not a comment—parse this line*/
}
上例读入一个文件并对其进行分析。“REM(remark的缩写)”用来标识正在被处理的文件中的一个注释行。因为注释行对程序不起任何作用,所以可以跳过它。在读入输入文件的每一行时,上例就把该行的前三个字母与"REM"进行比较。如果匹配,则该行就是注释行,于是就用continue语句返回到while语句,继续读入输入文件的下一行;否则,该行就是一条有效语句,于是就调用parse_line()函数对其进行分析。
break语句用来退出循环。下面是一个使用break语句的例子:
while (! feof(infile))
fread(inbuffer,80,1,infile) ;/* read in a line from input file * /
if (! strncmpi (inbuffer,"REM",3)) / * check if it is
a comment line * /
continue; /* it's a comment, so jump back to the while() * /
else
{
if (parse_line()==FATAL_ERROR) / * attempt to parse
this line * /
break; /* fatal error occurred,so exit the loop * /
}
这个例子建立在使用continue语句的那个例子的基础上。注意,在这个例子中,要检查parse_line()函数的返回值。如果parse_line()的返回值为FATAL_ERROR,就通过break语句立即退出while循环,并将控制权交给循环后面的第一条语句。