【MISRA-C 2012】浓缩版解读

文章目录

  • 1、前言
  • 2、简介
    • 2.1、如何看待MISRA-C 2012
    • 2.2、准则(guidelines)里面的指示(Directive)和规则(Rule)
    • 2.3、准则(guidelines)的级别(Category)
  • 3、若干重要的Directive和Rule
    • 3.1、指示(Directive)
      • Dir 2.1(必要) 所有的源文件编译过程不得有编译错误
      • Dir 4.4(建议)不应该注释掉代码
      • Dir 4.11(必要)检查传递给库函数参数值的有效性
    • 3.2、规则(Rule)
      • Rule 2.1(必要)不能含有不可达代码
      • Rule 2.2(必要)不能含有死代码
      • Rule 2.3~2.7(建议)不应该含有未使用的类型声明、标签、宏、形参
      • Rule 5.1~5.9(必要)宏、类型定义/声明、函数定义/声明、变量定义/声明都不得重名
      • Rule 5.3(必要)内部作用域声明的标识符不得隐藏外部作用域声明的标识符
      • Rule 7.2(必要)无符号整形常量都是用"u"或者"U"后缀
      • Rule 7.3(必要)小写字母"l"不能作为常量后缀
      • Rule 7.4(必要)不能把字符串常量赋值给对象,除非对象类型为const char*
      • Rule 8.4(必要)具备外部链接的标识符必须有显式可见的声明
      • Rule 8.5(必要)具备外部链接的标识符只能在一个文件声明一次
      • Rule 8.8(必要)具备内部链接的变量和函数都必须使用static修饰
      • Rule 8.11(建议)具备外部链接的数组声明,应该显式指定长度
      • Rule 9.1(强制)具备自动储存周期的对象,在设定它的值之前不能读取
      • Rule 10.3(必要)表达式的值不得赋给更窄的基本类型,也不得赋给不同的基本类型类别
      • Rule 11.9(必要)宏NULL必须为整数类型空指针常量的唯⼀允许形式
      • Rule 12.1(建议)应明确表达式中操作数的优先级
      • Rule 12.3(建议)不得使用逗号表达式
      • Rule 12.4(建议)对常量表达式进⾏求值不应导致整数回绕
      • Rule 15.4(建议)对于任何迭代语句,最多只应使⽤⼀个break或goto语句进⾏终⽌
      • Rule 15.5(建议)函数结尾应只有⼀个退出点
      • Rule 15.7(必要)所有if … else if构造都必须以⼀个else语句终⽌
      • Rule 16.4(必要)每个switch语句都必须有default标记
      • Rule 17.2(必要)不得使用递归调用
      • Rule 17.8(建议)不应该修改函数的形参
      • Rule 21.3~21.11(必要)不得使用下面标准库中的接口


  推荐的个人网站MISRA-C中文翻译,但是它少了Directives部分,Rule部分是比较详细的;
  推荐的MISRA-C条目整理文档:待补充

1、前言

  首先,MISRA-C是一个C语言标准,是一个文档,是收费的,所以网上的解读以及翻译都是有人在官网买过,然后根据官网的文档而来;

  其次,为什么要搞浓缩版,因为MISRA-C实在太长了,而且很多东西我们极其少用到,比如初始化数组:

uint8_t array[2] = {[0] = 1, [1] = 2}

  或者已经是默认行为如禁止使用goto等,所以小白在这边精炼一些对实际项目和工作有帮助的内容进行展开;

2、简介

2.1、如何看待MISRA-C 2012

  MISRA-C 2012是属于第三版的MISRA-C,一开始是为了汽车行业而专门定的C语言编程规范(不包含代码风格),说白了就是以前从事汽车行业开发的大佬,在积累N年的C语言编程经验后得出的宝典,宝典中沉淀着他们实际项目中遇到的奇葩问题的预防措施,就是只要你按照宝典的要求去写代码,那么就会减少很多奇奇怪怪的问题,把这些问题扼杀在摇篮当中,而不是等问题出现后再这样去写代码
【MISRA-C 2012】浓缩版解读_第1张图片

  其实每一个嵌入式公司基本都会有内部的编程规范,这些规范更多是软件总监等级别的人编写的,像华为的编程规范,阿里巴巴的编程规范,但是这些商业的编程规范都比较倾向于他们所属的业务,具有领域属性,很多规定都是为业务而生,所以不必诧异为什么里面会规定这样的内容,因为每一条内容的背后都会有血的教训。但是MISRA-C在第二版之后就不具备领域属性了,而是面向所有领域,跨行业的应用,因此现在很多公司的C语言编程规范,多多少少都带着MISRA-C的内容在里面,因为MISRA-C道出了C语言编程本身的各种问题,而不是某个领域的问题;

  所以你可以当MISRA-C是一个字典工具,偶尔看一看,或者编程的时候突然想起有相关的就查一下,不必一直啃食,因为很多内容如果你没经历过,大概就是从左耳进,右耳就出去了,当你先过一遍,脑子里大概知道有提及哪些内容,然后实际真的遇到后,你自然会回来查阅的;

