MISRA C:2012 标准中文详解

MISRA C:2012 标准中文详解 

来源 https://www.cnblogs.com/xywml/p/18742061

前言

在汽车工业、航空航天、医疗设备等高安全性和高可靠性要求的嵌入式系统领域,软件的质量至关重要。代码中的细微缺陷都可能导致严重的后果。为了解决嵌入式 C 语言开发中常见的安全性和可靠性问题,汽车产业软件可靠性协会 (MISRA) 发布了 MISRA C 编码标准。

1. MISRA C 概述

MISRA C (Motor Industry Software Reliability Association C) 是汽车工业 C 编码标准的缩写,由 MISRA 协会发布。其目标是为嵌入式系统中的 C 语言开发提供一套严格的编码规范,旨在:

  • 提升代码可靠性 (Reliability): 减少因编码错误导致的程序缺陷,提高系统运行的稳定性。
  • 提升代码可读性 (Readability): 统一代码风格,使代码更易于理解和维护,降低维护成本。
  • 提升代码可移植性 (Portability): 减少对特定编译器或硬件平台的依赖,增强代码在不同环境下的适应性。
  • 提升代码可维护性 (Maintainability): 规范的代码结构和风格,降低代码维护和升级的难度。
  • 提升代码安全性 (Safety): 避免潜在的安全漏洞,保障系统运行的安全。

MISRA C:2012 是该标准的第三版,于 2012 年发布,是对之前版本的重要升级和完善。它整合了之前的 AMD1、TC1 修订内容,并持续进行更新以适应新的技术发展和安全挑战。虽然 MISRA C 标准并不能 100% 保证程序零缺陷,但它能显著降低因编程错误引入问题的风险,是提升嵌入式系统软件质量的有效手段。

2. MISRA C:2012 的规则体系

MISRA C:2012 标准并非简单的代码风格指南,而是一套严谨的规则体系,它包含 指令 (Directives) 和 规则 (Rules) 两大类。

2.1 指令 (Directives)

指令 (Directives) 主要关注代码的组织、构建和文档等方面,旨在建立良好的开发流程和代码管理规范。MISRA C:2012 标准中共包含 16 条指令,下面将逐一介绍:

  • Dir 1.1 实现定义行为文档化:

    • 要求: 应该用文档记录并了解程序输出依赖的任何实现定义行为。
    • 解释: C 语言标准中有一些行为是“实现定义”的 (implementation-defined),这意味着这些行为的具体实现由编译器或平台决定。不同的编译器或平台可能会对这些行为有不同的实现,导致代码的行为在不同环境下不一致。为了确保代码的可移植性和可预测性,应该将程序依赖的任何实现定义行为记录在文档中,并确保开发团队充分理解这些行为。
    • 示例:
      • 整数类型的大小 (int, long 等) 在不同平台上可能是不同的。
      • 有符号整数的右移操作可能是算术右移 (保留符号位) 或逻辑右移 (不保留符号位)。
      • 结构体成员的对齐方式可能因编译器和平台而异。
  • Dir 2.1 零编译错误:

    • 要求: 所有源文件必须没有任何编译错误。
    • 解释: 这是代码质量最基本的要求。存在编译错误的代码无法生成可执行程序,更谈不上功能和性能。在开发过程中,应该及时修复所有编译错误,确保代码始终处于可编译状态。
  • Dir 3.1 需求可追溯性:

    • 要求: 所有代码应该可以追溯至文件化的需求。
    • 解释: 可追溯性是指代码和需求之间建立清晰的对应关系。良好的可追溯性可以帮助开发人员理解代码的设计意图,验证代码是否满足需求,并在需求变更时快速定位受影响的代码。
    • 示例:
      • 可以在代码注释中注明该代码段对应的需求编号或需求描述。
      • 可以使用需求管理工具来维护代码和需求之间的映射关系。
  • Dir 4.1 运行时故障最小化:

    • 要求: 运行时故障必须最小化。
    • 解释: 运行时故障是指程序在运行过程中发生的错误,例如空指针解引用、数组越界、除零错误等。这些故障会导致程序崩溃或产生不可预测的结果。在代码设计阶段就应考虑各种可能的错误情况,并采取措施预防运行时故障的发生,例如进行输入验证、边界检查、错误处理等。
  • Dir 4.2 所有汇编语言的使用应当用文档记录

    • 要求: 建议所有汇编的使用应当用文档记录。
    • 解释: 虽然在某些情况下, 为了提高性能或访问底层硬件, 可能需要使用汇编语言。 但是, 汇编语言的可读性, 可维护性和可移植性都比较差。 因此,MISRA C建议详细记录汇编代码的用途, 功能和接口, 以便其他开发人员理解和维护。
  • Dir 4.3 Assembly language shall be encapsulated and isolated

    • 要求: 汇编语言必须封装、隔离。
    • 解释: 为了减少汇编语言对代码可维护性和可移植性的影响, 应该将汇编代码封装在独立的函数或模块中, 并与 C 代码隔离。 这样可以限制汇编代码的作用范围, 降低代码的复杂性, 并方便后续的维护和移植。
  • Dir 4.4 Sections of code should not be 'commented out'

    • 要求: 建议代码部分不应当注释掉。
    • 解释:
      • 不合规代码示例:

        /*
        x = y + z; // 这段代码暂时不需要
        */
        
      • 合规代码示例:

        #if 0
        x = y + z; // 使用预处理指令
        #endif
        
      解释: 不应该用// ...或者/* ... */ 注释代码。而应该用#ifdef ...#endif等预编译指令。
  • Dir 4.5 Identifiers in the same namespace with overlapping visibility should be typographically unambiguous

*   要求: 建议同一命名空间中, 具有重叠可见性的标识符, 必须在排版上毫不含糊。
*   解释: 类似于C++名称遮掩问题。例如, 全局变量不要与局部变量重名。
  • Dir 4.6 typedefs that indicate size and signedness should be used in place of the basic numerical types

    • 要求: 建议指示大小, 符号的typedefs(类型定义), 应当用来替代基本的数字类型。
    • 解释: 例如, typedef定义的uint32_t, 用来替代32位无符号整型。
  • Dir 4.7 If a function returns error information, then that error information shall be tested

    • 要求: 如果一个函数返回错误信息, 那么错误信息应答被测试。

    • 解释:许多库函数和自定义函数会通过返回值来指示错误状态。 调用这些函数后, 应该检查返回值, 并进行相应的错误处理。 忽略错误信息可能导致程序在错误状态下继续运行, 产生不可预测的结果。

    • 示例:

    FILE *fp = fopen("myfile.txt", "r");
    if (fp == NULL) {
        // 处理文件打开失败的情况
        perror("fopen failed");
        return -1;
    }
    
  • Dir 4.8 If a pointer to a structure or union is never dereferenced within a translation unit, then the implementation of the object should be hidden

    要求:建议如果一个指向struct或union的指针, 在翻译单元内从未被解引用, 则应该隐藏该对象的实现。

    • 非合规代码示例:

      // A.h
      struct A {
          uint32_t id;
          uint8_t name[32];
      };
      //test.c
      #include <A.h> // 不必要
      void foo(struct A *p){
      //没有解引用
      }
      
      
    • 合规代码示例:

      // A.h
      struct A {
          uint32_t id;
          uint8_t name[32];
      };
      
      // test.c
      // 正确用法:前向声明
      struct A;
      void foo(struct A *p){
          //没有解引用
      }
      

    解释: 如果结构体A实现如下,如果test.c中, 从未对指向A类型对象的指针进行解引用, 也就没有访问其成员。此时, 在test.c文件中应该使用前向声明, 而不应该使用"#include <A.h>"。

  • Dir 4.9 A function should be used in preference to a function-like macro where they are interchangeable

    • 要求: 建议在函数、函数类的宏可以相互替换的地方, 应优先使用函数。
    • 解释: 因为函数会在编译期做类型检查, 更安全。
  • Dir 4.10 Precautions shall be taken in order to prevent the contents of a header file being included more than once

    • 要求: 应当采取预防措施, 防止头文件的内容被多次包含。

    • 解释: 通常, 使用"#ifndef ... #endif"。也可以使用"#pragma once", 不过想要编译器支持。

    • 示例:

    // my_header.h
    #ifndef MY_HEADER_H
    #define MY_HEADER_H
    
    // 头文件内容
    
    #endif
    
  • Dir 4.11 The validity of values passed to library functions shall be checked

    • 要求: 传递给库函数的值的有效性, 应当被检查。

    • 解释: 因为一些库函数有严格的限制域, 需要检查:

      许多数学函数(<math.h>中的math函数), 如:
      (1) 负数不允许传递给sqrt, log函数。
      (2) fmod第二个参数不应为0。

      当非小写字母的参数传递给函数toupper时(类似有tolower), 一些实现能产生非预期结果。

      <ctype.h>中的字符测试函数, 在传递无效值时, 表现出未定义行为。如, isalnum, isalpha, islower等。

      给abs函数传递最值负数时, 表现出未定义行为。最小负数值转换成整数值, 由于位宽限制, 无法正确转换。

    • 示例:

    #include <math.h>
    #include <stdio.h>
    
    int main() {
      double x = -1.0;
      if (x >= 0.0) {
        double result = sqrt(x);
        printf("sqrt(x) = %f\n", result);
      } else {
        printf("Error: Cannot compute square root of a negative number.\n");
      }
      return 0;
    }
    
  • Dir 4.12 Dynamic memory allocation shall not be used

    • 要求: 不应该使用动态内存分配。
    • 解释: 例如, 不应该使用malloc/free进行动态内存分配。 动态内存分配(malloc, free等)可能导致内存泄漏, 碎片, 以及难以预测的程序行为。 在安全关键的嵌入式系统中, 应该避免使用动态内存分配。
  • Dir 4.13 Functions which are designed to provide operations on a resource should be called in an appropriate sequence

    • 要求: 建议设计用来提供操作资源的函数, 应当以合适的序列进行调用。
    • 解释: 例如, 某个硬件模块的操作, 应当遵循一定顺序, 以符合硬件资源特性。 例如, 对于文件操作, 应该按照打开 -> 读/写 -> 关闭的顺序进行。

2.2 规则 (Rules)

规则 (Rules) 则更侧重于 C 语言编码的具体细节,针对 C 语言的各种特性和潜在的陷阱,提出了详细的编码约束。

2.2.1 标准 C 环境 (Standard C environment)

  • Rule 1.1 (强制): 标准 C 语法和约束: 程序不得违反标准C语法和约束,不应超出实现的翻译限制。

    • 解释: 程序必须只使用C语言特性及其库, 除非使用语言扩展, 否则程序不应: 1) 包含任何违反本标准中描述的语言语法行为; 2) 包含任何违反本标准规定的限制行为。
    • 示例:
      • 语法行为:不支持写const变量。
      • 语言扩展:一些C90编译器提供__inline关键字声明inline函数。许多编译器支持使用一些关键字定义对象位置, 如__zpage, __near, __far。
  • Rule 1.2 (强制): 避免语言扩展

    • 非合规代码示例 (使用 GNU C 扩展):

      // 使用 GNU C 扩展的语句表达式
      int max(int a, int b) {
          return ({
              int _a = a;
              int _b = b;
              _a > _b ? _a : _b;
          });
      }
      
      // 使用 GNU C 扩展的零长度数组
      struct my_struct {
          int data_len;
          char data[]; // 零长度数组 (非标准 C)
      };
      
      // GNU C 扩展的 typeof
      int x = 10;
      typeof(x) y = 20; // y 的类型与 x 相同 (int)
      
      //GNU C 扩展, 指定初始化器
      int arr[10] = { [2] = 10, [5] = 20 };
      
      //GNU C扩展, case 范围
      switch (value) {
      case 1 ... 10: // 处理 1 到 10 之间的值
        // ...
        break;
      // ...
      }
      
    • 合规代码示例 (使用标准 C):

      // 标准 C 代码
      int max(int a, int b) {
          return (a > b) ? a : b;
      }
      
      // 使用指针和动态内存分配代替零长度数组(MISRA C不允许动态分配, 这里仅做标准C演示)
      struct my_struct {
          int data_len;
          char *data; // 指针
      };
      
      // 标准 C 没有 typeof,需要明确类型
      int x = 10;
      int y = 20;
      
      //标准C 初始化
      int arr[10] = {0, 0, 10, 0, 0, 20, 0, 0, 0, 0};
      
      //标准C switch 语句
      switch (value) {
          case 1:
          case 2:
          case 3:
          case 4:
          case 5:
          case 6:
          case 7:
          case 8:
          case 9:
          case 10:
            // ...
            break;
          // ...
      }
      
    • 规则解释及益处:
      语言扩展虽然在某些情况下可以提供便利或提高性能,但它们通常是特定于编译器的,会导致代码的可移植性降低。如果将代码移植到不支持这些扩展的编译器或平台,就需要进行大量的修改。遵循 Rule 1.2,坚持使用标准 C 特性,可以最大程度地保证代码的可移植性。

  • Rule 1.3 (强制): 避免未定义和未指定行为:

    • 要求: 不应出现未定义或关键的未指定行为。
    • 解释:
      • 未定义行为 (Undefined behavior): 指 C 语言标准没有规定其行为的操作。当程序执行到未定义行为时,可能会发生任何情况,包括程序崩溃、产生错误的结果、正常运行等。
      • 未指定行为 (Unspecified behavior): 指 C 语言标准规定了可能的行为,但具体选择哪种行为由编译器实现决定。未指定行为本身不一定是错误,但它可能导致代码的行为在不同编译器或平台下不一致。
      • 关键的未指定行为是指那些可能导致严重后果或安全漏洞的未指定行为。
    • 示例:
      • 未定义行为:
        • 数组越界访问。
        • 空指针解引用。
        • 除以零。
        • 有符号整数溢出。
        • 修改字符串字面量。
        • 在信号处理函数中访问 volatile 对象 (除非该对象是 volatile sig_atomic_t 类型)。
      • 未指定行为:
        • 函数参数的求值顺序。
        • 表达式中子表达式的求值顺序 (除非有明确的序列点)。
        • printf 函数中格式化字符串中 %s 对应的参数不是指向字符串的指针。

2.2.2 未使用代码 (Unused code)

  • Rule 2.1 (强制): 不可达代码

    • 非合规代码示例:

      void foo() {
          int a = 1;
          if (a > 0) {
              printf("a is positive\n");
          } else {
              // 这段代码永远不会被执行
              printf("a is non-positive\n");
          }
          return; //return 之后的语句永远不会被执行
          printf("Unreachable code\n");
      }
      
    • 合规代码示例:

      void foo() {
          int a = 1;
          if (a > 0) {
              printf("a is positive\n");
          }
      }
      
    • 规则解释及益处:
      不可达代码是指在程序执行过程中永远不会被执行到的代码。它通常是由于逻辑错误、条件判断错误或冗余代码导致的。不可达代码的存在会增加代码的复杂性,降低可读性,并可能掩盖潜在的逻辑错误。

  • Rule 2.2 (强制): 死代码

    • 非合规代码示例:

      void bar() {
          int x = 10;
          int y = 20;
          x = x + y; // x 的值被计算,但后续没有使用, 也不会对外部造成影响
          printf("y = %d\n", y);
      }
      
      int compute(){
         int result = 1;
         result * 2; //计算结果没有被使用, 也不会对外部造成影响
         return result;
      }
      
    • 合规代码示例:

      void bar() {
          int y = 20;
          printf("y = %d\n", y);
      }
      int compute(){
         int result = 1;
         return result * 2;
      }
      
    • 规则解释及益处:
      死代码是指被执行但其结果不会对程序的输出或后续计算产生任何影响的代码。它可能是未使用的变量赋值、未使用的表达式计算等。死代码的存在会浪费计算资源,增加代码的复杂性,并可能干扰代码优化。

  • Rule 2.3 (建议): A project should not contain unused type declarations

    • 要求: 建议项目不应包含未使用类型声明。

    • 解释: 如果一个类型定义但未使用, 审核人不清楚该类型是否冗余, 或者遗留的未使用错误。

    • 非合规代码示例:

      typedef struct {
        int x;
        int y;
      } Point; // Point 类型未被使用
      
      void foo() {
          // ...
      }
      
    • 合规代码示例:

      // 删除未使用的类型声明, 或者在后续的代码中使用它
      void foo() {
          // ...
      }
      
  • Rule 2.4 (建议): A project should not contain unused tag declarations

    • 要求: 建议项目不应包含未使用标记声明。

    • 非合规代码示例:

      struct MyStruct; // 声明了标记 MyStruct,但从未定义或使用
      
      void foo() {
          // ...
      }
      
    • 合规代码示例:

      // 删除未使用的标记声明,或者定义并使用它
      void foo() {
          // ...
      }
      
  • Rule 2.5 (建议): A project should not contain unused macro declarations

    • 要求: 建议项目不应包含未使用宏定义声明。

    • 非合规代码示例:

      #define MAX_VALUE 100 // 定义了宏 MAX_VALUE,但从未使用
      
      void foo() {
          // ...
      }
      
    • 合规代码示例:

      // 删除未使用的宏定义,或者在后续的代码中使用它
        void foo() {
            // ...
        }
      
  • Rule 2.6 (建议): A function should not contain unused label declarations

    • 要求: 建议函数不应包含未使用标签声明。

    • 非合规代码示例:

      void foo() {
          int x = 10;
      start: // 定义了标签 start,但从未被 goto 语句引用
          x++;
      }
      
    • 合规代码示例:

      // 删除未使用的标签,或者添加 goto 语句引用它
      void foo() {
         int x = 10;
         x++;
      }
      
  • Rule 2.7 (建议): There should be no unused parameters in functions

    • 要求: 建议函数中, 不应有未使用参数。

    • 解释: 如果确实定义了未使用的参数, 为了确保函数兼容性, 可以形参名省略。

    • 非合规代码示例:

      void foo(int x, int y) { // 参数 y 未被使用
          printf("x = %d\n", x);
      }
      
    • 合规代码示例:

      void foo(int x, int /*y*/) { // 省略未使用参数的名称, 使用注释说明
          printf("x = %d\n", x);
      }
      // 或者, 如果 y 参数在未来可能会被使用, 可以添加 (void)y; 语句
      void foo(int x, int y) {
           (void)y; // 明确表示 y 参数未被使用
           printf("x = %d\n", x);
       }
      

