服务热线
- 服务电话:0756-3391906
- 软件测试技术:0756-3391891
- 嵌入式软件测试:0756-3391732
- hntest1:0731
- 网络测试技术:0756-3396982
- hntest2:0321
- 监督电话:0756-3391848
- 软件业务服务:0756-3391906
- 网络业务服务:0756-3396981
- 移动:10086
《嵌入式软件编码规范要求(C语言)》征求意见稿
来源:评测中心 发布时间:2015-07-30 09:10:43
本规范适用于采用C语言编写的嵌入式软件代码,涉及到的C语言,只限制在 ISO 9899:1990 Programming languages - C标准中定义的语言。
本文档被设计为应用在嵌入式系统的产品代码上。
本文档也可应用于对编译器和静态工具的比较检测,在选择静态检查工具时,需要该工具尽可能多地强制遵循本文档的规则。检验编译器和静态工具对本规则的符合性,可使用APPENDEX A中的符合性矩阵。
《GJB 5369-2005航天型号软件C语言安全子集》
《MISRA-C:2004 Guidelines for the use of the C language in critical system》
《ISO/IEC 9899:1990. Programming language –C. International Organization for Standardization. 1990》
The Motor Industry Software Reliability Association。
指在表达式执行后对程序运行环境可能会造成影响。赋值语句、自增等操作都是典型的具有单边作用的操作。
强制:强制要求遵循的规范。
建议:建议性的规范。
5.1.1 编程前需阅读硬件电路和芯片资料,熟悉芯片的各种内存、寄存器地址、中断服务、定时器、通讯接口等功能,必要时将相关信息加入程序注释中。(强制)
5.1.2 编程中需注意程序的存储空间(如指令空间大小、数据空间大小、堆栈空间大小等)是否超出系统有关限制。(强制)
5.1.3 注意硬件系统复位和软件复位的方法和区别,在程序中尽量使用芯片提供的看门狗功能实现硬件系统复位。(建议)
5.1.4 注意CPU对各种存储器的访问速度,尽量利用各种硬件设备自身的特点来提高程序效率。(建议)
若模块封装的较好,那么一般不会发生非法引用他人的空间。
5.1.6 注意所使用的编译器的位数,支持的C语言标准,调试程序所占用的内存,兼容性等特点。(建议)
5.1.7 注意编译器处理不同数据类型的原则及有关细节(建议)
如static局部变量将在内存数据区中生成,而非static局部变量将在堆栈中生成。这些细节对程序质量的保证非常重要。
5.1.8 使用联合体时,要明确该编译器联合体存储的细节(强制)
联合体的末尾有多少个填充单位、联合体的各个成员如何对齐、多字节的数据类型高低字节如何排放顺序等。
5.1.9 注意程序开发调试环境和实际应用环境的区别。(建议)
5.1.10 应用程序不要直接调用硬件端口,尽量使用操作系统驱动程序来调用硬件端口。(建议)
5.2.1 在使用malloc等其它函数获取内存,一定要判断申请内存是否成功。(强制)
5.2.2 动态内存的申请与释放必须配对,防止内存泄漏(强制)
应用场景主要包括:
(1)过程/函数中分配的内存,在过程/函数退出之前要释放。
(2)过程/函数中申请的(为打开文件而使用的)文件句柄,在过程/函数退出之前要关闭。
分配的内存不释放以及文件句柄不关闭,是较常见的错误,而且稍不注意就有可能发生。这类错误往往会引起很严重后果,且难以定位。
下列函数在退出之前,没有把分配的内存释放:
typedef unsigned char BYTE; int example_fun( BYTE gt_len, BYTE *gt_code ) { BYTE *gt_buf; gt_buf = (BYTE *) malloc (MAX_GT_LENGTH); ... //program code, include check gt_buf if or not NULL.
/* global title length error */ if (gt_len > MAX_GT_LENGTH) { return GT_LENGTH_ERROR; // 忘了释放gt_buf }
... // other program code } |
应该改为如下方式:
int example_fun( BYTE gt_len, BYTE *gt_code ) { BYTE *gt_buf; gt_buf = (BYTE * ) malloc ( MAX_GT_LENGTH ); ... // program code, include check gt_buf if or not NULL.
/* global title length error */ if (gt_len > MAX_GT_LENGTH) { free( gt_buf ); // 退出之前释放gt_buf return GT_LENGTH_ERROR; }
... // other program code } |
在实际编程过程中,稍不留心就会出现在一个模块中释放了某个内存块(如C语言指针),而另一模块在随后的某个时刻又使用了它。要防止这种情况发生。
内存操作主要是指对数组、指针、内存地址等的操作。内存操作越界是软件系统主要错误之一,后果往往非常严重,所以当我们进行这些操作时一定要仔细小心。
5.2.5 字符串连接使用strncat库函数代替strcat库函数,字符串拷贝使用strncpy库函数代替strcpy库函数,。(建议)
目的是避免长度不够引起的数组越界。
5.2.6 使用sprintf库函数时,需要注意字符长度(建议)
目的是避免长度不够引起的数组越界。
5.2.7 对于内存受限的系统,少用动态内存分配,尽量选用数组。(推荐)
一旦分配内存失败,导致系统运行异常。
5.2.8 动态分配内存需要考虑内存碎片问题。(推荐)
在内存受限的系统中,分配内存时,要考虑内存碎片的问题。
5.3.2 中断服务程序中不能使用printf()函数。(强制)
5.3.3 对于中断中使用到的非局部变量,在中断处理函数中需对其进行入栈保护。(强制)
5.3.4 中断处理程序中的变量,如果会被其他函数执行读操作或者写操作,那么在其他函数读写这个变量前,应先关中断,读写完,再开中断。(强制)
对于默认开启的中断,如果在某个函数中进行了关闭,在函数退出时需进行相应的开启。
对于默认关闭的中断,如果在某个函数中进行了开启,在函数退出时需进行相应的关闭。
5.3.6 中断服务程序(ISR)应尽量短小,其执行时间不要超过中断响应时间。(推荐)
5.3.7 尽量避免在中断服务程序(ISR)中进行浮点数运算。(推荐)
在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
5.4.1 严禁随意更改其它模块或系统的有关设置和配置。(强制)
5.4.3 充分了解系统的接口之后,再使用系统提供的功能。(强制)
5.5.1 系统运行之初,要初始化有关变量及运行环境。(强制)
5.5.2 系统运行之初,要对加载到系统中的数据进行一致性检查。(强制)
使用不一致的数据,容易使系统进入混乱状态和不可知状态。
5.5.3 注意硬件系统的工作电压,在硬件系统初始化之前,判断工作电压是否已经稳定。(建议)
在使用变量前必须确保变量曾被赋过值,保证不会对未赋值的变量进行读操作。建议在声明变量的同时初始化此变量:
unsigned int x; unsigned int y; y=x; /* x 没有初始值 */ |
下面的定义是不规范的:
int fn(void) { extern int x = 0; return(x); } |
5.6.3 数组、结构和联合的初始化列表需要显式描述(强制)
数组、结构和联合的初始化列表要以一对大括号括起来,并使用附加的大括号来指示嵌套的结构;程序员必须显式地考虑和描述复杂数据类型的所有元素,不能忽略某个元素的初始化。
int16_t y[3][2] = { 1, 2, 3, 4, 5, 6 }; /* not OK*/ int16_t y[3][2] = { {1, 2} }; /* not OK*/ int16_t y[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } }; /* OK */ |
枚举元素的初始化只有两种形式是安全的,一是初始化所有元素,二是只初始化第一个元素。
/*枚举元素的初始化必须完整*/ … enum E_type{num1, num2 = 2, num3}; |
C程序通常包含两类代码文件,一类文件用于程序的声明(declaration),称为头文件,通常以“.h”为后缀,另一类文件用于程序的实现(implementation),称为定义(definition)文件,以“.c”为后缀。推荐采用下面的模板对文件进行注释,一般可根据情况自行扩展。
/* * Copyright (c) 2007, 公司名称 * All rights reserved. * 文件名称: filename.h/.c * 文件标识: 见配置管理计划书 * 摘 要: 简要描述本文件的内容 * 当前版本: x.x, 编写者/修改者,修改时间,修改内容 * 历史版本: x.x, 编写者/修改者,修改时间,修改内容 */ |
推荐采用如下模板对函数头部进行注释:
/* * 函数介绍: * 参数: * 返回值 : * 备注 : */ |
5.7.3 建议一个文件中的程序总行不超过2000行(建议)
过长的程序文件不利于管理和维护。建议一个文件中的程序总行数不超过2000行。
函数中过长的代码行不利于管理和维护。建议一个函数中的程序总行数不超过200行。
C语言不支持注释的嵌套,使一些编译器支持这个语言扩展。
5.7.6 边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除(强制)
5.7.7 对代码的注释应放在其上方或右方(对单条语句的注释)相邻位置,不可放在下面(强制)
如下例子不符合规范:
repssn_ind = ssn_data[index].repssn_index; repssn_ni = ssn_data[index].ni ; /* get replicate sub system index and net indicator */ |
5.7.8 对于有物理含义的变量、常量,如果其命名不是充分自注释的,在声明时都必须加以注释,说明其物理含义。变量、常量、宏的注释应放在其上方相邻位置或右方(强制)
示例:
/* active statistic task number */ #define MAX_ACT_TASK_NUMBER 1000 #define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */ |
5.7.9 数据结构声明(包括数组、结构、类、枚举等),如果其命名不是充分自注释的,必须加以注释。对数据结构的注释应放在其上方相邻位置,对结构中的每个域的注释放在此域的右方(强制)
示例:
/* 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*/ }; |
5.7.10 全局变量要有较详细的注释,包括对其功能、取值范围、哪些函数或过程存取它以及存取时注意事项等的说明(强制)
示例:
/* 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; |
可使程序排版整齐,方便注释的阅读与理解。如下例子,排版不整齐,阅读稍感不方便:
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 } |
如下例子,显得代码过于紧凑:
/* code one comments */ program code one /* code two comments */ program code two |
应如下书写:
void example_fun( void ) { /* code one comments */ CodeBlock One
/* code two comments */ CodeBlock Two } |
5.7.13 在程序块的结束行右方加注释标记,以表明某程序块的结束(强制)
当代码段较长,特别是多重嵌套时,这样做可以使代码更清晰,更便于阅读。示例:
if (…)
{
program code
while (index < MAX_INDEX)
{
program code
} /* end of while (index < MAX_INDEX) */ /* 指明该条while语句结束*/
} /* end of if (…)*/ /* 指明是哪条if语句结束*/
5.7.14 一行中如果有空语句,那么该行只能有这条空语句,不能有别的语句,并且在这条空语句前不能有注释,注释必须在其后,用空格隔开。且不允许空语句不带注释独占一行(强制)
如下例子不正确:
while (index = = 0) { ; foo ( ) ; /*本行的错误是除了空语句还有一条语句*/ / * wait for turn * / ; /*本行的错误是在空语句前有注释*/ ;/ * wait for turn * / /*本行的错误是空语句与注释没用空格隔开*/ } |
适当地增加空格会使代码更加清晰。由于留空格所产生的清晰性是相对的,所以,在已经非常清晰的语句中没有必要再留空格,如果语句已足够清晰则括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C语言中括号已经是最清晰的标志了。在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格。示例:
/*逗号、分号只在后面加空格*/ int a, b, c; for(int i; i < 100; i++)
/*比较操作符, 赋值操作符=、+=,算术操作符+、%,逻辑操作符 &&、&,位域操作符<<、^等双目操作符的前后加空格*/ if (current_time >= MAX_TIME_VALUE) a = b + c; a *= 2; a = b ^ 2;
/*!、~、++、—、&(地址运算符)等单目操作符前后不加空格*/ *p = ‘a’; /*内容操作符*与内容之间*/ flag =! isEmpty; /*非操作!与内容之间*/ p =& mem; /*地址操作&与内容之间*/ i++; /*++,—与内容之间*/
/*->、 .前后不加空格*/ p->id = pid; /* ->指针前后不加空格*/ /*if、for、while、switch等与后面的括号间应加空格,使if等关键字更为 突出、明显*/ if (a >= b && c > d) |
示例:
/*文件之中不得存在无规则的空行,比如说多于个空行*/ in t a, b, c;
for(int i; i < 100; i++)
/*相对独立的程序块之间、变量说明之后必须加空行*/ if (!valid_ni(ni)) { … // program code }
repssn_ind = ssn_data[index].repssn_index; repssn_ni = ssn_data[index].ni; |
5.7.17 控制语句必须用大括号括起来,即使只有一条语句(强制)
在控制语句中(包括循环和if/else 结构),即使只有一条语句,循环体也要用大括号括起来。很好的编程习惯可以增强代码可读性,避免人为错误。以下为不好的编程风格:
if (a == 1) x = y; /* 不好的编程风格*/
while ((c = getchar()) != EOF) putchar(c); /* 不好的编程风格*/
for (;;) timetest(n); /*不好的编程风格*/ |
/*一行程序不能大于120字符。较长的语句(>120字符)要分成多行书写,
建议每行不超过80个字符。长表达式要在低优先级操作符处划分新行,
操作符放在新行之首,划分出的新行要进行适当的缩进,使排版整齐,语句
可读*/
/*循环、判断等语句中若有较长的表达式或语句,则要进行适应的划分,长
表达式要在低优先级操作符处划分新行,操作符放在新行之首*/
if ((taskno < max_act_task_number)
&& (n7stat_stat_item_valid (stat_item)))
{
… // program code
}
for (i = 0, j = 0; (i < BufferKeyword[word_index].word_length)
&& (j < NewKeyword.word_length); i++, j++)
{
… // program code
}
for (i = 0, j = 0;
(i < first_word_length) && (j < second_word_length);
i++, j++)
{
… // program code
}
/*若函数或过程中的参数较长,则要进行适当的划分*/
int stat_str_compare((BYTE *) & stat_object,
(BYTE *) & (act_task_table[taskno].stat_object),
sizeof (_STAT_OBJECT));
5.7.19 代码中的一行中只能有一个声明或者一条语句(强制)
int i; int j; /*多个声明*/ j = i; i++; /*多条语句*/ |
为了了解变量类型、意义,变量命名应采取统一形式:
指针标识符应以p_开头 全局变量使用前缀g_ unsigned 类型使用前缀u,相对的signed 类型不必表示 double 类型使用前缀 d,替代float 表示浮点数 short 类型使用前缀 s char 类型使用前缀 c int 类型使用前缀 i long 类型使用前缀 l |
#define MAX 100 #define PI = 3.14159 |
#define ADD(a, b) a + b /* ... */ x = 2 * ADD( 1, 2 ); /* ... */ /*此语句扩展为x = 2 * 1 + 2将会导致优先级的混乱,所以此宏应该写成:*/
#define ADD(a,b) ((a) + (b)) |
5.7.23 用括号“()”明确表达式的操作顺序,避免使用默认优先级(建议)
防止阅读程序时产生误解,防止因默认的优先级与设计思想不符而导致程序出错。示例:
word = (high << 8) | low; if ((a | b) && (a & c)) |
程序块要采用缩进风格编写,缩进的空格数为4个。函数或过程的开始、结构的定义及循环、判断等语句中的代码都要采用缩进风格,case语句下的情况处理语句也要遵从语句缩进要求。对齐只使用空格键,不使用TAB键。以免用不同的编辑器阅读程序时,因TAB键所设置的空格数目不同而造成程序布局不整齐。程序块的分界符(如大括号‘{’和‘}’)应各独占一行并且位于同一列,同时与引用它们的语句左对齐。在函数体的开始、类的定义、结构的定义、枚举的定义以及if、for、do、while、switch、case语句中的程序都要采用如上的缩进方式。
5.7.25 所有预处理指令前不允许留空格,预处理块中的语句缩进遵从一般语句的缩进要求(建议)
除非在参数表或循环中可以使用逗号操作符,否则逗号操作符的使用会使程序的可读性降低。
void foo(void) { unsigned int x = 10u; x++, x += 1; } |
5.7.27 #include语句中不要使用绝对路径(强制)
避免在#include 语句中使用绝对路径,应该使用编译器的配置选项去指定文件路径,这样源代码和文件所存放的位置没有依赖关系,便于代码分析与测试。
5.7.28 include指令中的头文件名称不能包含非标准的字符(强制)
文件的命名中不能使用 '(单引号)、"(双引号) 或 /*(注释符) 等字符。
5.7.29 #include语句之前只能出现其它的预处理指令或注释(建议)
代码文件中所有#include指令应该成组放置在接近文件顶部的位置。文件中可以放置到include语句之前的只能是其他预处理指令或注释。
头文件应该用于声明对象、函数、typedef和宏,而不应该包含占据存储空间的对象或函数(或它们的片断)的定义。这样就清晰地划分了只有C文件才包含可执行的源代码,而头文件只能包含声明。头文件中只存放“声明”而不存放“定义”。
5.8.2 应使用 typedef 在头文件中对基本类型定义(建议)
在不同的编译器中基本类型的长度是不同的,为了提高可移植性,应在统一的头文件中使用typedef 定义基本类型。
typedef char char_t; typedef signed char int8_t; typedef signed short int16_t; typedef signed int int32_t; typedef signed long int64_t; typedef unsigned char uint8_t; typedef unsigned short uint16_t; typedef unsigned int uint32_t; typedef unsigned long uint64_t; typedef float float32_t; typedef double float64_t; typedef long double float128_t; |
5.8.3 函数应当具有原型声明,并且函数类型和参数类型都应显式声明(强制)
函数应当具有原型声明,并且函数的类型和参数的类型都应显式声明。原型的使用使得编译器能够检查函数定义和调用的完整性。
static foo( void ); /* not OK */
static int foo( void ); /* OK*/
extern int printf(); /* not OK */
extern int printf(const char* s,...); /* OK */
一些编译器允许函数参数只有类型名没有标识符,但这样的参数并不能真正被使用,因此禁止函数参数只有类型名没有标识符。
5.8.5 禁止单独使用小写字母“l”或大写字母“O”作为变量名(强制) 建议该条归类到“声明定义类”
小写字母“l”很容易与数字“1”混淆,或大写字母“O”很容易与数字“0”混淆,因此禁止单独使用小写字母“l”或大写字母“O”作为变量名。
八进制数是以0开始,容易与十进制的数混淆,所以使用八进制数必须加以注释(强制)
程序中出现同名的局部变量和全局变量,虽然没有语法问题,但会对程序的可读性和可靠性造成影响。
一个过程名禁止被重用于其它之处。
void foo(void) { … }
Void func(int p) { … int foo = 0; /*函数名被重用 … }
|
一个标号名被重用掩盖了这个标号名的含义,编译是否通过依赖于编译器的差异而不同。
void foo(void) { int label = 0; /*标号名被重用*/ … label: … } |
5.8.10 对只设置一次的变量,使用const限定词进行声明(建议)
当变量被设置后,在其后的代码中又出现了该变量且该变量没有被改变,这种情况下,变量要加const限定。
在大多数C语言的编译器和连接器中,只有变量或函数名的前31个字符的唯一性会被检查。
当声明一个数组时,它的大小应该显式声明或者通过初始化进行隐式定义。在数组的大小可以显式确定的情况下,访问数组元素才会更为安全。
int array1[10]; /* OK */ extern int array2[] ; /* Not OK */ int array2[] = { 0, 10, 15 }; /* OK */ |
重新定义使用C的关键字,破坏了程序的可读性,因此禁止重新定义或使用C的关键字。
/*禁止重新定义使用C关键字*/ void foo(void) { unsigned int struct = 0; unsigned int union = 0; unsigned int sizeof = 0; unsigned int typedef = 0; unsigned int const = 0; unsigned int volatile = 0; … } |
如果是一个无参函数,必须使用func(void)的形式说明,禁止使用func()的形式说明。
5.8.15 non-void类型函数的所有出口路径都应该有一个明确的return语句表达式,而不应该只有return(强制)
如果定义了函数的返回类型为non-void,return语句会导致函数的返回类型为void。下面的例子不符合规范:
int func(void) { … return; /* 没有明确的return语句表达式 */ } |
5.8.16 在函数参数中不能使用static存储类标识符(强制)
下面的例子不符合规范:
int func(static int x); /* 不符合规范 */ |
在同一个宏中使用多个#或##,或同时使用#或##都是很危险的,因此禁止在同一个宏中使用多个#或##。
宏只能用于符号常量,类函数宏,类型限定符以及存储类说明。宏中不允许含有语句关键字和类型关键字。
5.8.19 应优先考虑使用函数而非函数式宏定义。在宏参数中不应该包含自增、自减或赋值等带有单边作用的操作符,宏参数中也不应包含函数调用(建议)
利 用类似函数式的宏定义来取代函数调用,主要是能够提高程序的运行速度。从代码安全的角度制定这一规则,主要有两点考虑:一是宏定义不能像函数调用那样提供 参数类型检查,错误的参数类型无法得到纠正,运行的结果就可能不正确。二是宏定义中的参数可能会多次求值,当参数表达式带有单边作用时,就会出现问题。
# define SQUARE(x) ( (x) * (x) ) a = 3 ; b = SQUARE(a + + ) ; /* 实际结果与预期结果不符 */ |
5.8.20 类函数宏调用时不能没有它的参数,并且参数不能多于或少于宏定义时的参数(强制)
下面的例子不符合规范:
例1: # define ABC(x) ( (x) + 3 ) b = ABC; 例2: #define MAX(a, b) ((a) > (b) ? (a) : (b)) long maxnum = MAX(intnum); |
宏预处理可以重新定义保留字,但这种做法会引起混淆,因此禁止重新定义保留字。
/*禁止重新定义保留字*/ #define FILE unsigned int … |
5.8.22 字符型变量必须明确定义是有符号还是无符号(强制)
若对字符型变量不明确定义是否有符号,则在执行时会作为有符号或无符号类型使用,因此要求字符型变量必须明确定义是有符号还是无符号。
/*字符型变量必须明确定义是有符号还是无符号*/ void foo(void) { char c = ‘c’; …. } |
5.8.23 在同一软件产品内,应规划好接口部分标识符(变量、结构、函数及常量)的命名,防止编译、链接时产生冲突(强制)
对接口部分的标识符应该有更严格限制,防止冲突。如可规定接口部分的变量与常量之前加上“模块”标识等。
5.8.24 在文件范围内声明和定义的所有对象或函数应该具有内部链接,除非是在需要外部链接的情况下(强制)
如果一个变量只是被同一文件中的函数所使用,那么就用static。类似地,如果一个函数只是在同一文件中的其他地方调用,那么就用static。使用static存储类标识符将确保标识符只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性。
虽然C语言允许在许多不同的上下文中对一个名字重新定义,但命名的唯一性可使呈呈现增加可读性,因此禁止对一个名字重新定义。
struct sss { int a; }; void sss() { int a = 0; … sss: int b = 0; … } |
5.8.26 用typedef自定义的类型禁止被重新定义(强制)
改变用户自定义的类型会引起混淆甚至能导致错误,因此用typedef自定义的类型禁止被重新定义。
typedef int mytype; /*用typedef自定义的类型禁止被重新定义*/ void foo(void) { typedef float mytype; … } |
5.8.27 标记符的类型定义不应放在函数声明或函数定义的参数列表中(强制)
在C++中,函数声明或函数定义的参数列表中包含标记符的类型定义是非法的,虽然这在C语言中是合法的,但应尽量避免。下面的例子不符合规范:
int foo(enum en { A, B, C } x); |
5.8.28 禁止在同一个文件中有#if而没有#endif(强制)
预处理语句的#if和#endif禁止分散在不同的文件之中。
5.8.29 被包含文件中的#else、 #elseif、 #endif不应该与父文件中的#if匹配(强制)
5.8.30 #if表达式中使用的宏应该已经被定义,并且#if表达式只能包含整型常量(强制)
如下用法不符合规范:
#define BIGINT 32767 #define BIGREAL 1e38 #if (BIGREAL > BIGINT) /* BIGREAL是浮点型常数 */ ... #endif |
5.8.31 应该避免一个头文件的内容被包含两次。为了防止头文件被重复引用,头文件中应当使用#ifndef / #define / #endif结构产生预处理块(强制)
一个头文件直接或间接的被包含两次都是不允许的。
5.8.32 函数中避免使用过多的参数,建议不要超过20个(建议)
5.8.33 避免在一个函数块中单独使用#define(建议)
宏定义会使程序的可读性降低,尤其是那些定义在内部块中的宏使用起来很危险,因为块和块的作用域是不相关的,因此避免在一个函数块中单独使用#define。
/*避免在一个函数块中单独使用#define*/
void foo(void)
{
unsigned int local_x = 0u;
#define BLOCK_DEF 100u
local_x = local + BLOCK_DEF 100u;
…
}
5.8.34 避免在一个函数块中单独使用#undef(建议)
宏定义会使程序的可读性降低,尤其是那些定义在内部块中的宏使用起来很危险,因为块和块的作用域是不相关的,因此避免在一个函数块中单独使用# undef。
#define BLOCK_DEF 100u /*避免在一个函数块中单独使用# undef */ void foo(void) { unsigned int local_x = 0u; local_x = local + BLOCK_DEF 100u; #undef BLOCK_DEF … } |
5.8.35 宏如果需要被重定义,应该先用‘#undef ’解除前面的定义(强制)
5.8.36 除了编译开关/头文件等特殊应用,应避免使用_EXAMPLE_TEST_之类以下划线开始和结尾的定义(强制)
5.8.37 限定符static仅能用于函数或对象的声明,不能用于定义标记(强制)
用static定义标记在C语言中将被忽略,下面的定义是不规范的:
static struct ST1 { int a; int b }; |
5.8.38 地址操作符&不能应用到一个声明为register 的对象(强制)
下面的例子是不规范的:
static void foo (void) { register int a; int *pa; pa = &a; /* 不符合规范 */ } register int array[10]; int * ptr = &array[0]; /* 不符合规范 */ |
5.8.39 在struct或union类型中至少要有一个成员(强制)
下面的例子不符合规范:
struct record { }; |
5.8.40 对struct或union成员使用存储类指示符是非法的(强制)
下面的例子不符合规范:
struct record { extern int field; }; |
5.8.41 struct或union类型的成员不能是void类型、函数类型、未知大小的数组和含有未知内容的struct或union(强制)
下面的例子不符合规范:
struct record { void member; /*void类型*/ int func(void); /*函数类型*/ int array[]; /*未知大小的数组*/ struct incomplete record; /*未知内容的struct*/ }; |
#pragma可以用来掩盖所有类型的问题,因此谨慎使用#pragma
条件判别成立时相应分支无执行语句,可能是由于而遗漏了,或是有意避免布尔表达式不成立时的情况。为了防止由于疏忽造成的遗漏,因此禁止条件判别成立时相应分支无执行语句。该准则违背通常为下面几种形式:
a) if(…) else b) if(…){}else c) if(…){;}else |
5.9.2 在if…else if语句中必须使用else分支(强制)
在if…else if语句中为了表明已经考虑了所有情况,必须使用else分支。
从清晰可读的角度出发,建议逻辑表达式采用显示的表达式。
if ((!(trcsta <= ISTRAC)) && (!(fdiret <= IT)))应该写成: if ((trcsta > ISTRAC) && ((fdiret > IT)) |
5.9.4 在switch语句中必须有default语句(强制)
如果switch语句中缺省了default语句,当所有的case语句的表达式值都不匹配时,则会跳转到整个switch语句的下一个语句执行。强制default语句的使用体现出已经考虑了各种情况的编程思想。
空switch语句不具备任何实际的操作内容,因此禁止使用空switch语句。
5.9.6 禁止switch语句中只包含default语句(强制)
如果switch语句中只包含default语句,则该switch语句的使用无任何实际价值。
5.9.7 所有非空的switch case子句都应该以break 语句结束(强制)
示例:
switch (value) { case ONE: ++x; /* 这里缺少一条break语句 */ case TWO: /* ... */ break; case THREE: /* 空case语句,这里没有break语句是符合规范的 */ default: break; } |
如果switch的控制表达式是一个常量,可以用if语句等其它形式代替。
5.9.9 switch语句中的表达式不能是逻辑表达式(强制)
因为逻辑表达式只有真和假两种情况,当需要对逻辑表达式判别时,应使用if …… else语句而不是switch语句。
5.9.10 每一个switch语句都应该至少有一个case子句(强制)
不允许在switch的case或default之前存在声明和语句
5.9.11 case表达式的类型应该与switch控制表达式的类型一致(强制)
如果case表达式的类型与switch控制表达式的类型不一致,有可能导致错误。
5.9.12 switch的子句不能位于一个嵌套的代码块中(强制)
由于switch语句的语法定义的非常宽松,有可能出现一些奇怪的、容易混淆的代码,这种情况应该尽量避免。下面的例子不符合规范:
int foo(int i, int p) { int ret = p;
switch (i) { case 0: if (ret > 2) { ret++; break; case 1: ret = 2; break; case 2: ret = 3; break; } default: ret = 4; break; } |
5.9.13 case标签表达式的大小不能超过switch控制表达式的大小(强制)
下面的例子不符合规范:
int foo( unsigned char c ) { switch ( c ) { case 300: /* 300超出了c的取值范围 */ |
浮点数不应用作循环控制变量,在循环执行时会得到不可预料的结果。
… for (ft = 0.0F; ft < 10.0F; ft = ft + 2.0F) ++loop; … |
循环变量应在在最小的范围内定义,以免变量被外部修改导致循环不可控制,即循环变量的作用域应最小。
5.9.16 for循环控制语句中的3 个表达式只能和循环控制相关(强制)
第一个表达式只能为循环变量赋初值,第二个表达式只能进行循环条件的判断,第三个表达式只能进行循环变量增(减) 值这样可以增强程序的可理解性。下面的例子不符合规范:
for (i == 0; i < 10; i++) /*第一个表达式不是赋值表达式*/ { } |
5.9.17 for 循环中,循环变量只能在for循环控制语句的第三个表达式中修改,不允许在循环体中修改(强制)
这样可以使循环保持清晰,增强程序的可理解性。
如果for循环只执行一次,则显得冗余。
下面的例子不符合规范:
loop = 5; while (loop) /*loop值一直未变*/ ... }
|
在循环中使用break语句使得循环有多于一个出口,因此避免在循环中使用break语句。
直接从函数中跳出破坏了程序的结构化,因此禁止直接从函数中跳出。
#include <setjmp.h>
/*禁止直接从函数中跳出*/
void foo(jmp_buf mark, unsigned int val)
{
…
longjmp(mark, val);
}
任何使用goto 语句的程序都可以重新写成不用goto 的形式,实现的功能是一样的,使用goto 语句破坏的程序的结构化,导致复杂性的增加,使得代码难于维护和测试。
5.10.1 尽量减少没有必要的数据类型默认转换与强制转换(强制)
对 编译系统默认的数据类型转换,要有充分的认识,默认的数据类型转换有可能损失数据的精度,可能改变数据的符号属性。表达式如果是函数的参数或函数的返回表 达式,不能出现默认数据类型转换。当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐 患。常数被默认转换为无符号数时,应充分考虑到无符号变量的空间大小。
5.10.2 禁止 signed 类型与 unsigned 类型之间的隐式转化(强制)
从有符号类型转换为无符号类型会导致符号的丢失。无符号数转换为有符号数,可能得到一个负值。
#define FAILSAFE (-1) … unsigned long err = FAILSAFE; /*无符号变量会得到一个很大的正值*/ … signed int i16_a; unsigned int ui16_a = 32767 + 60; /* 16位无符号整型数范围0--65535U */ i16_a= ui16_a; /*16位有符号整型数范围-32768--32767*/ /* 以上赋值语句,i16_a得到一个负值,而此负值可能不是我们所期望 */ |
5.10.3 禁止 int 类型与 float 类型的隐式转化(强制)
浮点类型与整型之间的相互转换为会导致精度的丢失,此为软件潜在的危险,容易出问题。
double d64_a; float f32_a; signed int i16_a; ... d64_a = i16_a; /* 整型数转换为浮点数 */ si16_a = f32_a; /* 浮点数转换为整型数 */ ... |
强制将其它类型转换为指针类型是很危险的,因此禁止对指针变量使用强制类型转换赋值。
void foo(void) { unsigned short s = 0; unsigned int *p1_ptr; p1_prt = (unsigned int*)s; … } |
5.11.1 不能把自动类型的局部变量的地址赋值给外部指针(强制)
本地变量的地址赋值给外部连接或者范围更大的指针,如果本地变量不是静态的,那么这种用法是不安全的,当局部变量出了范围,指针变为无效的。
extern int* pi;
void f(void) { int a; pi = &a; /* Not OK */ } |
对指针的控制是很困难的,当指针的指针超过两级时,使用起来更是具有风险,因此禁止指针的指针超过两级。
5.11.3 避免对指针类型使用>,>=,<,<=做逻辑比较(建议)
如果两个指针没有指向同一个对象,那么试图对指针做比较将导致异常。注意:允许指向超出数组尾部的元素,但对该元素的访问是禁止的。
void foo(UINT_32 * p1_ptr, UINT_32 * p2_ptr ) { if ( p1_ptr > p2_ptr ) /* 比较两个指针是不明智的 */ { /* ... */ } } |
5.11.4 指向不同数据类型的指针之间不能相互转换(强制)
因为不同的数据类型分配的字节空间可能不同,将指向多字节空间的指针转换为指向少字节空间的指针不会有问题,但反之就有可能出现错误。
5.11.5 指针转换过程中不允许丢失指针的const, volatile属性(强制)
示例:
int x; int *pi; /* 指向整形的指针 */ int *const pci1 = &x; /* const指针 */ const int *pci2; /* 指向const整形的指针 */ volatile int *pci3; /* 指向volatile整形的指针 */ ... pi = pci1; /* 符合规范 */ pi = (int *)pci2; /* 不符合规范 */ pi = (int *)pci3; /* 不符合规范 */ |
由于不能保证运算后得到的是正确的地址,所以对指针进行代数运算是不安全的。
对并非指向数组或数组元素的指针做整数加减运算(包括增值和减值)会导致异常。
void foo(void) { UINT_32 w; UINT_32 array[5]; UINT_32 * p1_ptr;
p1_ptr = array; w = *(p1_ptr + 8); } |
此处的算术运算仅仅限定于指针加减某个整数,比如ppoint = point -5, ppoint++等。下面的例子不符合规范:
int *px; int i; px = &i; px = px + 2; ++px; |
下面的例子符合规范:
int *px; int a[10]; px = a; px = px + 2; ++px; |
5.11.8 只有指向同一个数组的两个指针才允许相减(强制)
两个指针可指向同一数组的不同成员。下面的例子不符合规范:
int a[10]; int b[10]; int x, *p1, *p2; x = p1 – p2; |
5.11.9 只有指向同一数组的两个指针才允许用>, >=, < , <=等关系运算符进行比较(强制)
两个指针可指向同一数组的不同成员。下面的例子不符合规范:
char a[10]; char b[10]; int x; x = a < b; |
下面的例子符合规范:
char a[10]; char *p1, *p2; int x; p1 = a; p2 = a+2; x = p1 < p2; |
下面的例子不符合规范:
void foo(int *p, int n) { int r; if (n < -5) { r = p[n]; /* 指针的索引值为负 */ ... r = *(p + n); /* 指针的索引值为负 */ } } |
5.11.11 不允许对指针值可能为NULL的指针进行算术运算(强制)
这样能保证程序的安全性和可靠性。下面的例子不符合规范:
extern int buf[10]; void foo( int n) { int *p = NULL; for (i = 1; i <= n; ++i) { ... p = &buf[i]; ... } ++p; /* 对指针值可能为NULL的指针进行算术运算 */ } |
5.11.12 对传递到函数的指针参数应进行是否为空判断(强制)
5.11.13 调用返回类型为指针的函数后,应进行是否为空的判断(强制)
5.11.14 除常量字符串外,其他字符数组必须指定长度(强制)
5.11.15 局部变量的地址不允许在本对象消亡后传给另外一个对象(强制)
这条规范可以分为下面几种情况:(1)将动态分配的局部变量的地址赋值给一个带链接的或更宽范围的指针是危险的,一旦局部变量的内存空间被释放,这个指针会成为无效的指针(见例1)。(2)函数参数不允许返回动态数据的地址(见例2)。(3)函数参数不允许返回本地静态数据的地址(见例3)。下面的例子不符合规范:
例1:
extern int* pi;
void f(void)
{
int a;
pi = &a;
}
例2:
void foo(int *appi)
{
int bi = 1;
...
*appi = &bi;
return;
}
例3:
void foo(int *appi)
{
static int bi = 1;
...
*appi = &bi;
return;
}
5.11.16 一个未知大小的对象的指针不能成为加法或者减法操作的左操作数(强制
下面的例子不符合规范:
typedef struct TAG T;
static int foo(int n, void *pv) { T *v1 = 0;
v1 = v1 + n; /* 不符合规范 */ pv = pv + n; /* 不符合规范 */ v1 = v1 - n; /* 不符合规范*/ pv = pv - n; /* 不符合规范 */ } |
5.11.17 多线程应用中被几个任务共享的变量建议使用volatile(建议)
C编译器会对用户录入的代码进行优化,示例:
int a, b, c; a = inWord(0x100);/*读取I/O空间0x100端口的内容存入a变量*/ b = a; a = inWord(0x100);/*再次读取I/O空间0x100端口的内容存入a变量*/ c = a;
/*能被编译器优化为:*/ int a, b, c; a = inWord(0x100);/*读取I/O空间0x100端口的内容存入a变量*/ b = a; c = a;
/*样的优化结果可能导致错误,如果I/O空间0x100端口的内容在执行第一次读 操作后被其它程序写入新值,则其实第2次读操作读出的内容与第一次不同,b 和c的值应该不同。在变量a的定义前加上volatile关键字可以防止编译器的类似 优化,正确的做法是:*/ volatile int a; |
不允许访问超出数组边界的元素,可以允许谨慎访问数组边界后一个元素的地址。
int buf[10]; int i16_i; int i16_r = 0; ... for (i16_i = 2; i16_i <= 8; ++ i16_i) { buf[i + 2] = i16_i; /*数组越界,写*/ i16_r = i16_r + buf[i + 2]; /*数组越界,读*/ } |
5.11.19 数组声明为外部时,应该明确声明大小或者直接初始化确定(强制)
下面的声明是不规范的:
extern int array[ ]; |
对有符号类型进行移位运算会导致不可预料的后果。
位运算对有符号数是很危险的,因为符号位会被错误的改变。
i16_a = i16_a & 0x7F7F; /* Not OK */
无格式的字符型指既没有定义为unsigned也没有定义为signed的char类型。不同的编译器对其处理是不一样的,有的定义其可正可负,有的定义其只能为正。下面的例子不符合规范:
void f(char c1) { if (c1 < -3) /* 和负数常量比较 */ { c1++; } if (c1 < 0) /* 和零比较 */ { c1++; } } |
无符号值与负数常量比较,可能会得到错误的结果。下面的例子不符合规范:
int foo (unsigned char uca, unsigned short usa, unsigned int uia ) { if (uca < NEG) { … } if (usa < NEG) { … } if (uia < NEG) { … } } |
5.12.5 对变量进行移位操作必须保证不会产生溢出和数据截短(强制)
在 ISO C 标准中对变量的移位位数超过变量的大小是未定义的操作,一些编译器不检查移运算是否超出机器字长。对无符号数进行左移操作可能造成数据截短。
unsigned char uc8_c; int i16_i; unsigned ui16_i; i16_i = uc8_c << 7; /* OK */ i16_i = uc8_c << 12; /* OK */ i16_i = uc8_c << 16; /* Not OK */ ui16_i = ui16_i << 4; /* Not OK */ |
5.12.6 禁止将逻辑非的操作用于比较表达式的一个操作数上(强制)
if (!a == b) 表达式在以下两种条件下其结果为 true 1) a 为 0 并且 b 为 1 2) a 非 0 并且 b 为 0 建议使用括号来明确执行顺序 if (!(a == b)) |
C 语言中不包含布尔类型,但是有一些操作符返回布尔值(0/1), 这里我们称其布尔表达式,在布尔表达式中出现了赋值操作符(=,+=,-=),如可能是由于笔误,把==写成了=,这样造成程序并没有实行判断,而只是一赋值语句,程序异常。
如果布尔表达式中包含计算,赋值,函数调用等处理,应该把它分成两部分。
if ((fp = fopen("filename", "r")) != NULL) ... if (i16_i += i16_j) ++ i16_r; if(flag=false) { … }
|
算术操作符或者位操作符(& |)的两边操作数都不能是布尔类型表达式。
x = (a > b) + (c > d); /* Not OK */ x = (a > b) & (c > d); /* Not OK */ |
5.12.9 表达式的值必须在任何求值顺序下保持一致(强制)
防止同一个表达式在不同的编译器中得到的结果不一样。下面的例子不合规范:
x = b[i] + i++; /*不同的编译器给出的结果不一样,b[i]不一定先执行*/ x = 2; y = 3; x = y * x++; /*求值顺序无法确定*/ |
5.12.11 赋值运算符的左操作数不能进行转换操作,并且必须是一个可更改的对象(强制)
下面的例子不符合规范:
int c = 0;
float ff = 20.0F;
float gg = 5.0F;
const float f1 = 0.0F; /* 不符合规范 */
(char)c = ff * gg; /* 不符合规范 */
5.12.12 sizeof操作符不能用在有单边作用的表达式上(强制)
下面的例子不符合规范:
int i; int j; j = sizeof( i = 1234 ); /* “i = 1234”没有执行 */ |
5.12.13 逻辑运算符&&或者||右边不能包含单边作用(强制)
如果&&的左边已经为0或者||的左边已经为1,则右边不会执行。下面的例子不符合规范:
if(ishight) && (x == i++)) /* 如果ishight=0,那么i++不会执行 */ |
5.12.14 逻辑运算符(&&、|| 和!) 的操作数必须为一个有效的布尔值,布尔值表达式不允许进行逻辑运算以外的操作(建议)
这是为了防止误用。示例:
int TRUE = !0; /* 符合规范 */ int FALSE = !1; /* 符合规范 */ int FALSE = !2; /* 不符合规范 */ |
5.12.15 不允许无符号性的表达式进行一元减运算符(强制)
5.12.16 不允许在同一个表达式中混合使用+ + 和- -(建议)
这是为了防止阅读混乱,并防止出现歧义。
5.13.1 传递给一个函数的参数应该与声明的参数匹配(强制)
这个问题可以通过使用函数原型完全避免。一些编译器忽略实参与形参个数的检查,不标记此约束错误,需要程序员养成使用函数原型的习惯。
int foo(int a, int b); ... ret = foo(x); /* 少于 */ ret = foo(x, y, z); /* 多于 */ |
5.13.2 函数原型中的指针参数如果不是用于修改所指向的对象就应该声明为指向 const的指针(强制)
const限定应当用在所指向的对象而非指针,因为要保护的是指针指向的对象,以防止该对象在函数体内被意外修改。
void myfunc ( int16_t *param1, const int16_t *param2, int16_t *param3 ) /* param1 : 指针指向的对象被修改了 – 不需要 const 限定 param2 : 指针指向的对象没有被修改了 –需要 const 限定 param3 : 指针指向的对象没有被修改了 –缺少const 限定*/ { *param1 = *param2 + *param3; return; } |
5.13.3 禁止把 auto 类型的局部变量地址作为函数的返回值或者通过指针参数返回(强制)
return 语句不可返回指向“栈内存”的“指针”或者“引用”。auto 类型的局部变量在退出函数后会被自动销毁,内存变为没有意义。
void foo(int **appi) { int bi = 1; ... *appi = &bi; /* Not OK */ ... return; } int8_t * foobar (void) { int8_t local_auto; return (&local_auto); /* Not OK */ } |
5.13.4 避免在函数中使用 static 局部变量(建议)
尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。
int foo(void) { static int lx; /* Not OK */ ... }
|
函数参数在函数调用中未被使用,说明该参数没有实际的存在价值。
在函数的调用中,如果函数名后面不加“()”,表示函数的指针,而不是函数的调用。下面的例子可能与作者的本意有冲突:
int foo( void ); int main( void ) { foo; /* 函数名后面没有()*/ if (foo) /* 函数名后面没有(),不清楚是要测试函数foo的地址是否 为NULL,还是执行函数的调用 */ … } |
这些函数会导致程序终止执行,应谨慎使用这些函数。
递归调用特别是函数间的递归调用(如A->B->C->A),影响程序的可理解性;递归调用一般都占用较多的系统资源(如栈空间);递归调用对程序的测试有一定影响。故除非为某些算法或功能的实现方便,应减少没必要的递归调用。
下面的例子不符合规范:
goto label1: i++; /*执行不到的代码*/ label1: …… |
5.14.2 非空语句必须产生单边作用或者使程序流程改变(强制)
下面的例子不符合规范:
x > = 3 ; i; |
5.14.3 没有使用到的标识符、标记和变量都应该删除(强制)
从清晰可读的角度出发,建议逻辑表达式采用显示的表达式。
for(i, j = strlen(s) - 1; i < j; i++, j--) |
5.14.5 表达式语句上层不能包括多余的操作符和转换(强制)
下面的例子不符合规范:
a == b++; /*该表达式包含的操作符==没有被用到*/ (int)(a += 1); |
应仔细考虑循环体内的语句是否可以放在循环体之外,使循环体内工作量最小,从而提高程序的时间效率。如下代码效率不高:
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 */ |
5.15.2 在多重循环中,应将最忙的循环放在最内层(强制)
减少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]; } } |
5.15.3 避免循环体内含判断语句,应将循环语句置于判断语句的代码块之中(强制)
目的是减少判断次数。循环体中的判断语句是否可以移到循环体外,要视程序的具体情况而言,一般情况,与循环变量无关的判断语句可以移到循环体外。
5.15.4 注意switch-case语句的使用(推荐)
switch-case 语句,每一个由机器语言实现的测试和跳转仅仅是为了决定下一步要做什么,为了提高速度,可以把具体的情况按照它们发生的相对频率排序。即把最可能发生的情况放在第一,最不可能发生的情况放在最后,这样会减少平均的代码执行时间。
5.15.5 应避免使用标准库函数(推荐)
嵌入式系统编程应避免使用标准库函数,因为很多大的库例程设法处理所有可能的情况,所以占用了庞大的内存空间,因而应尽可能地减少使用标准库函数。
Appendix A:符合性矩阵
检验编译器和静态工具对本规则的符合性,可使用下面的符合性矩阵:
规范序号 | 是否为“强制”规范 | 编译器1 | 编译器2 | 静态分析工具1 | 静态分析工具2 |
5.1.1 | No | Warning 121 | Warning 100 | Error 23 | |
5.1.2 | No | Warning 86 | Warning 152 | ||
5.1.3 | No | Warning 94 | Warning 57 | ||
…… | …… | …… | …… | …… | …… |
合计(符合本规范的条数) | 50 | 36 | 66 | 28 |