技术人员设计程序的首要目的是用于技术人员沟通和交流,其次才是用于机器执行。程序的生命力在于用户使用,程序的成长在于后期的维护及根据用户需求更新和升级功能。若是你的程序只能由你来维护,当你离开这个程序时,你的程序也和你一块儿离开了,这将给公司和后来接手的技术人员带来巨大的痛苦和损失。所以,为了程序可读、易理解、好维护,你的程序须要遵照必定的规范,你的程序须要设计。程序员
“程序必须为阅读它的人而编写,只是顺便用于机器执行。”算法
——Harold Abelson 和 Gerald Jay Sussmanexpress
“编写程序应该以人为本,计算机第二。”编程
——Steve McConnell数组
为提升产品代码质量,指导仪表嵌入式软件开发人员编写出简洁、可维护、可靠、可测试、高效、可移植的代码,编写了本规范。 安全
本规范将分为完整版和精简版,完整版将包括更多的样例、规范的解释以及参考材料(what & why),而精简版将只包含规则部分(what)以便查阅。 数据结构
在本规范的最后,列出了一些业界比较优秀的编程规范,做为延伸阅读参考材料。架构
本规范主要包含如下两个方面的内容:app
一:为造成统一编程规范,从编码形式角度出发,本规范对标示符命名、格式与排版、注释等方面进行了详细阐述。ide
二:为编写出高质量嵌入式软件,从嵌入式软件安全及可靠性出发,本规范对因为C语言标准、C语言自己、C编译器及我的理解致使的潜在危险进行说明及规避。
本规范适用于济南金钟电子衡器股份有限公司仪表台秤产品部嵌入式软件的开发,也对其余嵌入式软件开发起必定的指导做用。
原则:编程时必须坚持的指导思想。
规则:编程时须要遵循的约定,分为强制和建议(强制是必须遵照的,建议是通常状况下须要遵照,但没有强制性)。
说明:对原则/规则进行必要的解释。
实例:对此原则/规则从正、反两个方面给出例子。
材料:扩展、延伸的阅读材料。
Unspecified:未详细说明的行为,这些是必须成功编译的语言结构,但关于结构的行为,编译器的编写者有某些自由。例如C语言中的“运算次序”问题。这样的问题有 22 个。 在某种方式上彻底相信编译器的行为是不明智的。编译器的行为甚至不会在全部可能的结构中都是一致的。
Undefined:未定义行为,这些是本质的编程错误,但编译器的编写者不必定为此给出错误信息。相应的例子是无效参数传递给函数,或函数的参数与定义时的参数不匹配。从安全性角度这是特别重要的问题,由于它们表明了那些不必定能被编译器捕捉到的错误。
Implementation-defined:实现定义的行为,这有些相似于“unspecified ”问题,其主要区别在于编译器要提供一致的行为并记录成文档。换句话说,不一样的编译器之间功能可能会有不一样,使得代码不具备可移植性,但在任一编译器内,行为应当是良好定义的。好比用在一个正整数和一个负整数上的整除运算“/ ”和求模运算符“% ”。存在76个这样的问题。从安全性角度,假如编译器彻底地记录了它的方法并坚持它的实现,那么它可能不是那样相当重要。尽量的状况下要避免这些问题。
声明(declaration):指定了一个变量的标识符,用来描述变量的类型,是类型仍是对象,
者函数等。声明,用于编译器(compiler)识别变量名所引用的实体。如下这些就是声明:
extern int bar;
extern int g(int,int);
double f(int,double);[对于函数声明,extern关键字是能够省略的。]
定义(definition):是对声明的实现或者实例化。链接器(linker)须要它(定义)来引用内存实体。与上面的声明相应的定义以下:
int bar;
int g(int lhs,int rhs) {returnlhs*rhs;}
double f(int i,double d) {returni+d;}
规则/原则<序号>(规则类型):规则内容。
[原始参考]
<序号>:每条规则都有一个序号,序号是按照章节目录-**的形式,从数字1开始。例如,若在此章节有个规则的话,序号为0.5-1。
(规则类型):或者是‘强制’,或者是‘建议’。
规则内容:此条规则的具体内容。
[原始参考]:指示了产生本条款或本组条款的可应用的主要来源。
规则1.1-1(强制):标识符(内部的和外部的)的有效字符不能多于31。
[UndefinedImplementation-defined]
说明:ISO 标准要求在内部标识符之间前31 个字符必须是不一样的,外部标识符之间前6 个字符必须是不一样的(忽略大小写)以保证可移植性。咱们这里放宽了此要求,要求内部、外部标示符的有效字符不能多于31便可。这样主要是便于编译器识别,代码清晰易读,并保证可移植性。
规则1.1-2(强制):具备内部做用域的标识符不该使用与具备外部做用域的标识符相同的
名称,在内部做用域里具备内部标示符会隐藏外部标识符。
说明:外部做用域和内部做用域的定义以下。文件范围内的标识符能够看作是具备最外部
(outermost )的做用域;块范围内的标识符看作是具备更内部(more inner)的做用域,连续嵌套的块,其做用域更深刻。若是内部做用域标示符和外部做用域标示符同名,内部做用域标示符会覆盖外部做用域标示符,致使程序混乱。
实例:
INT8U test;
{
INT8U test; /*定义了两个test */
test = 3; /*这将产生混淆 */
}
规则1.1-3(建议):具备静态存储期的对象或函数标识符不能重用。
说明:无论做用域如何,具备静态存储期的标识符都不该在系统内的全部源文件中重用。它包含带有外部连接的对象或函数,及带有静态存储类标识符的任何对象或函数。在一个文件中存在一个具备内部连接的标识符,而在另一个文件中存在着具备外部连接的相同名字的标识符,或者存在两个标示符相同的外部标示符。对用户来讲,这有可能致使混淆。
实例:
test1.c
/**定义了一个静态文件域变量test1*/
static INT8U test1;
void test_fun(void)
{
INT8U test1; /*定义了一个同名的局部变量test1*/
}
test2.c
/**在另外一个文件又定义了一个具备外部连接的文件域变量test1*/
INT8U test1;
原则1.1-4(强制):标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或你们基本能够理解的缩写,避免令人产生误解。
说明:标示符的命名尽可能作到见名知意,尽可能让别人快速理解你的代码。
实例:
好的命名方法:
INT8U debug_message;
INT16U err_num;
很差的命名方法:
INT8U dbmesg;
INT16U en;
原则1.1-5(强制):常见通用的单词缩写尽可能统一,不得使用汉语拼音、英语混用。
说明:简短的单词可使用略去‘元音’字母造成缩写,较长的单词可使用音节首字母
者单词前几个字母造成缩写,针对你们公认的单词缩写要统一。对于特定的项目要使
用的专有缩写应该注明或者作统一说明。
实例:常见单词缩写表(建议):
单词 |
缩写 |
单词 |
缩写 |
argument |
arg |
buffer |
buf |
clock |
clk |
command |
cmd |
compare |
cmp |
configuration |
cfg |
device |
dev |
error |
err |
hexadecimal |
hex |
increment |
inc |
initialize |
init |
maximum |
max |
message |
msg |
minimum |
min |
parameter |
param |
previous |
prev |
register |
reg |
semaphore |
sem |
statistic |
stat |
synchronize |
syn |
temp |
tmp |
|
|
原则1.1-6(建议):用正确的反义词组命名具备互斥意义的变量或相反动做的函数等。
实例:常见反义词表:
正义 |
反义 |
正义 |
反义 |
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 |
copy |
pase |
up |
down |
|
|
原则1.1-7(建议):标示符尽可能避免使用数字编号,除非逻辑上须要。
实例:
#define DEBUG_0_MSG
#define DEBUG_1_MSG
应改成更有意义的定义:
#define DEBUG_WARN_MSG
#define DEBUG_ERR_MSG
材料:《代码大全第2版》(Steve McConnell 著 金戈/汤凌/陈硕/张菲 译 电子工业出版社
2006年3月)"第11章变量命的力量"。
规则1.2-1(强制):文件名使用小写字母。
说明:因为不一样系统对文件名大小写处理不一样,Windows不区分文件名大小写,而Linux区分。因此文件名命名均采用小写字母,多个单词之间可以使用”_”分隔符。
实例:disp.h os_sem.c
规则1.2-2(建议):工程源码使用GB2312编码方式。
说明:程序里的注释可能会使用中文,GB2312是简体中文编码,大部分的编辑工具和集成IDE环境都支持GB2312编码,为避免中文乱码,建议使用GB2312对源码进行编码。若须要转换成其余编码格式,可以使用文本编码转换工具进行转换。
规则1.2-3(强制):工程源码使用版本管理工具进行版本管理。
说明:程序通常须要大量更新、修正、维护工做,且有时须要多人合做。使用版本管理工具能够帮助你提升工做效率。建议使用“Git”版本管理工具。
原则1.3-1(强制):变量命名应明确所表明的含义或者状态。
说明:变量名称可使用名词表述清楚的尽可能使用名词,使用名词没法描述清楚时,使用形
容词或者描述性的单词+名词的形式。变量通常为实体的属性、状态等信息,使用上
述方案通常能够解决变量名的命名问题,若是出现命名很困难或者没法给出合理的命
名方式时,问题可能出如今总体设计上,请从新审视设计。
规则1.3-2(强制):全局变量添加”G_”前缀,全局静态变量添加” S_ ”,局部静态变量添加”s_”前缀。使用大小写混合方式命名,大写字母用于分割不一样单词。
说明:添加前缀的缘由有两个。首先,使全局变量变得更醒目,提醒技术开发人员使用这些变量时要当心。其次,添加前缀使全局变量和静态变量变得和其余变量不一致,提醒技术开发人员尽可能少用全局变量。
实例:
/**出错信息 */
INT8U G_ErrMsg;
/**每秒钟转动圈数 */
static INT32U S_CirclePerSec;
规则1.3-3(强制):局部变量使用小写字母,若标示符比较复杂,使用’_’分隔符。
说明:局部变量所有使用小写字母,和全局变量有明显区分,使读者看到标示符就知道是何
种做用域的变量。
实例:
INT32U download_program_address;
规则1.3-4(强制):定义指针变量*紧挨变量名,全局指针变量使用大写P前缀”P_”,局部指针变量使用小写p前缀”p _”。
实例: INT8U *P_MsgAddress; /*全局变量*/
INT8U *p_msg; /*局部变量*/
原则1.4-1(强制):函数命名应该明确针对什么对象作出了什么操做。
说明:函数的功能是获取、修改实体的属性、状态等,采用“动词+名词”的方式能够知足上述需求,若出现使用此方式命名函数很困难或不能命名的状况,问题可能出如今总体设计上,请从新审视设计方案。
规则1.4-2(强制):具备外部连接的函数命名使用大小写混合的方式,首字母大写,用于分割不一样单词。
说明:函数具备外部连接属性的含义是函数经过头文件对外声明后,对其余文件或模块来
说是可见的。若是一个函数要在其余模块或者文件中使用,须要在头文件中声明该函
数。另外,在头文件声明函数,还能够促使编译器检查函数声明和调用的一致性。
实例:
char *GetErrMsg(ErrMsg *msg);
规则1.4-3(强制):具备文件内部连接属性的函数命名使用小写字母,使用’_’分隔符分割不一样单词,且使用static关键字限制函数做用域。
说明:函数具备内部连接属性的含义是函数只能在模块或文件内部调用,对文件或模块外来
说是不可见的。若是一个函数仅在模块内部或者文件内部使用,须要限制函数连接围,
使用static修饰符修饰函数,使其只具备内部连接属性。在源文件中声明一遍具备内
部连接的函数一样具备促使编译器检查函数声明和调用的一致性。
实例:
static char get_key(void);
规则1.4-4(强制):函数参数使用小写字母,各单词之间使用“_”分割,尽可能保持参数顺序从左到右为:输入、修改、输出。
说明:函数参数顺序为需输入参数值(这个值通常不修改,若不须要修改使用const关键字
修饰),需修改的参数(这个参数输入后用于提供数据,函数内部能够修改此参数),
输出参数(这个参数是函数输出值)。
规则1.5-1(强制):常量(#define定义的常量、枚举、const定义的常量)的定义使用全大写字母,单词之间加 ’_’分割的命名方式。
实例:
#define PI_ROUNDED 3.14
const double PI_ROUNDED = 3.14;
enum weekday{ SUN,MON,TUE,WED,THU,FRI,SAT };
规则1.5-2(建议):常数宏定义时,十六进制数的表示方法为0xFF。
说明:前面0x中的x小写,数据中的”A-F”大写。
规则1.6-1(强制):新定义类型名的命名应该明确抽象对象的含义,新类型名使用大写字母,单词之间加’_’分割,新类型指针在类型名前增长前缀”P_”。成员变量标示符前加类型名称前缀,首字母大写用于区分各个单词。
实例:typedef struct _STUDENT
{
StudentName;
StudentAge ;
......
}STUDENT , *P_ STUDENT;/* STUDENT 为新类型名称,P_ STUDENT 为新类型指针名*/
规则2.1.1-1(强制):头文件排版内容依次为包含的头文件、宏定义、类型定义、声明变量、声明函数。且各个种类的内容间空三行。
说明:头文件是模块对外的公用接口。在头文件中定义的宏,能够被其余模块引用。Project
中不建议使用所有变量,若使用则需在头文件里对外声明。模块对外的函数接口在模
块头文件里声明。
规则2.1.2-1(强制):源文件排版内容依次为包含的头文件、宏定义、具备外部连接属性的全局变量定义、模块内部使用的static变量、具备内部连接的函数声明、函数实现代码。且各个种类的内容间空三行。
说明:模块内部定义的宏,只能在该模块内部使用。只在模块内部使用的函数,需在源码文
件中声明,用于促使编译器检查函数声明和调用的一致性。
规则2.1.2-2(强制):程序块采用缩进风格编写,每级缩进4个空格。
说明:当前主流IDE都支持Tab缩进,使用Tab缩进须要打开和设置相关选项。宏定义、编译开关、条件预处理语句能够顶格。
规则2.1.2-3(强制):if、for、do、while、case、switch、defaul、typedef等语句独占一行,且这些关键字后需空一格。
说明:执行语句必须用缩进风格写,属于if、for、do、while、case、switch、default、typedef
等的下一个缩进级别。通常写if、for、do、while等语句都会有成对出现的{}‟,if、
for、do、while等语句后的执行语句建议增长成对的“{}”; 若是if/else语句块中只
有一条语句,也需增长“{}”。
实例:
for (i = 0; i < max_num; i++)
{
for (j = 0; j < max_num; j++)
{
If (name_found)
{
语句
}
else
{
语句
}
}
}
规则2.1.2-4(强制):进行双目运算、赋值时,操做符以前、以后要加空格;进行非对等操做时,若是是关系密切的当即操做符(如->),后不该加空格。
说明:采用这种方式书写代码,主要目的是使代码更清晰,使关键操做符更突出。
实例:
(1) 比较操做符, 赋值操做符"="、 "+=",算术操做符"+"、"%",逻辑操做符"&&"、"&",
位域操做符"<<"、"^"等双目操做符的先后加空格。
If (a > b)
a += 2;
b = a ^ 3;
(2) "!"、"~"、"++"、"--"、"&"(地址操做符)等单目操做符先后不加空格。
Search_dowm = !true;
a++;
(3) "->"、"."、”[]”先后不加空格。
Weight = G_Car->weight;
eye = People.eye;
array[8] = 8;
规则2.1.2-5(建议):一行只定义一个变量,一行只书写一条执行语句,多行同类操做时操做符尽可能保持对齐。
说明:一行定义一个变量,一行只书写一条执行语句,方便注释,多行同类操做对齐美观、整洁。
实例:
events_rdy = OS_FALSE;
events_rdy_nbr = 0;
events_stat = OS_STAT_RDY;
pevents = pevents_pend;
pevent = *pevents;
规则2.1.2-6(建议):函数内部局部变量定义和函数语句之间应空三行。
说明:局部变量定义和函数语句是相对独立的,并且空三行能够更清晰地表示出这种独立性。
原则3.1-1(强制):注释的内容要清楚、明了,含义准确,在代码的功能、意图层次上进行注释。
说明:注释的目的是让读者快速理解代码的意图。注释不是为了名词解释(what),而是说
明用途(why)。
实例:
以下注释纯属多余:
++i; // i增长1
if (data_ready) /* 若是data_ready为真 */
以下注释无任何参考价值:
// 时间有限,如今是:04,根原本不及想为何,也没人能帮我说清楚
原则3.1-2(强制):注释应分为两个角度进行,首先是应用角度,主要是告诉使用者如何使用接口(即你提供的函数),其次是实现角度,主要是告诉后期升级、维护的技术人员实现的原理和细节。
说明:每个产品均可以分为三个层次,产品自己是一个层次,这个层次之下的是你使用
的更小的组件,这个层次之上的是你为别人提供的服务。你这个产品的存在的价值
就在于把最底层的小部件的使用细节隐藏,同时给最上层的用户提供方便、简洁的
使用接口,知足用于需求。从这个角度来看软件的注释,你应该时刻想着你写的注释
是给那一层次的人员看的,若是是用户,那么你应该注重描述如何使用,若是是后期
维护者,那么你应该注重原理和实现细节。
原则3.1-3(强制):修改代码时,应维护代码周边的注释,使其代码和注释一致,再也不使用的注释应删除。
说明:注释的目的在于帮助读者快速理解代码使用方法或者实现细节,若注释和代码不一
致,会起到相反的做用。建议在修改代码前应该先修改注释。
规则3.1-4(建议):代码段不该被“注释掉”(comment out )。
说明:当源代码段不须要被编译时,应该使用条件编译来完成(如带有注释的#if或#ifdef 结
构)。为这种目的使用注释的开始和结束标记是危险的,由于C 不支持/**/嵌套的注
释,并且已经存在于代码段中的任何注释将影响执行的结果。
规则3.2-1(强制):文件注释需放到文件开头,具体格式见实例。
实例:
stm32f10x_dac.h
/**
******************************************************************************
* @file stm32f10x_dac.h
* @brief Thisfile contains all the functions prototypes for the DAC firmware
* library.
* @author MCD Application Team
* @version V3.5.0
* @date 11-March-2014
* @par Modification:添加函数,支持********<br>
* History
* Version:V3.0.1 <br>
* Author:***<br>
* Modification:添加函数,支持********<br>
* Version:V3.0.0 <br>
* Author:***<br>
* Modification:添加函数,支持********<br>
*************************************************************************
* @attention
*********************************************************
*/
说明:注释格式可被doxygen工具识别,其中@file、@brief、@author等是doxygen工具识别的关键字,注释内容能够为中文。
规则3.3-1(强制):函数注释分为头文件中函数原型声明时的注释和源文件中函数实现时的注释。头文件中的注释注重函数使用方法和注意事项,源文件中的注释注重函数实现原理和方法。具体格式见实例。
说明:函数原型声明的注释按照doxygen工具能够识别的格式进行注释,用于doxygen工具
生成头文件信息以及函数间的调用关系信息。源代码实现主要是注释函数实现原理及
修改记录,不需按照doxygen工具要求的注释格式进行注释。
实例:
头文件函数原型声明注释:
/**
********************************************************************
* @brief Configures the discontinuous mode for theselected ADC regular
* group channel.
* @param ADCx:where x can be 1, 2 or 3 to select the ADC peripheral.
* @param Number:specifies the discontinuous mode regular channel
* count value. This number must be between 1 and8.
* @retval None
* @par Usage:
* ADC_DiscModeChannelCountConfig(ADC1,6);<br>
* @par Tag:
* 此函数不能在中断里调用。
********************************************************************
*/
void ADC_DiscModeChannelCountConfig(ADC_TypeDef* ADCx, INT8U_tNumber);
源文件函数实现注释:
/*
********************************************************************
* @brief Configures the discontinuousmode for the selected ADC regular
* group channel.
* @param ADCx: where x can be 1, 2 or 3 toselect the ADC peripheral.
* @param Number: specifies the discontinuousmode regular channel
* count value. This number must bebetween 1 and 8.
* @retval None
* @par Modification:修改了********<br>
* History
* Modified by:***<br>
* Date: 2013-10-10
* Modification:修改了********<br>
********************************************************************
*/
void ADC_DiscModeChannelCountConfig(ADC_TypeDef*ADCx, INT8U_t Number)
{
赋值语句*********; /*关键语句的注释 */
语句***********; /*关键语句的注释格式 */
语句*******; /*实现*****************功能*/
}
规则3.3-1(强制):常量、全局变量须要注释,注释格式见实例。
实例:
/** Description of the macro */
#define XXXX_XXX_XX 0
/**Description of global variable */
INT8U G_xxx = 0;
说明:若全局变量在.c文件中定义,又在.h文件中声明,则在头文件中使用doxygen
格式注释,在源码文件中使用 /* Description of the globalvariable */的形式。防止doxygen生成两遍注释文档信息。
规则3.3-1(强制):局部变量,函数实现关键语句须要注释,注释格式见实例。
实例:
*pq->OSQIn++ = pmsg; /* Insert message into queue */
pq->OSQEntries++; /* Update the nbr of entries in the queue*/
if (pq->OSQIn== pq->OSQEnd)
{
pq->OSQIn = pq->OSQStart; /* Wrap IN ptr if we are at end of queue */
}
说明:局部变量,关键语句须要注释,从功能和意图上进行注释,而不是代码的重复。多条注释语句尽可能保持对齐,实现美观,整洁。
材料:
1. 《代码整洁之道》(RobertC.Martin 著 韩磊 译 人民邮电出版社2010年1月)第四章"注释”。
2. 《Doxygen中文手册》
项目版本号管理是项目管理的重要方面,咱们根据项目不一样的开发阶段制定了不一样的版本号命名规范。项目开发过程通常分为前期开发测试阶段、发布阶段、维护阶段这三个主要阶段,咱们分别制定了命名规范。
规则4.1-1(强制):处于开发、调试阶段的项目,版本号使用“V0.yz”的形式。
说明:处于新开发、调试阶段的项目,版本号使用“V0.yz” 的形式,好比新开发的项目正处在开发、调试阶段,这时可使用“ V0.10 ”这样的版本号。你认为完成了新的功能模块或总体架构作了很大的修改,能够根据状况增长 Y 或者 Z的值。好比,你开发阶段在“ V0.10 ”基础上新增长了一个功能模块你能够将版本号改成“V0.11”,作了比较大的修改,你能够将版本号定为“V0.20”。
规则4.2-1(强制):处于正式发布阶段的项目,版本号使用“Vx.y”的形式。
说明:处于正式发布的项目版本号使用“Vx.y”的形式。好比,你发布了一个正式面向市场的项目,你可使用“V1.0”做为正式的版本号。在“V1.0”基础上增长功能的正式版本,你可使用“V1.1”做为下一次正式版本的版本号,在“V1.0”基础上修正了大的BUG或者作了很大的改动,你可使用“V2.0”做为下一次正式版本号。
规则4.3-1(强制):处于维护阶段的项目,版本号使用“Vx.yz”的形式。
说明:处于维护阶段的项目版本号使用“Vx.yz”的形式。好比在"V1.1"的基础上修改了一个功能实现算法以实现高效率,则可使用"V1.11" 来表示这是在正式发布版本“V1.1”的基础上进行的一次修正,再次修正可使用“V1.12”。
原则5.1-1(强制):头文件用于声明模块对外接口,包括具备外部连接的函数原型声明、全局变量声明、定义的类型声明等。
说明:头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声
明,如对外提供的函数声明、宏定义、类型定义等。内部使用的函数声明不该放在
头文件中。 内部使用的宏、枚举、结构定义不该放入头文件中。变量定义不该放
在头文件中,应放在.c文件中。 变量的声明尽可能不要放在头文件中,亦即尽可能不要
使用全局变量做为接口。变量是模块或单元的内部实现细节,不该经过在头文件中
声明的方式直接暴露给外部,应经过函数接口的方式进行对外暴露。即便必须使用
全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。
材料:《C语言接口与实现》(David R. Hanson著 傅蓉 周鹏 张昆琪权威 译 机械工业出
版社 2004年1月)(英文版: "C Interfaces and Implementations")
规则5.1-2(强制):只能经过包含头文件的方式使用其余.c提供的接口,禁止在.c中经过extern的方式使用外部函数接口、变量。
说明:若a.c使用了b.c定义的foo()函数 ,则应当在b.h中声明externint foo(int input);并
在a.c中经过#include <b.h>来使用foo。禁止经过在a.c中写externint foo(int input);
来使用foo,后面这种写法容易在foo改变时可能致使声明和定义不一致。
规则5.1-3(强制):使用#define定义保护符,防止头文件重复包含。
说明:屡次包含一个头文件能够经过认真的设计来避免。若是不能作到这一点,就须要采起
阻止头文件内容被包含多于一次的机制。一般的手段是为每一个文件配置一个宏,当头
文件第一次被包含时就定义这个宏,并在头文件被再次包含时使用它以排除文件内
容。全部头文件都应当使用#define 防止头文件被多重包含,命名格式FILENAME_H_,
其中FILENAME 为头文件的名称。
实例:
若文件名为:stm32f10x_adc.h。
#ifndef STM32F10x_DAC_H_
#define STM32F10x_DAC_H_
…………
受保护的代码
#endif
规则5.2-1(强制):C的宏只能扩展为用大括号括起来的初始化、常量、小括号括起来的
表达式、类型限定符、存储类标识符或do-while-zero 结构。
说明:这些是宏当中全部可容许使用的形式。存储类标识符和类型限定符包括诸如extern 、
static和const这样的关键字。使用任何其余形式的#define 均可能致使非预期的行为,
或者是很是难懂的代码。特别的,宏不能用于定义语句或部分语句,除了do-while 结
构。宏也不能重定义语言的语法。宏的替换列表中的全部括号,无论哪一种形式的 ()、
{} 、[] 都应该成对出现。 do-while-zero 结构(见下面实例)是在宏语句体中惟一可接受的具备完整语句的形式。do-while-zero 结构用于封装语句序列并确保其是正确的。
注意:在宏语句体的末尾必须省略分号。
实例:
如下是合理的宏定义:
#define PI 3.14159F /*Constant */
#define XSTAL 10000000 /*Constant */
#define CLOCK (XSTAL / 16) /*Constant expression */
#define PLUS2(X) ( (X) + 2 ) /* Macro expanding to expression */
#define STOR extern /*storage class specifier */
#define INIT(value) { (value), 0, 0 } /*braced initialiser */
#define READ_TIME_32() \
do { \
DISABLE_INTERRUPTS(); \
time_now = (INT32U) TIMER_HI << 16; \
time_now = time_now | (INT32U) TIMER_LO; \
ENABLE_INTERRUPTS(); \
} while(0) /* example of do-while-zero */
如下是不合理的宏定义:
#define unsigned int long /* use typedef instead */
#defineSTARTIF if( /* unbalanced () and languageredefinition */
规则5.2-2(强制):在定义函数宏时,每一个参数实例都应该以小括号括起来。
实例:
一个abs 函数能够定义成:
#define abs (x) ( ( (x) >= 0 ) ? (x) : -(x) )
不能定义成:
#define abs(x) ( ( (x) >= 0 ) ? x : -x )
若是不坚持本规则,那么当预处理器替代宏进入代码时,操做符优先顺序将不会给出要
求的结果。
考虑前面第二个不正确的定义被替代时会发生什么:
z = abs ( a – b );
将给出以下结果:
z = ( ( a – b >= 0 ) ? a – b : -a – b );
子表达式 – a - b 至关于 (-a)-b ,而不是但愿的 –(a-b) 。把全部参数都括进小括号中就能够避免这样的问题。
规则5.2-3(建议):使用宏时,不容许参数数值发生变化。
实例:
以下用法可能致使错误。
#define SQUARE(a) ((a) * (a))
int a = 5;
int b;
b = SQUARE(a++); /*结果:a = 7,即执行了两次增。
正确的用法是:
b = SQUARE(a);
a++; /*结果:a = 6,即只执行了一次增*/
一样建议在调用函数时,参数也不要变化,若是某次软件升级将其中一个接口由函数实现转换成宏,那参数数值发生变化的调用将产生非预期效果。
规则5.2-4(建议):除非必要,应尽量使用函数代替宏。
说明:宏能提供比函数优越的速度,可是没有参数检查机制,不当的使用可能产生非预期后果。
规则5.3-1(强制):应该使用标明了大小和符号的typedef代替基本数据类型。不该使用基本数值类型char、int、short、long、float和double,而应使用typedef进行类型的定义。
说明:为了程序的跨平台移植性,咱们使用typedef定义指明了大小和符号的数据类型。
实例:
此实例是根据keil for ARM的数据类型大小进行的定义。
No. |
基本数据类型 |
Typedef定义 |
1 |
typedef unsigned char |
BOOLEAN |
2 |
typedef unsigned char |
INT8U |
3 |
typedef signed char |
INT8S |
4 |
typedef unsigned short |
INT16U |
5 |
typedef signed short |
INT16S |
6 |
typedef unsigned int |
INT32U |
7 |
typedef signed int |
INT32S |
8 |
typedef float |
FP32 |
9 |
typedef double |
FP64 |
应根据硬件平台和编译器的信息对基本类型进行定义。
规则5.3-2(建议):浮点应用应该适应于已定义的浮点标准。
说明:浮点运算会带来许多问题,一些问题(而不是所有)能够经过适应已定义的标准来克服。其中一个合适的标准是 ANSI/IEEE Std 754 [1] 。
C 语言给程序员提供了至关大的自由度并容许不一样数值类型能够自动转换。因为某些功能
性的缘由能够引入显式的强制转换,例如:
1. 用以改变类型使得后续的数值操做能够进行
2. 用以截取数值
3. 出于清晰的角度,用以执行显式的类型转换
为了代码清晰的目的而插入的强制转换一般是有用的,但若是过多使用就会致使程序的
可读性降低。正以下面所描述的,一些隐式转换是能够安全地忽略的,而另外一些则不能。
规则5.3.1-1(强制):强制转换只能向表示范围更窄的方向转换,且与被转换对象的类
型具备相同的符号。浮点类型值只能强制转换到更窄的浮点类型。
说明:这条规则主要是要求须要强制转换时,须明确被转换对象的表示范围及转换后的表示
范围。转换时尽可能保持符号一致,不一样符号对象之间不该出现强制转换。向更宽数据
范围转换并不能提升数据精确度,并无实际意义。在程序中尽可能规划好变量范围,
尽可能少使用强制转换。
规则5.3.1-2(强制):若是位运算符 ~ 和 << 应用在基本类型为unsigned char或unsigned
short 的操做数,结果应该当即强制转换为操做数的基本类型。
说明:当这些操做符(~ 和<<)用在 small integer 类型(unsigned char 或unsigned short )时,运算以前要先进行整数提高,结果可能包含并不是预期的高端数据位。
例如:
INT8U port= 0x5aU;
INT8U result_8;
INT16U result_16;
INT16U mode;
result_8 = (~port) >> 4; /* 不合规范 */
~port的值在16位机器上是 0xffa5 ,而在 32 位机器上是 0xffffffa5 。在每种状况下,result的值是0xfa ,然而指望值多是0x0a 。这样的危险能够经过以下所示的强制转换来避免:
result_8 = ( (INT8U) (~port )) >> 4; /* 符合规范 */
result_16 = ( (INT16U ) (~(INT16U) port ) ) >> 4 ; /*符合规范 */
当<<操做符用在 smallinteger 类型时会遇到相似的问题,高端数据位被保留下来。
例如:
result_16 = ( ( port << 4 ) & mode ) >> 6 ; /*不符合规范 */
result_16 的值将依赖于 int 实现的大小。附加的强制转换能够避免任何模糊性。
result_16 = ( ( INT16U) ( ( INT16U ) port << 4 ) & mode )>> 6 ; /* 符合规范 */
规则5.3.2-1(强制):如下类型之间不该该存在隐式类型转换。
1) 有符号和无符号之间没有隐式转换
2) 整型和浮点类型之间没有隐式转换
3) 没有从宽类型向窄类型的隐式转换
4) 函数参数没有隐式转换
5) 函数的返回表达式没有隐式转换
6) 复杂表达式没有隐式转换
规则5.3.3-1(强制):后缀“U”应该用在全部unsigned 类型的常量上。
整型常量的类型是混淆的潜在来源,由于它依赖于许多因素的复杂组合,包括:
1) 常数的量级
2) 整数类型实现的大小
3) 任何后缀的存在
4) 数值表达的进制(即十进制、八进制或十六进制)
例如,整型常量“40000”在32位环境中是 int 类型,而在 16位环境中则是long 类型。
值0x8000 在16位环境中是 unsigned int 类型,而在 32 位环境中则是(signed )int 类型。
注意:
1) 任何带有“U”后缀的值是unsigned 类型
2) 一个不带后缀的小于231的十进制值是signed 类型
可是:
1) 不带后缀的大于或等于215的十六进制数多是 signed 或unsigned 类型
2) 不带后缀的大于或等于231的十进制数多是 signed 或unsigned 类型
常量的符号应该明确。符号的一致性是构建良好形式的表达式的重要原则。若是一个常
数是unsigned 类型,为其加上“U”后缀将有助于避免混淆。当用在较大数值上时,后缀也许是多余的(在某种意义上它不会影响常量的类型);然然后缀的存在对代码的清晰性是种有价值的帮助。
指针类型能够归为以下几类:
1) 对象指针
2) 函数指针
3) void 指针
4) 空(null )指针常量(即由数值 0 强制转换为 void*类型)
涉及指针类型的转换须要明确的强制,除非在如下时刻:
1) 转换发生在对象指针和void 指针之间,并且目标类型承载了源类型的全部类型标识符。
2) 当空指针常量(void*)被赋值给任何类型的指针或与其作等值比较时,空指针常量被自动转化为特定的指针类型。
C 当中只定义了一些特定的指针类型转换,而一些转换的行为是实现定义的。
规则5.3.9-1(强制):转换不能发生在函数指针和其余除了整型以外的任何类型指针之间。
[Undefined]
说明:
函数指针到不一样类型指针的转换会致使未定义的行为。这意味着一个函数指
针不能转换成指向不一样类型函数的指针。
规则5.3.9-2(强制):对象指针和其余除整型以外的任何类型指针之间、对象指针和其余类
型对象的指针之间、对象指针和void指针之间不能进行转换。
[Undefined]
规则5.3.9-3(强制):不该在某类型对象指针和其余不一样类型对象指针之间进行强制转换。
说明:若是新的指针类型须要更严格的分配时这样的转换多是无效的。
实例:
INT8U *p1;
INT32U *p2;
p2= (INT32U *) p1; /*不符规范*/
规则5.4-1(强制):全部自动变量在使用前都应被赋值。
[Undefined]
说明:注意,根据ISO C[2] 标准,具备静态存储期的变量缺省地被自动赋予零值,除非通过了显式的初始化。实际中,一些嵌入式环境没有实现这样的缺省行为。静态存储期是全部以static存储类形式声明的变量或具备外部连接的变量的共同属性,自动存储期变量一般不是自动初始化的。
规则5.4-2(强制):应该使用大括号以指示和匹配数组和结构的非零初始化构造。
[Undefined]
说明:
ISO C[2]要求数组、结构和联合的初始化列表要以一对大括号括起来(尽管不这样作的行为是未定义的)。本规则更进一步地要求,使用附加的大括号来指示嵌套的结构。它迫使程序员显式地考虑和描述复杂数据类型元素(好比,多维数组)的初始化次序。
例如,下面的例子是二维数组初始化的有效(在ISO C [2]中)形式,但第一个与本规
则相违背:
在结构中以及在结构、数组和其余类型的嵌套组合中,规则相似。
还要注意的是,数组或结构的元素能够经过只初始化其首元素的方式初始化(为 0 或
NULL)。若是选择了这样的初始化方法,那么首元素应该被初始化为0(或NULL),
此时不须要使用嵌套的大括号。
实例:
INT16U test[3][2] = { 1, 2, 3, 4, 5, 6 }; /* 不符合此规则 */
INT16U test[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } }; /* 符合此规则 */
规则5.4-3(强制):在枚举列表中,“= ”不能显式用于除首元素以外的元素上,除非全部
的元素都是显式初始化的。
说明:
若是枚举列表的成员没有显式地初始化,那么C 将为其分配一个从0 开始的整数序列,首元素为0 ,后续元素依次加 1 。
如上规则容许的,首元素的显式初始化迫使整数的分配从这个给定的值开始。当采用这种方法时,重要的是确保所用初始化值必定要足够小,这样列表中的后续值就不会超出该枚举常量所用的int 存储量。
列表中全部项目的显式初始化也是容许的,它防止了易产生错误的自动与手动分配的混合。然而,程序员就该担负职责以保证全部值都处在要求的范围内以及值不是被无心复制的。
实例:
enum colour { red = 3, blue, green, yellow = 5 }; /* 不符合此规则 */
enum colour { red = 3, blue = 4, green = 5, yellow= 5 }; /* 符合此规则 */
虽然green和yellow的值都是5,但这符合规则。
enum colour { red = 1, blue, green, yellow }; /* 符合此规则 */
规则5.4-4(强制):函数应当具备原型声明,且原型在函数的定义和调用范围内都是可见
的。
[Undefined]
说明:原型的使用使得编译器可以检查函数定义和调用的完整性。若是没有原型,就不会迫使编译器检查出函数调用当中的必定错误(好比,函数体具备不一样的参数数目,调用和定义之间参数类型的不匹配)。事实证实,函数接口是至关多问题的肇因,所以本规则是至关重要的。对外部函数来讲,咱们建议采用以下方法,在头文件中声明函数(亦即给出其原型),并在全部须要该函数原型的代码文件中包含这个头文件,在实现函数功能的.c文件中也包含具备原型声明的头文件。 为具备内部连接的函数给出其原型也是良好的编程实践。
规则5.4-5(强制):定义或声明对象、函数时都应该显示指明其类型。
规则5.4-6(强制):函数的每一个参数类型在声明和定义中必须是等同的,函数的返回类型
也该是等同的。
[Undefined]
规则5.4-6(强制):函数应该声明为具备文件做用域。
[Undefined]
说明:在块做用域中声明函数会引发混淆并可能致使未定义的行为。
规则5.4-7(强制):在文件范围内声明和定义的全部对象或函数应该具备内部连接,除非
是在须要外部连接的状况下,具备内部连接属性的对象或函数应该使用static关键字修饰。
说明:若是一个变量只是被同一文件中的函数所使用,那么就用static。相似地,若是一个函数只是在同一文件中的其余地方调用,那么就用 static。使用 static存储类标识符将确保标识符只是在声明它的文件中是可见的,而且避免了和其余文件或库中的相同标识符发生混淆的可能性。具备外部连接属性的对象或函数在相应模块的头文件中声明,在须要使用这些接口的模块中包含此头文件。
规则5.4-8(强制):当一个数组声明为具备外部连接,它的大小应该显式声明或者经过初
始化进行隐式定义。
[Undefined]
实例:
INT8U array[10] ; /* 符合规范 */
extern INT8U array[] ; /* 不符合规范*/
INT8U array[] = { 0, 10, 15}; /* 符合规范 */
尽管能够在数组声明不完善时访问其元素,然而仍然是在数组的大小能够显式肯定的情
况下,这样作才会更为安全。
规则5.5-1(建议):不要过度依赖C 表达式中的运算符优先规则。
说明:括号的使用除了能够覆盖缺省的运算符优先级之外,还能够用来强调所使用的运算符。使用至关复杂的C 运算符优先级规则很容易引发错误,那么这种方法就能够帮助避免这样的错误,而且可使得代码更为清晰可读。然而,过多的括号会分散代码使其下降了可读性。所以,请合理使用括号来提升程序清晰度和可读性。
规则5.5-1(强制):不能在具备反作用的表达式中使用sizeof 运算符。
说明:当一个表达式使用了sizeof运算符,并指望计算表达式的值时,表达式是不会被计算的。sizeof只对表达式的类型有用。
实例:
INT32S i;
INT32S j;
j = sizeof (i = 1234); /* j的值是i类型的大小,但i的值并无赋值成1234 */
规则5.5-2(强制):逻辑运算符 && 或 || 的右手操做数不能包含反作用。
说明:C语言中存在表达式的某些部分不会被计算到,这取决于表达式中的其余部分。逻辑操做符&&或||在进行逻辑判断时,若仅判别左操做数就能肯定true or false的状况下,逻辑操做符的右操数将被忽略。
实例:
if ( high && ( x == i++ ) ) /* 不符合规则 */
若high为false,则整个表达式的布尔值也即为false,不用再去执行和判断右操做数。
规则5.5-3(建议):逻辑运算符(&&、| | 和 ! )的操做数应该是有效的布尔数。有效布尔
类型的表达式不能用作非逻辑运算符(&&、| | 和 ! )的操做数。
说明:有效布尔类型是表示真、假的一种数据类型,产生布尔类型的能够是比较,逻辑运算,但布尔类型数据只能进行逻辑运算。
规则5.5-4(强制):位运算符不能用于基本类型(underlying type )是有符号的操做数上。
[Implementation-defined]
说明:位运算(~ 、<<、>>、&、^ 和 | )对有符号整数一般是无心义的。好比,若是右移运算把符号位移动到数据位上或者左移运算把数据位移动到符号位上,就会产生问题。
规则5.5-6(建议):在一个表达式中,自增(++)和自减(- - )运算符不该同其余运算符
混合在一块儿。
说明:不建议使用同其余算术运算符混合在一块儿的自增和自减运算符是由于
1)它显著削弱了代码的可读性;
2)在不一样的变异环境下,会执行不一样的运算次序,产生不一样结果。
实例:
u8a = ++u8b +u8c--; /* 不符合规范 */
下面的序列更为清晰和安全:
++u8b;
u8a = u8b + u8c;
u8c--;
规则5.5-7(强制):浮点表达式不能作像‘>’ ‘<’ ‘==’ ‘!=’等 关系运算。
说明:float、double类型的数据都有必定的精确度限制,使用不一样浮点数表示规范或者不一样硬件平台可能致使关系运算的结果不一致。
规则5.5-8(强制):for语句的三个表达式应该只关注循环控制,for循环中用于计数的变量不该在循环体中修改。
说明:for 语句的三个表达式都给出时它们应该只用于以下目的:
第一个表达式初始化循环计数器;
第二个表达式包含对循环计数器和其余可选的循环控制变量的测试;
第三个表达式循环计数器的递增或递减。
规则5.5-9(强制):组成switch、while、do...while 或for 结构体的语句应该是复合语句。即便该复合语句只包含一条语句也要扩在{}里。
实例:
for ( i = 0 ; i< N_ELEMENTS ; ++i )
{
buffer[i] = 0; /* 仅有一条语句也需使用{} */
}
规则5.5-10(强制):if /else应该成对出现。全部的if ... else if 结构应该由else 子句结束。
规则5.5-11(强制):switch 语句中若是case 分支的内容不为空,那么必须以break 做为结束,最后分支应该是default分支。
原则5.6-1(强制):编写整洁函数,同时把代码有效组织起来。
说明:代码简单直接、不隐藏设计者的意图、用干净利落的抽象和直截了当的控制语句将函数有机组织起来。代码的有效组织包括:逻辑层组织和物理层组织两个方面。逻辑层,主要是把不一样功能的函数经过某种联系组织起来,主要关注模块间的接口,也就是模块的架构。物理层,不管使用什么样的目录或者名字空间等,须要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样能够方便查找。
规则5.6-2(强制):必定要显示声明函数的返回值类型,及所带的参数。若是没有要声明为void。
说明:C语言中不加类型说明的函数,一概自动按整型处理。
规则5.6-3(建议):不建议使用递归函数调用。
说明:有些算法使用分而治之的递归思想,但在嵌入式中栈空间有限,递归自己承载着可
用堆栈空间过分的危险,这能致使严重的错误。除非递归通过了很是严格的控制,
不然不可能在执行以前肯定什么是最坏状况(worst-case)的堆栈使用。
规则5.7-1(强制):除了指向同一数组的指针外,不能用指针进行数学运算,不能进行关系运算。
说明:这样作的目的一是使代码清晰易读,另外避免访问无效的内存地址。
规则5.7-2(强制):指针在使用前必定要赋值,避免产生野指针。
规则5.7-3(强制):不要返回局部变量的地址。
说明:局部变量是在栈中分配的,函数返回后占用的内存会释放,继续使用这样的内存是
危险的。所以,应该避免出现这样的危险。
实例:
INT8U *foobar (void)
{
INT8U local_auto;
return(&local_auto); /* 不符合规范 */
}
原则5.8-1(强制):结构功能单一,不要设计面面俱到的数据结构。
说明:相关的一组信息才是构成一个结构体的基础,结构的定义应该能够明确的描述一个对象,而不是一组相关性不强的数据的集合。设计结构时应力争使结构表明一种现实事务的抽象,而不是同时表明多种。结构中的各元素应表明同一事务的不一样侧面,而不该把描述没有关系或关系很弱的不一样事务的元素放到同一结构中。
规则5.9-1(强制):标准库中保留的标识符、宏和函数不能被定义、重定义或取消定义。
[Undefined]
说明:一般 #undef 一个定义在标准库中的宏是件坏事。一样很差的是,#define 一个宏名字,而该名字是C 的保留标识符或者标准库中作为宏、对象或函数名字的C 关键字。例如,存在一些特殊的保留字和函数名字,它们的做用为人所熟知,若是对它们从新定义或取消定义就会产生一些未定义的行为。这些名字包括defined、__LINE__、__FILE__、__DATE__ 、__TIME__、__STDC__、errno和assert。
规则5.9-2(强制):传递给库函数的值必须检查其有效性。
说明:C 标准库中的许多函数根据ISO [2] 标准 并不须要检查传递给它们的参数的有效性。即便标准要求这样,或者编译器的编写者声明要这么作,也不能保证会作出充分的检查。所以,程序员应该为全部带有严格输入域的库函数(标准库、第三方库及本身定义的库)提供适当的输入值检查机制。
具备严格输入域并须要检查的函数例子为:
math.h 中的许多数学函数,好比:
负数不能传递给sqrt 或log函数;
fmod 函数的第二个参数不能为零
toupper 和tolower:当传递给toupper函数的参数不是小写字符时,某些实现能产生并不是预期的结果(tolower 函数状况相似)
若是为ctype.h 中的字符测试函数传递无效的值时会给出未定义的行为
应用于大多数负整数的abs 函数给出未定义的行为 在math.h 中,尽管大多数数学库函数定义了它们容许的输入域,但在域发生错误时它们的返回值仍可能随编译器的不一样而不一样。所以,对这些函数来讲,预先检查其输入值的有效性就变得相当重要。
程序员在使用函数时,应该识别应用于这些函数之上的任何的域限制(这些限制可能
会也可能不会在文档中说明),而且要提供适当的检查以确认这些输入值位于各自域
中。固然,在须要时,这些值还能够更进一步加以限制。
有许多方法能够知足本规则的要求,包括:
1. 调用函数前检查输入值
2. 设计深刻函数内部的检查手段。这种方法尤为适应于实验室内开发的库,纵然它也能够用于买进的第三方库(若是第三方库的供应商声明他们已内置了检查的话)。
3. 产生函数的“封装”(wrapped)版本,在该版本中首先检查输入,而后调用原始的函数。
4. 静态地声明输入参数永远不会采起无效的值。
注意,在检查函数的浮点参数时(浮点参数在零点上为奇点),适当的作法是执行其是否为零的检查。然而若是当参数趋近于零时,函数值的量级趋近无穷的话,仍然有必要检查其在零点(或其余任何奇点)上的容限,这样能够避免溢出的发生。
[1] ANSI/IEEE Std 754, IEEE Standard for Binary Floating-Point Arithmetic,1985
[2] ISO/IEC 9899:1990. Programming languages - C.International Organization for
Standardization. 1990
[3] GB/T 15272-94 程序设计语言 C
[4] MISRA-C-:2004 Guidelines for the use of the C language in critical systems
================================================================