2.2.3 注释 (Comments)

  • Rule 3.1 (强制): The character sequences /* and // shall not be used within a comment

    • 要求: 字符序列 /* 和 // 不应在注释中使用。

    • 解释: 在注释中嵌套使用注释符号可能导致注释提前结束, 或产生意外的注释效果。

    • 非合规代码示例:

      /*
       * This is a comment.
       * // This is a nested comment, which is not allowed.
       */
      
      /* This is a comment /* with a nested comment */ , which is not allowed. */
      
    • 合规代码示例:

      /*
       * This is a comment.
       *  This is NOT a nested comment.
       */
      
  • Rule 3.2 (强制): Line-splicing shall not be used in // comments

    • 要求: 不得在注释//中使用续行符()。

    • 解释: 在//注释中使用续行符可能导致意外的代码被注释掉, 降低代码的可读性。

    • 非合规代码示例:

      // This is a comment \
      int x = 10; // 这行代码会被意外注释掉
      
    • 合规代码示例:

      // This is a comment.
      int x = 10;
      

2.2.4 字符集和词汇约定 (Character sets and lexical conventions)

  • Rule 4.1 (强制): Octal and hexadecimal escape sequences shall be terminated

    • 要求: 8进制和16进制高级转义序列应终止。

    • 解释: 如果8进制或16进制转义序列后跟其他转义序列, 可能出现混淆。如, 字符常量\x1f由1个字符组成(表示ASCII 16进制值为1f的单元分隔符US), 而\x1g由2个字符\x1(表示ASCII 16进制值为1的标题开始SOH)和g组成。

    • 非合规代码示例:

      const char *str = "\x1F"; // 16 进制转义序列未终止
      printf("%s\n", str);     // 输出结果可能不符合预期
      
    • 合规代码示例:

      const char *str = "\x1F\0"; // 使用空字符终止 16 进制转义序列
      // 或者
      const char *str = "\x1F" ""; // 使用空字符串字面量终止
      // 或者
      const char *str = "\x1F\x67"; // 使用另一个转义序列终止
      printf("%s\n", str);
      
    • 规则解释及益处:
      如果 16 进制或 8 进制转义序列后面紧跟其他字符,可能会导致编译器错误地解析转义序列,产生意外的字符值。为了避免这种混淆,应该使用明确的方式终止转义序列,例如使用空字符 \0 或空字符串字面量 "",或者另一个转义序列。

  • Rule 4.2 (建议): Trigraphs should not be used

    • 要求: 建议三字母词不应使用。

    • 解释: 三字母词(Trigraphs)由两个问号的序列, 跟着一个特殊的第三字符。例如, ??- 表示 ~(波浪线), ??) 表示 ]。它们可能与两个问号的其他用法混淆。 比如, 字符串"(Date should be in the form ??-??-??)"会被编译器解释为"(Date should be in the form ~~]"。 Trigraphs会在预处理阶段被替换。

    • 非合规代码示例:

      const char *date = "Date: ??-??-????"; // 使用了三字母词
      
    • 合规代码示例:

      const char *date = "Date: --/--/----"; //避免使用三字母词
      

2.2.5 标识符 (Identifiers)

  • Rule 5.1 (强制): External identifiers shall be distinct

    • 要求: 外部标识符应该不同。

    • 解释: 外部标识符是指具有外部链接的标识符, 例如全局变量和函数名。 不同的外部标识符应该具有不同的名称, 以避免链接时的冲突。

    • 非合规代码示例:

      // file1.c
      int my_global_variable;
      
      // file2.c
      int my_global_variable; // 与 file1.c 中的标识符冲突
      
    • 合规代码示例:

      // file1.c
      int my_global_variable_file1;
      
      // file2.c
      int my_global_variable_file2; // 使用不同的名称
      
  • Rule 5.2 (强制): Identifiers declared in the same scope and name space shall be distinct

    • 要求: 声明在同一作用域和命名空间的标识符, 应该是不同的。

    • 解释: 在同一个作用域和命名空间中, 不应该有重名的标识符。 这条规则适用于所有标识符, 包括变量名, 函数名, 类型名, 标签名等。

    • 非合规代码示例:

      void foo() {
          int x = 10;
          float x = 20.0f; // 与前一个 x 重名
      }
      
    • 合规代码示例:

      void foo() {
          int x_int = 10;
          float x_float = 20.0f; // 使用不同的名称
      }
      
  • Rule 5.3 (强制): An identifier declared in an inner scope shall not hide an identifier declared in an outer scope

    • 要求: 在内部作用域中声明的标识符, 不应隐藏外部作用域中声明的标识符。

    • 非合规代码示例:

      int count = 10; // 全局变量
      
      void my_function() {
          int count = 20; // 局部变量,与全局变量重名,隐藏了全局变量
          printf("count = %d\n", count); // 输出 20 (局部变量的值)
      }
      
    • 合规代码示例:

      int global_count = 10; // 全局变量使用不同的名称
      
      void my_function() {
          int local_count = 20;
          printf("local_count = %d\n", local_count);
          printf("global_count = %d\n", global_count);
      }
      
    • 规则解释及益处:
      在内部作用域 (例如函数内部) 使用与外部作用域 (例如全局变量) 相同的标识符名称会导致名称遮蔽 (name hiding)。在内部作用域中,访问该名称将引用内部作用域的变量,而不是外部作用域的变量。这可能会导致代码的逻辑错误和混淆。为了避免这种情况,应确保不同作用域的标识符具有不同的名称。

  • Rule 5.4 (强制): Macro identifiers shall be distinct

    • 要求: 宏定义标识符应该不同。

    • 解释: 不同的宏定义应该使用不同的名称, 以避免宏展开时的冲突和意外替换。

    • 非合规代码示例:

      #define MAX_SIZE 100
      #define MAX_SIZE 200 // 重复定义
      
    • 合规代码示例:

      #define MAX_SIZE_1 100
      #define MAX_SIZE_2 200 // 使用不同的名称
      
  • Rule 5.5 (强制): Identifiers shall be distinct from macro names

    • 要求: 标识符应与宏定义名称不同。

    • 解释: 标识符(变量名, 函数名, 类型名等)不应该与宏定义的名称相同, 以避免宏展开时发生意外替换。

    • 非合规代码示例:

      #define SIZE 10
      int SIZE = 20; // 变量名与宏名相同
      
    • 合规代码示例:

      #define ARRAY_SIZE 10
      int size = 20; // 使用不同的名称
      
  • Rule 5.6 (强制): A typedef name shall be a unique identifier

    • 要求: typedef类型名应该是独有的标识符。

    • 解释: typedef 定义的类型名应该与其他标识符(变量名, 函数名, 宏名, 其他类型名等)不同, 以避免命名冲突。

    • 非合规代码示例:

      int MyType;
      typedef int MyType; // 类型名与变量名冲突
      
    • 合规代码示例:

      int my_variable;
      typedef int MyType; // 使用不同的名称
      
  • Rule 5.7 (强制): A tag name shall be a unique identifier

    • 要求: 标记名应该是独有的标识符。

    • 解释: 标记名是指struct, union, enum的名称。 标记名应该与其他标识符(变量名, 函数名, 宏名, 类型名等)不同, 以避免命名冲突。

    • 非合规代码示例:

      int Point;
      struct Point { // 结构体标记名与变量名冲突
          int x;
          int y;
      };
      
    • 合规代码示例:

      int my_variable;
      struct Point { // 使用不同的名称
          int x;
          int y;
      };
      
  • Rule 5.8 (强制): Identifiers that define objects or functions with external linkage shall be unique

    • 要求: 使用外部链定义对象或函数的标识符应该唯一。

    • 解释: 不同源文件, 不要存在标识符重名的情况。

    • 非合规代码示例:

      // file1.c
      int my_global_variable;
      void foo() {}
      
      // file2.c
      int my_global_variable; // 与 file1.c 中的变量重名
      void foo() {} // 与 file1.c 中的函数重名
      
    • 合规代码示例:

      // file1.c
      int my_global_variable_file1;
      void foo_file1() {}
      
      // file2.c
      int my_global_variable_file2; // 使用不同的名称
      void foo_file2() {} // 使用不同的名称
      
  • Rule 5.9 (建议): Identifiers that define objects or functions with internal linkage should be unique

    • 要求: 建议使用内部链接定义的对象或函数应该唯一。

    • 解释: 同一源文件内,具有内部链接的标识符(例如使用 static 关键字声明的变量和函数)的名称应是唯一的,即使它们的作用域不同。虽然 C 语言允许在不同作用域中重复使用具有内部链接的标识符名称,但这会降低代码的可读性和可维护性,并可能导致混淆。

    • 非合规代码示例:

      // file1.c
      static int my_static_variable;
      static void foo() {}
      
      void bar() {
          static int my_static_variable; // 与全局的 my_static_variable 重名
          static void foo() {} // 与全局的 foo 重名
      }
      
    • 合规代码示例:

      // file1.c
      static int my_static_variable_global;
      static void foo_global() {}
      
      void bar() {
          static int my_static_variable_local; // 使用不同的名称
          static void foo_local() {} // 使用不同的名称
      }
      

2.2.6 类型 (types)

  • Rule 6.1 (强制): Bit-fields shall only be declared with an appropriate type

    • 要求: 位域只能用适当的类型声明。

    • 解释:

      • 对于 C90,适当的位域类型:unsigned int 或 signed int。
      • 对于 C99,适当的位域类型:
        • unsigned int 或 signed int;
        • 实现所允许的另一个显式有符号或显式无符号整型;
        • _Bool。
      • 注意:允许使用 typedef 来指定适当的类型。
    • 非合规代码示例 (C90):

      struct Flags {
          unsigned short is_enabled : 1; // 不符合 C90 规范
          enum Status status : 2;       // 不符合 C90 规范
          char data : 4;              // 不符合 C90 规范
      };
      
    • 合规代码示例 (C90):

      struct Flags {
          unsigned int is_enabled : 1;
          unsigned int status : 2;
          unsigned int data : 4;
      };
      // 或者使用 signed int (如果需要表示负数)
      struct Flagsw {
          signed int is_negative : 1;
      };
      
    • 非合规代码示例 (C99):

      //C99允许, 但MISRA C:2012限制了类型
      struct Flags {
          unsigned short is_enabled : 1; // 不符合 MISRA C:2012 规范
          enum Status status : 2;       // 不符合 MISRA C:2012 规范
          char data : 4;              // 不符合 MISRA C:2012 规范
      };
      
    • 合规代码示例 (C99):

        struct Flags {
          unsigned int is_enabled : 1;  // 合规
          unsigned int status : 2;       // 合规
          unsigned int data : 4;       // 合规
        };
        // 或者使用 _Bool
        struct Flagsw {
          _Bool is_valid : 1; // 合规
        }
      
  • Rule 6.2 (强制): Single-bit named bit fields shall not be of a signed type

    • 要求: 单比特命名位字段不应为带符号类型。

    • 解释: 单比特有符号位域的行为是实现定义的。在某些编译器上,它可能只能表示 0 和 -1,而在另一些编译器上,它可能只能表示 0 和 1。为了避免这种不确定性,单比特位域应使用无符号类型 (unsigned int 或 _Bool)。
      注意:该规则不适用于匿名位域。

    • 非合规代码示例:

      struct Data {
          signed int flag : 1; // 单比特有符号位域
      };
      
    • 合规代码示例:

      struct Data {
          unsigned int flag : 1; // 单比特无符号位域
      };
      // 或者使用 _Bool (C99)
      struct Data2 {
        _Bool flag : 1;
      }
      

2.2.7 文字和常量 (Literals and constants)

  • Rule 7.1 (强制): Octal constants shall not be used

    • 要求: 不应使用8进制常数。

    • 解释: 因为很容易跟10进制常数混淆,比如52(10进制),052(8进制,对应10进制42)。

    • 非合规代码示例:

      int x = 052; // 八进制常量 (等于十进制的 42)
      int y = 010; // 八进制常量 (等于十进制的 8)
      
    • 合规代码示例:

      int x = 42;  // 使用十进制
      int y = 8;   // 使用十进制
      
    • 规则解释及益处:
      八进制常量以数字 0 开头, 容易和十进制数混淆。

  • Rule 7.2 (强制): A "u" or "U" suffix shall be applied to all integer constants that are represented in an unsigned type

    • 要求: "u"或"U"后缀应该应用到整型常量,代表无符号类型。

    • 非合规代码示例:

      unsigned int value = 10; // 没有 'u' 或 'U' 后缀
      long unsigned val2 = 1000; //没有 'u' 或 'U' 后缀
      
    • 合规代码示例:

      unsigned int value = 10u; // 使用 'u' 后缀明确表示无符号类型
      // 或者
      unsigned int value = 10U; // 使用 'U' 后缀
      long unsigned val2 = 1000U;
      
    • 规则解释及益处:
      在 C 语言中,整数常量可以有多种类型 ( int, long, unsigned int, unsigned long 等)。如果不显式指定类型,编译器会根据常量的值和上下文进行类型推导。对于无符号整数常量,如果不添加 u 或 U 后缀,编译器可能会将其推导为有符号类型,导致潜在的类型不匹配问题。添加 u 或 U 后缀可以明确地告诉编译器该常量是无符号类型,避免类型推导错误。

  • Rule 7.3 (强制): The lowercase character 'l' shall not be used in a literal suffix

    • 要求: 小写字母'l'不应用于文字后缀。

    • 解释: 因为很容易跟数字'1'混淆。

    • 非合规代码示例:

      long int value = 123l; // 使用了小写字母 'l' 作为后缀
      
    • 合规代码示例:

      long int value = 123L; // 使用大写字母 'L' 作为后缀
      
  • Rule 7.4 (强制): A string literal shall not be assigned to an object unless the object's type is “pointer to const-qualified char”

    • 要求: 字符串文字不应赋值给对象,除非对象的类型是“指向常量限定字符的指针”。

    • 解释: 因为字符串文字是常量,传递给non-const对象的话,用户修改内容可能会导致异常。

    • 非合规代码示例:

      char *str = "hello"; // 字符串字面量赋值给 char*
      str[0] = 'H';       // 尝试修改字符串字面量,可能导致未定义行为
      
    • 合规代码示例:

      const char *str = "hello"; // 字符串字面量赋值给 const char*
      // str[0] = 'H'; // 编译错误,不能修改 const char* 指向的内容
      
      //如果需要修改, 则应该使用字符数组
      char str_arr[] = "hello";
      str_arr[0] = 'H'; //ok
      
    • 规则解释及益处:
      字符串字面量(例如 "hello")在 C 语言中是存储在只读内存区域的字符数组。 将其赋值给 char* 类型的指针,意味着可以通过指针修改该字符串的内容。 但是,尝试修改字符串字面量会导致未定义行为(undefined behavior), 可能会导致程序崩溃或产生不可预测的结果。
      将字符串字面量赋值给 const char* 类型的指针可以避免这个问题。 const 关键字表示指针指向的内容是常量,不能通过指针修改。 如果尝试通过 const char* 指针修改字符串字面量,编译器会产生错误,从而在编译阶段就发现问题。

