推荐的个人网站MISRA-C中文翻译,但是它少了Directives部分,Rule部分是比较详细的;
推荐的MISRA-C条目整理文档:待补充
首先,MISRA-C是一个C语言标准,是一个文档,是收费的,所以网上的解读以及翻译都是有人在官网买过,然后根据官网的文档而来;
其次,为什么要搞浓缩版,因为MISRA-C实在太长了,而且很多东西我们极其少用到,比如初始化数组:
uint8_t array[2] = {[0] = 1, [1] = 2}
或者已经是默认行为如禁止使用goto等,所以小白在这边精炼一些对实际项目和工作有帮助的内容进行展开;
MISRA-C 2012是属于第三版的MISRA-C,一开始是为了汽车行业而专门定的C语言编程规范(不包含代码风格),说白了就是以前从事汽车行业开发的大佬,在积累N年的C语言编程经验后得出的宝典,宝典中沉淀着他们实际项目中遇到的奇葩问题的预防措施,就是只要你按照宝典的要求去写代码,那么就会减少很多奇奇怪怪的问题,把这些问题扼杀在摇篮当中,而不是等问题出现后再这样去写代码。
其实每一个嵌入式公司基本都会有内部的编程规范,这些规范更多是软件总监等级别的人编写的,像华为的编程规范,阿里巴巴的编程规范,但是这些商业的编程规范都比较倾向于他们所属的业务,具有领域属性,很多规定都是为业务而生,所以不必诧异为什么里面会规定这样的内容,因为每一条内容的背后都会有血的教训。但是MISRA-C在第二版之后就不具备领域属性了,而是面向所有领域,跨行业的应用,因此现在很多公司的C语言编程规范,多多少少都带着MISRA-C的内容在里面,因为MISRA-C道出了C语言编程本身的各种问题,而不是某个领域的问题;
所以你可以当MISRA-C是一个字典工具,偶尔看一看,或者编程的时候突然想起有相关的就查一下,不必一直啃食,因为很多内容如果你没经历过,大概就是从左耳进,右耳就出去了,当你先过一遍,脑子里大概知道有提及哪些内容,然后实际真的遇到后,你自然会回来查阅的;
话不多说,MISRA-C原文档的各种总览、背景、工具等我都忽略过,直接进入主题,MISRA-C既然是一个编程规范,里面肯定是各种条例规定,理所当然最重要的内容叫准则(guidelines),可以理解为最顶层的规范,它由2部分组成:
Directive,翻译为指示,引导你需要做这个事情,但是这个事情没有判断对错的标准。实际项目是否符合规范是需要借助其它条件来证实的,单纯依赖MISAR-C是不能证实的。比如Dir 4.3提及汇编语言必须被封装并隔离,但是封装和隔离都是每一个项目根据实际情况来进行的,接口不统一、做法不统一,统一的只有这个思维,这就是Directive;
Rule,翻译为规则,明确代码就是要这样做,否则就违反标准。它不需要根据其它情况可以进行判定,比如Rule 15.1 说明不应该使用goto语句,你用了而且没有其它预防措施就是不符合MISRA-C规范,没有用或者用了但是有各种预防措施就符合这一条的规范,这就是Rule;
其中每一条Dir或者Rule都有3种类型的级别:
mandatory(强制):不允许违反;
required(必要):只有在明确限制、要求和预防措施的情况下才可以违反;
advisory(建议):在合理可行的范围内建议遵守;
这3个级别是相同重要的,差异点在于是否允许偏离标准,偏离标准的例子如下,为了某些特殊情况偏离标准,比如将 int 类型值强制转为指针来实现访问内存地址空间映射的 I/O,需要专门的文档记录这种违反MISRA-C的地方:
// 内存中的0x0002地址内数据映射了某一I/O端口数据
#define PORT (*(volatile unsigned char *)0x0002)
// 修改该位置数据就相当于修改了该I/O端口数据
PORT = 0x10u;
Directive和Rule加起来会有170+条,每一条在官网文档种都有描述,但是我们这里只选择若干重点的内容来展开,这些都是养成良好编程习惯的重要规范,对工作和实际项目有好处无坏处;
小白理解:每一次编译成功后,都需要保持项目的 0 warning,0 error
很多人会容忍warning的存在,但这是不对的,可能项目存在定义但未使用的代码然后触发warning,虽然无关紧要,但是太多的时候会把真正的warning掩盖掉,比如if (x = y)
这种写法会触发warning,如果你的项目本来就有几十个warning的时候,那么多这1个也很难去看到,于是你就错过了编译器帮你排查BUG的机会,当出现问题的时候需要花费额外的时间去排查。因此永远遵守0 warning,0 error能让编译器帮你识别很多问题;
小白理解:注释代码容易和真正的注释产生混淆,如果真的想暂时屏蔽一段代码,应该要明确标注出起点和终点,使用#if或者#ifdef起点然后#endif重点,但是我觉得最好的做法是删掉他们,利用git等代码管理工具来保存记录
很多人在实际项目中暂时取消某个功能就是使用注释,因为注释一遍都是有快捷键的,很方便就能选中代码然后注释掉,但是由于真正的注释和这种行为的注释用法是一样的,会让其他人看这个代码产生混淆,它到底有没有作用,如果真的存在一个能符合语法的注释它真的是一个注释,而且混淆在这些屏蔽代码里面,那么程序员将很难分清代码行为。
void FuncA(void)
{
uint8_t Cnt = 0;
//符合Dir 4.4写法
#if 0
Cnt++;
#endif
//不符合Dir 4.4写法
// Cnt++;
}
小白理解:在调用库函数之前,必须先检查参数是否有效,再进行调用,而不是相信库函数会自己检查,原则上就是不相信其它库的接口在遇到非法数据时行为是正常的,都要假设是异常的。
其实这种思维在Linux早已根深蒂固,Linux内核的设计思维之一就是不相信用户会正确使用内核函数,因此给用户加了各种权限,让用户需要通过各种验证后才能使用接口,所有我们在调用别人的库之前就需要检查入口,同时编写接口的时候也要做到入口参数的检查。
/* A.h */
extern void FuncA(uint8_t* pMsg);
extern uint8_t* GetMsgPointer(void);
/* B.c */
void FuncB(void)
{
uint8_t* pM = GetMsgPointer();
//符合Dir 4.11写法
if (NULL != pM) {
FuncA(pM);
}
//不符合Dir 4.11写法
FuncA(pM);
}
小白理解:原因是1这些代码占用Ram和Flash而且毫无意义纯属浪费,2是可能导致编译器产生一些又臭又长的跳转指令但实际并不需要,3是可能导致整个循环变慢。
很多不可达的代码都有warning,请遵守Dir 2.1保持0 warning,0 error。另外就是有比较难检查到的不可达代码,就比较依赖静态检查工具来进行了,但是一般遵守if-else或者switch-case的时候不要提前return,而且确保判断的值范围都在内部一般不会有什么大问题。
#include "stdafx.h"
typedef enum ErrStatus {
Success = 0,
Err_1,
Err_2
} ErrStatus;
ErrStatus f(int x) {
if (x < 0) {
return Err_1;
}
else
{
return Success;
}
}
int main()
{
ErrStatus x = f(5);
switch (x)
{
case Err_1:
printf("err 1"); break;
case Err_2:
printf("err 2 "); break; /* unreachable code */
default:
printf("Success"); break;
}
}
小白理解:把调试代码和取消的功能删干净再上传业务代码。
意在提醒实际项目中调试或者删除功能时,要把代码弄干净,写了一个debug接口,结束后忘记删除然后停留在业务上面,如果不调试它是没有任何作用的,占用Ram和Flash纯属浪费,还容易引起胡混淆;
extern volatile uint16_t v;
extern char *p;
void f(void)
{
uint16_t x;
(void)v; /* Compliant - 这种方式用于抑制编译器的未使用告警,是有意义的
,如果删除就会产生编译器告警,不视为dead code */
(int32_t) v; /* Non-compliant - the cast operator is dead */
v >> 3; /* Non-compliant - the >> operator is dead */
x = 3; /* Non-compliant - the = operator is dead
* - x is not subsequently read */
*p++; /* Non-compliant - result of * operator is not used */
(*p)++; /* Compliant - *p is incremented */
}
小白理解:我觉得这一堆跟Rule 2.2是一样的含义,没用的代码弄干净点,我推荐直接删掉,连#if和#ifdef都不要用,除非是必须的调试代码。
小白理解:在 C99 中规定外部链接标识符的有效识别长度为 31 个字符(是否大小写敏感取决于编译器),也就是前 31 个字符需要唯一,才能区分两个外部链接标识符表示不同的项。
我建议每一个对象的命名,都跟随其所属模块作为前缀,这样重名的概率低很多,比如EEPROM模块,那么里面的函数、变量、struct、enum等都是EEPROM_作为前缀,这样即使另一个模块比如Timer也有一个名字教Data的,那么EEPROM_Data就和Timer_Data不相同,另外是命名不要过长,模块_动作_名称,控制在31字符内;
/* 1234567890123456789012345678901********* Characters */
int32_t engine_exhaust_gas_temperature_raw;
int32_t engine_exhaust_gas_temperature_scaled; /* Non-compliant,两个变量名的前31个字符相同 */
/* 1234567890123456789012345678901********* Characters */
int32_t engine_exhaust_gas_temp_raw;
int32_t engine_exhaust_gas_temp_scaled; /* Compliant */
小白理解:函数内部的局部变量不要和全局变量重名
特别是循环计数变量,习惯用i,j,k没问题,但是一个函数内部每一个循环的计数名字都要不一样,可以有i,j,k,m,n,z,y……都行,最好就是明确当前循环计数的含义,比如当前循环是找A的,计数值变量就叫A_i或者其它具体含义,下一个循环是找B的就叫B_i或者其它具体含义,只有一个循环直接叫i或者其它具体含义;
void fn1(void)
{
int16_t i; /* Declare an object "i" */
{
int16_t i; /* Non-compliant - hides previous "i" ,第三种情况连续嵌套块*/
i = 3; /* Could be confusing as to which "i" this refers */
}
}
struct astruct
{
int16_t m;
};
extern void g(struct astruct *p);
int16_t xyz = 0; /* Declare an object "xyz" */
void fn2(struct astruct xyz) /* Non-compliant - outer "xyz" is
* now hidden by parameter name */
{
g(&xyz);
}
uint16_t speed;
void fn3(void)
{
typedef float32_t speed; /* Non-compliant - type hides object */
}
小白理解:不要相信编译器可以很好帮你处理常量类型
当你的的写法不一样的时候,4095是uint16_t或者uint32_t或者sint32_t都是不知道的,但是你只要写成4095u,那他无论怎么都是无符号型,而且必须是uint16_t起步,这样子就算和有符号的进行计算,也不会认为最高位是符号位了;
例如,整数常量 40000 在 32 位环境中属于带符号 int 类型,但在 16 位环境中属于 signed long 类型。数值 0x8000 在 16 位环境中属于 unsigned int 类型,但在 32 位环境中属于 signed int 类型。在 2-bit int 和 64-bit long 环境中:
void R_7_2(void)
{
use_int32(2147483647); /* int constant */
use_int32(0x7FFFFFFF); /* int constant */
use_int64(2147483648); /* long constant */
use_uint32(2147483648U); /* unsigned int constant */
use_uint32(0x80000000); /* unsigned int constant - Non-compliant */
use_uint32(0x80000000U); /* unsigned int constant */
}
小白理解:纯属和数字1长得很像,肉眼难以区分,干脆不用
const int64_t a = 0L;
const int64_t b = 0l; /* Non-compliant */
const uint64_t c = 0Lu;
const uint64_t d = 0lU; /* Non-compliant */
const uint128_t e = 0ULL;
const uint128_t f = 0Ull; /* Non-compliant */
const int128_t g = 0LL;
const int128_t h = 0ll; /* Non-compliant */
const float128_t m = 1.2L;
const float128_t n = 2.4l; /* Non-compliant */
小白理解:道理很简单大家都懂,字符串常量不可被改变,但是写接口的时候很多会忽略,只能说带来隐患,写上就100%没问题
extern void f1(char *s1);
extern void f2(const char *s2);
static void g2(void)
{
f1("string"); /* Non-compliant,形参为非const,实参是字符串常量 */
f2("string"); /* Compliant */
}
static char *name1(void)
{
return ("MISRA"); /* Non-compliant,返回参数类型非const */
}
static const char *name2(void)
{
return ("MISRA"); /* Compliant*/
}
void R_7_4(void)
{
char *s = "string"; /* Non-compliant */
const volatile char *p = "string"; /* Compliant */
"0123456789"[0] = '*'; /* Non-compliant,未定义行为 */
g2();
(void)name1();
(void)name2();
use_const_char_ptr(s);
use_const_volatile_char_ptr(p);
}
小白理解:说白了就是调用文件以外的变量、函数、宏等都需要有显式的声明,而且变量和函数都必须带extern
extern void func1(void);
extern void func2(int16_t x, int16_t y);
extern void func3(int16_t x, int16_t y);
void func1(void)
{
/* Compliant */
}
void func2(int16_t x, int16_t y)
{
/* Compliant */
}
void func3(int16_t x, uint16_t y)
{
/* Non-compliant - parameter types different,违反规则8.3 */
}
void func4(void)
{
/* Non-compliant - no declaration of func4 before this definition */
}
static void func5(void)
{
/* Compliant - rule does not apply to objects/functions with internal
* linkage */
}
小白理解:声明都放在.h里面,不要越界操作
这个是和Rule 8.4有关系的,Rule 8.4是说外部链接的标识需要有声明,这里则说只能声明一次,稍微转化一下就是,规定外部链接的标识符只能在.h里面声明且只有1次声明,换言之,不要在.c文件里面用extern把其它文件的变量或者函数给包含进来,这种属于不合规的做法,正常使用你只能#include一个.h头文件进来,如果只是临时调试使用,调试结束后记得删干净。
/* featureX.h */
extern int16_t a; /* Declare a */
/* file.c */
#include "featureX.h"
int16_t a = 0; /* Define a */
小白理解:不给别人用的变量和函数,都加上static修饰
1来可以防止重名问题,2来可以清晰阅读一个.c里面哪些只限于内部使用,3来添加权限属性,别人在.c里面强制extern你的static变量或者函数是会编译失败的,他必须再去你文件里面删掉static,也就是让越界行为变得复杂,稍微再防止一下
static int32_t x = 0; /* definition: internal linkage */
extern int32_t x; /* Non-compliant,先前已存在x的声明,
* 导致这个x的链接性就是内部的,
* 而不是我们平常认为的用extern修饰的是外部链接 */
static int32_t f(void); /* declaration: internal linkage */
int32_t f(void) /* Non-compliant */
{
return 1;
}
static int32_t g(void); /* declaration: internal linkage */
extern int32_t g(void) /* Non-compliant */
{
return 1;
}
小白理解:不要用extern uint8_t array[],一定要用#define LENGTH_MAX 10 extern uint8_t array[LENGTH_MAX]
没什么好说的,给被人用的数组一定要让别人有方法知道长度,否则就存在越界的隐患,你可以用函数返回,可以用全局变量,可以用宏,可以用枚举等等,一定要用其中一个
extern int32_t array1[10]; /* Compliant */
extern int32_t array2[]; /* Non-compliant */
小白理解:局部变量定义时即初始化,不要偷懒
这也是减少隐患的预防性编程,定义局部变量的时候顺手写一个赋值操作,这带来了确定性,天晓得下一个人会不会在局部变量还没赋值的时候使用,或者你不会,但下一个维护你代码的人会
static void f(bool_t b, uint16_t *p)
{
if (b)
{
*p = 3U;
}
}
static void g(void)
{
uint16_t u; /* Non-compliant declaration,u未被显式赋值 */
f(false, &u);
if (u == 3U) /* Non-compliant use - "u" has not been assigned a value. */
{
use_uint16(u); /* */
}
}
static void jmp_over_init(void)
{
goto L1; /* violates R.15.1 */
uint16_t x = 10u;
L1:
// 此处的x声明虽然被跳过,但x还是被正常声明了,可编译通过
x = x + 1u; /* Non-compliant - x has not been been assigned a value */
use_uint16(x);
}
void R_9_1(void)
{
bool_t b = get_bool();
uint16_t val = 3u;
f(b, &val);
use_uint16(val);
g();
jmp_over_init();
}
小白理解:运算类型始终保持一致性,多使用强制类型转换
这是避免编译器的隐形转换导致数据截断或者精度问题,编译器不一定会按照你的想法进行隐式转换,自己强制转换能掌控全局
u8a = 2; /* Compliant By 例外1 */
u8a = 2 * 24; /* Compliant By 例外1 */
uint8_t u8f = 1.0f; /* Non-compliant - unsigned and floating */
bool_t bla = 0; /* Non-compliant - boolean and signed,不符合例外1,因为bla不是无符号整型 */
cha = 7; /* Non-compliant - character and signed */
u8a = 'a'; /* Non-compliant - unsigned and character */
u8b = 1 - 2; /* Non-compliant - unsigned and signed,不符合例外1,因为1-2不是非负的 */
u8c += 'a'; /* Non-compliant - u8c = u8c + 'a' assigns character to unsigned */
s8a = K2; /* Non-compliant - Constant value does not fit */
u16a = u32a; /* Non-compliant - uint32_t to uint16_t */
s8a = -123L; /* Non-compliant - signed long to int8_t */
u8a = 6L; /* Non-compliant - signed long to uint8_t,不符合例外1 */
/* Standard Type has rank greater than int,
* so exception does not apply */
/* integer constant expression from + with value 5U and UTLR of unsigned char */
u8a = (uint16_t)2U + (uint16_t)3U; /* Compliant,例外1? */
/* integer constant expression from + with value 100000U and UTLR of unsigned int */
u16a = (uint16_t)50000U + (uint16_t)50000U; /* Non-compliant,不符合例外1,超过了u16的最大值 */
/* Top-level cast returns C standard type of unsigned short */
u8a = (uint16_t)(2U + 3U); /* Non-compliant,经过转换后不再是常量表达式,不符合例外1 */
小白理解:只能使用NULL来判断空指针,不能使用0
这个网上有很多解释了,(void*)0和0的含义是不一样的,你不能相信编译器会把他们认为是一样,所以统一使用NULL的(void*)0
#define MY_NULL_1 0
#define MY_NULL_2 (void *)0
#define MY_NULL_3 NULL
extern void f9(uint8_t *p);
int32_t *p1 = 0; /* Non-compliant */
int32_t *p2 = (void *)0; /* Compliant */
int32_t *p3 = MY_NULL_3; /* Compliant */
if (p1 == MY_NULL_1) /* Non-compliant - also breaks R.14.3 */
{
}
if (p2 == MY_NULL_2) /* Compliant - but breaks R.14.3 */
{
}
f9(NULL); /* Compliant for any conforming definition of
* NULL, such as:
* 0
* (void *)0
* (((0)))
* (((1 - 1)))
*/
小白理解:不要怜惜括号的使用,每一层计算都需要加
预防性编程的一种,把结果掌控在程序员手里,而不是选择相信编译器,比如很多人喜欢这样写if ( a >= b && c <= d),这完全没问题,但是MISRA-C更加推荐你这样写,if ( (a >= b) && (c <= d)),给每一个运算都加上括号维护起来,这是100%不会出现符号优先级问题的
小白理解:很少人用,优先级和副作用问题多,干脆不要用
逗号表达式最多出现在for的子句里面,比如for(i = 0, j = 0; ; i++, j++)这样子,看起来是完全没问题的如果只是如此简单,不过如果你的计算稍微复杂比如突然有一天j要从i开始,就改成了for(i = 0, j = i; ; i++, j++),请问如果你选择相信编译器,j的值是0还是没有赋值0之前的i值?还是那句话,不要依赖编译器,能自己完成的事情就不要让编译器去做,取消逗号表达式的使用
/* also violates R.14.2 */
for (i = 0, p = &a[0]; /* Non-compliant */
i < N;
++i, ++p) /* Non-compliant */
{
}
小白理解:常量的加法不应该溢出,这个是很难人工检查出来的,一般依赖静态检查,或者编程的时候注重常量的加法
// 与 case 标签关联的表达式必须是常量表达式。
// 如果在 case 表达式求值期间发生无符号环绕,则很可能是无意的。
// 在具有 16 位 int 类型的计算机上会导致以下示例中的回绕:
#define BASE 65024u
switch (x)
{
case BASE + 0u:
f();
break;
case BASE + 1u:
g();
break;
case BASE + 512u: /* Non-compliant - wraps to 0 */
h();
break;
}
小白理解:禁止!禁止!禁止!使用goto,另外遵循单点退出原则,并且退出点必须在模块的最后一句
单点退出原则也就是一个模块只能有一个退出点,这个的好处是约束了模块的行为是稳定的,它必然会跑到固定的退出点,而退出点固定在模块最后一句,意味着模块内每一句语句都会被执行,防止了一些锁的成对操作缺漏,或者模块的退出操作缺漏,预防性编程
#define LIMIT 100u
/* Note: All uses of goto also break R.15.1 */
void R_15_4(void)
{
uint32_t x;
uint32_t y;
uint32_t z;
for (x = 0; x < LIMIT; ++x)
{
if (ExitNow(x))
{
break; /* compliant - single exit from outer loop */
}
for (y = 0; y < x; ++y)
{
if (ExitNow(LIMIT - y))
{
break; /* compliant - single exit from inner loop
* 这个break仅用来退出本for循环而不是上级for,
* 所有和上面那个break不冲突 */
}
}
}
for (x = 0; x < LIMIT; ++x)
{
if (BreakNow(x))
{
break;
}
else if (GotoNow(x))
{
goto EXIT; /* Non-compliant - break and goto in loop */
}
else
{
KeepGoing(x);
}
}
EXIT:;
while (x != 0u)
{
if (x == 1u)
{
break;
}
while (y != 0u)
{
if (y == 1u)
{
// 这个goto直接退了两层while,和上面的break冲突
goto L1; /* Non-compliant (outer loop) Compliant (inner loop) */
/* goto causes early exit of both inner and outer loop */
}
}
}
L1:
z = x + y;
}
小白理解:和Rule 15.4的一个意思,遵守单点退出原则;
static bool_t f(uint16_t n, const char *p)
{
if (n > MAX)
{
return false; /* Non-compliant */
}
if (p == NULL)
{
return false; /* Non-compliant */
}
return true;
}
小白理解:强制让你思考else的内容,也就是让你多思考一个分支情况,防止发生错漏,实际项目很多时候就是说”啊,少考虑了这种情况“
if (flag_1)
{
action_f1();
}
else if (flag_2)
{
action_f2();
}
/* Non-compliant */
if (flag_1)
{
action_f1();
}
else if (flag_2)
{
action_f2();
}
else
{
; /* No action required - ; is optional */
}
小白理解:switch或许有你考虑不到的值,如果没有default,则switch会退出,你想debug的机会都难,多写一个default,里面加打印语句,100%不会错漏
switch (x)
{
case 0:
++x;
break;
case 1:
case 2:
break;
/* Non-compliant - default label is required */
}
小白理解:嵌入式的RAM太宝贵了,就算你有1M的RAM,也经不住递归的堆栈开辟,随时栈溢出的风险,除非你有G级别的RAM,但这已经不太属于嵌入式了,不要用是100%不会出现递归导致的栈溢出问题
static uint16_t fn_a(uint16_t parama)
{
uint16_t ret_val;
if (parama > 0U)
{
ret_val = parama * fn_a(parama - 1U); /* Non-compliant */
}
else
{
ret_val = parama;
}
return ret_val;
}
小白理解:参数或许需要拿来判断原始值,留住备份总是一个好习惯
int16_t glob = 0;
void proc(int16_t para)
{
para = glob; /* Non-compliant */
}
void f(char *p, char *q)
{
p = q; /* Non-compliant */
*p = *q; /* Compliant */
}
小白理解:不要相信标准库能很好兼容你的芯片,直接不用是100%不出标准库问题
stdlib.h中的动态内存接口、sethmp.h所有接口、signal.h所有接口、stdlib的标准IO接口、atof、atoi、atoll、atol、system终止函数、bsearch、qsort、time、date、tgmath.h所有接口
#include
void R_21_3(void)
{
char_t *p;
p = (char_t *)malloc(11U); /* Non-compliant: use of malloc */
if (p != NULL)
{
(void)realloc(p, 20U); /* Non-compliant: use of realloc */
}
free(p); /* Non-compliant: use of free */
p = (char_t *)calloc(10, sizeof(char_t)); /* Non-compliant: use of calloc */
free(p); /* Non-compliant: use of free */
}