2.2、准则(guidelines)里面的指示(Directive)和规则(Rule)

  话不多说,MISRA-C原文档的各种总览、背景、工具等我都忽略过,直接进入主题,MISRA-C既然是一个编程规范,里面肯定是各种条例规定,理所当然最重要的内容叫准则(guidelines),可以理解为最顶层的规范,它由2部分组成:

  1. Directive翻译为指示,引导你需要做这个事情,但是这个事情没有判断对错的标准。实际项目是否符合规范是需要借助其它条件来证实的,单纯依赖MISAR-C是不能证实的。比如Dir 4.3提及汇编语言必须被封装并隔离,但是封装和隔离都是每一个项目根据实际情况来进行的,接口不统一、做法不统一,统一的只有这个思维,这就是Directive;

  2. Rule翻译为规则,明确代码就是要这样做,否则就违反标准。它不需要根据其它情况可以进行判定,比如Rule 15.1 说明不应该使用goto语句,你用了而且没有其它预防措施就是不符合MISRA-C规范,没有用或者用了但是有各种预防措施就符合这一条的规范,这就是Rule;

2.3、准则(guidelines)的级别(Category)

  其中每一条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;

3、若干重要的Directive和Rule

  Directive和Rule加起来会有170+条,每一条在官网文档种都有描述,但是我们这里只选择若干重点的内容来展开,这些都是养成良好编程习惯的重要规范,对工作和实际项目有好处无坏处;

3.1、指示(Directive)

Dir 2.1(必要) 所有的源文件编译过程不得有编译错误

小白理解:每一次编译成功后,都需要保持项目的 0 warning,0 error

  很多人会容忍warning的存在,但这是不对的,可能项目存在定义但未使用的代码然后触发warning,虽然无关紧要,但是太多的时候会把真正的warning掩盖掉,比如if (x = y)这种写法会触发warning,如果你的项目本来就有几十个warning的时候,那么多这1个也很难去看到,于是你就错过了编译器帮你排查BUG的机会,当出现问题的时候需要花费额外的时间去排查。因此永远遵守0 warning,0 error能让编译器帮你识别很多问题;

Dir 4.4(建议)不应该注释掉代码

小白理解:注释代码容易和真正的注释产生混淆,如果真的想暂时屏蔽一段代码,应该要明确标注出起点和终点,使用#if或者#ifdef起点然后#endif重点,但是我觉得最好的做法是删掉他们,利用git等代码管理工具来保存记录

  很多人在实际项目中暂时取消某个功能就是使用注释,因为注释一遍都是有快捷键的,很方便就能选中代码然后注释掉,但是由于真正的注释和这种行为的注释用法是一样的,会让其他人看这个代码产生混淆,它到底有没有作用,如果真的存在一个能符合语法的注释它真的是一个注释,而且混淆在这些屏蔽代码里面,那么程序员将很难分清代码行为。

void FuncA(void)
{
	uint8_t Cnt = 0;
	
	//符合Dir 4.4写法
#if 0
	Cnt++;
#endif

	//不符合Dir 4.4写法
//	Cnt++;
}

Dir 4.11(必要)检查传递给库函数参数值的有效性

小白理解:在调用库函数之前,必须先检查参数是否有效,再进行调用,而不是相信库函数会自己检查,原则上就是不相信其它库的接口在遇到非法数据时行为是正常的,都要假设是异常的。

  其实这种思维在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);
}

3.2、规则(Rule)

Rule 2.1(必要)不能含有不可达代码

小白理解:原因是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;
    }
}

Rule 2.2(必要)不能含有死代码

小白理解:把调试代码和取消的功能删干净再上传业务代码。

  意在提醒实际项目中调试或者删除功能时,要把代码弄干净,写了一个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.3~2.7(建议)不应该含有未使用的类型声明、标签、宏、形参

小白理解:我觉得这一堆跟Rule 2.2是一样的含义,没用的代码弄干净点,我推荐直接删掉,连#if和#ifdef都不要用,除非是必须的调试代码。