2.2.8 声明和定义 (Declarations and definitions)

  • Rule 8.1 (强制): Types shall be explicitly specified

    • 要求: 类型应明确指定。

    • 解释: 因为C90标准允许在特定情形下, 省略类型, 此时, int类型为隐式指定。可能使用隐式int的情况示例如下:

      • 对象声明;
      • 参数声明;
      • 成员声明;
      • typedef 声明;
      • 函数返回类型。
    • 非合规代码示例:

      extern x;       // 隐式声明为 int 类型
      const y = 10; // 隐式声明为 int 类型
      
      f(void);     //隐式声明返回int
      
      struct W {
          const m; //隐式int
      }
      
    • 合规代码示例:

      extern int x;
      const int y = 10;
      
      int f(void);
      
      struct W {
          const int m;
      }
      
    • 规则解释及益处:
      在 C90 标准中,如果在声明中省略了类型说明符,编译器会默认将其视为 int 类型。这种隐式 int 规则容易导致代码的类型不明确,降低可读性,并可能引入类型相关的错误。为了避免这种情况,MISRA C:2012 要求所有声明都必须显式指定类型。

  • Rule 8.2 (强制): Function types shall be in prototype form with named parameters

    • 要求: 函数类型应该为带命名参数的原型。

    • 解释: 为了避免混淆, 或者不一致。

    • 非合规代码示例:

      int func();       // 没有参数列表
      void process(int); // 没有参数名称
      
    • 合规代码示例:

      int func(void); // 使用 void 表示没有参数
      void process(int value); // 使用命名参数
      
  • Rule 8.3 (强制): All declarations of an object or function shall use the same names and type qualifiers

    • 要求: 所有对象或函数的声明应使用相同的名字或类型限定符。

    • 解释: 相同基本类型的兼容版本可以互换, 如int, signed, signed int 都是等价的。而const, non-const则不可以互换。

    • 非合规代码示例:

      extern int x;
      extern const int x; // 类型限定符不一致
      extern void foo(int a);
      extern void foo(int b); // 参数名称不一致
      
    • 合规代码示例:

      extern int x;
      extern int x; // 类型限定符一致
      
      extern void foo(int a);
      extern void foo(int a); // 参数名称一致
      
  • Rule 8.4 (强制): A compatible declaration shall be visible when an object or function with external linkage is defined

    • 要求: 当对象或函数由外部链接定义时, 兼容声明应当是可见的。

    • 解释: 定义外部对象或函数时, 有一个定义, 必须对应一个声明。

    • 非合规代码示例:

      // 没有提前声明
      int global_var = 10; // 定义
      
      void foo() {
          // 没有提前声明
          extern void bar(); // 使用
          bar();
      }
      
      // 没有提前声明
      void bar() {} // 定义
      
    • 合规代码示例:

      // file.h
       extern int global_var; // 声明
       extern void bar(void); //声明
      
       //file.c
       #include "file.h"
       int global_var = 10; // 定义, 声明可见
      
       void foo() {
      
           bar();    //bar的声明在之前, 可见
       }
      
       void bar() {} // 定义
      
  • Rule 8.5 (强制): An external object or function shall be declared once in one and only one file

    • 要求: 外部对象或函数应在一个且仅在一个文件中声明。

    • 解释: 也就是说, 每个外部对象或函数, 应当仅在一个.h文件中声明一次。

    • 非合规代码示例:

      // header1.h
      extern int global_var; // 声明
      
      // header2.h
      extern int global_var; // 重复声明
      
    • 合规代码示例:

      // my_global.h
      extern int global_var; // 只在一个头文件中声明
      
      // file1.c
      #include "my_global.h"
      
      // file2.c
      #include "my_global.h"
      
  • Rule 8.6 (强制): An identifier with external linkage shall have exactly one external definition

    • 要求: 外部链接的标识符应有一个外部定义。

    • 解释: 包含2点: 1) 标识符必须有定义;2) 定义只能有一个。

    • 非合规代码示例:

      // file1.c
      extern int global_var; // 声明,没有定义
      // file2.c 没有定义
      
      // 或者
      // file1.c
      int global_var;
      
      // file2.c
      int global_var; // 重复定义
      
    • 合规代码示例:

      // my_global.h
      extern int global_var; // 声明
      
      // my_global.c
      int global_var = 10; // 定义,且只有一处
      
  • Rule 8.7 (建议): Functions and objects should not be defined with external linkage if they are referenced in only one translation unit

    • 要求: 如果函数和对象仅在一个翻译单元中引用,则不应由外部链接定义。

    • 解释: 这是为了限制对象可见性, 减少不同翻译单元之间的耦合。

    • 非合规代码示例:

      // file1.c
      int helper_function(int x) { // 外部链接,但仅在 file1.c 中使用
          return x * 2;
      }
      
      void public_function() {
          int result = helper_function(10);
          // ...
      }
      
    • 合规代码示例:

      // file1.c
      static int helper_function(int x) { // 使用 static 关键字限制为内部链接
      return x * 2;
      }
      
      void public_function() {
          int result = helper_function(10);
          // ...
      }
      
  • Rule 8.8 (强制): The static storage class specifier shall be used in all declarations of objects and functions that have internal linkage

    • 要求: 静态存储类说明符 应该用于所有内部链接的对象和函数的声明。

    • 解释: 对于只在当前翻译单元中使用的对象和函数, 应该使用static关键字声明, 明确表示其内部链接。

    • 非合规代码示例:

      // file1.c
      int helper_variable; // 没有使用 static,默认为外部链接
      
      void helper_function(int x) { // 没有使用 static,默认为外部链接
          // ...
      }
      
    • 合规代码示例:

      // file1.c
      static int helper_variable; // 使用 static
      
      static void helper_function(int x) { // 使用 static
          // ...
      }
      
  • Rule 8.9 (建议): An object should be defined at block scope if its identifier only appears in a single function

    • 要求: 如果对象的标识符仅出现在单个函数中,则应在块范围内定义该对象。

    • 解释: 减少对象的可见性, 提高代码的可维护性。 如果一个变量只在一个函数内部使用, 应该将其定义为局部变量, 而不是全局变量或文件作用域的静态变量。

    • 非合规代码示例:

      int temp; // 全局变量,但只在 process_data 函数中使用
      
      void process_data(int data) {
          temp = data * 2;
          // ...
      }
      
    • 合规代码示例:

      void process_data(int data) {
          int temp = data * 2; // 定义为局部变量
          // ...
      }
      
  • Rule 8.10 (强制): An inline function shall be declared with the static storage class

    • 要求: 内联函数应与静态存储类一同声明。

    • 解释: 内联函数声明为外部链接, 但没有在同一翻译单元内定义, 会导致未定义行为。 调用外部链接的内联函数, 可能调用外部函数的定义, 或者使用内联定义, 这会影响执行速度。 注意: 可通过内联函数置于将头文件, 使得内联函数在多个翻译单元内可用。

    • 非合规代码示例:

      // my_header.h
      inline int square(int x) { // 没有使用 static
          return x * x;
      }
      
    • 合规代码示例:

      // my_header.h
      static inline int square(int x) { // 使用 static
          return x * x;
      }
      
  • Rule 8.11 (建议): When an array with external linkage is declared, its size should be explicitly specified

    • 要求: 当外部链接的数组声明时,它的尺寸应明确指定。

    • 解释: 该规则仅应用于非定义的声明。声明中明确数组尺寸, 便于检查一致性, 以及边界检查。

    • 非合规代码示例:

      extern int arr[]; // 没有指定数组大小
      
    • 合规代码示例:

      extern int arr[10]; // 明确指定数组大小
      
  • Rule 8.12 (强制): Within an enumerator list, the value of an implicitly specified enumeration constant shall be unique

    • 要求: 在枚举列表中, 隐式指定枚举常量值是唯一的。

    • 解释: 未指定值的枚举成员默认递增, 注意不能与其他成员同值。

    • 非合规代码示例:

      enum Color {
        RED,        // 默认为 0
        GREEN,      // 默认为 1
        BLUE = 1,   // 显式指定为 1,与 GREEN 重复
        YELLOW      // 默认为 2
      };
      
    • 合规代码示例:

      enum Color {
        RED,        // 默认为 0
        GREEN,      // 默认为 1
        BLUE,       // 默认为 2
        YELLOW      // 默认为 3
      };
      
      // 或者显式指定所有值
      enum Color2 {
           RED = 1,
           GREEN = 2,
           BLUE = 4,
           YELLOW = 8
      }
      
  • Rule 8.13 (建议): A pointer should point to a const-qualified type whenever possible

    • 要求: 指针应尽可能指向常量限定类型。

    • 解释: 如果一个函数不需要修改指针指向的数据, 应该将指针参数声明为指向const的指针。 这可以防止意外修改数据, 提高代码的安全性, 并且可以提高代码的可读性(明确表明函数不会修改数据)。

    • 非合规代码示例:

      void print_string(char *str) { // 没有使用 const
          printf("%s\n", str);
      }
      
    • 合规代码示例:

      void print_string(const char *str) { // 使用 const
          printf("%s\n", str);
      }
      
  • Rule 8.14 (强制): The restrict type qualifier shall not be used

    • 要求: restrict类型限定符不应使用。
    • 解释: C中restrict关键字用于告诉编译器, 对象已经被指针所引用, 不能通过除该指针外所有其他直接或间接的防晒修改该对象的内容。当使用restrict类型限定符时, 可能会改善由编译器生成的代码效率。但使用restrict同时, 也要求程序员必须确保两个指针所指内存区域没有重叠。MISRA C:2012出于安全考虑,禁止使用。

2.2.9 初始化 (Initialization)

  • Rule 9.1 (强制): The value of an object with automatic storage duration shall not be read before it has been set

    • 强制: 自动变量在设置之前不允许读取。

    • 非合规代码示例:

      void func() {
          int a; // 未初始化
          int b = a; // 读取未初始化的值
      }
      
      int foo(){
         int x;
         if (x) { //x未初始化
         }
      }
      
    • 合规代码示例:

      void func() {
          int a = 0; // 初始化
          int b = a;
      }
      
      int foo(){
         int x = 0;
         if (x) {
         }
      }
      
    • 规则解释及益处:
      读取未初始化的值会导致未定义行为, 结果不可预测。

  • Rule 9.2 (强制): The initializer for an aggregate or union shall be enclosed in braces

    • 要求: 聚合体或联合体的初值应该包含在大括号中。

    • 解释: 聚合体是指数组(array)或类(class)或结构体(struct)。 注意:{ 0 }形式的初始化器,可以设置所有值为0,而无需嵌套括号。

    • 非合规代码示例:

      int arr[3] = {1, 2, 3}; // 对于简单数组,可以省略最外层大括号, 但不建议
      struct point {
          int x;
          int y;
      } p = {1, 2}; // 对于结构体,也可以省略最外层大括号, 但不建议
      
      //多维数组
      int matrix[2][2] = {1, 2, 3, 4}; //不建议
      
    • 合规代码示例:

      int arr[3] = {1, 2, 3}; // 保持一致性
      struct point {
          int x;
          int y;
      } p = { {1, 2} }; // 使用嵌套大括号, 或者 {1, 2}
      // 对于所有元素初始化为 0 的情况, 可以用 {0}
      int arr2[5] = {0};
      
      //多维数组
      int matrix[2][2] = { {1, 2}, {3, 4} }; //建议
      
    • 规则解释及益处:
      聚合类型(数组和结构体)初始化, 使用大括号可以提高代码可读性, 避免歧义, 尤其是在嵌套聚合类型的情况下。

  • Rule 9.3 (强制): Arrays shall not be partially initialized

    • 要求: 数组不应部分初始化。

    • 解释: 必须为每个数组元素设定初值。

    • 非合规代码示例:

      int arr[5] = {1, 2}; // 只初始化了前两个元素,其余元素隐式初始化为 0, 不建议
      
    • 合规代码示例:

       int arr[5] = {1, 2, 0, 0, 0}; // 显式初始化所有元素
       // 或者使用 {0} 初始化所有元素为 0
       int arr2[5] = {0};
      
  • Rule 9.4 (强制): An element of an object shall not be initialised more than once

    • 要求: 对象的元素不应初始化超过一次。

    • 非合规代码示例:

      int arr[3] = { [0] = 1, [0] = 2, [1] = 3 }; // 重复初始化 arr[0]
      
    • 合规代码示例:

        int arr[3] = { [0] = 1, [1] = 3, [2] = 0 };
      
  • Rule 9.5 (强制): Where designated initialisers are used to initialize an array object the size of the array shall be specified explicitly

    • 要求: 如果指定的初始化器用于初始化数组对象,那么应明确指定数组的大小。

    • 非合规代码示例:

      int arr[] = { [0] = 1, [2] = 3 }; // 没有显式指定数组大小
      
    • 合规代码示例:

      int arr[5] = { [0] = 1, [2] = 3 }; // 显式指定数组大小
      

2.2.10 基本类型模型 (The essential type model)

  • Rule 10.1 (强制): Operands shall not be of an inappropriate essential type

    • 要求: 操作数不应具有不恰当的基本类型。

    • 解释: 算术操作数的基本类型类别, 总结如下表:

      Opeartor	Operand	Boolean	character	enum	signed	unsigned	floating
      []	integer	3	4			1
      ++		3	4	5
      --		3	4	5			8
      + -	either	3
      * /	either	3	4	5
      %	either	3	4	5		1
      < > <= >=	either
      == !=	either
      ! && !!	any	2	2	2	2	2	2
      << >>	left	3	4	5,6	6		1
      << >>	right	3	4	7	7		1
      ~ & ! ^	any	3	4	5,6	6		1
      ?:	1st		2	2	2	2	2
      ?:	2nd and 3rd
      

      上面的数字对应下面原理编号:

      1. 对这些操作数使用floating类型,是违反约束的。
      2. 本质上Boolean类型的表达式,应该用于操作数解释为Boolean值的地方。
      3. 本质上Boolean类型的操作数,不应用于操作数解释为数值的地方。
      4. 本质上character类型的操作数,不应用于操作数解释为数值的地方。字符数据的数值是由实现定义的。
      5. 本质上enum类型的操作数,不应用于算术操作,因为enum对象使用整型定义实现。涉及枚举对象的操作,可能产生意外类型的结果。注意匿名枚举中的枚举常量,本质上具有带符号类型。
      6. 移位和逐位操作仅对本质上无符号的操作数执行。本质上有符号类型的使用产生的数值,是由定义实现的。
      7. 移位运算的右侧操作符,应该是本质上无符号类型,以确保负移位不会刀子划未定义行为。
      8. 本质上有符号类型的操作数,不应用于一元减号运算符的操作数,因为结果的符号性由实现的int大小决定。
    • 示例:

      //不符合规则的
      float arr[10];
      int index = 1.1f; //不符合, 浮点数不能做下标
      arr[index] = 0;
      
      bool flag = 1; //不符合, 布尔类型不能做算术运算
      int x = flag + 1;
      
      char c = 'A';
      int y = c + 1; //不符合, 字符类型不应作为数值
      
      //符合规则的
      float arr[10];
      int index = 1;
      arr[index] = 0;
      
      bool flag = true;
      int x = flag ? 1 : 0; //使用条件运算符
      
      char c = 'A'; //字符类型
      
  • Rule 10.2 (建议): Expressions of essentially character type shall not be used inappropriately in addition and subtraction operations

    • 要求: 本质上为字符类型的表达式,不应用在不正确的加法和减法运算中。

    • 解释: 当数据不代表数值时,带字符类型的表达式不能用于算术运算。然而有些情况是允许字符数据的运算的。
      例如:
      1)2个字符类型的操作数的减法,可用于在数字'0'到'9'和相应的序数之间转换。
      2)字符类型和无符号类型的加法,可能用于序数到相应数字范围'0'至'9的转换。
      3)从字符类型到无符号类型底减法,可能用于小写转大写。

    • 非合规代码示例:

      char c = 'a';
      int x = c + 10;   // 不符合:字符类型与整数相加
      int y = c - '0'; //可以使用, 但是要保证c是数字字符
      
    • 合规代码示例:

      char c = '5';
      if(c >= '0' && c <= '9') { //确保是数字字符
          int x = c - '0'; // 可以用于数字字符和数值之间的转换
      }
      
  • Rule 10.3 (强制): The value of an expression shall not be assigned to an object with a narrower essential type or of a different essential type category

    • 要求: 表达式的值不应赋值给本质类型较窄 或 不同的基本类型 的对象。

    • 解释: 宽数字赋值给窄数字,会发生截断。

    • 非合规代码示例:

      uint32_t large_value = 0xFFFFFFFF;
      uint16_t small_value = large_value; // 数据截断,small_value 的值不等于 large_value
      
      float f = 3.14f;
      int i = f; // 浮点数赋值给整数,丢失小数部分
      
    • 合规代码示例:

      uint32_t large_value = 0xFFFFFFFF;
      uint16_t small_value;
      
      if (large_value <= 0xFFFF) {
          small_value = (uint16_t)large_value; //显式转换
      } else {
          // 处理溢出情况
      }
      
      float f = 3.14f;
      int i = (int)f; //显式转换
      
    • 规则解释及益处:
      将较宽类型(如 uint32_t)的值赋值给较窄类型(如 uint16_t)的变量时,可能会发生数据截断,导致信息丢失。为了避免这种情况,在赋值之前应该进行检查,确保值不会超出较窄类型的范围, 或者进行显式类型转换。

  • Rule 10.4 (强制): Both operands of an operator in which the usual arithmetic conversions are performed shall have the same essential type category

    • 要求: 通常的算术转换时的运算符的2个操作数,应具有相同的基本类型类别。

    • 解释:
      C 语言的算术运算符在对不同类型的操作数进行运算时,会进行 “寻常算术转换” (usual arithmetic conversions)。 转换的目的是将两个操作数转换为同一类型,然后再进行运算。 MISRA C 要求进行算术运算的两个操作数应属于同一基本类型类别(例如,都是有符号整数、都是无符号整数或都是浮点数)。这条规则的目的是避免隐式类型转换可能导致的意外结果和精度损失。

    • 非合规代码示例:

      unsigned int u = 10u;
      int s = -5;
      long result = u + s; // 不符合:unsigned int 和 int 属于不同类别
      
    • 合规代码示例:

      unsigned int u = 10u;
      int s = -5;
      
      // 将两个操作数显式转换为同一类别 (例如,都转换为 unsigned int)
      unsigned long result = (unsigned long)u + (unsigned long)s; //不推荐, 可能会有负数问题
      // 或者都转换为int
      long result2 = (long)s + (long)u; //如果能保证u可以被int表示
      
      // 更安全的做法是确保两个操作数在运算前就具有相同的类型
      unsigned int u = 10u;
      unsigned int s = 5u; // 如果 s 必须表示负数,则需要重新设计
      unsigned int result = u + s;
      
    • 规则解释及益处:
      不同类型类别的操作数进行算术运算时,C 语言会进行隐式类型转换。这种隐式转换可能不是程序员所期望的,并可能导致意外的结果或精度损失。例如,将 unsigned int 和 int 相加时,int 会被转换为 unsigned int,如果 int 为负数,则转换后的值会是一个很大的正数。

  • Rule 10.5 (建议): The value of an expression should not be cast to an inappropriate essential type

    • 要求: 建议表达式的值不应转型为不恰当的本质类型。

    • 解释: 强制类型转换应该谨慎使用,避免将表达式的结果强制转换为不恰当的类型。 “不恰当” 的类型转换包括:

      • 将指针类型转换为任何其他类型(除了 void* 或从 void* 转换)。
      • 将浮点类型转换为任何其他类型(除了整数类型,并且要确保没有溢出或精度损失)。
      • 将复数类型转换为任何其他类型。
      • 将枚举类型转换为任何其他类型(除了整数类型,并且要确保没有溢出)。
      • 将 _Bool 类型转换为任何其他类型(除了整数类型)。
      • 将一个本质类型(非typedef)转换为具有不同基本类型的typedef类型
    • 非合规代码示例:

      float x = 3.14f;
      char *p = (char *)x;     // 不恰当:浮点数转换为指针
      
      int arr[5];
      float f = (float)arr;     //不恰当: 数组转换为浮点数
      
      typedef signed int sint_t;
      unsigned int u = 10u;
      sint_t s = (sint_t)u; // 不恰当,unsigned int 转换为 signed int
      
    • 合规代码示例:

      float x = 3.14f;
      int i = (int)x;     // 浮点数转换为整数(确保不会溢出)
      
      typedef unsigned int uint_t;
      unsigned int u = 10u;
      uint_t u2 = (uint_t)u; // 合规, 本质类型相同
      
  • Rule 10.6 (强制): The value of a composite expression shall not be assigned to an object with wider essential type

    • 要求: 复合表达式的值不应赋值给具有更宽基本类型的对象。

    • 解释: 复合表达式是由多个操作数和运算符组成的表达式。这条规则的目的是避免在计算过程中进行不必要的类型提升,然后在赋值时又进行类型转换。

    • 非合规示例:

      uint8_t a = 10u;
      uint8_t b = 20u;
      uint16_t c = a + b; // a + b 是复合表达式,结果为 uint8_t,然后隐式转换为 uint16_t
      
    • 合规代码示例:

      uint8_t a = 10u;
      uint8_t b = 20u;
      // 显式地将复合表达式的结果转换为目标类型
      uint16_t c = (uint16_t)(a + b);
      
      // 或者,如果能保证结果不会溢出,也可以将 a 和 b 转换为更宽的类型
      uint16_t a16 = a;
      uint16_t b16 = b;
      uint16_t c = a16 + b16;
      
    • 规则解释及益处:
      可以让运算过程提升数值宽度,但不要在赋值时提升。

  • Rule 10.7 (强制): If a composite expression is used as one operand of an operator in which the usual arithmetic conversions are performed then the other operand shall not have wider essential type

    • 要求: 如果一个复合表达式被用作执行常用算术转换的运算符的一个操作数,另一个运算数不应具有更宽的本质类型。

    • 解释: 这条规则与 Rule 10.6 相辅相成,目的是控制复合表达式的类型提升。它要求在进行算术运算时,如果一个操作数是复合表达式,那么另一个操作数的类型不能比复合表达式的结果类型更宽。

    • 非合规代码示例:

      uint8_t a = 10u;
      uint8_t b = 20u;
      uint32_t c = 30u;
      uint32_t result = (a + b) * c; // (a + b) 是复合表达式,结果为 uint8_t,c 为 uint32_t
      
    • 合规代码示例:

      uint8_t a = 10u;
      uint8_t b = 20u;
      uint32_t c = 30u;
      
      // 显式地将复合表达式的结果转换为更宽的类型
      uint32_t result = (uint32_t)(a + b) * c;
      
      // 或者,将较窄类型的操作数提升为更宽的类型
      uint32_t a32 = a;
      uint32_t b32 = b;
      uint32_t result = (a32 + b32) * c;
      
  • Rule 10.8 (强制): The value of a composite expression shall not be cast to a different essential type category or a wider essential type

    • 要求: 复合表达式的值不应转型为不同本质类型类别或一更宽的本质类型。

    • 解释: 类似于 Rule 10.6 和 10.7, 目的是避免不必要的类型转换。

    • 非合规代码示例:

      uint8_t a = 10u;
      uint8_t b = 20u;
      uint32_t result = (uint32_t)(a + b); //不建议
      
      int8_t x = 1;
      int8_t y = 2;
      float z = (float)(x + y); //不建议
      
    • 合规代码示例:

      uint8_t a = 10u;
      uint8_t b = 20u;
      uint8_t result = a + b; //如果能确保不会溢出
      
      int8_t x = 1;
      int8_t y = 2;
      int8_t z = x + y; //如果能确保类型匹配
      

2.2.11 指针类型转换 (Pointer type conversions)

  • Rule 11.1 (强制): Conversions shall not be performed between a pointer to a function and any other type

    • 要求: 转换不应在函数指针和其他类型指针之间进行。

    • 解释: 函数指针和其他类型指针(如 void、int 等)之间的转换是未定义行为,可能导致程序崩溃或产生不可预测的结果。

    • 非合规代码示例:

      void foo() {}
      
      void (*func_ptr)() = foo;
      int *int_ptr = (int *)func_ptr; // 不允许:函数指针转换为 int*
      void *void_ptr = (void*)func_ptr; //不允许
      
      typedef void (*FuncPtr)(void);
      FuncPtr my_func = (FuncPtr)0x12345678; // 不允许:整数转换为函数指针
      
    • 合规代码示例:

      void foo() {}
      
      void (*func_ptr)() = foo; //正确
      
  • Rule 11.2 (强制): Conversions shall not be performed between a pointer to incomplete and any other type

    • 要求: 转换不应在不完整类型和其他类型指针之间进行。

    • 解释: 不完整类型指:

      • void 类型
      • 未知大小数组
      • 具有不完整类型元素的数组
      • 未定义的结构体,联合体,或枚举(只有声明)
      • 指向已声明但未定义的类的指针
      • 声明但未定义的类
    • 异常情况:

      • null 指针常量能转换为指向不完整类型的指针。
      • 指向不完整类型的指针,能转换为 void。
    • 非合规代码示例:

      struct S; // 前向声明(不完整类型)
      struct S *s_ptr;
      
      int *int_ptr = (int *)s_ptr; // 不允许:不完整类型指针转换为 int*
      //void *可以转为任意类型指针
      
    • 合规代码示例:

        struct S; // 前向声明(不完整类型)
        struct S *s_ptr;
        void *void_ptr = (void*)s_ptr;//可以, 但是不推荐
      
  • Rule 11.3 (强制): A cast shall not be performed between a pointer to object type and a pointer to a different object type

    • 要求: 转型不应在指向不同对象类型的指针之间进行。

    • 解释: 指向不同对象类型的指针可能具有不同的大小和对齐要求。强制转换这些指针可能导致未定义行为,例如访问越界或未对齐的内存。

    • 非合规代码示例:

      int x = 10;
      int *int_ptr = &x;
      char *char_ptr = (char *)int_ptr; // 不允许:int* 转换为 char*
      float *float_ptr = (float*)&x;    //不允许
      
    • 合规代码示例:
      如果需要访问不同类型的数据, 应该使用memcpy进行内存拷贝, 或者使用union:

      // 使用 memcpy
      int x = 10;
      char char_arr[sizeof(int)];
      memcpy(char_arr, &x, sizeof(int));
      
      // 使用 union
      union {
          int i;
          float f;
      } data;
      
      data.i = 10;
      float f = data.f; //避免使用union
      
    • 规则解释及益处:
      不同对象类型指针的转换可能导致地址不对齐问题。例如, uint32_t 类型通常要求 4 字节对齐, 而uint8_t 类型是 1 字节对齐。如果将uint8_t 数组的地址强制转换为uint32_t, 然后通过uint32_t 指针访问内存, 可能会导致未定义行为(在某些架构上, 未对齐的访问会导致硬件异常)。

  • Rule 11.4 (建议): A conversion should not be performed between a pointer to object and an integer type

    • 要求: 建议转型不应在指向对象的指针和整型之间进行。

    • 解释: 指针和整数之间的转换是实现定义的,可能导致未定义行为或信息丢失。指针的大小和表示方式可能与整数类型不同。

    • 非合规代码示例:

      int arr[5];
      int *ptr = &arr[0];
      uintptr_t addr = (uintptr_t)ptr; // 不建议:指针转换为整数
      int *ptr2 = (int *)addr;       // 不建议:整数转换回指针
      
    • 合规代码示例:
      通常, 避免指针和整数之间的转换。如果需要存储指针的数值表示, 可以使用uintptr_t或intptr_t类型(C99), 但仍然需要谨慎使用, 确保转换后的值在目标平台上有效。

  • Rule 11.5 (建议): A conversion should not be performed from pointer to void into pointer to object

    • 要求: 建议不应将指向void的指针转型为指向对象的指针。

    • 解释: 反过来可以: 能将指向对象的指针转型为void指针。 将 void* 转换为对象指针需要显式类型转换,这可能会掩盖类型错误。 应该尽量避免这种转换,或者在转换时进行额外的检查。

    • 非合规代码示例:

      void *void_ptr;
      int *int_ptr = (int *)void_ptr; // 不建议:void* 转换为 int*
      
    • 合规代码示例:

      //避免 void* 到具体类型的转换
      
  • Rule 11.6 (强制): A cast shall not be performed between pointer to void and an arithmetic type

    • 要求: 转型不应在void指针和算术类型之间进行。

    • 解释: void* 和算术类型之间的转换没有意义, 并且可能导致未定义行为。

    • 非合规代码示例:

      void *void_ptr;
      int x = (int)void_ptr;       // 不允许
      float f = (float)void_ptr;    //不允许
      
  • Rule 11.7 (强制): A cast shall not be performed between pointer to object and a non-integer arithmetic type

    • 要求: 转型不应在指向对象的指针和非整型算术类型之间进行。

    • 解释: 指向对象的指针和非整型算术类型(如float, double等)之间的转换没有意义, 并且可能导致未定义行为。

    • 非合规代码示例:

      int arr[5];
      int *ptr = &arr[0];
      float f = (float)ptr; // 不允许
      
  • Rule 11.8 (强制): A cast shall not remove any const or volatile qualification from the type pointed to by a pointer

    • 要求: 转型不应移除来自指针指向的类型的任何const 或 volatile限定符。

    • 解释: const 和 volatile 限定符用于保证程序的正确性和安全性。 移除这些限定符可能导致意外修改只读数据或错误地优化掉对 volatile 变量的访问。

    • 非合规代码示例:

      const int x = 10;
      const int *const_ptr = &x;
      int *non_const_ptr = (int *)const_ptr; // 不允许:移除 const 限定符
      
      volatile int y;
      volatile int *volatile_ptr = &y;
      int *non_volatile_ptr = (int *)volatile_ptr; // 不允许:移除 volatile 限定符
      
    • 合规代码示例:

      const int x = 10;
      const int *const_ptr = &x;
      const int *another_const_ptr = const_ptr; // 正确:保留 const 限定符
      
      volatile int y;
      volatile int *volatile_ptr = &y;
      volatile int *another_volatile_ptr = volatile_ptr; // 正确:保留 volatile 限定符
      
  • Rule 11.9 (强制): The macro NULL shall be the only permitted form of integer null pointer constant

    • 要求: NULL宏是唯一允许的整型空指针常量的形式。

    • 解释: 在 C 语言中,空指针常量可以表示为整数 0 或 (void*)0。为了代码的清晰和一致性,MISRA C 规定只能使用 NULL 宏来表示空指针常量。

    • 非合规代码示例:

      int *ptr = 0; // 不允许:使用整数 0 表示空指针
      int *ptr2 = (void*)0; //不允许
      
    • 合规代码示例:

      #include <stddef.h> // 包含 NULL 的定义
      
      int *ptr = NULL; // 正确:使用 NULL 宏
      

2.2.12 表达式 (Expressions)

  • Rule 12.1 (建议): The precedence of operators within expressions should be made explicit

    • 要求: 应明确表达式中运算符的优先级。

    • 解释: 虽然C语言有明确的运算符优先级规则, 但是过度依赖这些规则会让代码难以阅读和理解。使用括号可以消除歧义, 提高可读性。

    • 非合规代码示例:

      int x = a + b * c; // 依赖运算符优先级,可读性较差
      int y = a << b + 1; //不清晰
      
    • 合规代码示例:

      int x = a + (b * c); // 使用括号明确优先级,可读性更好
      int y = a << (b + 1); //更清晰
      
    • 规则解释及益处:
      虽然C语言有明确的运算符优先级规则, 但是过度依赖这些规则会让代码难以阅读和理解。使用括号可以消除歧义, 提高可读性。

  • Rule 12.2 (强制): The right hand operand of a shift operator shall lie in the range zero to one less than the width in bits of the essential type of the left hand operand

    • 要求: 移位运算符的右手操作数应在0~1之间, 比左操作数的基本类型的位宽小。

    • 解释: 右移操作数必须在合法范围内, 否则会导致未定义行为。

    • 非合规代码示例:

      uint8_t x = 10u;
      uint8_t y = x >> 8; // 不符合:右移位数大于等于左操作数位宽
      uint8_t z = x << -1; //不允许, 负数
      
    • 合规代码示例:

      uint8_t x = 10u;
      uint8_t y = x >> 7; // 右移位数小于左操作数位宽
      
  • Rule 12.3 (建议): The comma operator should not be used

    • 要求: 建议逗号操作符不应使用。

    • 解释: 逗号运算符允许在单个表达式中执行多个操作,但它会降低代码的可读性,并可能导致意外的副作用。

    • 非合规代码示例:

      int x = 10;
      int y = 20;
      int z = (x++, y++); // 使用逗号运算符,不建议
      
    • 合规代码示例:

      int x = 10;
      int y = 20;
      x++; // 将逗号运算符分隔的操作分开写
      y++;
      int z = y;
      
  • Rule 12.4 (建议): Evaluation of constant expressions should not lead to unsigned integer wrap-around

    • 要求: 建议常量表达式的值计算不应导致无符号整型环绕(溢出)。

    • 解释: 无符号整型表达式没有严格的的溢出, 取而代之的是环绕(wrap-around)。

    • 非合规代码示例:

      #define MAX_U8 255u
      uint8_t x = MAX_U8 + 1u; // 发生环绕, 结果为 0, 不建议
      
    • 合规代码示例:

      //避免在常量表达式中导致无符号整数环绕
      //如果需要, 使用更大范围的类型, 或者添加运行时检查
      

2.2.13 副作用 (Side effects)

  • Rule 13.1 (强制): Initialiser lists shall not contain persistent side effects

    • 要求: 初始化列表不应包含持久副作用。

    • 解释: C90限制聚合类型的初始化器仅包含常量。但C99允许初值包含运行时计算的表达式, 也允许作为匿名初始化对象的复合文字。初始化器列表中表达式求值过程中, 副作用发生的顺序锁未指定的, 因此, 如果这些副作用持续存在, 那么初始化的行为不可预测。

    • 非合规代码示例:

      int x = 0;
      int arr[3] = { x++, x++, x++ }; // 不允许:初始化列表中包含副作用
      
      int y = 0;
      int foo(){
          y++;
          return y;
      }
      int arr2[3] = { foo(), 2, 3}; //不允许, 副作用
      
    • 合规代码示例:

      int x = 0;
      int arr[3] = { 0, 1, 2 }; // 避免在初始化列表中使用副作用
      
      // 或者, 如果确实需要使用变量初始化, 则分开赋值
      int arr2[3];
      arr2[0] = x++;
      arr2[1] = x++;
      arr2[2] = x++;
      
  • Rule 13.2 (强制): The value of an expression and its persistent side effects shall be the same under all permitted evaluation orders

    • 要求: 表达式的值和它的持久副作用, 应在所有允许的计算顺序情况下相同。

    • 解释: C 语言标准并没有严格规定表达式中子表达式的求值顺序(除非有明确的序列点)。 这意味着编译器可以自由选择不同的求值顺序。 如果表达式中包含副作用(例如修改变量的值), 并且这些副作用会影响表达式的最终结果, 那么不同的求值顺序可能会导致不同的结果。

    • 非合规代码示例:

      int x = 0;
      int y = x++ + x++; // 结果依赖于 x++ 的求值顺序,可能导致未定义行为
      
      int foo(int a, int b) {
          return a + b;
      }
      int z = 0;
      int result = foo(z++, z++); //不确定哪个参数先计算
      
    • 合规代码示例:

      int x = 0;
      int y = x + 1;
      x += 2; // 将副作用操作分开
      
      int foo(int a, int b) {
        return a + b;
      }
      
      int z = 0;
      int result = foo(z, z + 1); //避免副作用
      z += 2;
      
  • Rule 13.3 (建议): A full expression containing an increment (++) or decrement (--) operator should have no other potential side effects other than that caused by the increment or decrement operator

    • 要求: 建议。包含自增或自减运算符的完整表达式,除了由自增或自减运算符引起的副作用外,不应有其他潜在的副作用。

    • 非合规代码示例:

      int arr[5] = {1, 2, 3, 4, 5};
      int i = 0;
      int x = arr[i++] + arr[i]; // 结果依赖于 i++ 的求值顺序,可能导致未定义行为
      
      extern int foo(int);
      int y = 0;
      int z = foo(y++) + y; //不建议
      
    • 合规代码示例:

      int arr[5] = {1, 2, 3, 4, 5};
      int i = 0;
      int x = arr[i];
      i++;
      x += arr[i]; // 将自增/自减操作与其他操作分离
      
      extern int foo(int);
      int y = 0;
      int z = foo(y); //先调用函数, 再自增
      y++;
      z = z + y;
      
    • 规则解释及益处:
      在一个表达式中多次使用自增/自减运算符, 或者将自增/自减运算符与其他运算符混合使用, 可能会导致代码的行为难以预测, 甚至导致未定义行为。 这是因为 C 语言标准并没有严格规定表达式中不同部分的求值顺序。 为了避免这种问题, 应该将自增/自减操作与其他操作分离, 使代码的求值顺序更加清晰。

  • Rule 13.4 (建议): The result of an assignment operator should not be used

    • 要求: 建议赋值运算符的结果不应使用。

    • 解释: 使用赋值表达式的结果会让代码难以阅读, 并且容易引入错误(例如, 将=和==混淆)。

    • 非合规代码示例:

      int x, y;
      if ((x = y) != 0) { // 不建议:使用赋值表达式的结果
          // ...
      }
      
    • 合规代码示例:

      int x, y;
      x = y; // 将赋值操作和判断操作分开
      if (x != 0) {
          // ...
      }
      
  • Rule 13.5 (强制): The right hand operand of a logical && or || operator shall not contain persistent side effects

    • 要求: 逻辑运算符 && 或 || 运算符不应包含持久副作用。

    • 解释: C 语言的逻辑运算符 && 和 || 具有短路求值特性。

      • 对于 &&,如果左操作数为 false,则不会计算右操作数。
      • 对于 ||,如果左操作数为 true,则不会计算右操作数。
        如果 && 或 || 的右操作数包含副作用,那么这些副作用可能不会发生,导致代码的行为不确定。
    • 非合规代码示例:

      int x = 0;
      if (is_valid() && (x++ > 0)) { // 不允许:右操作数包含副作用
          // ...
      }
      
    • 合规代码示例:

      int x = 0;
      if (is_valid()) {
          if (x++ > 0) { // 将副作用操作移到 if 语句内部
              // ...
          }
      }
      //或者
      bool valid = is_valid();
      int temp = x;
      x++;
      if(valid && (temp > 0)) {
      }
      
  • Rule 13.6 (强制): The operand of the sizeof operator shall not contain any expression which has potential side effects

    • 强制: sizeof运算符的操作数不应包含任何存在潜在副作用的表达式。

    • 解释: sizeof 运算符在编译时求值,其操作数不会被执行。如果在 sizeof 的操作数中包含带有副作用的表达式,这些副作用将不会发生,可能会导致代码逻辑错误。

    • 非合规代码示例:

      int x = 10;
      size_t size = sizeof(x++); // 不允许:sizeof 操作数包含副作用
      
    • 合规代码示例:

      int x = 10;
      size_t size = sizeof(x); // 正确:sizeof 操作数不包含副作用
      