Rule 5.1~5.9(必要)宏、类型定义/声明、函数定义/声明、变量定义/声明都不得重名

小白理解:在 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 */

Rule 5.3(必要)内部作用域声明的标识符不得隐藏外部作用域声明的标识符

小白理解:函数内部的局部变量不要和全局变量重名

  特别是循环计数变量,习惯用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 */
}

Rule 7.2(必要)无符号整形常量都是用"u"或者"U"后缀

小白理解:不要相信编译器可以很好帮你处理常量类型

  当你的的写法不一样的时候,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 */
}

Rule 7.3(必要)小写字母"l"不能作为常量后缀

小白理解:纯属和数字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 */

Rule 7.4(必要)不能把字符串常量赋值给对象,除非对象类型为const char*

小白理解:道理很简单大家都懂,字符串常量不可被改变,但是写接口的时候很多会忽略,只能说带来隐患,写上就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);
}

Rule 8.4(必要)具备外部链接的标识符必须有显式可见的声明

小白理解:说白了就是调用文件以外的变量、函数、宏等都需要有显式的声明,而且变量和函数都必须带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 */
}

Rule 8.5(必要)具备外部链接的标识符只能在一个文件声明一次

小白理解:声明都放在.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 */

Rule 8.8(必要)具备内部链接的变量和函数都必须使用static修饰

小白理解:不给别人用的变量和函数,都加上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;
}

Rule 8.11(建议)具备外部链接的数组声明,应该显式指定长度

小白理解:不要用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 */

Rule 9.1(强制)具备自动储存周期的对象,在设定它的值之前不能读取

小白理解:局部变量定义时即初始化,不要偷懒

  这也是减少隐患的预防性编程,定义局部变量的时候顺手写一个赋值操作,这带来了确定性,天晓得下一个人会不会在局部变量还没赋值的时候使用,或者你不会,但下一个维护你代码的人会

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();
}

Rule 10.3(必要)表达式的值不得赋给更窄的基本类型,也不得赋给不同的基本类型类别

小白理解:运算类型始终保持一致性,多使用强制类型转换

  这是避免编译器的隐形转换导致数据截断或者精度问题,编译器不一定会按照你的想法进行隐式转换,自己强制转换能掌控全局

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 */

Rule 11.9(必要)宏NULL必须为整数类型空指针常量的唯⼀允许形式

小白理解:只能使用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)))
           */

Rule 12.1(建议)应明确表达式中操作数的优先级

小白理解:不要怜惜括号的使用,每一层计算都需要加

  预防性编程的一种,把结果掌控在程序员手里,而不是选择相信编译器,比如很多人喜欢这样写if ( a >= b && c <= d),这完全没问题,但是MISRA-C更加推荐你这样写,if ( (a >= b) && (c <= d)),给每一个运算都加上括号维护起来,这是100%不会出现符号优先级问题的

Rule 12.3(建议)不得使用逗号表达式

小白理解:很少人用,优先级和副作用问题多,干脆不要用

  逗号表达式最多出现在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 */
{
}

Rule 12.4(建议)对常量表达式进⾏求值不应导致整数回绕

小白理解:常量的加法不应该溢出,这个是很难人工检查出来的,一般依赖静态检查,或者编程的时候注重常量的加法

// 与 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;
}

Rule 15.4(建议)对于任何迭代语句,最多只应使⽤⼀个break或goto语句进⾏终⽌

小白理解:禁止!禁止!禁止!使用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.5(建议)函数结尾应只有⼀个退出点

小白理解:和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;
}

Rule 15.7(必要)所有if … else if构造都必须以⼀个else语句终⽌

小白理解:强制让你思考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 */
}

Rule 16.4(必要)每个switch语句都必须有default标记

小白理解:switch或许有你考虑不到的值,如果没有default,则switch会退出,你想debug的机会都难,多写一个default,里面加打印语句,100%不会错漏

switch (x)
{
    case 0:
        ++x;
        break;
    case 1:
    case 2:
        break;
        /* Non-compliant - default label is required */
}

Rule 17.2(必要)不得使用递归调用

小白理解:嵌入式的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;
}

Rule 17.8(建议)不应该修改函数的形参

小白理解:参数或许需要拿来判断原始值,留住备份总是一个好习惯

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 */
}

Rule 21.3~21.11(必要)不得使用下面标准库中的接口

小白理解:不要相信标准库能很好兼容你的芯片,直接不用是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    */
}

你可能感兴趣的:(#,MISRA-C,c语言,MISRA-C,MISRA,MISRA-C,2012,C,2012,MISRA,C)