2.2.14 控制状态表达式 (Control statement expressions)

  • Rule 14.1 (建议): A loop counter shall not have essentially floating type

    • 要求: 循环计数器不应有实质上的浮点类型。

    • 解释: 浮点数存在精度问题, 使用浮点数作为循环计数器可能导致循环次数不准确。

    • 非合规代码示例:

      for (float f = 0.0f; f < 10.0f; f += 0.1f) { // 不建议:使用浮点数作为循环计数器
          // ...
      }
      
    • 合规代码示例:

      for (int i = 0; i < 100; i++) { // 使用整数作为循环计数器
          float f = i / 10.0f;
          // ...
      }
      
  • Rule 14.2 (强制): A for loop shall be well-formed

    • 要求: 一个循环应有良好的结构。

    • 解释: MISRA C 对 for 循环的结构有具体要求,主要包括:

      • 循环应该只有一个循环计数器。
      • 循环计数器应该在 for 循环的初始化部分进行初始化。
      • 循环计数器应该在 for 循环的更新部分进行更新(递增或递减)。
      • 循环计数器的类型应该是整型或枚举类型。
      • 循环条件应该只涉及循环计数器和循环不变量的比较。
      • 循环体内不能修改循环计数器,也不能使用 goto 语句跳转到循环外部或从循环外部跳转到循环内部。
    • 非合规代码示例:

      for (int i = 0; i < 10; ) {
          // 在循环体内修改循环控制变量
          if (some_condition) {
              i += 2;
          } else {
              i++;
          }
      }
      
      // 多个循环控制变量
      for(int i = 0, j = 0; i < 10; i++, j++){
      }
      
      //修改循环不变量
      int limit = 10;
      for(int i = 0; i < limit; i++) {
         limit--;
      }
      
    • 合规代码示例:

      for (int i = 0; i < 10; i++) {
          if (some_condition) {
              // 通过其他方式处理特殊情况,不直接修改循环控制变量
              // ...
          }
      }
      
    • 规则解释及益处:
      在for循环体内修改循环控制变量, 会让代码难以理解和维护。for循环的控制部分 (初始化, 条件, 更新) 应该清晰地描述循环的逻辑。 如果需要在循环体内修改循环控制变量, 应该考虑使用while循环, 或者将修改逻辑移到循环控制部分。

  • Rule 14.3 (强制): Controlling expressions shall not be invariant

    • 要求: 控制表达式不应是不变的。

    • 解释: 该规则适用于:
      if/while/for/do..while/switch的条件;
      ?:运算符的第一个操作数。
      控制表达式不应该始终为 true 或始终为 false。如果控制表达式始终为 true,则循环可能永远不会结束(死循环),或者 if 语句的某个分支永远不会被执行。如果控制表达式始终为 false,则循环体永远不会被执行,或者 if 语句的某个分支永远不会被执行。
      例外情况:

      • 无限循环。
      • do..while 循环的控制表达式允许为0。
    • 非合规代码示例:

      if (true) { // 控制表达式始终为 true
          // ...
      }
      
      while (1) { // 控制表达式始终为 true (除非是故意设计的无限循环)
          // ...
      }
      
      for ( ; 0; ) { //不应该出现
      }
      
    • 合规代码示例:

      int x = get_value();
      if (x > 0) { // 控制表达式的值取决于变量 x
          // ...
      }
      
      while (check_condition()) { // 控制表达式的值取决于函数 check_condition() 的返回值
          // ...
      }
      
  • Rule 14.4 (强制): The controlling expression of an if statement and the controlling expression of an iteration-statement shall have essentially Boolean type

    • 要求: if语句的控制表达式和迭代语句的控制表达式, 应具有本质上布尔类型。

    • 解释: if 语句和迭代语句(for、while、do-while)的控制表达式应该具有本质上的布尔类型。这意味着表达式的结果应该是 true 或 false,或者可以隐式转换为 bool 类型 (C99)。在 C90 中,虽然没有 bool 类型,但控制表达式的结果应该被视为逻辑真或假(非零值为真,零值为假)。为了代码的清晰和可移植性,MISRA C 建议控制表达式应显式地与 0 进行比较,或者使用逻辑运算符 (!, &&, ||) 将其转换为布尔值。

    • 非合规代码示例:

      int x = 10;
      if (x) { // 不建议:使用整数作为控制表达式, 应该显式与 0 比较
          // ...
      }
      
      float f = 3.14f;
      while (f) { // 不建议:使用浮点数作为控制表达式
          // ...
      }
      
      int *ptr = get_pointer();
      if(ptr) { //不建议, 应该显式与 NULL 比较
      }
      
    • 合规代码示例:

      int x = 10;
      if (x != 0) { // 显式地将整数与 0 进行比较
          // ...
      }
      
      float f = 3.14f;
      while (f > 0.0f) { // 显式地将浮点数与 0.0f 进行比较, 或者定义一个误差范围
        //
      }
      
      int *ptr = get_pointer();
      if(ptr != NULL) { // 显式地与 NULL 进行比较
      }
      
      // 或者, 定义 bool 类型 (C99)
      #include <stdbool.h> // 引入 bool 类型
      
      bool is_valid = (x != 0);
      if (is_valid) {
          // ...
      }
      

2.2.15 控制流 (Control flow)

  • Rule 15.1 (建议): The goto statement should not be used

    • 要求: 建议goto语句不应使用。

    • 解释: goto 语句会破坏代码的结构化, 让代码的控制流难以追踪, 降低代码的可读性和可维护性。 在大多数情况下, 可以使用if-else, while, for, break, continue等结构化控制语句来代替goto。

    • 非合规代码示例:

      void process_data(int data) {
          if (data < 0) {
              goto error_handling; // 使用 goto 跳转
          }
          // ... 处理正常数据 ...
          return;
      
      error_handling:
          // ... 处理错误 ...
          return;
      }
      
    • 合规代码示例:

      void process_data(int data) {
          if (data < 0) {
              // ... 处理错误 ...
          } else {
              // ... 处理正常数据 ...
          }
      }
      
    • 规则解释及益处:
      goto语句会破坏代码的结构化, 让代码的控制流难以追踪, 降低代码的可读性和可维护性。 在大多数情况下, 可以使用if-else, while, for, break, continue等结构化控制语句来代替goto。

  • Rule 15.2 (强制): The goto statement shall jump to a label declared later in the same function

    • 要求: goto语句应跳转到同一函数内、goto语句后声明的标签。

    • 解释: 这条规则限制了 goto 语句只能向后跳转,不能向前跳转。 向前跳转的 goto 语句会形成循环,可能导致死循环或难以理解的代码逻辑。

    • 非合规代码示例:

      void foo() {
          int x = 0;
      loop_start: // 向前跳转的标签
          x++;
          if (x < 10) {
              goto loop_start; // 不允许:向前跳转
          }
      }
      
    • 合规代码示例:

      void foo() {
          int x = 0;
      
      start:
          if (x < 10) {
            //
            x++;
            goto start; //不允许, 向前跳转了
          }
      
          if (x > 10) {
             goto end;
          }
          x = 0;
      end:
          return;
      }
      //应该避免使用goto, 上述例子只是为了演示规则
      
  • Rule 15.3 (强制): Any label referenced by a goto statement shall be declared in the same block, or in any block enclosing the goto statement

    • 要求: goto语句引用的任何标签,应在相同块中声明,或者任何块中声明围住goto语句。

    • 解释: 这条规则限制了 goto 语句只能在同一个代码块内跳转,或者跳转到外层代码块。不允许 goto 语句跳转到其他代码块或内层代码块。

    • 非合规代码示例:

      void foo() {
          int x = 0;
          if (x > 0) {
              goto label1; // 不允许:跳转到其他代码块
          }
          if (x < 0) {
             label1: // label1 不在同一个 block
              x = 0;
          }
      
          while (x < 10) {
             if (x == 5) {
                goto label2; // 不允许:跳转到外层代码块
             }
          }
      label2:
          x = 0;
      }
      
    • 合规示例:
      避免使用goto.

  • Rule 15.4 (建议): There should be no more than one break or goto statement used to terminate any iteration statement

    • 要求: 建议不应有超过1个break或goto语句终止任意迭代语句。

    • 解释: 在循环语句( for, while, do-while)中, 应该只使用一个break或goto语句来提前终止循环。 使用多个break或goto语句会使循环的控制流变得复杂, 降低代码的可读性。

    • 非合规代码示例:

      void foo() {
          int x = 0;
          while (x < 10) {
              if (x == 5) {
                  break; // 第一个 break
              }
              if (x == 8) {
                  break; // 第二个 break,不允许
              }
              x++;
          }
      }
      
    • 合规代码示例:

      void foo() {
          int x = 0;
          bool done = false;
          while (x < 10 && !done) {
              if (x == 5) {
                  done = true; // 使用标志变量控制循环
              } else if (x == 8) {
                  done = true; // 使用标志变量控制循环
              } else {
                  x++;
              }
          }
      }
      
  • Rule 15.5 (建议): A function should have a single point of exit at the end

    • 要求: 建议函数应在末尾有单一退出点。

    • 解释: 意思是说, 每个函数只能有一个return语句。

    • 非合规代码示例:

      int process_data(int data) {
          if (data < 0) {
              return -1; // 第一个返回点
          }
          // ... 处理数据 ...
          return result; // 第二个返回点
      }
      
    • 合规代码示例:

      int process_data(int data) {
          int result = 0; // 初始化返回值
          if (data < 0) {
              result = -1; // 设置返回值
          } else {
              // ... 处理数据 ...
          }
          return result; // 单一返回点
      }
      
    • 规则解释及益处:
      函数应在末尾有单一退出点。

  • Rule 15.6 (强制): The body of an iteration-statement or a selection-statement shall be a compound statement

    • 要求: 迭代语句或选择语句的本体应为复合语句。

    • 解释: 复合语句称为语句块, 使用大括号包裹。该条规则意思是循环语句和if语句本体, 需要使用{}包裹。除了一种特殊情况: if语句后马上跟着else。

    • 非合规代码示例:

      if (x > 0)
          printf("x is positive\n"); // 单条语句,没有使用大括号
      
      for (int i = 0; i < 10; i++)
          printf("%d\n", i); // 单条语句, 没有使用大括号
      
      while(x > 0)
          x--; //单条语句, 没有使用大括号
      
    • 合规代码示例:

      if (x > 0) {
          printf("x is positive\n"); // 使用大括号
      }
      
      for (int i = 0; i < 10; i++) {
          printf("%d\n", i);   //使用大括号
      }
      
      while(x > 0) {
        x--;
      }
      
    • 规则解释及益处:
      即使if、for、while等控制语句只包含一条语句, 也应该使用大括号将其括起来, 避免因为后续添加语句时忘记添加大括号而导致逻辑错误。

  • Rule 15.7 (强制): All if . . else if constructs shall be terminated with an else statement

    • 要求: 所有if..else,都应该以else终止。

    • 解释: 除非没有else,只要有else,最终必须以else终止, 处理其他情形。

    • 非合规代码示例:

      if (x > 0) {
          // ...
      } else if (x < 0) {
          // ...
      } // 缺少最后的 else 分支
      
    • 合规代码示例:

      if (x > 0) {
          // ...
      } else if (x < 0) {
          // ...
      } else {
          // 处理 x == 0 的情况
      }
      

2.2.16 switch 语句 (Switch statements)

  • Rule 16.1 (强制): All switch statements shall be well-formed

    • 要求: 所有switch语句应具有良好形式。

    • 解释: 一个结构良好的 switch 语句应符合以下要求:

      • 应该有一个 switch 表达式。
      • 应该至少有两个 case 标签。
      • 应该有一个 default 标签。
      • 每个 case 标签和 default 标签后应该跟随一个语句块(可以是复合语句)。
      • 除了有意设计的 fall-through 情况,每个语句块应该以 break 语句结束。
    • 非合规代码示例:

      //缺少break
      switch(x) {
          case 1:
             foo();
          case 2:
             bar();
             break;
      }
      
      //缺少default
      switch(y) {
         case 1:
           foo();
           break;
      }
      
    • 合规代码示例:

      switch(x) {
          case 1:
             foo();
             break;
          case 2:
             bar();
             break;
          default:
             break;
      }
      
  • Rule 16.2 (强制): A switch label shall only be used when the most closely-enclosing compound statement is the body of a switch statement

    • 要求: switch label只应用在封闭的复合语句是switch语句的主体的时候。

    • 解释: label是指case, default label。本条规则意思是, label只能出现在switch主体复合语句的最外层, 不能穿插在其他复合语句内。

    • 非合规代码示例:

      switch (x) {
          case 1:
              if (y > 0) {
                  case 2: // 不允许:case 标签嵌套在 if 语句中
                      break;
              }
              break;
          default:
              break;
      }
      
    • 合规代码示例:避免此类用法

  • Rule 16.3 (强制): An unconditional break statement shall terminate every switch-clause

    • 要求: 无条件的break语句应终止每个子句。

    • 解释: 除了有意设计的 fall-through 情况,每个 case 分支的代码块应该以 break 语句结束。

    • 例外情况: 控制流跳转到下一个switch子句。

    • 非合规代码示例:

      switch (value) {
          case 1:
              printf("case 1\n");
              // 缺少 break 语句,导致 case 穿透
          case 2:
              printf("case 2\n");
              break;
          default:
              printf("default\n");
              break;
      }
      
    • 合规代码示例:

      switch (value) {
          case 1:
              printf("case 1\n");
              break; // 添加 break 语句
          case 2:
              printf("case 2\n");
              break;
          default:
              printf("default\n");
              break;
      }
      
      // 如果需要 fall-through,可以使用注释说明
      switch (x) {
          case 1:
          case 2:
              // ... 处理 case 1 和 case 2 的相同逻辑 ...
              break; // 注意这里只有一个 break
          case 3:
              // ...
              break;
          default:
              // ...
              break;
      }
      
    • 规则解释及益处:
      在switch语句中,如果case分支的代码块没有以break语句结束, 程序会继续执行下一个case分支的代码块, 这被称为"case 穿透"。 Case 穿透有时可以被用来实现一些特殊的逻辑, 但在大多数情况下, 它会导致逻辑错误。 为了避免 case 穿透, 应该在每个case分支的代码块末尾添加break语句。

  • Rule 16.4 (强制): Every switch statement shall have a default label

    • 要求: 每个switch语句应有default标签。
    • 解释: default 标签用于处理 switch 表达式的值与所有 case 标签都不匹配的情况。提供 default 标签可以确保 switch 语句总是有一个出口,避免意外的行为。
  • Rule 16.5 (强制): A default label shall appear as either the first or the last switch label of a switch statement

    • 要求: 默认标签应作为第一个或最后一个switch标签出现。
  • Rule 16.6 (强制): Every switch statement shall have at least two switch-clauses

    • 要求: 每个switch语句应有至少2个switch子句。
    • 解释: 如果 switch 语句只有一个 case 标签,那么它可以用 if 语句代替。
  • Rule 16.7 (强制): A switch-expression shall not have essentially Boolean type

    • 要求: switch表达式不应具有本质上布尔类型。
    • 解释: switch 语句的表达式应该具有整数类型或枚举类型,不应该具有布尔类型。如果 switch 表达式具有布尔类型,那么它只有两种可能的值(true 和 false),这种情况下应该使用 if-else 语句代替。

2.2.17 函数 (Functions)

  • Rule 17.1 (强制): The features of <stdarg.h> shall not be used

    • 要求: <stdarg.h>的功能不应使用。
    • 解释: <stdarg.h> 头文件提供了处理可变参数列表的功能。但是,可变参数列表的使用会降低代码的可读性和可维护性,并且可能导致类型安全问题。
  • Rule 17.2 (强制): Functions shall not call themselves, either directly or indirectly

    • 要求: 函数不应直接或间接地调用自己。
    • 解释: 直接或间接的递归调用可能导致栈溢出,尤其是在嵌入式系统中,栈空间通常比较有限。
  • Rule 17.3 (强制): A function shall not be declared implicitly

    • 强制: 函数不应隐式声明。

    • 解释: 在 C90 中,如果没有显式地声明函数原型,编译器会假定函数返回 int 类型,并且对参数类型不做任何假设。这种隐式声明可能导致类型不匹配和未定义行为。

    • 非合规代码示例:

      void foo() {
          int x = bar(); // bar 函数没有声明
      }
      
    • 合规代码示例:

      int bar(void);    //显示声明
      void foo() {
          int x = bar();
      }
      
  • Rule 17.4 (强制): All exit paths from a function with non-void return type shall have an explicit return statement with an expression

    • 强制: 具有非void返回类型的函数的所有退出路径, 应具有显式return语句。

    • 非合规代码示例:

      int add(int a, int b) {
          // 缺少 return 语句
          a + b;
      }
      
      int foo(int x) {
        if(x > 0) {
          return 1;
        }
        //缺少 return
      }
      
    • 合规代码示例:

      int add(int a, int b) {
          return a + b; // 显式 return 语句
      }
      
      int foo(int x) {
        if(x > 0) {
          return 1;
        }
        return 0;
      }
      
    • 规则解释及益处:
      对于非void返回类型的函数, 所有函数退出路径都应该有显式的return语句, 并且返回值。

  • Rule 17.5 (建议): The function argument corresponding to a parameter declared to have an array type shall have an appropriate number of elements

    • 要求: 建议与被声明为拥有数组类型的参数对应的函数参数,应具有适当的元素数量。

    • 解释: 也就是说, 实参数组元素数量, 必须与函数参数的数组元素数量相匹配。

    • 非合规代码示例:

      void process_array(int arr[10]) { // 函数期望一个包含 10 个元素的数组
          // ...
      }
      
      void foo() {
          int my_arr[5];
          process_array(my_arr); // 不符合:传递了一个只有 5 个元素的数组
      }
      
    • 合规代码示例:

      void process_array(int arr[10]) { // 函数期望一个包含 10 个元素的数组
          // ...
      }
      
      void foo() {
          int my_arr[10];
          process_array(my_arr); // 符合
      }
      //更建议使用指针和长度
      void process_array2(int *arr, size_t len) {
         //可以使用len判断长度
      }
      
  • Rule 17.6 (强制): The declaration of an array parameter shall not contain the static keyword between the [ ]

    • 强制: 数组参数的声明, 不应在[]中间包含static关键字的参数。

    • 解释: 在 C99 中,可以在数组参数的声明中使用 static 关键字来指定数组的最小大小。但是,MISRA C 禁止这种用法,因为它可能导致兼容性问题,并且不是所有编译器都支持。

    • 非合规代码示例:

      void process_array(int arr[static 10]); // 不允许:在数组参数声明中使用 static
      
  • Rule 17.7 (强制): The value returned by a function having non-void return type shall be used

    • 要求: 非void返回类型的函数,必须返回值被使用。

    • 解释: 如果一个函数具有非 void 返回类型,那么它的返回值通常包含重要的信息,例如操作结果、状态码或错误信息。忽略函数的返回值可能会导致程序逻辑错误或资源泄漏。

    • 非合规代码示例:

      int get_data();
      
      void foo() {
          get_data(); // 忽略了 get_data() 的返回值
      }
      
    • 合规代码示例:

      int get_data();
      
      void foo() {
         int data = get_data(); // 使用函数的返回值
         // ... 使用 data ...
      }
      
      //如果确定不需要返回值, 可以使用(void)强制转换
      void bar() {
        (void)get_data();
      }
      
  • Rule 17.8 (建议): A function parameter should not be modified

    • 要求: 建议函数参数不应修改。

    • 解释: 函数参数具有自动存储周期, C语言允许修改参数, 但这么做会让人迷惑, 而且与程序员期望冲突。不熟悉C的程序员可能会修改参数, 而期望修改实参。

    • 非合规代码示例:

      void foo(int x) {
         x = x + 1; //修改了形参
      }
      
    • 合规代码示例:

      void foo(int x) {
          int local_x = x + 1; //使用局部变量
      }
      
      //或者使用const
      void bar(const int x) {
         //x++; //编译错误
      }
      

2.2.18 指针和数组 (Pointers and arrays)

  • Rule 18.1 (强制): A pointer resulting from arithmetic on a pointer operand shall address an element of the same array as that pointer operand

    • 要求: 指针操作数的算术运算产生的指针, 应寻址与该指针操作数相同的数组元素。

    • 解释: 也就是说, 指针指向一个数组, 与整型数进行算术运算, 得到的结果是另一个数组元素地址, 或者指向数组末尾元素的下一个位置。

    • 非合规代码示例:

      int arr[5] = {1, 2, 3, 4, 5};
      int *ptr = &arr[0];
      ptr = ptr + 10; // 指针越界, 不允许
      
      int arr2[5];
      ptr = arr2; // 指向了不同的数组
      
    • 合规代码示例:

      int arr[5] = {1, 2, 3, 4, 5};
      int *ptr = &arr[0];
      
      // 确保指针运算结果在数组范围内
      if (ptr + 2 < &arr[5]) {
          ptr = ptr + 2;
      }
      
      ptr = &arr[4];
      ptr++; //指向 arr[4] 的下一个位置, 可以
      ptr++; //不允许, 超出了数组范围
      
  • Rule 18.2 (强制): Subtraction between pointers shall only be applied to pointers that address elements of the same array

    • 要求: 指针之间的减法, 应当仅仅用于寻址相同数组的指针。

    • 解释: 只有当两个指针指向同一个数组的元素,或者其中一个指针指向数组末尾元素的下一个位置时,才能进行指针减法运算。指针减法的结果是两个指针之间的元素个数(ptrdiff_t 类型)。

    • 非合规代码示例:

      int arr1[5];
      int arr2[5];
      int *ptr1 = &arr1[0];
      int *ptr2 = &arr2[0];
      ptrdiff_t diff = ptr2 - ptr1; // 不允许:指向不同数组的指针相减
      
    • 合规代码示例:

      int arr[5];
      int *ptr1 = &arr[0];
      int *ptr2 = &arr[4];
      ptrdiff_t diff = ptr2 - ptr1; // 允许:指向同一数组的指针相减
      // diff 的值为 4
      
      ptr2 = &arr[5]; //可以, 指向末尾下一个位置
      diff = ptr2 - ptr1; //diff = 5
      
  • Rule 18.3 (强制): The relational operators >, >=, < and <= shall not be applied to objects of pointer type except where they point into the same object

    • 要求: 关系运算符 >, >=, <, <=,不应用于对象的指针类型,除了它们指向相同对象。

    • 解释: 只有当两个指针指向同一个数组(或结构体/联合体)的元素,或者其中一个指针指向数组(或结构体/联合体)末尾元素的下一个位置时,才能使用关系运算符 (>, >=, <, <=) 比较指针。

    • 非合规代码示例:

      int arr1[5];
      int arr2[5];
      int *ptr1 = &arr1[0];
      int *ptr2 = &arr2[0];
      if (ptr1 < ptr2) { // 不允许:比较指向不同数组的指针
          // ...
      }
      
    • 合规代码示例:

      int arr[5];
      int *ptr1 = &arr[0];
      int *ptr2 = &arr[4];
      if (ptr1 < ptr2) { // 允许:比较指向同一数组的指针
          // ...
      }
      
  • Rule 18.4 (建议): The +, -, += and -= operators should not be applied to an expression of pointer type

    • 要求: 建议+,-,+=,-=运算符不应用于指针类型的表达式。

    • 解释: 注意与Rule 18.1的区别。这条规则是建议不要直接对指针使用这些运算符, 而是建议使用下标或指针递增/递减。

    • 非合规代码示例:

      int arr[5];
      int *ptr = &arr[0];
      ptr = ptr + 2; // 不建议直接使用 +
      
    • 合规代码示例:

      int arr[5];
      int *ptr = &arr[0];
      ptr += 2;       //建议使用 +=
      ptr = &arr[2]; //建议使用取址
      
      //或者
      int index = 0;
      ptr = &arr[index];
      index += 2;
      ptr = &arr[index]; //使用下标
      
  • Rule 18.5 (建议): Declarations should contain no more than two levels of pointer nesting

    • 要求: 建议声明不应包含超过2级指针嵌套。

    • 解释: 指的是同一处超过2级指针, 而不是同一行代码。

    • 非合规代码示例:

      int ***ptr; // 不允许:超过两级指针
      
    • 合规代码示例:

      int **ptr; // 允许:两级指针
      int *ptr2; //允许
      
  • Rule 18.6 (强制): The address of an object with automatic storage shall not be copied to another object that persists after the first object has ceased to exist

    • 要求: 具有自动存储的对象地址, 不应拷贝到 在第一个对象不存在之后, 另一个存在的对象。

    • 解释: 通过地址引用已经释放的对象, 会导致未定义行为。

    • 非合规代码示例:

      int *get_pointer() {
         int local_var = 10;
         return &local_var; // 返回局部变量的地址,不允许
      }
      
      void foo() {
        int *ptr = get_pointer(); // ptr 现在是一个悬空指针
        // ... 使用 ptr ... // 未定义行为
      }
      
    • 合规代码示例:

      // 应该返回变量的值, 而不是地址
      int get_value() {
          int local_var = 10;
          return local_var;
      }
      
      // 或者,如果确实需要返回地址,则应该使用静态局部变量或动态分配的内存
      int *get_static_pointer() {
          static int static_var = 10; // 静态局部变量
          return &static_var;
      }
      
  • Rule 18.7 (强制): Flexible array members shall not be declared

    • 要求: 灵活的数组成员不应声明。

    • 解释: 灵活的数组成员是指弹性数组。这样会导致sizeof无法正确计算结构体大小。

    • 非合规代码示例:

      struct MyStruct {
          int data_len;
          int data[]; // 不允许:灵活数组成员
      };
      
  • Rule 18.8 (强制): Variable-length array types shall not be used

    • 要求: 可变长度数组类型不应使用。

    • 解释: 可变长度数组 (VLA) 是 C99 引入的特性,它允许数组的大小在运行时确定。但是,VLA 的使用会带来一些问题:

      • VLA 的大小可能超出栈的限制,导致栈溢出。
      • VLA 的大小可能难以预测,导致安全漏洞。
      • 某些编译器可能不支持 VLA。
    • 非合规代码示例:

      void foo(int n) {
          int arr[n]; // 不允许:可变长度数组
          // ...
      }
      

2.2.19 存储重叠 (Overlapping storage)

  • Rule 19.1 (强制): An object shall not be assigned or copied to an overlapping object
*   强制: 对象不应赋值或拷贝到一个重叠对象。
*   解释: 存储重叠,是指两个对象的存储空间存在相互重叠的情况,即共用了一段内存。
*   非合规代码示例:

    ```c
    #include <string.h>

    char str[20] = "Hello, world!";
    memcpy(str + 5, str, 10); // 内存区域重叠, 结果未定义,不允许

    int arr[5] = {1, 2, 3, 4, 5};
    arr[0] = arr[0]; //虽然没有问题, 但是不允许

    union Data {
       int i;
       float f;
    } data;
    data.i = 10;
    data.f = data.i; //不允许, 虽然看起来没有问题
    ```

*   合规代码示例:

    ```c
    #include <string.h>
    #include <stdio.h>

    int main() {
      char str[20] = "Hello, world!";
      char temp[20];

      // 先将需要拷贝的内容复制到一个临时缓冲区
      strncpy(temp, str, sizeof(temp) -1);
      temp[sizeof(temp) - 1] = '\0';

      // 再从临时缓冲区复制到目标位置
      strncpy(str + 5, temp, sizeof(str) - 5 -1 );
      str[sizeof(str) - 1] = '\0';

      printf("%s\n", str); // 输出: HelloHello, world!
      return 0;
    }
    ```

*   规则解释及益处:
    当使用memcpy等内存拷贝函数时, 如果源内存区域和目标内存区域有重叠, 会导致未定义行为。 为了避免这种情况, 可以使用memmove函数, 或者将数据先拷贝到一个临时缓冲区, 然后再从临时缓冲区拷贝到目标位置。
  • Rule 19.2 (建议): The union keyword should not be used

    • 要求: 建议union关键字不应使用。
    • 解释: 联合体各成员重叠内存, 写一个成员会导致其他成员也发生改变。union 的使用会降低代码的可读性和可维护性,并且可能导致难以发现的错误。

2.2.20 预处理指令 (Preprocessing directives)

  • Rule 20.2 (强制): The ' , " or \ characters and the / or // character sequences shall not occur in a header file name*

    • 要求: ' , " 或 \ 字符和 /*或//字符序列不应出现在头文件名中。

    • 解释: 在 #include 指令中使用的头文件名中,不应包含以下字符或字符序列:

      • 单引号 (')
      • 双引号 (")
      • 反斜杠 ()
      • C 风格注释的开始 (/*)
      • C++ 风格注释的开始 (//)
        这些字符或字符序列在头文件名中具有特殊含义,或者可能导致编译错误。包含这些字符可能表明头文件名不正确,或者试图进行某种形式的代码注入。
    • 非合规代码示例:

      #include "my_header'.h"   // 不允许:头文件名包含单引号
      #include "my_"header.h"    // 不允许:头文件名包含双引号
      #include "my_header\.h"    // 不允许:头文件名包含反斜杠
      #include "my_/*comment*/header.h" // 不允许:头文件名包含注释
      
    • 合规代码示例:

      #include "my_header.h" // 正确:头文件名只包含允许的字符
      #include <my_header.h> // 正确
      
  • Rule 20.3 (强制): The #include directive shall be followed by either a <filename> or "filename" sequence

    • 要求: #include指令应跟着<文件名>或"文件名"序列。

    • 解释: #include 指令用于包含其他头文件。它有两种形式:

      • #include <filename>:用于包含标准库头文件或编译器提供的头文件。
      • #include "filename":用于包含用户自定义的头文件。
        这条规则要求 #include 指令后面必须紧跟这两种形式之一,不能有其他内容。
    • 非合规代码示例:

      #include MY_HEADER // 不允许:缺少尖括号或双引号
      #define HEADER "my_header.h"
      #include HEADER // 不允许:使用宏代替文件名
      
    • 合规代码示例:

      #include <stdio.h>      // 正确:包含标准库头文件
      #include "my_header.h" // 正确:包含用户自定义头文件
      
  • Rule 20.4 (强制): A macro shall not be defined with the same name as a keyword

    • 要求: 宏定义不应使用相同的名称,作为关键字。

    • 解释: C 语言的关键字具有特殊的含义,不能用作标识符(变量名、函数名、宏名等)。如果将宏定义为与关键字同名,会导致编译错误或未定义行为。

    • 非合规代码示例:

      #define int my_int // 不允许:宏名与关键字 int 同名
      #define if my_if     //不允许
      
    • 合规代码示例:避免此类用法

  • Rule 20.5 (建议): #undef should not be used

    • 要求: 建议#undef不应使用。

    • 解释: #undef 指令用于取消已定义的宏。虽然 #undef 在某些情况下可能有用,但它会降低代码的可读性和可维护性。过度使用 #undef 可能导致宏的定义和取消定义之间的关系混乱,难以追踪。

    • 非合规代码示例:

      #define MAX_SIZE 100
      // ... 使用 MAX_SIZE ...
      #undef MAX_SIZE // 不建议:取消定义宏
      // ... 现在 MAX_SIZE 不再有效 ...
      
    • 合规代码示例:

      // 尽量避免使用 #undef
      // 如果确实需要不同的 MAX_SIZE,可以使用不同的宏名
      #define MAX_SIZE_PHASE1 100
      // ...
      #define MAX_SIZE_PHASE2 200
      
  • Rule 20.6 (强制): Tokens that look like a preprocessing directive shall not occur within a macro argument

    • 要求: 看起来像预处理指令的令牌, 不应出现在宏参数中。

    • 解释: 如果宏参数中包含看起来像预处理指令的令牌(例如 #ifdef、#include 等),可能会导致宏展开错误或意外的行为。

    • 非合规代码示例:

      #define MY_MACRO(arg) printf("%s\n", #arg)
      
      MY_MACRO(#include <stdio.h>); // 不允许:宏参数中包含 #include
      
    • 合规代码示例:避免此类用法

  • Rule 20.7 (强制): Expressions resulting from the expansion of macro parameters shall be enclosed in parentheses

    • 要求: 宏参数扩展产生的表达式, 应包含在括号中。

    • 解释: 在宏定义中, 如果宏参数出现在表达式中, 应该将宏参数用括号括起来。 这样可以避免宏展开后因为运算符优先级问题而导致结果与预期不符。

    • 非合规代码示例:

      #define SQUARE(x) x * x
      int result = SQUARE(2 + 3); // 展开为 2 + 3 * 2 + 3, 结果为 11
      
    • 合规代码示例:

      #define SQUARE(x) ((x) * (x))
      int result = SQUARE(2 + 3); // 展开为 ((2 + 3) * (2 + 3)), 结果为 25
      
    • 规则解释及益处:
      在宏定义中, 如果宏参数出现在表达式中, 应该将宏参数用括号括起来。 这样可以避免宏展开后因为运算符优先级问题而导致结果与预期不符。

  • Rule 20.8 (强制): The controlling expression of a #if or #elif preprocessing directive shall evaluate to 0 or 1

    • 要求: #if或#elif预处理指令的控制表达式, 应该评估为0或1。

    • 解释: #if 和 #elif 预处理指令用于条件编译。它们的控制表达式应该是一个常量表达式,其结果应该是 0 或 1。0 表示条件为假,1 表示条件为真。

    • 非合规代码示例:

      #if 2 // 不建议:控制表达式的值不是 0 或 1
      // ...
      #endif
      
      #define VALUE 10
      #if VALUE > 5 //不建议, 应该定义为bool类型
      //
      #endif
      
    • 合规代码示例:

      #if 1 // 正确:控制表达式的值为 1
      // ...
      #endif
      
      #if 0 // 正确:控制表达式的值为 0
      // ...
      #endif
      
      #define VALUE 1
      #if VALUE == 1
      //
      #endif
      
  • Rule 20.9 (强制): All identifiers used in the controlling expression of #if or #elif preprocessing directives shall be #define'd before evaluation

    • 要求: 所有用在#if或#elif预处理指定的控制表达式, 应在计算值之前用#define定义。

    • 解释: 在 #if 或 #elif 的控制表达式中使用的所有标识符都应该是已定义的宏。如果使用了未定义的宏,预处理器会将其替换为 0,这可能导致意外的结果。

    • 非合规代码示例:

      #if UNDEFINED_MACRO == 1 // 不允许:使用了未定义的宏
      // ...
      #endif
      
    • 合规代码示例:

      #define MY_MACRO 1
      #if MY_MACRO == 1 // 正确:使用了已定义的宏
      // ...
      #endif
      
  • Rule 20.10 (建议): The # and ## preprocessor operators should not be used

    • 要求: 建议#和## 预处理运算符不应使用。

    • 解释: # (字符串化运算符) 和 ## (标记粘贴运算符) 是 C 预处理器提供的两个特殊运算符。

      • # 运算符将其操作数转换为字符串字面量。
      • ## 运算符将其左右两侧的操作数连接成一个单独的标记。
        MISRA C 建议避免使用这两个运算符,因为它们会降低代码的可读性和可维护性,并且可能导致意外的宏展开结果。
    • 非合规代码示例:

      #define STRINGIZE(x) #x
      const char *str = STRINGIZE(hello); // 使用 # 运算符
      
      #define CONCAT(x, y) x ## y
      int xy = CONCAT(10, 20); // 使用 ## 运算符
      
    • 合规代码示例:

      //避免使用
      
  • Rule 20.11 (强制): A macro parameter immediately following a # operator shall not immediately be followed by a ## operator

    • 要求: 紧跟在#操作符的宏定义参数, ##不应紧随其后。

    • 非合规代码示例:

      #define A(x) #x##B //不允许
      
  • Rule 20.12 (强制): A macro parameter used as an operand to the # or ## operators, which is itself subject to further macro replacement, shall only be used as an operand to these operators

    • 要求: 用作#或##运算符操作数的宏参数,其本身需要进一步的宏替换,只能用作这些运算符的操作数。

    • 非合规代码示例:

      #define A(x) #x
      #define B(x) x
      #define C(x) B(A(x)) //不符合
      
  • Rule 20.13 (强制): A line whose first token is # shall be a valid preprocessing directive

    • 要求: 第一个标记为#的行, 应为有效预处理指令。

    • 解释: 以 # 开头的行应该是一个有效的预处理指令,例如 #include、#define、#ifdef 等。如果 # 后面跟随的不是有效的预处理指令,会导致编译错误。

    • 非合规代码示例:

      # this is not a valid directive // 不允许:# 后面不是有效的预处理指令
      
  • Rule 20.14 (强制): All #else, #elif and #endif preprocessor directives shall reside in the same file as the #if, #ifdef or #ifndef directive to which they are related

    • 要求: 所有#else,#elif和#endif预处理指令应与它们相关的#if,#ifdef或#ifndef指令位于同一文件。

    • 解释: 这条规则是为了保证条件编译指令的完整性和一致性。#if、#ifdef、#ifndef、#else、#elif 和 #endif 应该成对出现,并且位于同一个文件中。

    • 非合规代码示例:

      // header1.h
      #ifndef MY_HEADER_H
      #define MY_HEADER_H
      
      // header2.h
      #else // 不允许:#else 与 #ifndef 不在同一个文件中
      #endif
      
    • 合规代码示例:

      // header.h
      #ifndef MY_HEADER_H
      #define MY_HEADER_H
      // ...
      #else
      //
      #endif
      

2.2.21 标准库 (Standard libraries)

  • Rule 21.1 (强制): #define and #undef shall not be used on a reserved identifier or reserved macro name

    • 要求: #define和#undef不应用于保留标识符或者保留宏名称。

    • 解释: 保留标识符, 保留宏名称, 是指标准库用到的标识符或名称。例如, __LINE__, errno。

    • 非合规代码示例:

      #define errno my_errno // 不允许:重定义标准库中的标识符
      #undef NULL         //不允许
      
  • Rule 21.2 (强制): A reserved identifier or macro name shall not be declared

    • 要求: 保留标识符或宏名称不应声明。

    • 解释: 直接#include对应头文件即可, 不应再单独声明。

    • 非合规代码示例:

      extern int errno;   // 不允许:声明标准库中的保留标识符
      int NULL;          //不允许
      
  • Rule 21.3 (强制): The memory allocation and deallocation functions of <stdlib.h> shall not be used

    • 要求: <stdlib.h>内存分配和回收函数, 不应使用。
    • 解释: 指malloc/free/calloc/realloc系列函数。 动态内存分配(malloc, free等)可能导致内存泄漏, 碎片, 以及难以预测的程序行为。 在安全关键的嵌入式系统中, 应该避免使用动态内存分配。
  • Rule 21.4 (强制): The standard header file <setjmp.h> shall not be used

    • 要求: 标准头文件<setjmp.h>不应使用。
    • 解释: 该头文件包含setjmp, longjmp指令。setjmp 和 longjmp 函数用于实现非局部跳转,它们会破坏程序的结构化,使代码难以理解和维护, 并且可能导致资源泄漏。
  • Rule 21.5 (强制): The standard header file <signal.h> shall not be used

    • 要求: 标准头文件<signal.h>不应使用。
    • 解释: 该头文件包含信号处理设施。信号处理机制是操作系统提供的, 用于处理异步事件。 在嵌入式系统中, 信号处理可能会导致时序问题和不可预测的行为。
  • Rule 21.6 (强制): The Standard Library input/output routines shall not be used.

    • 要求: 标准库输入输出例程不应使用。
    • 解释: 该规则用于<stdio.h>提供的函数, 对于宽字符版本是<wchar.h>。标准库 I/O 函数(如 printf、scanf、fopen、fclose 等)可能会带来安全风险和不可预测的行为,特别是在资源受限的嵌入式系统中。例如:
      • printf 和 scanf 的格式化字符串漏洞。
      • 文件操作可能导致资源泄漏或竞争条件。
      • 标准 I/O 函数可能依赖于底层操作系统,降低代码的可移植性。
  • Rule 21.7 (强制): The atof, atoi, atol and atoll functions of <stdlib.h> shall not be used

    • 要求: <stdlib.h>的atof, atoi, atol和atoll函数, 不应使用。

    • 解释: 当不能转换时, 这些函数有未定义行为。这些字符串转换函数在遇到无效输入时会产生未定义行为,例如:

      • 输入字符串不能转换为有效的数字。
      • 转换结果超出目标类型的范围。
        应该使用 strtol、strtoul、strtod 等函数代替,这些函数可以提供更可靠的错误处理。
    • 非合规代码示例:

      char *str = "abc";
      int x = atoi(str); // 未定义行为
      
    • 合规代码示例:

      #include <stdlib.h>
      #include <errno.h>
      
      char *str = "123";
      char *endptr;
      long value = strtol(str, &endptr, 10); // 10 表示十进制
      
      if (*endptr != '\0') {
          // 处理转换错误
      }
      
      if ((errno == ERANGE && (value == LONG_MAX || value == LONG_MIN))) {
         //处理溢出
      }
      
  • Rule 21.8 (强制): The library functions abort, exit, getenv and system of <stdlib.h> shall not be used

    • 要求: <stdlib.h>的库函数abort, exit, getenv和system, 不应使用。
    • 解释:
      • abort():立即终止程序,可能不会进行正常的清理操作(如刷新缓冲区、关闭文件等)。
      • exit():正常终止程序,但可能依赖于底层操作系统。
      • getenv():获取环境变量,可能导致安全风险(如泄露敏感信息)或依赖于不可移植的平台特性。
      • system():执行系统命令,可能导致安全风险(如命令注入)和不可预测的行为。
  • Rule 21.9 (强制): The library functions bsearch and qsort of <stdlib.h> shall not be used

    • 要求: <stdlib.h>的库函数bsearch和qsort, 不应使用。
    • 解释: bsearch 和 qsort 函数是通用的搜索和排序函数,它们依赖于函数指针来实现自定义的比较逻辑。在安全关键的嵌入式系统中,函数指针的使用可能会带来风险。此外,qsort 函数的实现可能不是稳定的,并且在最坏情况下可能具有 O(n^2) 的时间复杂度。
  • Rule 21.10 (强制): The Standard Library time and date routines shall not be used

    • 要求: 标准库时间和日期例程, 不应使用。
    • 解释: <time.h>中等设施, 都不能使用。时间和日期函数没有指定、定义和实现定义的行为。在嵌入式系统中,时间处理可能非常关键,标准库的时间和日期函数可能无法满足特定的需求,或者可能依赖于底层操作系统,降低代码的可移植性。
  • Rule 21.11 (强制): The standard header file <tgmath.h> shall not be used

    • 要求: 标准头文件<tgmath.h>不应使用。
    • 解释: <tgmath.h> 头文件提供了类型泛型数学函数,它可以根据参数类型自动选择合适的数学函数(如 sin、cos、sqrt 等)。但是,这种类型泛型机制可能会导致代码的可读性降低,并且可能引入意外的类型转换。
  • Rule 21.12 (建议): The exception handling features of <fenv.h> should not be used

    • 要求: 建议<fenv.h>的异常处理功能, 不应使用。
    • 解释: <fenv.h> 头文件提供了对浮点环境的访问和控制,包括浮点异常、舍入模式等。但是,浮点异常处理机制可能依赖于底层硬件和操作系统,并且可能导致代码的行为难以预测。

2.2.22 资源 (Resources)

  • Rule 22.1 (强制): All resources obtained dynamically by means of Standard Library functions shall be explicitly released

    • 要求: 应明确释放通过标准库功能动态获得的所有资源。

    • 解释: 资源包括:

      • 动态分配的内存 (虽然 MISRA C 禁止使用动态内存分配,但如果使用了,则必须正确释放)。
      • 打开的文件。
      • 创建的互斥锁、信号量等同步对象。
      • 其他通过标准库函数获得的系统资源。
        如果程序获得了这些资源但没有及时释放,会导致资源泄漏,最终可能导致程序崩溃或系统不稳定。
    • 示例:

      #include <stdio.h>
      
      int main() {
          FILE *fp = fopen("myfile.txt", "w"); // 打开文件
          if (fp == NULL) {
              // 处理错误
              return -1;
          }
      
          // ... 使用文件 ...
      
          fclose(fp); // 关闭文件,释放资源
          return 0;
      }
      
  • Rule 22.2 (强制): A block of memory shall only be freed if it was allocated by means of a Standard Library function

    • 强制: 只有通过标准库函数分配内存块时,才能释放内存块。

    • 解释: malloc/free 配套使用(如果使用了动态内存分配)。

    • 非合规代码示例:

      int x;
      int *p = &x;
      free(p); //错误, 不能释放局部变量的地址
      
  • Rule 22.3 (强制): The same file shall not be open for read and write access at the same time on different streams

    • 要求: 相同文件不应打开, 在同一时间不同流上进行读写访问。

    • 解释: 同时读写同一文件, 可能导致文件数据异常。

    • 非合规代码示例:

      #include <stdio.h>
      int main() {
         FILE *fp1 = fopen("data.txt", "r");
         FILE *fp2 = fopen("data.txt", "w"); // 不允许:同时以读写模式打开同一文件
         // ...
         fclose(fp1);
         fclose(fp2);
         return 0;
      }
      
    • 合规代码示例:

      #include <stdio.h>
      int main() {
        //先读取
        FILE *fp1 = fopen("data.txt", "r");
        //使用 fp1
        fclose(fp1);
      
        //再写入
        FILE *fp2 = fopen("data.txt", "w");
        //使用fp2
        fclose(fp2);
        return 0;
      }
      
  • Rule 22.4 (强制): There shall be no attempt to write to a stream which has been opened as read-only

    • 强制: 不应尝试写作为只读打开的流。

    • 非合规代码示例:

      #include <stdio.h>
      int main() {
         FILE *fp = fopen("data.txt", "r");
         if(fp != NULL) {
            fprintf(fp, "Trying to write"); // 错误:尝试写入只读文件
         }
         fclose(fp);
         return 0;
      }
      
  • Rule 22.5 (强制): A pointer to a FILE object shall not be dereferenced

    • 强制: 指向 FILE 对象的指针不应解引用。

    • 解释: FILE 对象是标准库用于表示文件流的结构体。FILE 对象的内部结构是实现定义的,不应该直接访问其成员。这样做会导致代码不可移植,并且可能破坏 FILE 对象的内部状态。

    • 非合规代码示例:

      #include <stdio.h>
      int main() {
         FILE *fp = fopen("data.txt", "r");
         if(fp != NULL) {
            //int fd = fp->_fileno; // 错误:尝试访问 FILE 对象的内部成员, 不同平台成员可能不一样
            //int ch = *fp; // 错误, 解引用 FILE 指针
         }
         fclose(fp);
         return 0;
      }
      
    • 合规示例:

        #include <stdio.h>
        int main() {
            FILE *fp = fopen("myfile.txt", "r");
            if (fp == NULL) {
                perror("fopen");
                return 1;
            }
      
            int ch;
            while ((ch = fgetc(fp)) != EOF) { // 使用标准 I/O 函数
                putchar(ch);
            }
      
            if (ferror(fp)) {
                perror("fgetc");
            }
      
            fclose(fp);
            return 0;
        }
      
  • Rule 22.6 (强制): The value of a pointer to a FILE shall not be used after the associated stream has been closed

    • 强制: 关联流关闭后, FILE 指针不应再使用。

    • 解释: 一旦使用 fclose 函数关闭了文件流,与之关联的 FILE 指针将变为无效。任何尝试使用该无效指针的行为都是未定义的,可能导致程序崩溃或产生不可预测的结果。

    • 非合规代码示例:

      #include <stdio.h>
      
      int main() {
        FILE *fp = fopen("data.txt", "r");
        // ...
        fclose(fp);
        fprintf(fp, "Trying to write"); // 错误:在文件关闭后使用 FILE 指针
        return 0;
      }
      
  • Rule 22.7 (强制): The macro EOF shall only be used with the unmodified return value of any Standard Library function capable of returning EOF

    • 要求: 宏 EOF 只得与任何能够返回 EOF 的标准库函数的未修改返回值比较。

    • 解释: EOF 是一个宏,通常定义为 -1,表示文件结束或输入错误。许多标准 I/O 函数(如 fgetc、fgets、fscanf 等)在遇到文件结束或发生错误时会返回 EOF。这条规则要求:

      • 只能将 EOF 与这些函数的返回值进行比较。
      • 不能修改这些函数的返回值,然后再与 EOF 比较。
      • 不能将EOF与其他值进行比较。
    • 非合规代码示例:

      #include <stdio.h>
      
      int main() {
          int ch;
          FILE *fp = fopen("myfile.txt", "r");
          if (fp == NULL) {
              return 1;
          }
      
          ch = fgetc(fp);
          if (ch + 1 == EOF + 1) { // 错误:修改了返回值
              // ...
          }
          if (1 == EOF) { //错误, 不应该与其他值比较
      
          }
          fclose(fp);
          return 0;
      }
      
    • 合规代码示例:

      #include <stdio.h>
      int main() {
          int ch;
          FILE *fp = fopen("myfile.txt", "r");
          if(fp == NULL) {
             return 1;
          }
      
          while ((ch = fgetc(fp)) != EOF) { // 正确:直接比较返回值和 EOF
              putchar(ch);
          }
      
          fclose(fp);
          return 0;
      }
      
  • Rule 22.8 (强制): The value of errno shall be set to zero prior to a function call which sets errno on failure

    • 要求: 在调用 errno 设置函数之前必须将 errno 值设置为零。

    • 解释: errno 是一个全局变量,用于存储最近一次发生的错误代码。许多标准库函数在发生错误时会设置 errno 的值。这条规则要求在调用可能设置 errno 的函数之前,先将 errno 清零。这是因为 errno 的值不会被自动清除,如果不清零,可能会错误地认为发生了错误。

    • 非合规代码示例:

       #include <stdio.h>
       #include <errno.h>
       #include <math.h>
       int main() {
      
           double x = -1.0;
           double result = sqrt(x); // 产生错误, 设置 errno
      
           if (errno != 0) { // 可能错误地认为发生了错误, 因为 errno 可能在之前就被设置了
               perror("sqrt");
           }
           return 0;
       }
      
    • 合规代码示例:

      #include <stdio.h>
      #include <errno.h>
      #include <math.h>
      int main() {
          errno = 0; // 清零 errno
          double x = -1.0;
          double result = sqrt(x);
      
          if (errno != 0) { // 正确:检查 errno
              perror("sqrt");
          }
          return 0;
      }
      
  • Rule 22.9 (强制): The value of errno shall be tested against zero after calling a function which is permitted to set errno

    • 要求: 调用 errno 设置函数后必须检测 errno 值是否为零。

    • 解释: 在调用可能设置 errno 的函数之后,应该立即检查 errno 的值是否为非零。如果 errno 为非零,则表示发生了错误,应该进行相应的错误处理。

    • 非合规代码示例:

      #include <math.h>
      
      int main() {
        double x = -1.0;
        double result = sqrt(x); // 可能会设置 errno
        // 没有检查 errno
      }
      
    • 合规代码示例:

      #include <stdio.h>
      #include <errno.h>
      #include <math.h>
      int main() {
          errno = 0; // 清零 errno
          double x = -1.0;
          double result = sqrt(x);
      
          if (errno != 0) { // 正确:检查 errno
              perror("sqrt"); // 处理错误
          }
          return 0;
      }
      
  • Rule 22.10 (强制): The value of errno shall only be tested when the last function to be called was a Standard Library function which is permitted to set errno

    • 要求: 只有上一个被调用的函数是允许设置 errno 的标准库函数的情况下才能检测 errno 值。

    • 解释:
      并非所有标准库函数都会设置 errno。在调用非标准库函数或不设置 errno 的标准库函数后,errno 的值可能不再反映最近一次发生的错误。
      因此,只有在调用了允许设置 errno 的标准库函数后,立即检查 errno 的值才有意义。可以查阅相关函数的文档, 确认是否会设置errno.

    • 非合规代码示例:

      #include <stdio.h>
      #include <errno.h>
      #include <math.h>
      
      int my_function() {
        // ... 自定义函数,不设置 errno ...
        return 0;
      }
      
      int main() {
        errno = 0;
        double x = -1.0;
        double result = sqrt(x); //sqrt会设置errno
        my_function(); // 调用了自定义函数
        if (errno != 0) { // 错误:在调用了不设置 errno 的函数后检查 errno
          // ...
        }
        return 0;
      }
      
    • 合规代码示例:

      #include <stdio.h>
      #include <errno.h>
      #include <math.h>
      int main() {
          errno = 0; // 清零 errno
          double x = -1.0;
          double result = sqrt(x);
      
          if (errno != 0) { // 正确:在调用可能设置 errno 的函数后立即检查
              perror("sqrt");
          }
          return 0;
      }
      

3. MISRA C:2012 的执行强度

MISRA C:2012 标准中的指令和规则根据其重要性和执行方式,被分为不同的类别:

  • 强制 (Mandatory): 最严格的类别,必须严格遵守,任何违反强制规则的代码都应被视为不合格。
  • 要求 (Required): 同样需要严格遵守,但相比强制规则,可能在某些特定情况下可以进行偏差解释 (deviation)。
  • 建议 (Advisory): 最佳实践建议,鼓励尽可能遵守,但可以根据项目具体情况进行权衡和调整。

此外,规则还分为 可静态检查 (Statically Checkable) 和 不可静态检查 (Non-Statically Checkable) 两类。可静态检查的规则可以通过静态代码分析工具自动检测,而不可静态检查的规则则需要人工代码审查或运行时监控来验证。

4. 如何应用 MISRA C:2012

在实际项目中应用 MISRA C:2012 标准,需要从项目初期就将 MISRA C 规范纳入开发流程中:

  1. 制定 MISRA C 执行策略: 明确项目需要遵守的 MISRA C 规则级别 (例如,全部强制和要求规则,部分建议规则),以及偏差解释的流程和标准。
  2. 选择并配置静态代码分析工具: 选择支持 MISRA C:2012 标准的静态代码分析工具 (例如, PC-lint, Klocwork, Coverity, QA-C, Polyspace 等),并将其集成到开发环境和持续集成流程中,实现代码的自动化 MISRA C 检查。
  3. 代码审查: 对于静态代码分析工具无法检测的规则,或者需要进行偏差解释的情况,需要进行人工代码审查。代码审查应该由熟悉 MISRA C 标准的资深工程师进行。
  4. 使用支持MISRA C的库: 尽可能使用已经符合MISRA C标准的库函数。
  5. 代码生成: Simulink,Etas,EB Tresos,Vector全家桶等相关软件支持生成符合MISRA C标准的代码。

5. 总结

MISRA C:2012 标准是一套严谨而全面的 C 语言编码规范,是提升嵌入式系统代码质量、保障系统安全性的重要工具,此套标准我认为不仅仅只能用于汽车行业,而是应该推广出来,最后附上速查手册。

附录:MISRA C:2012 规则速查手册

MISRA C:2012 规则覆盖情况

所有类别支持总计可判定支持总计不可判定支持总计覆盖比例
强制 (Mandatory) 121 121   36 54   157 175 90%
要求 (Required) 88 88   23 32   111 120 93%
建议 (Advisory) 28 28   2 11   30 39 77%

MISRA C:2012 规则列表

Analyze 编码规则规则编号规则名称类别可判定性是否支持
C2301 Dir 1.1 如果程序的输出取决于某实现定义的行为,则必须记录并理解该行为 要求 不可判定 否
C2302 Dir 2.1 所有源文件编译过程中不得有编译错误 要求 不可判定 否
C2303 Dir 3.1 所有代码必须可被追溯到文档化的需求 要求 不可判定 否
C2304 Dir 4.1 必须尽量减少运行错误 要求 不可判定 否
C2305 Dir 4.2 应记录所有汇编语言的使用 建议 不可判定 否
C2306 Dir 4.3 汇编语言必须被封装并隔离 要求 不可判定 否
C2307 Dir 4.4 不应“注释掉”代码段 建议 不可判定 否
C2308 Dir 4.5 在同一命名空间内,应确保外形重合的标识符的排版不易混淆 建议 不可判定 否
C2309 Dir 4.6 应使用表示大小和符号性的类型定义(typedef)代替基本数据类型 建议 不可判定 否
C2310 Dir 4.7 如果函数返回了错误信息,那么必须检测该错误信息 要求 不可判定 否
C2311 Dir 4.8 如果一个翻译单元内,指向结构体或联合体的指针永不被解引用,那么 应该隐藏该对象的实现 建议 不可判定 否
C2312 Dir 4.9 在可以使用函数或类函数宏的情况下,应优先使用函数 建议 不可判定 否
C2313 Dir 4.10 应采取措施预防头文件的内容被多次包含 要求 不可判定 否
C2314 Dir 4.11 必须检查传递给库函数的值的有效性 要求 不可判定 否
C2315 Dir 4.12 不得使用动态内存分配 要求 不可判定 是
C2316 Dir 4.13 应以适当顺序调用对资源进行运算的函数 建议 不可判定 否
C2317 Dir 4.14 应检查来源于外部的值的有效性 要求 不可判定 否
C2201 Rule 1.1 程序不得违反C语言标准语法和约束,不得超出实现的翻译限制 要求 可判定 是
C2202 Rule 1.2 不应使用语言扩展 建议 不可判定 否
C2203 Rule 1.3 不得出现未定义或严重的未指定行为 要求 不可判定 否
C2204 Rule 1.4 不得使用新涌现的语言特性 要求 可判定 是
C2007 Rule 2.1 项目不得含有不可达代码 要求 不可判定 是
C2006 Rule 2.2 不得有死代码 要求 不可判定 是
C2005 Rule 2.3 项目不应含有未使用的类型声明 建议 可判定 是
C2004 Rule 2.4 项目不应含有未使用的标签(tag)声明 建议 可判定 是
C2003 Rule 2.5 项目不应含有未使用的宏声明 建议 可判定 是
C2002 Rule 2.6 项目不应含有未使用的标记(label)声明 建议 可判定 是
C2001 Rule 2.7 函数中不应有未使用的形参 建议 可判定 是
C2102 Rule 3.1 不得在注释中使用字符序列/*和// 要求 可判定 是
C2101 Rule 3.2 不得在//注释中使用行拼接 要求 可判定 是
C1002 Rule 4.1 八进制和十六进制转义序列必须被终止 要求 可判定 是
C1001 Rule 4.2 不应使用三字母词(trigraphs) 建议 可判定 是
C1109 Rule 5.1 不得使用重名的外部标识符 要求 可判定 是
C1108 Rule 5.2 在同一作用域和命名空间内声明的标识符不得重名 要求 可判定 是
C1107 Rule 5.3 内部作用域声明的标识符不得隐藏外部作用域声明的标识符 要求 可判定 是
C1106 Rule 5.4 宏标识符不得重名 要求 可判定 是
C1105 Rule 5.5 标识符不得与宏的名称重名 要求 可判定 是
C1104 Rule 5.6 类型定义(typedef)名称必须是唯一标识符 要求 可判定 是
C1103 Rule 5.7 标签名称必须是唯一标识符 要求 可判定 是
C1102 Rule 5.8 必须使用唯一标识符定义含有外部链接的对象或函数 要求 可判定 是
C1101 Rule 5.9 应使用唯一标识符定义含有内部链接的对象或函数 建议 可判定 是
C0702 Rule 6.1 只得使用合适的类型来声明位域(bit-fields) 要求 可判定 是
C0701 Rule 6.2 用一位命名的位域不得为有符号类型 要求 可判定 是
C0904 Rule 7.1 不得使用八进制常量 要求 可判定 是
C0903 Rule 7.2 所有表现为无符号类型的整型常量都必须使用“u”或“U”后缀 要求 可判定 是
C0902 Rule 7.3 小写字母“I”不得用作字面量后缀 要求 可判定 是
C0901 Rule 7.4 不得将字符串字面量赋值给对象,除非对象类型为“指向 const 修饰的 char 的指针” 要求 可判定 是
C0514 Rule 8.1 必须明确指定类型 要求 可判定 是
C0513 Rule 8.2 函数类型必须为带有命名形参的原型形式 要求 可判定 是
C0512 Rule 8.3 对同一对象或函数的所有声明必须使用同样的名字和类型修饰符 要求 可判定 是
C0511 Rule 8.4 对含有外部链接的对象或函数进行定义时,必须有可见的兼容声明 要求 可判定 是
C0510 Rule 8.5 外部对象或函数只得在一个文件里声明一次 要求 可判定 是
C0509 Rule 8.6 含有外部链接的标识符必须有且只有一个外部定义 要求 可判定 是
C0508 Rule 8.7 不应使用外部链接定义仅在一个翻译单元中引用的函数和对象 建议 可判定 是
C0507 Rule 8.8 对含有内部链接的对象和函数进行的所有声明都必须使用静态(static) 存储类说明符 要求 可判定 是
C0506 Rule 8.9 如果对象标识符只在一个函数中出现,那么应该在块作用域(block scope)中定义该对象 建议 可判定 是
C0505 Rule 8.10 必须使用静态存储类别声明内联函数 要求 可判定 是
C0504 Rule 8.11 对含有外部链接的数组进行定义时,应显式指定其大小 建议 可判定 是
C0503 Rule 8.12 枚举列表里一个隐式指定的枚举常量的值应是唯一的 要求 可判定 是
C0502 Rule 8.13 指针应尽量指向const修饰的类型 建议 不可判定 否
C0501 Rule 8.14 不得使用restrict类型修饰符 要求 可判定 是
C1205 Rule 9.1 对于具有自动存储周期的对象,不得在设定它的值之前读取它的值 强制 不可判定 是
C1204 Rule 9.2 聚合或联合体的初始化器应包含在大括号“{}”内 要求 可判定 是
C1203 Rule 9.3 不得对数组进行部分初始化 要求 可判定 是
C1202 Rule 9.4 最多只得初始化一次对象的元素 要求 可判定 是
C1201 Rule 9.5 对数组对象进行指定初始化时,必须显式指定数组大小 要求 可判定 是
C0808 Rule 10.1 操作数不得为不合适的基本类型 要求 可判定 是
C0807 Rule 10.2 不得在加减运算中不恰当地使用基本字符类表达式 要求 可判定 是
C0806 Rule 10.3 表达式的值不得赋给更窄的基本类型,也不得赋给不同的基本类型类别 要求 可判定 是
C0805 Rule 10.4 执行寻常算术转换的运算符的两个操作数必须属于同一基本类型类别 要求 可判定 是
C0804 Rule 10.5 表达式的值不应强制转换为不合适的基本类型 建议 可判定 是
C0803 Rule 10.6 复合表达式的值不得赋给具有更宽基本类型的对象 要求 可判定 是
C0802 Rule 10.7 寻常算术转换中,如果运算符的一个操作数为复合表达式,则另一个操 作数不得具有更宽类型 要求 可判定 是
C0801 Rule 10.8 复合表达式的值不得强制转换为不同基本类型类别或更宽类型 要求 可判定 是
C1409 Rule 11.1 指向函数的指针和任何其他类型之间不得相互转换 要求 可判定 是
C1408 Rule 11.2 指向不完整类型的指针和任何其他类型之间不得相互转换 要求 可判定 是
C1407 Rule 11.3 指向对象类型的指针和指向不同对象类型的指针之间不得强制转换 要求 可判定 是
C1406 Rule 11.4 指向对象的指针和整数类型之间不应相互转换 建议 可判定 是
C1405 Rule 11.5 指向void的指针不应转换为指向对象的指针 建议 可判定 是
C1404 Rule 11.6 指向void的指针和算术类型之间不得强制转换 要求 可判定 是
C1403 Rule 11.7 指向对象的指针和非整数类型的算术类型之间不得强制转换 要求 可判定 是
C1402 Rule 11.8 强制转换不得移除指针所指向类型的任何const或volatile修饰 要求 可判定 是
C1401 Rule 11.9 宏NULL必须为整数类型空指针常量的唯一允许形式 要求 可判定 是
C0605 Rule 12.1 应明确表达式中操作数的优先级 建议 可判定 是
C0604 Rule 12.2 移位运算符的右操作数的范围下限为零,上限须比左操作数的基本类型 的位宽度小一 要求 不可判定 是
C0603 Rule 12.3 不得使用逗号运算符(,) 建议 可判定 是
C0602 Rule 12.4 对常量表达式进行求值不应导致整数回绕 建议 可判定 是
C0601 Rule 12.5 sizeof运算符的操作数不得是声明为“数组类型”的函数形参 强制 可判定 是
C1606 Rule 13.1 初始化器列表不得含有持续的副作用(persistent side effect) 要求 不可判定 是
C1605 Rule 13.2 采用不同的求值顺序时(只要允许采用该顺序),表达式的值和表达式 的持续的副作用必须相等 要求 不可判定 是
C1604 Rule 13.3 含有一个自增(++)或自减(--)运算符的完整表达式,除因自增或自 减运算符引起的副作用外,不应含有其他潜在副作用 建议 可判定 是
C1603 Rule 13.4 不得使用赋值运算符的结果 建议 可判定 是
C1602 Rule 13.5 逻辑与(&&)和逻辑或(运算符的右操作数不得含有持续的副作用) 要求 不可判定 是
C1601 Rule 13.6 sizeof运算符的操作数不得包含任何有潜在副作用的表达式 强制 可判定 是
C1704 Rule 14.1 循环计数器不得为基本浮点类型 要求 不可判定 是
C1703 Rule 14.2 for循环必须格式良好 要求 不可判定 是
C1702 Rule 14.3 控制表达式不得为不变量 要求 不可判定 是
C1701 Rule 14.4 if语句和迭代语句的控制表达式必须为基本布尔类型 要求 可判定 是
C1807 Rule 15.1 不应使用goto语句 建议 可判定 是
C1806 Rule 15.2 同一函数中,goto语句只得跳转到在其后声明的标记(label) 要求 可判定 是
C1805 Rule 15.3 goto语句引用的标记必须在同一代码块或上级代码块中声明 要求 可判定 是
C1804 Rule 15.4 对于任何迭代语句,最多只应使用一个break或goto语句进行终止 建议 可判定 是
C1803 Rule 15.5 函数结尾应只有一个退出点 建议 可判定 是
C1802 Rule 15.6 迭代语句或分支语句的主体必须为复合语句 要求 可判定 是
C1801 Rule 15.7 所有if ... else if构造都必须以一个else语句终止 要求 可判定 是
C1907 Rule 16.1 所有switch语句必须格式良好(well-formed) 要求 可判定 是
C1906 Rule 16.2 switch标记只得出现在形成switch语句主体的复合语句最外层 要求 可判定 是
C1905 Rule 16.3 每个switch子句(switch-clause)都必须以一个无条件break语句终止 要求 可判定 是
C1904 Rule 16.4 每个switch语句都必须有default标记 要求 可判定 是
C1903 Rule 16.5 在switch语句中,default标记必须是第一个或最后一个switch标记 要求 可判定 是
C1902 Rule 16.6 每个switch语句都必须有两个或以上switch子句 要求 可判定 是
C1901 Rule 16.7 switch表达式不得是基本布尔类型 要求 可判定 是
C1508 Rule 17.1 不得使用<stdarg.h>的特性 要求 可判定 是
C1507 Rule 17.2 函数不得直接或间接调用自身 要求 不可判定 是
C1506 Rule 17.3 不得隐式声明函数 强制 可判定 是
C1505 Rule 17.4 所有函数退出路径,如果为非空(non-void)返回类型,则必须有一个 包含表达式的显式return语句 强制 可判定 是
C1504 Rule 17.5 如果函数形参声明为数组类型,其对应的实参必须具有适当数量的元素 建议 不可判定 是
C1503 Rule 17.6 声明数组形参时,[]内不得包含关键字static 强制 可判定 是
C1502 Rule 17.7 函数返回值若不为非空返回类型(non-void return type),则必须被使 用 要求 可判定 是
C1501 Rule 17.8 不应修改函数形参 建议 不可判定 是
C1308 Rule 18.1 对指针操作数进行算术运算得来的指针只得用于寻址同一数组的元素 要求 不可判定 是
C1307 Rule 18.2 指针之间的减法运算只得用于寻址同一数组元素的指针 要求 不可判定 是
C1306 Rule 18.3 大小比较运算符>、>=、<和<=不得用于指针类型的对象,除非两个指针 指向同一对象 要求 不可判定 是
C1305 Rule 18.4 +、-、+=和-=运算符不得用于指针类型的表达式 建议 可判定 是
C1304 Rule 18.5 声明应含有最多两层嵌套指针 建议 可判定 是
C1303 Rule 18.6 不得将自动存储对象的地址复制给在该对象不复存在后仍然存在的另一 个对象 要求 不可判定 是
C1302 Rule 18.7 不得声明灵活数组成员(flexible array members) 要求 可判定 是
C1301 Rule 18.8 不得使用变长数组(variable-length array) 要求 可判定 是
C0302 Rule 19.1 不得将对象赋值或复制给与其重叠的对象 强制 不可判定 是
C0301 Rule 19.2 不应使用关键字union 建议 可判定 是
C0114 Rule 20.1 #include指令之前仅应出现预处理指令或注释 建议 可判定 是
C0113 Rule 20.2 头文件名中不得出现字符'、"或\、以及字符序列/*和// 要求 可判定 是
C0112 Rule 20.3 #include指令后面必须是<filename>或"filename"序列 要求 可判定 是
C0111 Rule 20.4 定义宏名称时不得与关键字同名 要求 可判定 是
C0110 Rule 20.5 不得使用#undef 建议 可判定 是
C0109 Rule 20.6 宏实参中不得有形似预处理指令的词符 要求 可判定 是
C0108 Rule 20.7 宏形参扩展得到的表达式必须在括号内 要求 可判定 是
C0107 Rule 20.8 预处理指令#if或#elif的控制表达式求值结果必须为0或1 要求 可判定 是
C0106 Rule 20.9 预处理指令#if或#elif的控制表达式中的所有标识符必须被#define定义才 能求值 要求 可判定 是
C0105 Rule 20.10 不应使用预处理运算符#和## 建议 可判定 是
C0104 Rule 20.11 如果宏形参后面紧跟#运算符,则不得再紧跟##运算符 要求 可判定 是
C0103 Rule 20.12 用作#或##运算符的操作数的宏形参,如果自身需要进一步进行宏替 换,则只得作为#或##的操作数使用 要求 可判定 是
C0102 Rule 20.13 以#开始的代码行必须为有效预处理指令 要求 可判定 是
C0101 Rule 20.14 所有预处理指令#else、#elif和#endif都必须和它们对应的#if、#ifdef和 #ifndef指令位于同一文件中 要求 可判定 是
C0420 Rule 21.1 #define和#undef不得用于保留标识符(reserved identifier)或保留宏名 称(reserved macro name) 要求 可判定 是
C0419 Rule 21.2 不得声明保留标识符(reserved identifier) 和保留宏名称(reserved macro name) 要求 可判定 是
C0418 Rule 21.3 不得使用<stdlib.h>提供的内存分配和回收(deallocation) 函数 要求 可判定 是
C0417 Rule 21.4 不得使用标准头文件<setjmp.h> 要求 可判定 是
C0416 Rule 21.5 不得使用标准头文件<signal.h> 要求 可判定 是
C0415 Rule 21.6 不得使用标准库输入/输出函数 要求 可判定 是
C0414 Rule 21.7 不得使用<stdlib.h>提供的标准库函数atof,atoi, atol,以及atoll函数 要求 可判定 是
C0413 Rule 21.8 不得使用<stdlib.h>提供的标准库终止函数(termination function) 要求 可判定 是
C0412 Rule 21.9 不得使用<stdlib.h>提供的标准库函数bsearch和qsort函数 要求 可判定 是
C0411 Rule 21.10 不得使用标准库中的时间(time)和日期(date)函数 要求 可判定 是
C0410 Rule 21.11 不得使用标准头文件<tgmath.h> 要求 可判定 是
C0409 Rule 21.12 不应使用<fenv.h>的异常处理特性 建议 可判定 是
C0408 Rule 21.13 传递给<ctype.h>函数的值必须能够表示为无符号char或EOF类型 强制 不可判定 是
C0407 Rule 21.14 不得使用标准库函数 memcmp 比较空终止字符串(null terminated string) 要求 不可判定 是
C0406 Rule 21.15 指向标准库函数memcpy,memmove和memcmp的指针实参必须全部为 指向兼容类型的限定或非限定版本的指针 要求 可判定 是
C0405 Rule 21.16 标准库函数memcmp的指针实参必须指向指针类型,或者指向基本有符 号类型,基本布尔类型或基本枚举类型 要求 可判定 是
C0404 Rule 21.17 使用<string.h>提供的字符串处理函数(string handling functions)产生 的访问不得超越指针形参引用的对象的边界 强制 不可判定 是
C0403 Rule 21.18 size_t实参若传递给任意<string.h>提供的函数,则必须有恰当的值 强制 不可判定 是
C0402 Rule 21.19 标准库函数localeconv, getenv, setlocale或strerror返回的指针只得作 为const修饰类型的指针使用 强制 不可判定 是
C0401 Rule 21.20 标准库函数 asctime / ctime / gmtime / localtime / localeconv / getenv / setlocale / strerror 返回的指针后面不得紧跟对同一函数的调用 强制 不可判定 是
C0421 Rule 21.21 不得使用<stdlib.h>中的标准库函数system 要求 可判定 是
C0210 Rule 22.1 通过标准库函数动态获取的所有资源都必须被显式释放 要求 不可判定 是
C0209 Rule 22.2 只有通过标准库函数分配的内存块才能被释放 强制 不可判定 是
C0208 Rule 22.3 不得在不同文件流上同时打开同一文件进行读写访问 要求 不可判定 是
C0207 Rule 22.4 不得对只读文件流进行写入操作 强制 不可判定 是
C0206 Rule 22.5 不得解引用(dereference)指向FILE对象的指针 强制 不可判定 是
C0205 Rule 22.6 不得在相关文件流关闭后使用FILE指针的值 强制 不可判定 是
C0204 Rule 22.7 宏EOF只得与任何能够返回EOF的标准库函数的未修改返回值比较 要求 不可判定 是
C0203 Rule 22.8 在调用errno设置函数之前必须将errno值设置为零 要求 不可判定 是
C0202 Rule 22.9 调用errno设置函数后必须检测errno值是否为零 要求 不可判定 是
C0201 Rule 22.10 只有上一个被调用的函数是errno设置函数的情况下才能检测errno值 要求 不可判定 是

 

============ End

 

posted @ 2025-10-19 11:08  lsgxeva  阅读(1771)  评论(0)    收藏  举报