MISRA-C编程规范

1 MISRA-C概述

1.1 背景与目的

MISRA-C全称 Motor Industry Software Reliability Association (汽车工业软件可靠性协会),是由汽车制造商、零部件供应商、工程咨询师代表组成的联盟,旨在"推动开发安全相关的嵌入式软件在车辆及其他嵌入式系统中的最佳实践"。

MISRA C起源于汽车制造业,旨在通过制定严格的编码规范来提高嵌入式软件的质量、安全性和可移植性。随着技术的发展,MISRA C已逐渐扩展到其他领域,如轨道交通、医疗器械、航空航天等,成为编写安全攸关C代码的最佳实践。

1.2 发展历程

MISRA C先后发布了多个版本,其中MISRA C:2012是当前的最新版本。该版本于2019年2月发布(也有说法是2012年发布,但可能指的是初步发布或广泛认可的年份),整合了此前发布的修订案(AMD)和技术更正(TC)的内容,共包含175条编码准则。

二十世纪九十年代,汽车电子产品的重要性日益凸显,汽车电子产品中的软件也随之变得举足轻重。人们迫切地需要一个编码规范来保证汽车电子软件的可靠性,在这一背景下,MISRA C编码规范应运而生。经过多年的发展,ISRA C已经成为世界范围内使用最为广泛的、面向多个行业的C语言编码规范。

汽车工业软件可靠性协会先后发布了三代MISRA C编码规范:

1998年,第一代MISRA C编码规范:MISRA C:1998(MC1)

2004年,第二代MISRA C编码规范:MISRA C:2004(MC2)

2013年,第三代MISRA C编码规范:MISRA C:2012(MC3)

MISRA C:2012是由汽车工业软件可靠性协会(MISRA, Motor Industry Software Reliability Association)制定的一套针对C语言的编码准则,旨在提升嵌入式系统的安全性和可移植性。

1.3 编码准则内容

MISRA C:2012的编码准则分为两大类:指令(Directives)和规则(Rules)。

  • 指令:共17项,不具备完整描述,仅分析源代码无法进行合规性判定,需要结合设计文档或开发人员经验综合判定。
  • 规则:共158条,具备完整的描述和要求,无需额外的信息就可以对源代码进行合规性判定。

这些准则涵盖了标准C环境、未使用代码处理、数据类型和表达式、控制流、函数等多个方面,旨在最大限度地消除或减少编程错误。

1.4 重要性与作用

MISRA C:2012的重要性在于它提供了一个统一的、可遵循的C语言编程标准,有助于减少因编程习惯、风格差异等因素导致的软件缺陷。同时,遵循MISRA C:2012也有助于提升代码的可读性、可维护性和可移植性,降低后期维护和升级的成本。

1.5 实施与检查

为了确保代码符合MISRA C:2012的要求,通常需要借助静态分析工具进行代码合规性检查。这些工具能够自动识别代码中的潜在问题,并给出相应的改进建议。此外,人工代码审查也是确保代码质量的重要手段之一。

1.6 结论

综上所述,MISRA C:2012是一套针对C语言的、旨在提升嵌入式系统安全性和可移植性的编码准则。它包含了丰富的编码规则和指令,为开发人员提供了明确的指导和规范。通过遵循MISRA C:2012的要求,并借助适当的工具进行代码检查和审查,可以显著降低软件缺陷的风险,提高软件的整体质量。

2 MISRA-C:视野

3 MISRA-C:范围

4 使用 MISRA-C

5 规则简介

6 规则

6.1 环境

  • 规则 1.1(强制): 所有代码都必须遵照 ISO 9899:1990 “Programming languages - C”,由 ISO/IEC 9899/COR1:1995,ISO/IEC 9899/AMD1:1995,和 ISO/IEC 9899/COR2:1996 修订。[MISRA Guidelines Table 3;IEC 61508 Part 7:Table C.1]

    这些方针建立在 ISO 9899:1990 [2] 之上,它是由 ISO/IEC 9899/COR1:1995 [4],ISO/IEC 9899/AMD1:1995 [5] 以及 ISO/IEC 9899/COR2:1996 [6] 修订的。并没有适应于 ISO 9899:1999 [8] 标准版本的声明,本文档中任何提及“Standard C”的地方都是指旧有的 ISO9899:1990 [2]标准。

    必须认识到“背离”(描述见 4.3.2 节)的提出是必要的,这能允许一定的语言扩展,比如对硬件特性的支持。

    当 ISO 9899:1990 5.2.4 [2] 中指定的环境限制超出了范围,就需要提出“背离”,除了后面的规则 5.1。

  • 规则 1.2(强制): 不能有对未定义行为或未指定行为的依赖性。

    这项规则要求任何对未定义行为或未指定行为的依赖,除非在其他规则中做了特殊说明,都应该避免。如果其他某项规则中声明了某个特殊行为,那么就只有这项特定规则在其需要时给出背离性。这些问题的完整列表详见 ISO 9899:1990 附录 G [2]。

  • 规则 1.3(强制): 多个编译器和/或语言只能在为语言/编译器/汇编器所适合的目标代码定义了通用接口标准时使用。 [未指定 11]

    如果一个模块是以非 C 语言实现的或是以不同的 C 编译器编译的,那么必须要保证该模块能够正确地同其他模块集成。C 语言行为的某些特征依赖于编译器,于是这些行为必须能够为使用的编译器所理解。例如:栈的使用、参数的传递和数据值的存储方式(长度、排列、别名、覆盖,等等)。

  • 规则 1.4(强制): 编译器/链接器要确保 31 个有效字符和大小写敏感能被外部标识符支持。 [未定义 7;实现 5、6]

    ISO 标准要求外部标识符的头 6 个字符是截然不同的。然而由于大多数编译器/链接器允许至少 31 个有效字符(如同内部标识符),因此对这样严格而并不具有帮助性的限制的适应性被认为是不必要的 。

    必须检查编译器/链接器具有这种特性,如果编译器/链接器不能满足这种限制,就使用编译器本身的约束。

  • 规则 1.5(建议): 浮点应用应该适应于已定义的浮点标准

    浮点运算会带来许多问题,一些问题(而不是全部)可以通过适应已定义的标准来克服。其中一个合适的标准是 ANSI/IEEE Std 754 [21]。

    同规则 6.3 相一致,浮点类型的定义提供了一个注释所用浮点标准的机会,如:

    /* IEEE 754 single-precision floating-point */
    typedef float float32_t; 
    

6.2 语言扩展

  • 规则 2.1(强制): 汇编语言应该被封装并隔离。 [未指定 11]

    在需要使用汇编指令的地方,建议以如下方式封装并隔离这些指令:(a) 汇编函数、(b) C 函数、(c) 宏。

    出于效率的考虑,有时必须要嵌入一些简单的汇编指令,如开关中断。如果不管出于什么原因需要这样做,那么最好使用宏来完成。

    需要注意的是,内嵌的汇编语言的使用是对标准 C 的扩展,因此也需要提出对规则1.1 的背离。

    #define NOP asm (“ NOP”); 
    
  • 规则 2.2(强制): 源代码应该使用 /*…*/ 类型的注释。

    这排除了如 // 这样 C99 类型的注释和 C++类型的注释,因为它在 C90 中是不允许的。许多编译器支持 // 类型的注释以做为对 C90 的扩展。预处理指令(如#define)中 // 的使用可以改变,/*…*/和//的混合使用也是不一致的。这不仅是类型问题,因为不同的编译器(在 C99之前)可能会有不同的行为。

  • 规则 2.3(强制): 字符序列 /* 不应出现在注释中。

    C 不支持注释的嵌套,尽管一些编译器支持它以做为语言扩展。一段注释以 /* 开头,直到第一个 */ 为止,在这当中出现的任何/*都违反了本规则。考虑如下代码段:

    /* some comment, end comment marker accidentally omitted
    <<New Page>>
    Perform_Critical_Safety_Function (X);
    /* this comment is not compliant */ 
    

    在检查包含函数调用的页中,假设它是可执行代码。

    因为可能会省略掉注释的结束标记,那么对安全关键函数的调用将不会被执行。

  • 规则 2.4(建议): 代码段不应被“注释掉”(comment out)。

    当源代码段不需要被编译时,应该使用条件编译来完成(如带有注释的#if 或#ifdef 结构)。为这种目的使用注释的开始和结束标记是危险的,因为 C 不支持嵌套的注释,而且已经存在于代码段中的任何注释将影响执行的结果。

6.3 文档

  • 规则 3.1(强制): 所有实现定义(implementation-defined)的行为的使用都应该文档化。

    本规则要求,任何对实现定义的行为的依赖——这些行为在其他规则中没有特别说明的——都应该写成文档,例如对编译器文档的参考。如果一个特定的行为在其他规则中被显式说明了,那么只有那项规则在其需要时给出背离。完整问题的描述详见 ISO 9899:1990 附录 G [2]。

  • 规则 3.2(强制): 字符集和相应的编码应该文档化。

    例如,ISO 10646 [22]定义了字符集映射到数字值的国际标准。出于可移植性的考虑,字符常量和字符串只能包含映射到已经文档化的子集中的字符。

  • 规则 3.3(建议): 应该确定、文档化和重视所选编译器中整数除法的实现。 [实现 18]

    当两个有符号整型数做除法时,ISO 兼容的编译器的运算可能会为正或为负。首先,它可能以负余数向上四舍五入(如,-5/3 = -1,余数为-2),或者可能以正余数向下四舍五入(如, -5/3 = -2,余数为+1)。重要的是要确定这两种运算中编译器实现的是哪一种,并以文档方式提供给编程人员,特别是第二种情况(通常这种情况比较少)。

  • 规则 3.4(强制): 所有#pragma 指令的使用应该文档化并给出良好解释。 [实现 40]

    这项规则为本文档的使用者提供了产生其应用中使用的任何 pragma 的要求。每个 pragma的含义要写成文档,文档中应当包含完全可理解的对 pragma 行为及其在应用中之含义的充分描述。

    应当尽量减少任何 pragma 的使用,尽可能地把它们本地化和封装成专门的函数。

  • 规则 3.5(强制): 如果做为其他特性的支撑,实现定义(implementation-defined)的行为和位域(bitfields)集合应当文档化。 [未指定 10;实现 30、31]

    这是在使用了规则 6.4 和规则 6.5 中描述的非良好定义的位域时遇到的特定问题。C 当中的位域是该语言中最缺乏良好定义的部分之一。位域的使用可能体现在两个主要方面:

    • 为了在大的数据类型(同 union 一起)中访问独立的数据位或成组数据位。该用法是不允许的(见规则 18.4)。
    • 为了访问用于节省存储空间而打包的标志(flags)或其他短型(short-length)数据。

    为了有效利用存储空间而做的短型数据的打包,是本文档所设想的唯一可接受的位域使用。假定结构元素只能以其名字来访问,那么程序员就无需设想结构体中位域的存储方式。

    我们建议结构的声明要保持位域的设置,并且在同一个结构中不得包含其他任何数据。要注意的是,在定义位域的时候不需要追随规则 6.3,因为它们的长度已经定义在结构中了。

    如果编译器带有一个开关以强制位域遵循某个特定的布局,那么它有助于下面的判断。

    例如下面可接受的代码:

    struct message     /* Struct is for bit-fields only */
    {
    signed int little: 4; /* Note: use of basic types is required */
    unsigned int x_set: 1;
    unsigned int y_set: 1;
    }message_chunk; 
    

    如果要使用位域,就得注意实现定义的行为所存在的领域及其潜藏的缺陷(意即不可移植性)。特别地,程序员应当注意如下问题:

    • 位域在存储单元中的分配是实现定义(implementation-defined)的,也就是说,它们在存储单元(通常是一个字节)中是高端在后(high end)还是低端在后(low end) 的。
    • 位域是否重叠了存储单元的界限同样是实现定义的行为(例如,如果顺序存储一个 6 位的域和一个 4 位的域,那么 4 位的域是全部从新的字节开始,还是其中 2 位占据一个字节中的剩余 2 位而其他 2 位开始于下个字节)。
  • 规则 3.6(强制): 产品代码中使用的所有库都要适应本文档给出的要求,并且要经过适当的验证。[IEC 61508 Part 3]

    本规则的对象是产品代码中的任意库,因此这些库可能包含编译器提供的标准库、其他第三方的库或者实验室中自己开发的库。这是由 IEC 61508 Part 3 建议的。

6.4 字符集

  • 规则 4.1(强制): 只能使用 ISO C 标准中定义的 escape 序列。 [未定义 11;实现 11]

    参见 5.2.2 节中关于有效 escape 序列的 ISO 标准。

  • 规则 4.2(强制): 不能使用三字母词(trigraphs)。

    三字母词由 2 个问号序列后跟 1 个确定字符组成(如,??- 代表“~”(非)符号,而??)代表“]”符号)。它们可能会对 2 个问号标记的其他使用造成意外的混淆,例如字符串

    “(Date should be in the form ??-??-??)”

    将不会表现为预期的那样,实际上它被编译器解释为

    “(Date should be in the form ~~)”

6.5 标识符

  • 规则 5.1(强制): 标识符(内部的和外部的)的有效字符不能多于 31。 [未定义 7;实现 5、6]

    ISO 标准要求在内部标识符之间前 31 个字符必须是不同的以保证可移植性。即使编译器支持,也不能超出这个限制。

    ISO 标准要求外部标识符之间前 6 个字符必须是不同的(忽略大小写)以保证最佳的可移植性。然而这条限制相当严格并被认为不是必须的。本规则的意图是为了在一定程度上放宽ISO 标准的要求以适应当今的环境,但应当确保 31 个字符/大小写的有效性是可以由实现所支持的。

    使用标识符名称要注意的一个相关问题是发生在名称之间只有一个字符或少数字符不同的情况,特别是名称比较长时,当名称间的区别很容易被误读时问题就比较显著,比如 1(数字 1)和 l(L 的小写)、0 和 O、2 和 Z、5 和 S,或者 n 和 h。建议名称间的区别要显而易见。 在这问题上的特定方针可以放在风格指南中(见 4.2.2 节)。

  • 规则 5.2(强制): 具有内部作用域的标识符不应使用与具有外部作用域的标识符相同的名称,这会隐藏了外部标识符

    外部作用域和内部作用域的定义如下。文件范围内的标识符可以看做是具有最外部(outermost)的作用域;块范围内的标识符看做是具有更内部(more inner)的作用域;连续嵌套的块,其作用域更深入。本规则只是不允许一个第二深层(second inner)的定义隐藏其外层的定义,如果第二个定义没有隐藏第一个定义,那么就不算违反本规则。

    在嵌套的范围中,使用相同名称的标识符隐藏其他标识符会使得代码非常混乱。例如:

    int16_t i;
    {
    int16_t i; /* This is a different variable */
    /* This is not compliant */
    i = 3; /* It could be confusing as to which I this refers */
    } 
    
  • 规则 5.3(强制): typedef 的名字应当是唯一的标识符。

    typedef 的名称不能重用,不管是做为其他 typedef 或者任何目的。例如:

    {
    typedef unsigned char uint8_t;
    }
    {
    typedef unsigned char uint8_t; /* Not compliant – redefinition */
    }
    {
    unsigned char uint8_t; /* Not compliant – reuse of uint8_t */
    } 
    

    typedef 的名称不能在程序中的任何地方重用。如果类型定义是在头文件中完成的,而该头文件被多个源文件包含,不算违背本规则。

  • 规则 5.4(强制): 标签(tag)名称必须是唯一的标识符。

    程序中标签的名字不可重用,不管是做为另外的标签还是出于其他目的。ISO 9899:1990 [2]没有定义当一个聚合体的声明以不同形式的类型标识符(struct 或 union)使用同一个标签时的行为。标签的所有使用或者用于结构类型标识符,或者用于联合类型标识符,例如:

    struct stag { uint16_t a; uint16_t b; };
    struct stag a1 = { 0, 0 }; /* Compliant – compatible with above */
    union stag a2 = { 0, 0 }; /* Not compliant – not compatible with previous declarations */
    
    void foo (void)
    {
    struct stag { uint16_t a; }; /* Not compliant – tag stag redefined */
    } 
    

    如果类型定义是在头文件中完成的,且头文件被多个源文件包含,那么规则不算违背。

  • 规则 5.5(建议): 具有静态存储期的对象或函数标识符不能重用。

    不管作用域如何,具有静态存储期的标识符都不应在系统内的所有源文件中重用。它包含带有外部链接的对象或函数,及带有静态存储类标识符的任何对象或函数。

    由于编译器能够理解这一点而且决不会发生混淆,那么对用户来说就存在着把不相关的变量以相同名字联系起来的可能性。

    这种混淆的例子之一是,在一个文件中存在一个具有内部链接的标识符,而在另外一个文件中存在着具有外部链接的相同名字的标识符。

  • 规则 5.6(建议): 一个命名空间中不应存在与另外一个命名空间中的标识符拼写相同的标识符,除了结构和联合中的成员名字。

    命名空间与作用域(scope)是不同的,本规则不考虑作用域。例如,ISO C 允许在一个作用域内为标签(tag)和 typedef 使用相同的标识符(vector)

    typedef struct vector ( uint16_t x ; uint16_t y ; uint16_t z ; ) vector ;
    /* Rule violation ^^ ^^ */ 
    

    ISO C 定义了许多不同的命名空间(见 ISO 9899 :1990 6.1.2.3 [2])。技术上,在彼此独立的命名空间中使用相同的名字以代表完全不同的项目是可能的,然而由于会引起混淆,通常不赞成这种做法,因此即使是在独立的命名空间中名字也不能重用。

    下面给出了违背此规则的例子,其中 value 在不经意中代替了 record.value:

    struct { int16_t key ; int16_t value ; } record ;
    int16_t value; /* Rule violation – 2nd use of value */
    record.key = 1;
    value = 0; /* should have been record.value */ 
    

    相比之下,下面的例子没有违背此规则,因为两个成员名字不会引起混淆:

    struct device_q { struct device_q *next ; /* ... */ }
    devices[N_DEVICES] ;
    struct task_q { struct task_q *next ; /* … */ }
    tasks[N_TASKS];
    device[0].next = &devices[1];
    tasks[0].next = &tasks[1]; 
    
  • 规则 5.7(建议): 不能重用标识符名字。

    不考虑作用域,系统内任何文件中不应重用标识符。本规则和规则 5.2、5.3、5.4、5.5 和 5.6 一同使用。

    struct air_speed
    {
    uint16_t speed; /* knots */
    } *x;
    struct gnd_speed
    {
    uint16_t speed; /* mph */
    /* Not Compliant – speed is in different units */
    } *y;
    x->speed = y->speed; 
    

    当标识符名字用在头文件且头文件包含在多个源文件中时,不算违背本规则。使用严格的命名规范可以支持本规则。

6.6 类型

  • 规则 6.1(强制): 单纯的 char 类型应该只用做存储和使用字符值。 [实现 14]

  • 规则 6.2(强制): signed char 和 unsigned char 类型应该只用做存储和使用数字值。

    有三种不同的 char 类型:(单纯的)char、unsigned char、signed char。unsigned char 和 signed char 用于数字型数据,char 用于字符型数据。单纯 char 类型的符号是实现定义的,不应依赖。

    单纯 char 类型所能接受的操作只有赋值和等于操作符(=、==、!=)。

  • 规则 6.3(建议): 应该使用指示了大小和符号的 typedef 以代替基本数据类型。

    不应使用基本数值类型 char、int、short、long、float 和 doulbe,而应使用特定长度(specific-length)的 typedef。规则 6.3 帮助我们认清存储类型的大小,却不能保证可移植性,这是因为整数提升(integral promotion)的不对称性。关于整数提升的讨论,见节 6.10。仍然 很重要的是要理解整数大小的实现。

    程序员应该注意这些定义之下的 typedef 的实际实现。

    比如,本文档中建议为所有基本数值类型和字符类型使用如下所示的 ISO(POSIX)的 typedef。对于 32 位计算机,它们是:

    typedef char char_t;
    typedef signed char int8_t;
    typedef signed short int16_t;
    typedef signed int int32_t;
    typedef signed long int64_t;
    typedef unsigned char uint8_t;
    typedef unsigned short uint16_t;
    typedef unsigned int uint32_t;
    typedef unsigned long uint64_t;
    typedef float float32_t;
    typedef double float64_t;
    typedef long double float128_t;
    

    在位域类型的说明中,typedef 是不必要的。

  • 规则 6.4(强制): 位域只能被定义为 unsigned int 或 singed int 类型。 [未定义 38;实现 29]

    因为 int 类型的位域可以是 signed 或 unsigned,使用 int 是由实现定义的。由于其行为未被定义,所以不允许为位域使用 enum、short 或 char 类型。

  • 规则 6.5(强制): unsigned int 类型的位域至少应该为 2 bits 长度

    1 bit 长度的有符号位域是无用的。

6.7 常量

  • 规则 7.1(强制): 不应使用八进制常量(零除外)和八进制 escape 序列。 [Koenig 9]

    任何以“0”(零)开始的整型常量都被看做是八进制的,所以这是危险的,如在书写固定长度的常量时。例如,下面为 3 个数字位的总线消息做数组初始化时将产生非预期的结果(052 是八进制的,即十进制的 42):

    code[1] = 109; /* equivalent to decimal 109 */
    code[2] = 100; /* equivalent to decimal 100 */
    code[3] = 052; /* equivalent to decimal 42 */
    code[4] = 071; /* equivalent to decimal 57 */ 
    

    八进制的 escape 序列是有问题的,这是因为在八进制 escape 结尾不经意引入一个十进制数会产生另外一个字符。下面例子中,第一个表达式的值是实现定义的,因为其字符常量包含了两个字符,“\10”和“9”。第二个字符常量表达式包含了单一字符“\100”,如果字符 64 不在基本运算字符集中,这也将是由实现定义的。

    code[5] = ‘\109’ ; /* implementation-defined, two character constant */
    code[6] = ‘\100’ ; /* set to 64, or implementation-defined */ 
    

    最好根本不要使用八进制常量或 escape 序列,并且要静态检查它们是否出现。整数常量 0 (做为单个数字书写的)严格说来是八进制常量,然而在此规则下它也是允许的。

6.8 声明与定义

  • 规则 8.1(强制): 函数应当具有原型声明,且原型在函数的定义和调用范围内都是可见的。 [未定义 22、23]

    原型的使用使得编译器能够检查函数定义和调用的完整性。如果没有原型,就不会迫使编译器检查出函数调用当中的一定错误(比如,函数体具有不同的参数数目,调用和定义之间参数类型的不匹配)。事实证明,函数接口是相当多问题的肇因,因此本规则是相当重要的。

    对外部函数来说,我们建议采用如下方法,在头文件中声明函数(亦即给出其原型),并在所有需要该函数原型的代码文件中包含这个头文件(见规则 8.8)。

    为具有内部链接的函数给出其原型也是良好的编程实践。

  • 规则 8.2(强制): 不论何时声明或定义了一个对象或函数,它的类型都应显式声明。

    extern x; /* Non-compliant – implicit int type */
    extern int16_t x ; /* Compliant – explicit type */
    const y ; /* Non-compliant – implicit int type */
    const int16_t y ; /* Compliant – explicit type */
    static foo (void) ; /* Non-compliant – implicit type */
    static int16_t foo (void) ; /* Compliant – explicit type */ 
    
  • 规则 8.3(强制): 函数的每个参数类型在声明和定义中必须是等同的,函数的返回类型也该是等同的。 [未定义 24;Koenig 59 – 62]

    参数与返回值的类型在原型和定义中必须匹配,这不仅要求等同的基本类型,也要求包含 typedef 名称和限定词在内的类型也要相同。

  • 规则 8.4(强制): 如果对象或函数被声明了多次,那么它们的类型应该是兼容的。 [未定义 10]

    兼容类型的定义是冗长复杂的(详见 ISO 9899 :1990 [2],节 6.1.2.6、6.5.2、6.5.3、6.5.4)。 两个等同的类型必然是兼容的,而两个兼容的类型不需要等同。例如,下面的类型对是兼容的:

    signed int int
    char[5] char []
    unsigned short int unsigend short 
    
  • 规则 8.5(强制): 头文件中不应有对象或函数的定义。

    头文件应该用于声明对象、函数、typedef 和宏,而不应该包含或生成占据存储空间的对象或函数(或它们的片断)的定义。这样就清晰地划分了只有 C 文件才包含可执行的源代码,而头文件只能包含声明。

  • 规则 8.6(强制): 函数应该声明为具有文件作用域。 [未定义 36]

    在块作用域中声明函数会引起混淆并可能导致未定义的行为。

  • 规则 8.7(强制): 如果对象的访问只是在单一的函数中,那么对象应该在块范围内声明。

    可能的情况下,对象的作用域应该限制在函数内。只有当对象需要具有内部或外部链接时才能为其使用文件作用域。当在文件范围内声明对象时,使用规则 8.10。良好的编程实践是,在不必要的情况下避免使用全局标识符。对象声明在最外层或最内层的做法主要是种风格问题。

  • 规则 8.8(强制): 外部对象或函数应该声明在唯一的文件中。 [Koenig 66]

    通常这意味着在一个头文件中声明一个外部标识符,而在定义或使用该标识符的任何文件中包含这个头文件。例如,在头文件 featureX.h 中声明:

    extern int16_t a ; 
    

    然后对 a 进行定义:

    #include <featureX.h>
    int16_t a = 0 ; 
    

    工程中存在的头文件可能是一个或多个,但是任何一个外部对象或函数都只能在一个头文件中声明。

  • 规则 8.9(强制): 具有外部链接的标识符应该具有准确的外部定义。 [未定义 44;Koenig 55、63 – 65]

    一个标识符如果存在多个定义(在不同的文件中)或者甚至没有定义,那么其行为是未经定义的。不同文件中的多个定义是不允许的,即使这些定义相同也不允许;进而如果这些定义不同或者标识符的初始值不同,问题显然很严重。

  • 规则 8.10(强制): 在文件范围内声明和定义的所有对象或函数应该具有内部链接,除非是在需要外部链接的情况下。 [Koenig 56、57]

    如果一个变量只是被同一文件中的函数所使用,那么就用 static。类似地,如果一个函数只是在同一文件中的其他地方调用,那么就用 static。使用 static 存储类标识符将确保标识符只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性。

  • 规则 8.11(强制): static 存储类标识符应该用于具有内部链接的对象和函数的定义和声明。

    static 和 extern 存储类标识符常常是产生混淆的原因。良好的编程习惯是,把 static 关键字一致地应用在所有具有内部链接的对象和函数的声明上。

  • 规则 8.12(强制): 当一个数组声明为具有外部链接,它的大小应该显式声明或者通过初始化进行隐式定义。 [未定义 46]

    int array1[10] ; /* Compliant */
    extern int array2[] ; /* Not compliant */
    int array2[] = { 0, 10, 15 }; /* Compliant */
    

    尽管可以在数组声明不完善时访问其元素,然而仍然是在数组的大小可以显式确定的情况下,这样做才会更为安全。

6.9 初始化

  • 规则 9.1(强制): 所有自动变量在使用前都应被赋值。 [未定义 41]

    本规则的意图是使所有变量在其被读之前已经写过了,除了声明中的初始化。

    注意,根据 ISO C 标准,具有静态存储期的变量缺省地被自动赋予零值,除非经过了显式的初始化。实际中,一些嵌入式环境没有实现这样的缺省行为。静态存储期是所有以 static 存储类形式声明的变量或具有外部链接的变量的共同属性,自动存储期变量通常不是自动初始化的。

  • 规则 9.2(强制): 应该使用大括号以指示和匹配数组和结构的非零初始化构造。 [未定义 42]

    ISO C 要求数组、结构和联合的初始化列表要以一对大括号括起来(尽管不这样做的行为是未定义的)。本规则更进一步地要求,使用附加的大括号来指示嵌套的结构。它迫使程序员显式地考虑和描述复杂数据类型元素(比如,多维数组)的初始化次序。

    例如,下面的例子是二维数组初始化的有效(在 ISO C 中)形式,但第一个与本规则相违背:

    int16_t y[3][2] = { 1, 2, 3, 4, 5, 6 }; /* not compliant */
    int16_t y[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } } ; /* compliant */
    

    在结构中以及在结构、数组和其他类型的嵌套组合中,规则类似。

    还要注意的是,数组或结构的元素可以通过只初始化其首元素的方式初始化(为 0 或NULL)。如果选择了这样的初始化方法,那么首元素应该被初始化为 0(或 NULL),此时不需要使用嵌套的大括号。

    ISO 标准 [2] 包含了更多的初始化例子。

  • 规则 9.3(强制): 在枚举列表中,“=”不能显式用于除首元素之外的元素上,除非所有的元素都是显式初始化的。

    如果枚举列表的成员没有显式地初始化,那么 C 将为其分配一个从 0 开始的整数序列, 首元素为 0,后续元素依次加 1。

    如上规则允许的,首元素的显式初始化迫使整数的分配从这个给定的值开始。当采用这种方法时,重要的是确保所用初始化值一定要足够小,这样列表中的后续值就不会超出该枚举常量所用的 int 存储量。

    列表中所有项目的显式初始化也是允许的,它防止了易产生错误的自动与手动分配的混合。然而,程序员就该担负职责以保证所有值都处在要求的范围内以及值不是被无意复制的。

    enum colour { red = 3, blue, green, yellow = 5 } ; /* not compliant */
    /* green and yellow represent the same value – this is duplication */
    enum colour { red = 3, blue = 4, green = 5, yellow = 5 }; /* compliant */
    /* green and yellow represent the same value – this is duplication */ 
    

6.10 数值类型转换

  • 6.10.1 隐式和显式类型转换

    C 语言给程序员提供了相当大的自由度并允许不同数值类型可以自动转换。由于某些功能性的原因可以引入显式的强制转换,例如:

    • 用以改变类型使得后续的数值操作可以进行
    • 用以截取数值
    • 出于清晰的角度,用以执行显式的类型转换

    为了代码清晰的目的而插入的强制转换通常是有用的,但如果过多使用就会导致程序的可读性下降。正如下面所描述的,一些隐式转换是可以安全地忽略的,而另一些则不能。

  • 6.10.2 隐式转换的类型

    存在三种隐式转换的类别需要加以区分。

    1. 整数提升(Integral promotion)转换

      整数提升描述了一个过程,借此过程数值操作总是在 int 或 long(signed 或 unsigned)整 型操作数上进行。其他整型操作数(char、short、bit-field 和 enum)在数值操作前总是先转化为 int 或 unsigned int 类型。这些类型称为 small integer 类型。

      整数提升的规则命令,在大多数数值操作中,如果 int 类型能够代表原来类型的所有值,那么 small integer 类型的操作数要被转化为 int 类型;否则就被转化为 unsigned int。

      注意,整数提升:

      • 仅仅应用在 small integer 类型上
      • 应用在一元、二元和三元操作数上
      • 不能用在逻辑操作符(&&、||、!)的操作数上
      • 用在 switch 语句的控制表达式上

      整数提升经常和操作数的“平衡”(balancing,后面提到)发生混淆。事实上,整数提升发生在一元操作的过程中,如果二元操作的两个操作数是同样类型的,那么也可以发生在二元操作之上。

      由于整数提升,两个类型为 unsigned short 的对象相加的结果总是 signed int 或 unsigned int 类型的;事实上,加法是在后面两种类型上执行的。因此对于这样的操作,就有可能获得一个其值超出了原始操作数类型大小的结果。例如,如果 int 类型的大小是 32 位,那么就能够把两个 short(16 位)类型的对象相乘并获得一个 32 位的结果,而没有溢出的危险。另一方面,如果 int 类型仅是 16 位,那么两个 16 位对象的乘积将只能产生一个 16 位的结果,同时必须对操作数的大小给出适当的限制。

      整数提升还可以应用在一元操作符上。例如,对一个 unsigned char 操作数执行位非(~) 运算,其结果典型地是 signed int 类型的负值。

      整数提升是 C 语言中本质上的不一致性,在这当中 small integer 类型的行为与 long 和 int 类型不同。MISRA-C 鼓励使用 typedef。然而,由于众多整型的行为是不一致的,忽略基本类型(见后面的描述)可能是不安全的,除非对表达式的构造方式给出一些限制。后面规则的意图是想中和整数提升的后果以避免这些异常。

    2. 赋值转换

      赋值转换发生在:

      • 赋值表达式的类型被转化成赋值对象的类型时
      • 初始化表达式的类型被转化成初始化对象的类型时
      • 函数调用参数的类型被转化成函数原型中声明的形式参数的类型时
      • 返回语句中用到的表达式的类型被转化成函数原型中声明的函数类型时
      • switch-case 标签中的常量表达式的类型被转化成控制表达式的提升类型时。这个转换仅用于比较的目的

      每种情况中,必要时数值表达式的值是无条件转换到其他类型的。

    3. 平衡转换(Balancing conversions)

      平衡转换的描述是在 ISO C 标准中的“Usual Arithmetic Conversions”条目下。这套规则提供一个机制,当二元操作符的两个操作数要平衡为一个通用类型时或三元操作符(? :)的第二、第三个操作数要平衡为一个通用类型时,产生一个通用类型。平衡转换总是涉及到两个不同类型的操作数;其中一个、有时是两个需要进行隐式转换。整数提升(上面描述的)的过程使得平衡转换规则变得复杂起来,在整数提升时,small integer 类型的操作数首先要提升到 int 或 unsigned int 类型。整数提升是常见的数值转换,即使两个操作数的类型一致。

      与平衡转换明显相关的操作符是:

      • 乘除 *、/、%
      • 加减 +、-
      • 位操作 &、^、|
      • 条件操作符 (… ? … : …)
      • 关系操作符 >、>=、<、<=
      • 等值操作符 ==、!=

      其中大部分操作符产生的结果类型是由平衡过程产生的,除了关系和等值操作符,它们产生具有 int 类型的布尔值。

      要注意的是,位移操作符(<<和>>)的操作数不进行平衡,运算结果被提升为第一个操作数的类型;第二个操作数可以是任何有符号或无符号的整型。

  • 6.10.3 危险的类型转换

    类型转换过程中存在大量潜在的危险需要加以避免:

    • 数值的丢失:转化后的类型其数值量级不能被体现
    • 符号的丢失:从有符号类型转换为无符号类型会导致符号的丢失
    • 精度的丢失:从浮点类型转换为整型会导致精度的丢失

    对于所有数据和所有可能的兼容性实现来说,唯一可以确保为安全的类型转换是:

    • 整数值进行带符号的转换到更宽类型
    • 浮点类型转换到更宽的浮点类型

    当然,在实践中,如果假定了典型类型的大小,也能够把其他类型转换归类为安全的。普遍来说,MISRA-C:2004 采取的原则是,利用显式的转换来辨识潜藏的危险类型转换。

    类型转换中还有其他的一些危险需要认清。这些问题产生于 C 语言的难度和误解,而不是由于数据值不能保留。

    1)整数提升中的类型放宽:整数表达式运算的类型依赖于经过整数提升后的操作数的类型。总是能够把两个 8 位数据相乘并在有量级需要时访问 16 位的结果。有时而不总是能够把两个 16 位数相乘并得到一个 32 位结果。这是 C 语言中比较危险的不一致性,为了避免混淆,安全的做法是不要依赖由整数提升所提供的类型放宽。考虑如下例子:

    uint16_t u16a = 40000; /* unsigned short / unsigned int ? */
    uint16_t u16b = 30000; /* unsigned short / unsigned int ? */
    uint32_t u32x; /* unsigned int / unsigned long ? */
    u32x = u16a + u16b; /* u32x = 70000 or 4464 ? */ 
    

    期望的结果是 70000,但是赋给 u 的值在实际中依赖于 int 实现的大小。如果 int 实现的大小是 32 位,那么加法就会在有符号的 32 位数值上运算并且保存下正确的值。如果 int 实现的大小仅是 16 位,那么加法会在无符号的 16 位数值上进行,于是会发生折叠(wraparound)现象并产生值 4464(70000%65536)。无符号数值的折叠(wraparound) 是经过良好定义的甚至是有意的;但也会存在潜藏的混淆。

    2)类型计算的混淆:程序员中常见的概念混乱也会产生类似的问题,人们经常会以为参与运算的类型在某种方式上受到被赋值或转换的结果类型的影响。例如,在下面的代码中,两个 16 位对象进行 16 位的加法运算(除非被提升为 32 位 int),其结果在赋值时被转换为 uint32_t 类型。

    u32x = u16a + u16b;

    并非少见的是,程序员会认为此表达式执行的是 32 位加法——因为 u32x 的类型。

    对这种特性的混淆不只局限于整数运算或隐式转换,下面的例子描述了在某些语句中,结果是良好定义的但运算并不会按照程序员设想的那样进行。

    • u32a = (uint32_t) (u16a * u16b);
    • f64a = u16a / u16b ;
    • f32a = (float32_t) (u16a / u16b) ;
    • f64a = f32a + f32b ;
    • f64a = (float64_t) (f32a + f32b) ;

    3)数学运算中符号的改变:整数提升经常会导致两个无符号的操作数产生一个(signed)int 类型的结果。比如,如果 int 是 32 位的,那么两个 16 位无符号数的加法将产生一个有符号的 32 位结果;而如果 int 是 16 位的,那么同样运算会产生一个无符号的 16 位结果。

    4)位运算中符号的改变:当位运算符应用在无符号短整型时,整数提升会有某些特别不利的反响。比如,在一个 unsigned char 类型的操作数上做位补运算通常会产生其值为负的 (signed)int 类型结果。在运算之前,操作数被提升为 int 类型,并且多出来的那些高位被补运算置 1。那些多余位的个数,若有的话,依赖于 int 的大小,而且在补运算后接右移运算是危险的。

    为了避免上述问题产生的危险,重要的是要建立一些准则以限制构建表达式的方式。这里首先给出某些概念的定义。

  • 6.10.4 基本类型(underlying type)

    表达式的类型是指其运算结果的类型。当两个 long 类型的值相加时,表达式具有 long 类型。大多数数值运算符产生其类型依赖于操作数类型的结果。另一方面,某些操作符会给出具有 int 类型的布尔结果而不管其操作数类型如何。所以,举例来说,当两个 long 类型的项用关系运算符做比较时,该表达式的类型为 int。

    术语“基本类型”的定义是,在不考虑整数提升的作用下描述由计算表达式而得到的类型。

    当两个 int 类型的操作数相加时,结果是 int 类型,那么表达式可以说具有 int 类型。

    当两个 unsigned char 类型的数相加时,结果也是 int 类型(通常如此,因为整数提升的原 因),但是该表达式基本的类型按照定义则是 unsigned char。

    术语“基本类型”不是 C 语言标准或其他 C 语言的文本所认知的,但在描述如下规则时它很有用。它描述了一个假想的对 C 语言的违背,其中不存在整数提升而且常用的数值转换一致性地应用于所有的整数类型。引进这样的概念是因为整数提升很敏感且有时是危险的。整数提升是 C 语言中不可避免的特性,但是这些规则的意图是要使整数提升的作用能够通过不利用发生在 small integer 操作数上的宽度扩展来中和。

    当然,C 标准没有显式地定义在缺乏整数提升时 small integer 类型如何平衡为通用类型,尽管标准确实建立了值保留(value-perserving)原则。

    当 int 类型的数相加时,程序员必须要确保运算结果不会超出 int 类型所能体现的值。如果他没有这样做,就可能会发生溢出而结果值是未定义的。这里描述的方法意欲使得在做 small integer 类型的加法时使用同样的原则;程序员要确保两个 unsigned char 类型的数相加的结果是能够被 unsigned char 体现的,即使整数提升会引起更大类型的计算。换句话说,对表达式基本类型的遵守要多于其真实类型。

    整数常量表达式的基本类型

    C 语言的一个不利方面是,它不能定义一个 char 或 short 类型的整型常量。比如,值“5” 可以通过附加的一个合适的后缀来表示为 int、unsigned int、long 或 unsigned long 类型的常量; 但没有合适的后缀用来创建不同的 char 或 short 类型的常量形式。这为维护表达式中的类型一致性提出了困难。如果需要为一个 unsigned char 类型的对象赋值,那么或者要承受对一个整数类型的隐式转换,或者要实行强制转换。很多人会辩称在这种情况下使用强制转换只能导致可读性下降。

    在初始化、函数参数或数值表达式中需要常量时也会遇到同样的问题。然而只要遵守强类型(strong typing)原则,这个问题就会比较乐观。

    解决该问题的一个方法是,想象整型常量、枚举常量、字符常量或者整型常量表达式具有适合其量级的类型。这个目标可以通过以下方法达到,即延伸基本类型的概念到整型常量上,并想象在可能的情况下数值常量已经通过提升想象中的具有较小基本类型的常量而获得。

    这样,整型常量表达式的基本类型就可以如下定义:

    1. 如果表达式的真实类型是(signed)int,其基本类型就是能够体现其值的最小的有符号整型。
    2. 如果表达式的真实类型是 unsigned int,其基本类型就是能够体现其值的最小的无符号整型。
    3. 在所有其他情况下,表达式的基本类型与其真实类型相同。

    在常规的体系结构中,整型常量表达式的基本类型可以根据其量级和符号确定如下:

    注意,基本类型是人造的概念,它不会以任何方式影响实际运算的类型。这个概念的提出只是为了定义一个在其中可以构建数值表达式的安全框架。

  • 6.10.5 复杂表达式(complex expressions)

    后面章节中描述的类型转换规则在某些地方是针对“复杂表达式”的概念。术语“复杂 表达式”意味着任何不是如下类型的表达式:

    • 非常量表达式
    • lvalue(即一个对象)
    • 函数的返回值

    应用在复杂表达式的转换也要加以限制以避免上面总结出的危险。特别地,表达式中的数值运算序列需要以相同类型进行。

    以下是复杂表达式:

    • s8a + s8b
    • ~u16a
    • u16a >> 2
    • foo (2) + u8a
    • *ppc + 1
    • ++u8a

    以下不是复杂表达式,尽管某些包含了复杂的子表达式:

    • pc[u8a]
    • foo (u8a + u8b)
    • ++ppuc
    • ** (ppc + 1)
    • pcbuf[s16a * 2]
  • 6.10.6 隐式类型转换

    规则 10.1(强制): 下列条件成立时,整型表达式的值不应隐式转换为不同的基本类型:

    a) 转换不是带符号的向更宽整数类型的转换,或者

    b) 表达式是复杂表达式,或者

    c) 表达式不是常量而是函数参数,或者

    d) 表达式不是常量而是返回的表达式。

    规则 10.2(强制):

    下列条件成立时,浮点类型表达式的值不应隐式转换为不同的类型:

    a) 转换不是向更宽浮点类型的转换,或者

    b) 表达式是复杂表达式,或者

    c) 表达式是函数参数,或者

    d) 表达式是返回表达式。

    还要注意,在描述整型转换时,始终关注的是基本类型而非真实类型。

    这两个规则广泛地封装了下列原则:

    • 有符号和无符号之间没有隐式转换
    • 整型和浮点类型之间没有隐式转换
    • 没有从宽类型向窄类型的隐式转换
    • 函数参数没有隐式转换
    • 函数的返回表达式没有隐式转换
    • 复杂表达式没有隐式转换

    限制复杂表达式的隐式转换的目的,是为了要求在一个表达式里的数值运算序列中,所有的运算应该准确地以相同的数值类型进行。注意这并不是说表达式中的所有操作数必须具备相同的类型。

    表达式 u32a + u16b + u16c 是合适的——两个加法在概念上(notionally)都以 U32 类型进行

    表达式 u16a + u16b + u32c 是不合适的——第一个加法在概念上以 U16 类型进行,第二个加法是 U32 类型的。

    使用名词“在概念上”是因为,在实际中数值运算的类型将依赖于 int 实现的大小。通过遵循这样的原则,所有运算都以一致的(基本)类型来进行,能够避免程序员产生的混淆和与整数提升有关的某些危险。

    extern void foo1 (uint8_t x);
    int16_t t1 (void)
    {
    …
    foo1 (u8a); /* compliant */
    foo1 (u8a + u8b); /* compliant */
    foo1 (s8a); /* not compliant */
    foo1 (u16a); /* not compliant */
    foo1 (2); /* not compliant */
    foo1 (2U); /* compliant */
    foo1 ( (uint8_t) 2 ); /* compliant */
    … s8a + u8a /* not compliant */
    … s8a + (int8_t) u8a /* compliant */
    s8b = u8a; /* not compliant */
    … u8a + 5 /* not compliant */
    … u8a + 5U /* compliant */
    … u8a + (uint8_t) 5 /* compliant */
    u8a = u16a; /* not compliant */
    u8a = (uint8_t) u16a; /* compliant */
    u8a = 5UL; /* not compliant */
    … u8a + 10UL /* compliant */
    u8a = 5U; /* compliant */
    … u8a + 3 /* not compliant */
    … u8a >> 3 /* compliant */
    … u8a >> 3U /* compliant */
    pca = “P”; /* compliant */
    … s32a + 80000 /* compliant */
    … s32a + 80000L /* compliant */
    f32a = f64a; /* not compliant */
    f32a = 2.5; /* not compliant –
    unsuffixed floating
    constants are of type
    double */
    u8a = u8b + u8c; /* compliant */
    s16a = u8b + u8b; /* not compliant */
    s32a = u8b + u8c; /* not compliant */
    f32a = 2.5F; /* compliant */
    u8a = f32a; /* not compliant */
    s32a = 1.0; /* not compliant */
    f32a = 1; /* not compliant */
    f32a = s16a; /* not compliant */
    … f32a + 1 /* not compliant */
    … f64a * s32a /* not compliant */
    …
    return (s32a); /* not compliant */
    …
    return (s16a); /* compliant */
    …
    return (20000); /* compliant */
    …
    return (20000L); /* not compliant */
    …
    return (s8a); /* not compliant */
    …
    return (u16a); /* not compliant */
    } 
    
    int16_t foo2 (void)
    {
    …
    … (u16a + u16b) + u32a /* not compliant */
    … s32a + s8a + s8b /* compliant */
    … s8a + s8b + s32a /* not compliant */
    f64a = f32a + f32b; /* not compliant */
    f64a = f64b + f32a; /* compliant */
    f64a = s32a / s32b; /* not compliant */
    u32a = u16a + u16a; /* not compliant */
    s16a = s8a; /* compliant */
    s16a = s16b + 20000; /* compliant */
    s32a = s16a + 20000; /* not compliant */
    s32a = s16a + (int32_t) 20000; /* compliant */
    u16a = u16b + u8a; /* compliant */
    foo1 (u16a); /* not compliant */
    foo1 (u8a + u8b); /* compliant */
    …
    return s16a; /* compliant */
    … 
    return s8a; /* not compliant */
    }
    
  • 6.10.7 显式转换(强制转换)

    规则 10.3(强制): 整型复杂表达式的值只能强制转换到更窄的类型且与表达式的基本类型具有相同的符号。

    规则 10.4(强制): 浮点类型复杂表达式的值只能强制转换到更窄的浮点类型。

    如果强制转换要用在任何复杂表达式上,可以应用的转换的类型应该严格限制。如 6.10 节所阐释的,复杂表达式的转换经常是混淆的来源,保持谨慎是明智的做法。为了符合这些规则,有必要使用临时变量并引进附加的语句。

    … (float32_t) (f64a + f64b) /* compliant */
    … (float64_t) (f32a + f32b) /* not compliant */
    … (float64_t) f32a /* compliant */
    … (float64_t) (s32a / s32b) /* not compliant */
    … (float64_t) (s32a > s32b) /* not compliant */
    … (float64_t) s32a / (float32_t) s32b /* compliant */
    … (uint32_t) (u16a + u16b) /* not compliant */
    … (uint32_t) u16a + u16b /* compliant */
    ... (uint32_t) u16a + (uint32_t) u16b /* compliant */
    ... (int16_t) (s32a – 12345) /* compliant */
    ... (uint8_t) (u16a * u16b) /* compliant */
    ... (uint16_t) (u8a * u8b) /* not compliant */
    ... (int16_t) (s32a * s32b) /* compliant */
    ... (int32_t) (s16a * s16b) /* not compliant */
    … (uint16_t) (f64a + f64b) /* not compliant */
    … (float32_t) (u16a + u16b) /* not compliant */
    … (float64_t) foo1 (u16a + u16b) /* compliant */
    … (int32_t) buf16a[u16a + u16b] /* compliant */ 
    

    规则 10.5(强制): 如果位运算符 ~ 和 << 应用在基本类型为 unsigned char 或 unsigned short 的操作数,结果应该立即强制转换为操作数的基本类型。

    当这些操作符(~和<<)用在 small integer 类型(unsigned char 或 unsigned short)时,运 算之前要先进行整数提升,结果可能包含并非预期的高端数据位。例如:

    uint8_t port = 0x5aU;
    uint8_t result_8;
    uint16_t result_16;
    uint16_t mode;
    result_8 = (~port) >> 4; /* not compliant */ 
    

    ~port 的值在 16 位机器上是 0xffa5,而在 32 位机器上是 0xffffffa5。在每种情况下,result 的值是 0xfa,然而期望值可能是 0x0a。这样的危险可以通过如下所示的强制转换来避免:

    result_8 = ( ( uint8_t ) (~port ) ) >> 4; /* compliant */
    result_16 = ( ( uint16_t ) (~(uint16_t) port ) ) >> 4 ; /* compliant */
    

    当<<操作符用在 small integer 类型时会遇到类似的问题,高端数据位被保留下来。例如:

    result_16 = ( ( port << 4 ) & mode ) >> 6 ; /* not compliant */ 
    

    result_16 的值将依赖于 int 实现的大小。附加的强制转换可以避免任何模糊性。

    result_16 = ( ( uint16_t ) ( ( uint16_t ) port << 4 ) & mode ) >> 6 ; /* compliant */
    
  • 6.10.8 整数后缀

    规则 10.6(强制): 后缀“U”应该用在所有 unsigned 类型的常量上。

    整型常量的类型是混淆的潜在来源,因为它依赖于许多因素的复杂组合,包括:

    • 常数的量级
    • 整数类型实现的大小
    • 任何后缀的存在
    • 数值表达的进制(即十进制、八进制或十六进制)

    例如,整型常量“40000”在 32 位环境中是 int 类型,而在 16 位环境中则是 long 类型。 值 0x8000 在 16 位环境中是 unsigned int 类型,而在 32 位环境中则是(signed)int 类型。

    注意:

    • 任何带有“U”后缀的值是 unsigned 类型
    • 一个不带后缀的小于 231的十进制值是 signed 类型

    但是:

    • 不带后缀的大于或等于 215 的十六进制数可能是 signed 或 unsigned 类型
    • 不带后缀的大于或等于 231 的十进制数可能是 signed 或 unsigned 类型

    常量的符号应该明确。符号的一致性是构建良好形式的表达式的重要原则。如果一个常数是 unsigned 类型,为其加上“U”后缀将有助于避免混淆。当用在较大数值上时,后缀也许是多余的(在某种意义上它不会影响常量的类型);然而后缀的存在对代码的清晰性是种有价值的帮助。

6.11 指针类型转换

指针类型可以归为如下几类:

  • 对象指针
  • 函数指针
  • void 指针
  • 空(null)指针常量(即由数值 0 强制转换为 void*类型)

涉及指针类型的转换需要明确的强制,除非在以下时刻:

  • 转换发生在对象指针和 void 指针之间,而且目标类型承载了源类型的所有类型标识符
  • 当空指针常量(void*)被赋值给任何类型的指针或与其做等值比较时,空指针常量被自动转化为特定的指针类型

C 当中只定义了一些特定的指针类型转换,而一些转换的行为是实现定义的。

  • 规则 11.1(强制): 转换不能发生在函数指针和其他除了整型之外的任何类型指针之间。[未定义 27、28]

    函数指针到不同类型指针的转换会导致未定义的行为。举个例子,这意味着一个函数指针不能转换成指向不同类型函数的指针。

  • 规则 11.2(强制): 对象指针和其他除整型之外的任何类型指针之间、对象指针和其他类型对象的指针之间、对象指针和 void 指针之间不能进行转换。 [未定义 29]

    这些转换未经定义。

  • 规则 11.3(建议): 不应在指针类型和整型之间进行强制转换 [实现 24]

    当指针转换到整型时所需要的整型的大小是实现定义的。尽可能的情况下要避免指针和整型之间的转换,但是在访问内存映射寄存器或其他硬件特性时这是不可避免的。

  • 规则 11.4(建议): 不应在某类型对象指针和其他不同类型对象指针之间进行强制转换。

    如果新的指针类型需要更严格的分配时这样的转换可能是无效的。

    uint8_t * p1;
    uint32_t * p2;
    p2 = (uint32_t *) p1; /* Imcompatible alignment ? */ 
    
  • 规则 11.5(强制): 如果指针所指向的类型带有 const 或 volatile 限定符,那么移除限定符的强制转换是不允许的。 [未定义 39、40]

    任何通过强制转换移除类型限定符的企图都是对类型限定符规则的违背。注意,这里所指的限定符与任何可以应用在指针本身的限定符不同。

    uint16_t x;
    uint16_t * const cpi = &x; /* const pointer */
    uint16_t * const * pcpi ; /* pointer to const pointer */
    const uint16_t * * ppci ; /* pointer to pointer to const */
    uint16_t * * ppi;
    const uint16_t * pci; /* pointer to const */
    volatile uint16_t * pvi; /* pointer to volatile */
    uint16_t * pi;
    …
    pi = cpi; /* Compliant – no conversion
    no cast required */
    pi = (uint16_t *)pci; /* Not compliant */
    pi = (uint16_t *)pvi ; /* Not compliant */
    ppi = (uint16_t *)pcpi ; /* Not compliant */
    ppi = (uint16_t *)ppci ; /* Not compliant */
    

6.12 表达式

  • 规则 12.1(建议): 不要过分依赖 C 表达式中的运算符优先规则

    括号的使用除了可以覆盖缺省的运算符优先级以外,还可以用来强调所使用的运算符。使用相当复杂的 C 运算符优先级规则很容易引起错误,那么这种方法就可以帮助避免这样的错误,并且可以使得代码更为清晰可读。然而,过多的括号会分散代码使其降低了可读性。

    下面的方针给出了何时使用括号的建议:

    1)赋值运算符的右手操作数不需要使用括号,除非右手端本身包含了赋值表达式:

    x = a + b; /* acceptable */ 
    x = (a + b); /* ( ) not required */ 
    

    2)一元运算符的操作数不需要使用括号:

    x = a * -1; /* acceptable */ 
    x = a * (-1); /* ( ) not required */ 
    

    3)否则,二元和三元运算符的操作数应该是 cast-expressions(见 6.3.4 节 ISO 9899:1990 [2]),除非表达式中所有运算符是相同的。

    x = a + b + c; /* acceptable, but care needed */
    x = f ( a + b, c ); /* no ( ) required for a + b */
    x = ( a == b ) ? a : ( a – b );
    if (a && b && c) /* acceptable */
    x = (a + b) – (c + d);
    x = (a * 3) + c + d;
    x = (uint16_t) a + b; /* no need for ( ( uint16_t ) a ) */ 
    

    4)即使所有运算符都是相同的,也可以使用括号控制运算的次序。某些运算符(如,加法和乘法)在代数学上结合律的,而在 C 中未必如此。类似地,涉及混合类型的整数运算(许多规则不允许)因为整数提升的存在可以产生不同的结果。下面的例子是按照 16 位的实现写成的,它描述了加法不是结合的以及表达式结构清晰的重要性:

    uint16_t a = 10;
    uint16_t b = 65535;
    uint32_t c = 0;
    uint32_t d;
    d = (a + b) + c; /* d is 9; a + b wraps modulo 65536 */
    d = a + (b + c); /* d is 65545 */
    /* this example also deviates from several other rules */
    

    注意,规则 12.5 是本规则的特例,它只能应用在逻辑运算符(&& 和 | |)上。

  • 规则 12.2(强制): 表达式的值在标准所允许的任何运算次序下都应该是相同的。 [未指定 7-9;未定义 18]

    除了少数运算符(特别地,函数调用运算符 ( )、&&、| |、? : 和 , (逗号) )之外,子表达式所依据的运算次序是未指定的并会随时更改。这意味着不能信任子表达式的运算次序,特别不能信任可能会发生副作用(side effect)的运算次序。在表达式运算中的某些点上,如果能保证所有先前的副作用都已经发生,那么这些点称为“序列点(sequence point)”。序列点和副作用的描述见 ISO 9899:1990 [2]的 5.1.2.3 节、6.3 节和 6.6 节。

    注意,运算次序的问题不能使用括号来解决,因为这不是优先级的问题。

    下面的条款告诉我们对运算次序的依赖是如何发生的,并由此帮助我们采纳本规则。

    自增或自减运算符

    做为能产生错误的例子,考虑

    x = b[i] + i++; 
    

    根据 b[i] 的运算是先于还是后于 i ++ 的运算,表达式会产生不同的结果。把增值 运算做为单独的语句,可以避免这个问题。那么:

    x = b[i] + i;  
    i ++; 
    

    函数参数

    函数参数的运算次序是未指定的。

    x = func ( i++, i); 
    

    根据函数的两个参数的运算次序不同,表达式会给出不同的结果。

    函数指针

    如果函数是通过函数指针调用的,那么函数标识符和函数参数运算次序是不可信任的。

    p->task_start_fn (p++); 
    

    函数调用

    函数在被调用时可以具有附加的作用(如,修改某些全局数据)。可以通过在使用函数的表达式之前调用函数并为值采用临时变量的方法避免对运算次序的依赖。

    例如:x = f (a) + g (a);

    可以写成 :x = f (a); x += g (a);

    做为可以产生错误的例子,考虑下面的表达式,它从堆栈中取出两个值,从第一个值中减去第二个值,再把结果放回栈中:

    push ( pop () – pop () );

    根据哪一个 pop () 函数先进行计算(因为 pop()具有副作用)会产生不同的结果。

    嵌套的赋值语句

    表达式中嵌套的赋值可以产生附加的副作用。不给这种能导致对运算次序的依赖提供任何机会的最好做法是,不要在表达式中嵌套赋值语句。

    例如,下面的做法是不赞成的:

    x = y = y = z / 3;
    x = y = y++; 
    

    volatile 访问

    类型限定符 volatile 是 C 提供的,用来表示那些其值可以独立于程序的运行而自由更改的对象(例如输入寄存器)。对带有 volatile 限定类型的对象的访问可能改变它的值。C 编译器不会优化对 volatile 的读取,而且,据 C 程序所关心的,对 volatile 的读取具有副作用(改变 volatile 的值)。

    做为表达式的一部分通常需要访问 volatile 数据,这意味着对运算次序的依赖。建议对 volatile 的访问尽可能地放在简单的赋值语句中,如:

    volatile uint16_t v; 
    /* … */
    x = v; 
    

    本规则讨论了带有副作用的运算次序问题。要注意子表达式的运算次数同样会带来问题,本规则没有提及。这是函数调用的问题,其中函数是以宏实现的。例如,考虑下面的函数宏及其调用:

    #define MAX (a, b) ( ( (a) > (b) ) ? (a) : (b) )
    /* … */
    z = MAX (i++, j); 
    

    当 a > b 时,该定义计算了两次第一个参数而在 a <= b 时只计算了一次。这样,宏调用根据 i 和 j 的值,对 i 增加了一次或两次。

    应该说明的是,比如那些由浮点的四舍五入引起的量级依赖(magnitude-dependent)的作用也没有在这里涉及。尽管可能发生副作用的运算次序是未定义的,运算结果在另一方面是良好定义的并被表达式的结构所控制。在下面的例子中,f1 和 f2 是浮点变量;F3、F4 和 F5 代表浮点类型的表达式。

    f1 = F3 + (F4 + F5);

    f2 = (F3 + F4 ) + F5;

    加法运算的次序由括号的位置决定,至少表面如此。即,首先 F4 的值加上 F5 然后加上F3,给出 f1 的值。假定 F3、F4 和 F5 没有副作用,那么它们的值独立于它们被计算的次序。然而,赋给 f1 和 f2 的值不能保证是相同的,因为浮点的四舍五入后紧接加法的运算将依赖于被加的值。

  • 规则 12.3(强制): 不能在具有副作用的表达式中使用 sizeof 运算符。

    C 当中存在的一个可能的编程错误是为一个表达式使用了 sizeof 运算符并期望计算表达式。然而表达式是不会被计算的:sizeof 只对表达式的类型有用。为避免这样的错误,sizeof 不能用在具有副作用的表达式中,因为此时其副作用不会发生。例如:

    int32_t i;
    int32_t j;
    j = sizeof (i = 1234);
    /* j is set to the sizeof the type of i which is an int */
    /* i is not set to 1234 */ 
    
  • 规则 12.4(强制): 逻辑运算符 && 或 | | 的右手操作数不能包含副作用。

    C 当中存在这样的情况,表达式的某些部分不会被计算到。如果这些子表达式具有副作用,那么副作用可能会发生也可能不会发生,这依赖于其他子表达式的值。

    可以导致这种问题的运算符是 &&、| | 和 ? : 。前两种情况(逻辑运算符)下,右手操作数的计算是有条件的,依赖于左手操作数的值。在 ? : 运算符情况下,或者第二个操作数被计算,或者第三个操作数被计算,却不会两者都被计算。两种逻辑运算符之一中,右手操作数的条件计算能轻易导致问题出现,如果程序员依赖副作用的发生。? : 运算符是被特殊用以在两个子表达式之间进行选择,因此导致错误的可能性较小。

    例如:

    if ( ishigh && ( x == i++ ) ) /* Not compliant */
    if ( ishigh && ( x == f (x) ) ) /* Only acceptable if f(x) is
    known to have no side effects */ 
    

    可以产生副作用的运算在 ISO 9899:1990 [2]的 5.1.2.3 节 中描述成 volatile 对象的访问、对象的修改、文件的修改或是执行某些运算的函数的调用,这里,函数所执行的这些运算可以导致函数所运行的环境状态的改变。

  • 规则 12.5(强制): 逻辑 && 或 | | 的操作数应该是 primary-expressions。

    “Primary expressions”定义在 ISO 9899:1990 [2]的 6.3.1 节中。本质上它们或是单一的标识符,或是常量,或是括号括起来的表达式。本规则的作用是要求,如果操作数不是单一的标识符或常量,那么它必须被括起来。在这种情况下,括号对于代码的可读性和确保预期的行为都是非常重要的。如果表达式只由逻辑 && 序列组成或逻辑 | | 序列组成,就不需要使用括号。

    例如:

    if ( ( x == 0 ) && ishigh ) /* make x == 0 primary */
    if ( x || y || z ) /* exception allowed, if x, y and z are Boolean */
    if ( x || ( y && z ) ) /* make y && z primary */
    if ( x && ( !y ) ) /* make !y primary */
    if ( ( is_odd (y) ) && x ) /* make call primary */ 
    

    如果表达式只由逻辑 && 序列组成或逻辑 | | 序列组成,就不需要使用括号。

    if ( ( x > c1 ) && ( y > c2 ) && ( z > c3 ) ) /* compliant */
    if ( ( x > c1 ) && ( y > c2 ) || (z > c3 ) ) /* not compliant */
    if ( ( x > c1 ) && ( ( y > c2 ) || ( z > c3 ) ) ) /* compliant extra ( ) used */ 
    

    注意,本规则是规则 12.1 的特例。

  • 规则 12.6(建议): 逻辑运算符(&&、| | 和 !)的操作数应该是有效的布尔数。有效布尔类型的表达式不能用做非逻辑运算符(&&、| | 和 !)的操作数 [Koenig 48]

    逻辑运算符 &&、| | 和 ! 很容易同位运算符 &、| 和 ~ 混淆。见术语表中的“Boolean expressions”。

  • 规则 12.7(强制): 位运算符不能用于基本类型(underlying type)是有符号的操作数上。 [实现 17-19]

    位运算(~、<<、>>、&、^ 和 | )对有符号整数通常是无意义的。比如,如果右移运算把符号位移动到数据位上或者左移运算把数据位移动到符号位上,就会产生问题。

    基本类型的描述见 6.10 节。

  • 规则 12.8(强制): 移位运算符的右手操作数应该位于零和某数之间,这个数要小于左手操作数的基本类型的位宽。 [未定义 32]

    例如,如果左移或右移运算的左手操作数是 16 位整型,那么要确保它移动的位数位于 0 和 15 之间。

    基本类型的描述见节 6.10。

    有多种确保遵循本规则的方法。对右手操作数来说,最简单的是使其为一个常数(其值可以静态检查)。使用无符号整型可以保证该操作数非负,那么只有其上限需要检查(在运行时动态检查或者通过代码复查)。否则这两种限制都要被检查。

    u8a = (uint8_t) (u8a << 7); /* compliant */
    u8a = (uint8_t) (u8a << 9); /* not compliant */
    u16a = (uint16_t) ( (uint16_t) u8a << 9 ); /* compliant */ 
    
  • 规则 12.9(强制): 一元减运算符不能用在基本类型无符号的表达式上。

    把一元减运算符用在基本类型为 unsigned int 或 unsigned long 的表达式上时,会分别产生类型为 unsigned int 或 unsigned long 的结果,这是无意义的操作。把一元减运算符用在无符号短整型的操作数上,根据整数提升的作用它可以产生有意义的有符号结果,但这不是好的方法。

    基本类型的描述见节 6.10。

  • 规则 12.10(强制): 不要使用逗号运算符。

    使用逗号运算符通常不利于代码的可读性,可以使用其他方法达到相同的效果。

  • 规则 12.11(建议): 无符号整型常量表达式的计算不应产生折叠(wrap-around)。

    因为无符号整型表达式不会严格意义上地溢出,但会以模的方式产生折叠,因此任何无符号整型常量表达式的有效“溢出”将不会被编译器检测到。尽管在运行时(run-time)有很好的理由依赖由无符号整型提供的模运算,但在编译时(compile-time)计算常量表达式该理由就不那么明显了。因此任何发生折叠的无符号整型常量表达式都可能表示编程错误。

    本规则同等地应用于翻译过程的所有阶段。对于在编译时计算所选择的常量表达式,编译器以这样的方式计算,即其计算结果与在目标机上的运算结果相同,除了条件预处理指令。对这样的指令,可以使用常见的算术运算法则(见 ISO 9899:1990 [2]中 6.4 节),但是 int 和 unsigned int 的行为会被分别替代成好像它们是 long 或 unsigned long 一样。

    例如,在 int 类型为 16 位、long 类型为 32 位的机器上:

    #define START 0x8000
    #define END 0xFFFF
    #define LEN 0x8000
    #if ( (START + LEN ) > END )
    #error Buffer Overrun /* OK because START and LEN are unsigned long */
    #endif
    #if ( ( ( END – START ) – LEN ) < 0 )
    #error Buffer Overrun
    /* Not OK: subtraction result wraps around to 0xFFFFFFFF */
    #endif
    /* contrast the above START + LEN with the following */
    if ( ( START + LEN ) > END )
    {
    error ( “Buffer overrun “ );
    /* Not OK: START + LEN wraps around to 0x0000 due to
    unsigned
    int arithmetic */
    } 
    
  • 规则 12.12(强制): 不应使用浮点数的基本(underlying)的位表示法(bit representation) [未指定 6;实现 20]

    浮点数的存储方法可以根据编译器的不同而不同,因此不应使用直接依赖于存储方法的浮点操作。应该使用内置(in-built)的运算符和函数,它们对程序员隐藏了存储细节。

  • 规则 12.13(建议): 在一个表达式中,自增(++)和自减(- -)运算符不应同其他运算符混合在一起。

    不建议使用同其他算术运算符混合在一起的自增和自减运算符,是因为:

    • 它显著削弱了代码的可读性
    • 它为语句引入了其他的副作用,可能存在未定义的行为

    把这些操作同其他算术操作隔离开是比较安全的。

    例如,下面的语句是不适合的:

    u8a = ++u8b + u8c--; /* Not compliant */ 
    

    下面的序列更为清晰和安全:

    ++u8b;
    u8a = u8b + u8c;
    u8c --; 
    

6.13 控制语句表达式

  • 规则 13.1(强制): 赋值运算符不能使用在产生布尔值的表达式上。 [Koenig 6]

    任何被认为是具有布尔值的表达式上都不能使用赋值运算。这排除了赋值运算符的简单与复杂的使用形式,其操作数是具有布尔值的表达式。然而,它不排除把布尔值赋给变量的操作。

    如果布尔值表达式需要赋值操作,那么赋值操作必须在操作数之外分别进行。这可以帮助避免“=”和“= =”的混淆,帮助我们静态地检查错误。

    见术语表中的“Boolean expressions”。

    例如:

    x = y;
    if (x != 0)
    {
    foo ();
    } 
    

    不能写成:

    if ( ( x = y ) != 0 ) /* Boolean by context */
    {
    foo ();
    } 
    

    或者更坏的:

    if (x = y) 
    {  
    foo (); 
    } 
    
  • 规则 13.2(建议): 数的非零检测应该明确给出,除非操作数是有效的布尔类型。

    当要检测一个数据不等于零时,该测试要明确给出。本规则的例外是该数据代表布尔类型的值,虽然在 C 中布尔数实际上也是整数。本规则的着眼点是在代码的清晰上,给出整数和逻辑数之间的清晰划分。

    例如,如果 x 是个整数,那么:

    if ( x != 0) /* Correct way of testing x is non-zero */
    if ( y ) /* Not compliant, unless y is effectively Boolean data (e.g. a flag). */ 
    

    见术语表中的“Boolean expressions”。

  • 规则 13.3(强制): 浮点表达式不能做相等或不等的检测。

    这是浮点类型的固有特性,等值比较通常不会计算为 true,即使期望如此。而且,这种比较行为不能在执行前做出预测,它会随着实现的改变而改变。例如,下面代码中的测试结果就是不可预期的:

    float32_t x, y;
    /* some calculations in here */
    if ( x == y) /* not compliant */
    { /* … */ }
    if (x == 0.0f) /* not compliant */ 
    

    间接的检测同样是有问题的,在本规则内也是禁止的。例如:

    if ( ( x <= y ) && ( x >= y ) )
    { /* … */ } 
    

    为了获得确定的浮点比较,建议写一个实现比较运算的库。这个库应该考虑浮点的粒度 (FLT_EPSILON)以及参与比较的数的量级。见规则 13.4 和规则 20.3。

  • 规则 13.4(强制): for 语句的控制表达式不能包含任何浮点类型的对象。

    控制表达式可能会包含一个循环计数器,检测其值以决定循环的终止。浮点变量不能用于此目的。舍入误差和截取误差会通过循环的迭代过程传播,导致循环变量的显著误差,并且在进行检测时很可能给出不可预期的结果。例如,循环执行的次数可以随着实现的改变而改变,也是不可预测的。见规则 13.3。

  • 规则 13.5(强制): for 语句的三个表达式应该只关注循环控制。

    for 语句的三个表达式都给出时它们应该只用于如下目的:

    第一个表达式 初始化循环计数器 (例子中的 i)

    第二个表达式 应该包含对循环计数器(i)和其他可选的循环控制变量的测试

    第三个表达式 循环计数器(i)的递增或递减

  • 规则 13.6(强制): for 循环中用于迭代计数的数值变量不应在循环体中修改。

    不能在循环体中修改循环计数器。然而可以修改表现为逻辑数值的其他循环控制变量。例如,指示某些事情已经完成的标记,然后在 for 语句中测试。

    flag = 1;
    for ( i = 0; ( i < 5 ) && (flag == 1 ); i++)
    {
    /* … */
    flage = 0; /* Compliant – allows early termination of loop */
    i = i + 3; /* Not compliant – altering the loop counter */
    } 
    
  • 规则 13.7(强制): 不允许进行结果不会改变的布尔运算。

    如果布尔运算产生的结果始终为“true”或始终为“false”,那么这很可能是编程错误。

    enum ec { RED, BLUE, GREEN } col;
    …
    if (u16a < 0) /* Not compliant – u16a is always >= 0 */
    …
    if (u16a <= 0xffff) /* Not compliant – always true */
    …
    if (s8a < 130) /* Not compliant – always true */
    …
    if ( ( s8a < 10 ) && ( s8a > 20) ) /* Not compliant – always false */
    …
    if ( ( s8a < 10 ) || ( s8a > 5) ) /* Not compliant – always true */
    …
    if ( col <= GREEN ) /* Not compliant – always true */
    …
    if (s8a > 10)
    {
    if (s8a > 5) /* Not compliant – s8a is not volatile */
    {
    }
    } 
    

6.14 控制流

  • 规则 14.1(强制): 不能有不可到达(unreachable)的代码。

    本规则是针对那些在任何环境中都不能到达的代码,这些代码在编译时就能被标识出不可到达。规则排除了可以到达但永远不会执行的代码(如,保护性编程代码(defensive programming) )。

    如果从相关的入口到某部分代码之间不存在控制流路径,那么这部分代码就是不可到达的。例如,在无条件控制转移代码后的未标记代码就是不可到达的:

    switch ( event )
    {
    case E_wakeup:
    do_wakeup ();
    break; /* unconditional control transfer */
    do_more (); /* Not compliant – unreachable code */
    /* … */
    default:
    /* … */
    break;
    } 
    

    对整个函数来说,如果不存在调用它的手段,那么这个函数将是不可到达的。

  • 规则 14.2(强制): 所有非空语句(non-null statement)应该:

    a) 不管怎样执行都至少有一个副作用(side-effect),或者

    b) 可以引起控制流的转移

    任何语句(非空语句),如果既没有副作用也不引起控制流的改变,通常就会指示出编程错误,因此要进行对这样语句的静态检测。例如,下面的语句在执行时不一定带有副作用:

    /* assume uint16_t x;
    and uint16_t i; */
    …
    x >= 3u; /* not compliant: x is compared to 3,
    and the answer is discarded */
    

    注:“null statement”和“side effect”分别定义在 ISO 9899:1990 [2] 中节 6.6.3 和 5.1.2.3。

  • 规则 14.3(强制): 在预处理之前,空语句只能出现在一行上;其后可以跟有注释,假设紧跟空语句的第一个字符是空格。

    通常不会故意包含空语句,但是在使用它们的地方,它们应该出现在它们本身的行上。空语句前面可以有空格以保持缩进的格式。如果一条注释跟在空语句的后面,那么至少要有一个空格把空语句和注释分隔开来。需要这样起分隔作用的空格是因为它给读者提供了重要的视觉信息。遵循本规则使得静态检查工具能够为与其他文本出现在一行上的空语句提出警告,因为这样的情形通常表示编程错误。例如:

    while ( ( port & 0x80 ) == 0 )
    {
    ; /* wait for pin – Compliant */
    /* wait for pin */ ; /* Not compliant, comment before ; */
    ;/* wait for pin – Not compliant, no white-space char after ; */
    } 
    
  • 规则 14.4(强制): 不应使用 goto 语句。

  • 规则 14.5(强制): 不应使用 continue 语句。

  • 规则 14.6(强制): 对任何迭代语句至多只应有一条 break 语句用于循环的结束。

    这些规则关心的是良好的编程结构。循环中允许有一条 break 语句,因为这允许双重结果的循环或代码优化。

  • 规则 14.7(强制): 一个函数在其结尾应该有单一的退出点。

    这是由 IEC 61508 良好的编程格式要求的。

  • 规则 14.8(强制): 组成 switch、while、do...while 或 for 结构体的语句应该是复合语句。

    组成 switch 语句或 while、do ... while 或 for 循环结构体的语句应该是复合语句(括在大括号里),即使该复合语句只包含一条语句。

    例如:

    for ( i = 0 ; i < N_ELEMENTS ; ++i )
    {
    buffer[i] = 0; /* Even a single statement must be in braces */
    }
    while ( new_data_available ) 
    process_data (); /* Incorrectly not enclosed in braces */
    service_watchdog (); /* Added later but, despite the appearance
    (from the indent) it is actually not
    part of the body of the while statement,
    and is executed only afgter the loop has
    terminated */
    

    注意,复合语句及其大括号的布局应该根据格式指南来确定。上述只是个例子。

  • 规则 14.9(强制): if(表达式)结构应该跟随有复合语句。else 关键字应该跟随有复合语句或者另外的 if 语句。

    例如:

    if ( test1 )
    {
    x = 1 ; /* Even a single statement must be in braces */
    }
    else if ( test2 )
    {
    x = 0; /* No need for braces in else if */
    }
    else
    x = 3; /* This was (incorrectly) not enclosed in braces */
    y = 2; /* This line was added later but, despite the appearance
    (from the indent) it is actually not part of the else,
    and is executed unconditionally */ 
    

    注意,复合语句及其大括号的布局应该根据格式指南来确定。上述只是个例子。

  • 规则 14.10(强制): 所有的 if ... else if 结构应该由 else 子句结束。

    不管何时一条 if 语句跟有一个或多个 else if 语句都要应用本规则;最后的 else if 必须跟有一条 else 语句。而 if 语句然后就是 else 语句的简单情况不在本规则之内。

    对最后的 else 语句的要求是保护性编程(defensive programming)。else 语句或者要执行适当的动作,或者要包含合适的注释以说明为何没有执行动作。这与 switch 语句中要求具有最后一个 default 子句(规则 15.3)是一致的。

    例如,下面的代码是简单的 if 语句:

    if ( x > 0)
    {
    log_error (3) ;
    x = 0 ;
    } /* else not needed */ 
    

    而下面的代码描述了 if,else if 结构:

    if ( x < 0 )
    {
    log_error (3); 
    x = 0;
    }
    else if (y < 0)
    {
    x = 3;
    }
    else /* this else clause is required, even if the */
    { /* programmer expects this will never be reached */
    /* no change in value of x */
    } 
    

6.15 switch 语句

C 当中 switch 语句的语法是软弱的,允许复杂的非结构化的行为。下面的文字描述了 MISRA-C 中定义的 switch 语句的标准语法。它们和与之相关的规则强制了简单一致的 switch 语句结构。

下面的语法规则是对 C 标准语法规则的补充,所有未在下面定义的语法规则都在 C 标准中做出了定义。

 switch-statement:
 switch ( expression ) { case-label-clause-list default-label-clause }
 case-label-clause-list:
 case-label case-clauseopt
 case-label-clause-list case-label case-clauseopt
 case-label:
 case constant-expression :
 case-clause:
 statement-listopt break;
 { declaration-listopt statement-listopt break; }
 default-label-clause:
 default-label default-clause
 default-label:
 default :
 default-clause:
 case-clause
 and
 statement:
 /* labelled_statement removed */
 compound_statement
 expression_statement
 selection_statement
 iteration_statement 
 /* jump_statement removed or just restricted to return depending on other rules */ 

规则中还使用了下面的术语:

switch label 或者是 case 标签,或者是 default 标签

case clause 任何两个 switch label 之间的代码

default clause default 标签和 switch 语句结尾之间的代码

switch clause 或者是 case clause,或者是 default clause

switch 语句只应包含 switch label 和 switch clause 而没有其他代码。

  • 规则 15.1(强制): switch 标签只能用在当最紧密封闭(closely-enclosing)的复合语句是switch 语句体的时候

    case 或 default 标签的范围应该是做为 switch 语句体的复合语句。所有 case 子句和default 子句应该具有相同的范围。

  • 规则 15.2(强制): 无条件的 break 语句应该终止每个非空的 switch 子句。 [Koenig 22-24]

    每个 switch 子句中的最后一条语句应该是 break 语句,或者如果 switch 子句是复合语句,那么复合语句的最后一条语句应该是 break 语句。

  • 规则 15.3(强制): switch 语句的最后子句应该是 default 子句。

    对最后的 default 子句的要求是出于保护性编程。该子句应该执行适当的动作,或者包含合适的注释以说明为何没有执行动作。

  • 规则 15.4(强制): switch 表达式不应是有效的布尔值。

    见术语表中的“Boolean expressions”。

    例如:

    switch ( x == 0) /* not compliant – effectively Boolean */
    {
    … 
    
  • 规则 15.5(强制): 每个 switch 语句至少应有一个 case 子句。

    例如:

    switch (x)
    {
    uint8_t var; /* not compliant – decl before 1st case */
    case 0:
    a = b;
    break; /* break is required here */
    case 1: /* empty clause, break not required */
    case 2:
    a = c; /* executed if x is 1 or 2 */
    if ( a == b )
    {
    case 3: /* not compliant – case is not allowed here */
    } 
    break;
    case 4:
    a = b; /* not compliant – non empty drop through */
    case 5:
    a = c;
    break;
    default: /* default clause is required */
    errorflag = 1; /* should be non-empty if possible */
    break; /* break is required here, in case a
    future modification turns this into a
    case clause */
    } 
    

6.16 函数

  • 规则 16.1(强制): 函数定义不得带有可变数量的参数 [未指定 15;未定义 25、45、61、70-76]

    本特性存在许多潜在的问题。用户不应编写使用可变数量参数的附加函数。这排除了stdarg.h、va_arg、va_start 和 va_end 的使用。

  • 规则 16.2(强制): 函数不能调用自身,不管是直接还是间接的。

    这意味着在安全相关的系统中不能使用递归函数调用。递归本身承载着可用堆栈空间过度的危险,这能导致严重的错误。除非递归经过了非常严格的控制,否则不可能在执行之前确定什么是最坏情况(worst-case)的堆栈使用。

  • 规则 16.3(强制): 在函数的原型声明中应该为所有参数给出标识符

    出于兼容性、清晰性和可维护性的原因,应该在函数的原型声明中为所有参数给出名字。

  • 规则 16.4(强制): 函数的声明和定义中使用的标识符应该一致

  • 规则 16.5(强制): 不带参数的函数应当声明为具有 void 类型的参数 [Koenig 59-62]

    函数应该声明为具有返回类型(见规则 8.2),如果函数不返回任何数据,返回类型为 void。类似地,如果函数不带参数,参数列表应声明为 void。例如函数 myfunc,如果既不带参数也不返回数据则应声明为:

    void myfunc ( void ); 
    
  • 规则 16.6(强制): 传递给一个函数的参数应该与声明的参数匹配。 [未定义 22]

    这个问题可以通过使用函数原型完全避免——见规则 8.1。本规则被保留是因为编译器可能不会标记这样的约束错误。

  • 规则 16.7(建议): 函数原型中的指针参数如果不是用于修改所指向的对象,就应该声明为指向 const 的指针。

    本规则会产生更精确的函数接口定义。const 限定应当用在所指向的对象而非指针,因为要保护的是对象本身。

    例如:

    void myfunc ( int16_t *param1, const int16_t *param2, int16_t *param3 )
    /* param1 : Addresses an object which is modified – no const
    param2 : Addresses an object which is not modified – const required
    param3 : Addresses an object which is not modified – const missing */
    {
    *param1 = *param2 + *param3;
    return;
    }
    /* data at address param3 has not been changed, but this is not const therefore
    not compliant */ 
    
  • 规则 16.8(强制): 带有 non-void 返回类型的函数其所有退出路径都应具有显式的带表达式的 return 语句。 [未定义 43]

    表达式给出了函数的返回值。如果 return 语句不带表达式,将导致未定义的行为(而且编译器不会给出错误)。

  • 规则 16.9(强制): 函数标识符的使用只能或者加前缀&,或者使用括起来的参数列表,列表可以为空。 [Koenig 24]

    如果程序员写为:

    if (f) /* not compliant – gives a constant non-zero value which is the address of fuse
    either f () or &f */
    {
    /* … */
    } 
    

    那么就不会清楚其意图是要测试函数的地址是否为 NULL,还是执行函数 f ()的调用。

  • 规则 16.10(强制): 如果函数返回了错误信息,那么错误信息应该进行测试。

    一个函数(标准库中的函数、第三方库函数、或者是用户定义的函数)能够提供一些指示错误发生的方法。这可以通过使用错误标记、特殊的返回数据或者其他手段。不管什么时候函数提供了这样的机制,调用程序应该在函数返回时立刻检查错误指示。

    然而要注意到,相对于在函数完成后才检测错误的做法(见规则 20.3)来说,对函数输入值的检查是更为鲁棒的防止错误的手段。还要注意到,对 errno 的使用(为了返回函数的错误信息)是笨拙的并应该谨慎使用(见规则 20.5)。

6.17 指针和数组

  • 规则 17.1(强制): 指针的数学运算只能用在指向数组或数组元素的指针上。 [未定义 30]

    对并非指向数组或数组元素的指针做整数加减运算(包括增值和减值)会导致未定义的行为。

  • 规则 17.2(强制): 指针减法只能用在指向同一数组中元素的指针上。[未定义 31]

    只有当两个指针指向(或至少好象是指向了)同一数组内的对象时,指针减法才能给出良好定义的结果。

  • 规则 17.3(强制): >、>=、<、<= 不应用在指针类型上,除非指针指向同一数组。 [未定义 33]

    如果两个指针没有指向同一个对象,那么试图对指针做比较将导致未定义的行为。注意:允许指向超出数组尾部的元素,但对该元素的访问是禁止的。

  • 规则 17.4(强制): 数组的索引应当是指针数学运算的唯一可允许的方式

    数组的索引是指针数学运算的唯一可接受的方式,因为它比其他的指针操作更为清晰并由此具有更少的错误倾向。本规则禁止了指针数值的显式运算。数组索引只能应用在定义为数组类型的对象上。任何显式计算的指针值潜在地会访问不希望访问的或无效的内存地址。指针可以超出数组或结构的范围,或者甚至可以有效地指向任意位置。见规则 21.1。

    void my_fn (uint8_t * p1, uint8_t p2[] )
    {
    uint8_t index = 0 ;
    uint8_t *p3 ;
    uint8_t *p4 ;
    *p1 = 0 ;
    p1 ++ ; /* not compliant – pointer increment */
    p1 = p1 + 5 ; /* not compliant – pointer increment */
    p1[5] = 0 ; /* not compliant – p1 was not declared as an array */
    p3 = &p1[5]; /* not compliant – p1 was not declared as an array */
    p2[0] = 0;
    index ++;
    index = index + 5;
    p2[index] = 0; /* compliant */
    p4 = &p2[5]; /* compliant */
    }
    uint8_t a1[16];
    uint8_t a2[16];
    my_fn (a1, a2);
    my_fn (&a1[4], &a2[4]);
    uint8_t a[10];
    uint8_t *p;
    p = a;
    * (p + 5) = 0; /* not compliant */
    p[5] = 0; /* compliant */
    
  • 规则 17.5(建议): 对象声明所包含的间接指针不得多于 2 级

    多于 2 级的间接指针会严重削弱对代码行为的理解,因此应该避免。

    typedef int8_t * INTPTR; 
    struct s {
    int8_t * s1; /* compliant */
    int8_t * s2; /* compliant */
    int8_t * s3; /* compliant */
    };
    struct s * ps1; /* compliant */
    struct s ** ps2; /* compliant */
    struct s *** ps3; /* not compliant */
    int8_t ** ( *pfunc1 ) (); /* compliant */
    int8_t ** ( **pfunc2 ) (); /* compliant */
    int8_t ** ( ***pfunc3 ) (); /* not compliant */
    int8_t *** ( ** pfunc4 ) (); /* not compliant */
    void function ( int8_t * par1,
    int8_t ** par2,
    int8_t *** par3, /* not compliant */
    INTPTR * par4,
    INTPTR * const * const par5, /* not compliant */
    int8_t * par6[],
    int8_t ** par7[] ) /* not compliant */
    {
    int8_t * ptr1 ;
    int8_t ** ptr2 ;
    int8_t *** ptr3 ; /* not compliant */
    INTPTR * ptr4 ;
    INTPTR * const * const ptr5 ; /* not compliant */
    int8_t * ptr6[10];
    int8_t ** ptr7[10];
    }
    

    类型解释:

    • par1 和 ptr1 是指向 int8_t 的指针。
    • par2 和 ptr2 是指向 int8_t 的指针的指针。
    • par3 和 ptr3 是指向 int8_t 的指针的指针的指针。这是三级,因此不适合。
    • par4 和 ptr4 扩展开是指向 int8_t 的指针的指针。
    • par5 和 ptr5 扩展开是指向 int8_t 的指针的 const 指针的 const 指针。这是三级,因此不合适。
    • par6 是指向 int8_t 的指针的指针,因为数组被转化成指向数组初始元素的指针。
    • ptr6 是 int8_t 类型的指针数组。
    • par7 是指向 int8_t 的指针的指针的指针,因为数组被转化成指向数组初始元素的指针。这是三级,因此不合适。
    • ptr7 是指向 int8_t 的指针的指针数组。这是合适的。
  • 规则 17.6(强制): 自动存储对象的地址不应赋值给其他的在第一个对象已经停止存在后仍然保持的对象。 [未定义 9、26]

    如果一个自动对象的地址赋值给其他的或者具有更大作用域的对象、或者静态对象、或者从一个函数返回的对象,那么在初始对象停止存在(其地址成为无效)时,包含地址的对象可能会延续存在。

    例如:

    int8_t * foobar (void)
    {
    int8_t local_auto;
    return (&local_auto); /* not compliant */
    }
    

6.18 结构与联合

  • 规则 18.1(强制): 所有结构与联合的类型应该在转换单元(translation unit)的结尾是完善的。 [未定义 35]

    结构或联合的完整声明应该包含在任何涉及该结构的转换单元之内。见 ISO 9899:1990 [2] 中 6.1.2.5 节关于对不完整类型的详细描述。

    struct tnode * pt; /* tnode is incomplete at this point */
    struct tnode
    {
    int count;
    struct tnode * left;
    struct tnode * right;
    }; /* type tnode is now complete */ 
    
  • 规则 18.2(强制): 对象不能赋值给重叠(overlapping)对象。 [未定义 34、35]

    当两个对象创建后,如果它们拥有重叠的内存空间并把一个拷贝给另外一个时,该行为是未定义的。

  • 规则 18.3(强制): 不能为了不相关的目的重用一块内存区域。

    本规则涉及了使用内存存储某些数据并在程序仍然运行期间的其他时刻使用同一块内存存储不相关数据的技术。很明显这依赖于两段不同的数据块,它们存在于程序运行期间不相连的时间段而且从来不会被同时请求。

    对于安全相关的系统,不建议这样做,因为它会带来许多危险。比如:一个程序可能试图访问某个区域的某种类型的数据,而当时该区域正在存储其他类型的数据(如,由中断引起的)。这两种类型的数据在存储上可能不同而且可能会侵占其他数据。这样在每次切换使用时,数据可能不会正确初始化。这样的做法在并发系统中尤其是危险的。

    然而要了解有时出于效率的原因可能会要求这样的存储共享。此时就非常需要采取检测手段以确保错误类型的数据永远不会被访问,确保数据始终被适当地初始化了以及确保对其他部分数据(如,由分配不同引起的)的访问是不可能的。所采取的检测手段应该归纳为文档,并在违背本规则的背离内被证明是正确的。

    可以使用联合或其他手段做到这一点。

    注意,MISRA-C 不想对编译器翻译源代码的实际行为给出限制,因为用户通常对此没有有效的控制。所以,举例来说,编译器中的内存分配,不管是动态堆、动态堆栈或静态的,可能会仅仅因为轻微的代码变化就发生显著的改变,即使是在相同的优化级别上。还要注意的是,某些优化会合理地产生,甚至在用户没有做出这样的请求时。

  • 规则 18.4(强制): 不要使用联合。 [实现 27]

    规则 18.3 禁止为不相关的目的重用内存区域,然而,即使内存区域是为了相关目的重用的,仍然会存在数据被误解的风险。因此,提出本规则禁止针对任何目的而使用联合。

    尽管如此,要认识到在某些情况下需要谨慎地使用联合以构建有效率的实现。发生这些情况时,如果所有相关的定义实现的行为被归纳为文档了,那么对本规则的违背也是可以接受的。实践中,可以在设计文档内指出编译器手册的实现章节。与此有关的实现行为的种类是:

    • 填充——在联合的结尾插入了多少填充
    • 排列——在联合中排列了多少结构成员
    • 存储次序(endianess)——数据字中最有效的字节是存储在最低内存地址还是最高地址
    • 位次序(bit-order)——字节中的位是如何计数的以及如何分配在位域中的

    对如下情况违背规则是可以接受的:(a)数据的打包和解包,如在发送和接收消息时;(b)实现变量录取(variant records),假设变量是由通用的域(common fields)区分的。不带区分 器(differentiator)的变量录取在任何情况下都是不合适的。

    数据打包和解包

    在本例中,使用联合访问一个 32 位数据字的字节以存储从高字节优先的网络上接收到的字节。这种特殊假设:

    • uint32_t 类型占据 32 位
    • uint8_t 类型占据 8 位
    • 实现把数据字的最高有效字节存储在最低的内存地址

    实现接收和打包的代码如下:

    typedef union {
    uint32_t word;
    uint8_t bytes[4];
    } word_msg_t;
    uint32_t read_word_big_endian (void)
    {
    word_msg_t tmp;
    tmp.bytes[0] = read_byte ();
    tmp.bytes[1] = read_byte ();
    tmp.bytes[2] = read_byte ();
    tmp.bytes[3] = read_byte (); 
    return (tmp.word);
    } 
    

    值得注意的是上面的函数体可以写成如下可移植的形式:

    uint32_t read_word_big_endian (void)
    {
    uint32_t word;
    word = ( ( uint32_t ) read_byte () ) << 24;
    word = word | ( ( ( uint32_t ) read_byte () ) << 16 );
    word = word | ( ( ( uint32_t ) read_byte () ) << 8 );
    word = word | ( ( uint32_t ) read_byte () ) ;
    return (word);
    } 
    

    不幸的是,面临可移植的实现时,大多数编译器产生的代码远非有效率的。当程序对高执行速度和低内存使用的要求胜于移植性时,使用联合的实现是可以考虑的。

    变量录取

    联合通常用于实现变量录取。每个变量享有共同的域并具有其特有的附加域。本例基于CAN 总线校准协议(CAN Calibration Protocal, CCP),其中每个发送给 CCP 客户端的 CAN 消息共享两个通用的域,每个域占一个字节。其后最多可附加 6 个字节,这些字节的解释依赖于存储在第一个字节中的消息类型。

    这个特定实现依赖于如下假设:

    • uint16_t 类型占据 16 位
    • uint8_t 类型占据 8 位
    • 排列和打包的规则是,uint8_t 和 uint16_t 类型的结构成员之间不存在间隙

    为了简化,本例中只考虑两种消息类型。这里给出的代码是不完整的,只是用来描述变量录取而不是做为 CCP 的实现模块。

    /* The fields common to all CCP messages */
    typedef struct {
    uint8_t msg_type;
    uint8_t sequence_no;
    } ccp_common_t;
    /* CCP connect message */
    typedef struct {
    ccp_common_t common_part;
    uint16_t station_to_connect;
    } ccp_connect_t;
    /* CCP disconnect message */
    typedef struct {
    ccp_common_t common_part;
    uint8_t disconnect_command;
    uint8_t pad; 
    uint16_t station_to_disconnect;
    } ccp_disconnect_t;
    /* The variant */
    typedef union {
    ccp_common_t common;
    ccp_connect_t connect;
    ccp_disconnect_t disconnect;
    } ccp_message_t;
    void process_ccp_message (ccp_message_t *msg)
    {
    switch (msg->common.msg_type)
    {
    case Ccp_connect:
    if (MY_STATION == msg->connect.station_to_connect)
    {
    ccp_connect ();
    }
    break;
    case Ccp_disconnect:
    if (MY_STATION == msg->disconnect.station_to_disconnect)
    {
    if (PERM_DISCONNECT == msg->disconnect.disconnect_command)
    {
    ccp_disconnect ();
    }
    }
    break;
    default:
    break; /* ignore unknown commands */
    }
    } 
    

6.19 预处理指令

  • 规则 19.1(建议): 文件中的#include 语句之前只能是其他预处理指令或注释。

    代码文件中所有#include 指令应该成组放置在接近文件顶部的位置。本规则说明,文件中可以优先#include 语句放置的只能是其他预处理指令或注释。

  • 规则 19.2(建议): #include 指令中的头文件名字里不能出现非标准字符。 [未定义 14]

    如果在头文件名字预处理标记的 < 和 > 限定符或 ” 和 ” 限定符之间使用了 ‘ ,\ ,或 /* 字符,该行为是未定义的。

  • 规则 19.3(强制): #include 预处理指令应该跟随或”filename”序列。 [未定义 48]

    例如,如下语句是允许的:

    #include “ filename.h”
    #include <filename.h>
    #define FILE “filename.h”
    #include FILE 
    
  • 规则 19.4(强制): C 的宏只能扩展为用大括号括起来的初始化、常量、小括号括起来的表达式、类型限定符、存储类标识符或 do-while-zero 结构。 [Koenig 82-84]

    这些是宏当中所有可允许使用的形式。存储类标识符和类型限定符包括诸如 extern、static 和 const 这样的关键字。使用任何其他形式的#define 都可能导致非预期的行为,或者是非常难懂的代码。

    特别的,宏不能用于定义语句或部分语句,除了 do-while 结构。宏也不能重定义语言的语法。宏的替换列表中的所有括号,不管哪种形式的 ()、{}、[] 都应该成对出现。

    do-while-zero 结构(见下面的例子)是在宏语句体中唯一可接受的具有完整语句的形式。 do-while-zero 结构用于封装语句序列并确保其是正确的。注意:在宏语句体的末尾必须省略分号。

    例如:

    /* The following are compliant */
    #define PI 3.14159F /* Constant */
    #define XSTAL 10000000 /* Constant */
    #define CLOCK (XSTAL / 16) /* Constant expression */
    #define PLUS2(X) ( (X) + 2 ) /* Macro expanding to expression */
    #define STOR extern /* storage class specifier */
    #define INIT(value) { (value), 0, 0 } /* braced initialiser */
    #define READ_TIME_32 () \
    do { \
    DISABLE_INTERRUPTS (); \
    time_now = (uint32_t) TIMER_HI << 16; \
    time_now = time_now | (uint32_t) TIMER_LO; \
    ENABLE_INTERRUPTS (); \
    } while (0) /* example of do-while-zero */
    /* the following are NOT compliant */
    #define int32_t long /* use typedef instead */
    #define STARTIF if ( /* unbalanced () and language redefinition */ 
    
    
  • 规则 19.5(强制): 宏不能在块中进行 #define 和 #undef。

    C 语言中,尽管在代码文件中的任何位置放置#define 或#undef 是合法的,但把它们放在块中会使人误解为好像它们存在于块作用域。

    通常,#define 指令要放在接近文件开始的地方,在第一个函数定义之前。而#undef 指令通常不一定需要(见规则 19.6)。

  • 规则 19.6(强制): 不要使用#undef。

    通常,#undef 是不需要的。当它出现在代码中时,能使宏的存在或含义产生混乱。

  • 规则 19.7(建议): 函数的使用优先选择函数宏(function-like macro)。 [Koenig 78-81]

    由于宏能提供比函数优越的速度,函数提供了一种更为安全和鲁棒的机制。在进行参数的类型检查时尤其如此。函数宏的问题在于它可能会多次计算参数。

  • 规则 19.8(强制): 函数宏的调用不能缺少参数 [未定义 49]

    这是一个约束错误,但是预处理器知道并忽略此问题。函数宏中的每个参数的组成必须至少有一个预处理标记,否则其行为是未定义的。

  • 规则 19.9(强制): 传递给函数宏的参数不能包含看似预处理指令的标记。 [未定义 50]

    如果任何参数的行为类似预处理指令,使用宏替代函数时的行为将是不可预期的。

  • 规则 19.10(强制): 在定义函数宏时,每个参数实例都应该以小括号括起来,除非它们做为#或##的操作数。 [Koenig 78-81]

    函数宏的定义中,参数应该用小括号括起来。例如一个 abs 函数可以定义成:

    #define abs (x) ( ( (x) >= 0 ) ? (x) : -(x) ) 
    

    不能定义成:

    #define abs (x) ( ( (x) >= 0 ) ? x : -x ) 
    

    如果不坚持本规则,那么当预处理器替代宏进入代码时,操作符优先顺序将不会给出要求的结果。

    考虑前面第二个不正确的定义被替代时会发生什么: z = abs ( a – b );

    将给出如下结果: z = ( ( a – b >= 0 ) ? a – b : -a – b );

    子表达式 – a - b 相当于 (-a)-b,而不是希望的 –(a-b)。把所有参数都括进小括号中就可以避免这样的问题。

  • 规则 19.11(强制): 预处理指令中所有宏标识符在使用前都应先定义,除了#ifdef 和#ifndef 指令及 defined()操作符。

    如果试图在预处理指令中使用未经定义的标识符,预处理器有时不会给出任何警告但会假定其值为零。#ifdef、#ifndef 和 defined()用来测试宏是否存在并由此进行排除。

    例如:

    #if x < 0 /* x assumed to be zero if not defined */ 
    

    在标识符被使用之前要考虑使用#ifdef 进行测试。

    注意,预处理标识符可以使用#define 指令来定义也可以在编译器调用所指定的选项中定义。然而更多的是使用#define 指令。

  • 规则 19.12(强制): 在单一的宏定义中最多可以出现一次 # 或 ## 预处理器操作符。 [未指定 12]

    与 # 或 ## 预处理器操作符相关的计算次序如果未被指定则会产生问题。为避免该问题,在单一的宏定义中只能使用其中一种操作符(即,一个 #、或一个 ##、或都不用)。

  • 规则 19.13(建议): 不要使用# 或 ## 预处理器操作符。 [未指定 12]

    与 # 或 ## 预处理器操作符相关的计算次序如果未被指定则会产生问题。编译器对这些操作符的实现是不一致的。为避免这些问题,最好不要使用它们。

  • 规则 19.14(强制): defined 预处理操作符只能使用两种标准形式之一。 [未定义 47]

    defined 预处理操作符的两种可允许的形式为:

    • defined (identifier)
    • defined identifier

    任何其他的形式都会导致未定义的行为,比如:

    #if defined (X > Y) /* not compliant – undefined behaviour */
    

    在 #if 或 #elif 预处理指令的扩展中定义的标记也会导致未定义的行为,应该避免。如:

    #define DEFINED defined  
    #if DEFINED (X) /* not compliant – undefined behaviour */ 
    
  • 规则 19.15(强制): 应该采取防范措施以避免一个头文件的内容被包含两次。

    当转换单元(translation unit)包含了复杂层次的嵌套头文件时,会发生某头文件被包含多于一次的情形。这最多会导致混乱。如果它导致了多个定义或定义冲突,其结果将是未定义的或者是错误的行为。

    多次包含一个头文件可以通过认真的设计来避免。如果不能做到这一点,就需要采取阻止头文件内容被包含多于一次的机制。通常的手段是为每个文件配置一个宏;当头文件第一次被包含时就定义这个宏,并在头文件被再次包含时使用它以排除文件内容。

    例如,一个名为“ahdr.h”的文件可以组织如下:

    #ifndef AHDR_H
    #define AHDR_H
    /* The following lines will be excluded by the preprocessor if the file is included
    more than once */
    …
    #endif 
    

    或者可以使用下面的形式

    #ifdef AHDR_H
    #error Header file is already included
    #else
    #define AHDR_H
    /* The following lines will be excluded by the preprocessor if the file is included
    more than once */
    …
    #endif
    
  • 规则 19.16(强制): 预处理指令在句法上应该是有意义的,即使是在被预处理器排除的情况下。

    当一段源代码被预处理指令排除时,每个被排除语句的内容被忽略直到出现一个 #else、 #elif 或 #endif 指令(根据上下文内容)。如果其中一个被排除指令的组成形式不好(badly formed),编译器忽略它时不会给出任何警告,这将带来不幸的后果。

    本规则要求所有预处理指令在句法上是有效的,即使它们出现在被排除的代码块中。

    特别地,要确保 #else 和 #endif 指令后不要跟随除空格之外的任何字符。在强制执行这个 ISO 要求时编译器并非始终一致。

    #define AAA 2
    ...
    int foo (void)
    {
    int x = 0 ;
    ...
    #ifndef AAA
    x = 1 ;
    #else1 /* Not compliant */
    x = AAA ;
    #endif
    ...
    retun x ;
    } 
    
  • 规则 19.17(强制): 所有的 #else、#elif 和 #endif 预处理指令应该同与它们相关的 #if 或 #ifdef 指令放在相同的文件中。

    当语句块的包含和排除是被一系列预处理指令控制时,如果所有相关联的指令没有出现在同一个文件中就会产生混乱。本规则要求所有的预处理指令序列 #if / #ifdef ... #elif ... #else ... #endif 应该放在同一个文件中。遵循本规则会保持良好的代码结构并能避免维护性问题。

    注意,这并不排除把所有这样的指令放在众多被包含文件中的可能性,只要与某一序列相关的所有指令放在一个文件中即可。

    /***file.c***/
    #define A
    ...
    #ifdef A
    ...
    #include “ file1.h”
    #
    #endif
    ...
    #if 1
    #include “ file2.h”
    ...
    EOF 
    
    /***file1.h***/
    #if 1
    ...
    #endif /* Compliant */
    EOF
    
    /***file2.h***/
    ...
    #endif /* Not compliant */ 
    

6.20 标准库

  • 规则 20.1(强制): 标准库中保留的标识符、宏和函数不能被定义、重定义或取消定义。 [未定义 54、57、58、62]

    通常 #undef 一个定义在标准库中的宏是件坏事。同样不好的是,#define 一个宏名字,而该名字是 C 的保留标识符或者标准库中做为宏、对象或函数名字的 C 关键字。例如,存在一些特殊的保留字和函数名字,它们的作用为人所熟知,如果对它们重新定义或取消定义就会产生一些未定义的行为。这些名字包括 defined、__LINE__、__FILE__、__DATE__、 __TIME__、__STDC__、errno 和 assert。

    undef 的使用也可以参见规则 19.6。

    ISO C 的保留标识符在本文档中参见 7.1.3 节和 7.13 节关于 ISO 9899 :1990 [2],同时也应该被编译器的编写者写入文档。通常,所有以下划线开始的标识符都是保留的。

  • 规则 20.2(强制): 不能重用标准库中宏、对象和函数的名字。

    如果程序员使用了标准库中宏、对象或函数的新版本(如,功能增强或输入值检查),那么更改过的宏、对象或函数应该具有新的名字。这是用来避免不知是使用了标准的宏、对象或函数还是使用了它们的更新版本所带来的任何混淆。所以,举例来说,如果 sqrt 函数的新版本被写做检查输入值非负,那么这新版本不能命名为“sqrt”而应该给出新的名字。

  • 规则 20.3(强制): 传递给库函数的值必须检查其有效性。 [未定义 60、63;实现 45、47]

    C 标准库中的许多函数根据 ISO 标准 [2] 并不需要检查传递给它们的参数的有效性。即使标准要求这样,或者编译器的编写者声明要这么做,也不能保证会做出充分的检查。因此,程序员应该为所有带有严格输入域的库函数(标准库、第三方库及自己定义的库)提供适当的输入值检查机制。

    具有严格输入域并需要检查的函数例子为:

    • math.h 中的许多数学函数,比如:负数不能传递给 sqrt 或 log 函数;fmod 函数的第二个参数不能为零。
    • toupper 和 tolower:当传递给 toupper 函数的参数不是小写字符时,某些实现能产生并非预期的结果(tolower 函数情况类似)。
    • 如果为 ctype.h 中的字符测试函数传递无效的值时会给出未定义的行为。
    • 应用于大多数负整数的 abs 函数给出未定义的行为。

    在 math.h 中,尽管大多数数学库函数定义了它们允许的输入域,但在域发生错误时它们的返回值仍可能随编译器的不同而不同。因此,对这些函数来说,预先检查其输入值的有效性就变得至关重要。

    程序员在使用函数时,应该识别应用于这些函数之上的任何的域限制(这些限制可能会也可能不会在文档中说明),并且要提供适当的检查以确认这些输入值位于各自域中。当然,在需要时,这些值还可以更进一步加以限制。

    有许多方法可以满足本规则的要求,包括:

    • 调用函数前检查输入值 。
    • 设计深入函数内部的检查手段。这种方法尤其适应于实验室内开发的库,纵然它也可以用于买进的第三方库(如果第三方库的供应商声明他们已内置了检查的话)。
    • 产生函数的“封装”(wrapped)版本,在该版本中首先检查输入,然后调用原始的函数。
    • 静态地声明输入参数永远不会采取无效的值。

    注意,在检查函数的浮点参数时(浮点参数在零点上为奇点),适当的做法是执行其是否为零的检查。这对规则 13.3 而言是可以接受的例外,不需给出背离。然而如果当参数趋近于零时,函数值的量级趋近无穷的话,仍然有必要检查其在零点(或其他任何奇点)上的容限,这样可以避免溢出的发生。

  • 规则 20.4(强制): 不能使用动态堆的内存分配 [未指定 19;未定义 91、92;实现 69;Koenig 32]

    这排除了对函数 alloc、malloc、realloc 和 free 的使用。

    涉及动态内存分配时,存在整个范围内的未指定的、未定义的和实现定义的行为,以及其他大量的潜在缺陷。动态堆内存分配能够导致内存泄漏、数据不一致、内存耗尽和不确定的行为。

    注意,某些实现可能会使用动态堆内存的分配以实现其他函数(如库 string.h 中的函数)。 如果这种情况发生,也需要避免使用这些函数。

  • 规则 20.5(强制): 不要使用错误指示 errno。 [实现 46;Koenig 73]

    errno 做为 C 的简捷工具,在理论上是有用的,但在实际中标准没有很好地定义它。一个非零值可以指示问题的发生,也可以不用它指示;做为结果不应该使用它。即使对于那些已经良好定义了 errno 的函数而言,宁可在调用函数前检查输入值也不依靠 errno 来捕获错误(见规则 16.10)。

  • 规则 20.6(强制): 不应使用库中的宏 offsetof。 [未定义 59]

    当这个宏的操作数的类型不兼容或使用了位域时,它的使用会导致未定义的行为。

  • 规则 20.7(强制): 不应使用 setjmp 宏和 longjmp 函数 [未指定 14;未定义 64-67;Koenig 74]

    setjmp 和 longjmp 允许绕过正常的函数调用机制,不应该使用。

  • 规则 20.8(强制): 不应使用信号处理工具 [未定义 68、69;实现 48-52;Koenig 74]

    信号处理包含了实现定义的和未定义的行为。

  • 规则 20.9(强制): 在产品代码中不应使用输入/输出库。 [未指定 2-5,16-18;未定义 77-89;实现 53-68]

    这包含文件和 I/O 函数 fgetpos、fopen、ftell、gets、perror、remove、rename 和 ungetc。

    文件 I/O 具有大量未指定的、未定义的和实现定义的行为。本文档中假定正常情况下嵌入式系统的产品代码中不需要它们。

    如果产品代码中需要 stdio.h 中的任意特性,那么需要了解与此特性相关的某些问题。

  • 规则 20.10(强制): 不应使用库中的函数 atof、atoi 和 atol。 [未定义 90]

    当字符串不能被转换时,这些函数具有未定义的行为。

  • 规则 20.11(强制): 不应使用库中的函数 abort、exit、getenv 和 system。 [未定义 93;实现 70-73]

    正常情况下,嵌入式系统不需要这些函数,因为嵌入式系统一般不需要同环境进行通讯。如果一个应用中必需这些函数,那么一定要在所处环境中检查这些函数的实现定义的行为。

  • 规则 20.12(强制): 不应使用库中的时间处理函数。 [未指定 22;未定义 97;实现 75、76]

    包括 time、strftime。这个库同时钟有关。许多方面都是实现定义的或未指定的,如时间的格式。如果要使用 time.h 中的任一功能,那么必须要确定所用编译器对它的准确实现,并给出背离。

6.21 运行时错误

  • 规则 21.1(强制): 最大限度降低运行时错误必须要确保至少使用了下列方法之一: a) 静态分析工具/技术; b) 动态分析工具/技术; c) 显式的代码检测以处理运行时故障。 [未定义 19、26、94]

    运行时检测是个问题,它不专属于 C,但 C 程序员需要加以特别的注意。这是因为 C 语言在提供任何运行时检测方面能力较弱。对于鲁棒的软件来说,动态检测是必需的,但 C 的实现不需要。因此 C 程序员需要谨慎考虑的问题是,在任何可能出现运行时错误的地方增加代码的动态检测。

    当表达式仅仅由处在良好定义范围内的值组成时,倘若可以声称,对于所有处在定义范围内的值来说不会发生异常的话,运行时检测就不是必需的。如果使用了这样的声明,它应该与它所依赖的假设一起组织成文档。然而如果使用了这种方法,一定要小心的是,后续的代码改变可能会使原先的假设无效,或者要小心出于某种原因而改变了原先的假设。

    下面的条款给出了在何处需要考虑提供动态检测的指导:

    1)数学运算错误

    它包括表达式运算错误,如溢出、下溢出、零除或移位时有效位的丢失。

    考虑整数溢出,注意,无符号的整数计算不会产生严格意义上的溢出(产生未定义的值),但是会产生值的折叠(wrap-around)(产生经过定义的但可能是错误的值)。

    2)指针运算

    确保在动态计算一个地址时,被计算的地址是合理的并指向某个有意义的地方。特别要保证指向一个结构或数组的内部,那么当指针增加或者改变后仍然指向同一个结构或数组。参见指针运算的限制——规则 17.1、17.2、17.4。

    3)数组界限错误

    在使用数组索引元素前要确保它处于数组大小的界限之内

    4)函数参数

    见规则 20.3

    5)引用指针的值(pointer dereferencing)

    如果一个函数返回一个指针,而接下来该指针的值被引用,那么程序首先要检查指针不是 NULL。在一个函数内部,指出哪个指针可以保持 NULL 哪个指针不可以保持 NULL 是相对简单的事情。而跨越函数界限,尤其是在调用定义在其他文件或库中的函数时,这就困难得多。

    /* Given a pointer to a message, check the message header and return
    * a pointer to the body of the message or NULL if the message is
    * invalid */
    const char_t *msg_body (const char_t *msg)
    {
    const char_t *body = NULL;
    if (msg != NULL)
    {
    if (msg_header_valid (msg) )
    {
    body = &msg[MSG_HEADER_SIZE];
    }
    }
    return (body);
    }
    …
    char_t msg_buffer[MAX_MSG_SIZE];
    const char_t *payload;
    …
    payload = msg_body (msg_buffer);
    if (payload != NULL)
    {
    /* process the message payload */
    } 
    

    用于最小化运行时错误的技术应该详细计划并写成文档,比如,在设计标准、测试计划、静态分析配置文件、代码检查清单中。

7 References

[1] MISRA Guidelines for the Use of the C Language In Vehicle Based Software, ISBN
0-9524156-9-0. Motor Industry Research Association. Nuneaton. April 1998
[2] ISO/IEC 9899:1990. Programming languages - C. International Organization for
Standardization. 1990
[3] Hatton L... Safer C - Developing Software for High-integrity and Safety-critical
Systems. ISBN 0-07-707640-0. McGraw-Hill, 1994
[4] ISO/IEC 9899:COR1:1995. Technical Corrigendum 1, 1995
[5] ISO/IEC 9899:AMD1:1995. Amendment 1. 1995
[6] ISO/IEC 9899:COR2:1996. Technical Corrigendum 2. 1996
[7] ANSI X3.159-1989. Programming languages - C, American National Standards
Institute. 1989
[8] ISO/IEC 9899:1999. Programming languages - C. International Organization for
Standardization, 1999
[9] MISRA Development Guidelines for Vehicle Based Software, ISBN 0-9524156-0-7.
Motor Industry Research Association. Nuneaton, November 1994
[10] CRR80, The Use of Commercial Off-the-Shelf (COTS) Software in Safety Related
Applications. ISBN 0-7176-0984-7, HSE Books '
[11] ISO 9001:2000. Quality management systems - Requirements. International]
Organization for Standardization. 2000
[12] ISO 90003:2004. Software engineering - Guidelines for the application of ISO
9001:2000 to computer software. International Organization for Standardization. 2004
[13] The TickIT Guide. Using ISO 9001:2000 for Software Quality Management System
Construction, Certification and Continual Improvement, Issue 5, British Standards
Institution. 2001
[14] Straker D.. C Style: Standards and Guidelines. ISBN 0-13-116898-3. Prentice Hall
1991
[15] Fenton N.E. and Pfleeger S.L., Software Metrics: A Rigorous and Practical Approach,
2nd Edition. ISBN 0-534-95429-1. PWS. 1998
[16] MISRA Report 5 Software Metrics. Motor Industry Research Association. Nuneaton.
February 1995
[17] MISRA Report 6 Verification and Validation. Motor Industry Research Association.
Nuneaton. February 1995
[18] Kemighan B.W.. Ritchie D.M.. The C programming language. 2nd edition, ISBN
0-13-110362-8. Prentice Hall. 1988 (note: The 1st edition is not a suitable reference
document as it does not describe ANSI/ISO C)
[19] Koenig A., C Traps and Pitfalls, ISBN 0-201 -17928-8, Addison-Wesley, 1988 
[20] IEC 61508, Functional safety of electrical/electronic/programmable electronic
safety-related systems. International Electrotechnical Commission, in 7 parts published
between 1998 and 2000
[21] ANSI/IEEE Std 754, IEEE Standard for Binary Floating-Point Arithmetic, 1985
[22] ISO/IEC 10646:2003, Information technology - Universal Multiple-Octet Coded
Character Set (UCS), International Organization for Standardization, 2003 

8 Appendix A: Summary of rules

本附录给出第 6 节中所有规则的汇总。

环境
1.1 (req) 所有代码都必须遵照 ISO 9899:1990 “Programming languages - C”,由
ISO/IEC 9899/COR1:1995 , ISO/IEC 9899/AMD1:1995 , 和 ISO/IEC
9899/COR2:1996 修订。
1.2 (req) 不能有对未定义行为或未指定行为的依赖性。
1.3 (req) 多个编译器和/或语言只能在为语言/编译器/汇编器所适合的目标代码定义
了通用接口标准时使用。
1.4 (req) 编译器/链接器要确保 31 个有效字符和大小写敏感能被外部标识符支持。
1.5 (adv) 浮点应用应该适应于已定义的浮点标准

语言扩展
2.1 (req) 汇编语言应该被封装并隔离。
2.2 (req) 源代码应该使用 /*…*/ 类型的注释。
2.3 (req) 字符序列 /* 不应出现在注释中。
2.4 (adv) 代码段不应被“注释掉”(comment out)。

文档
3.1 (req) 所有实现定义(implementation-defined)的行为的使用都应该文档化。
3.2 (req) 字符集和相应的编码应该文档化。
3.3 (adv) 应该确定、文档化和重视所选编译器中整数除法的实现。
3.4 (req) 所有#pragma 指令的使用应该文档化并给出良好解释。
3.5 (req) 如果做为其他特性的支撑,实现定义(implementation-defined)的行为和位
域(bitfields)集合应当文档化。
3.6 (req) 产品代码中使用的所有库都要适应本文档给出的要求,并且要经过适当的
验证。

字符集
4.1 (req) 只能使用 ISO C 标准中定义的 escape 序列。
4.2 (req) 不能使用三字母词(trigraphs)。

标识符
5.1 (req) 标识符(内部的和外部的)的有效字符不能多于 31。
5.2 (req) 具有内部作用域的标识符不应使用与具有外部作用域的标识符相同的名
称,这会隐藏了外部标识符。
5.3 (req) typedef 的名字应当是唯一的标识符。
5.4 (req) 标签(tag)名称必须是唯一的标识符。
5.5 (adv) 具有静态存储期的对象或函数标识符不能重用。
5.6 (adv) 一个命名空间中不应存在与另外一个命名空间中的标识符拼写相同的标识
符,除了结构和联合的成员名字。
5.7 (adv) 不能重用标识符名字。

类型
6.1 (req) 单纯的 char 类型应该只用做存储和使用字符值。
6.2 (req) signed char 和 unsigned char 类型应该只用做存储和使用数字值。
6.3 (adv) 应该使用指示了大小和符号的 typedef 以代替基本类型。
6.4 (req) 位域只能被定义为 unsigned int 或 singed int 类型。
6.5 (req) unsigned int 类型的位域至少应该为 2 bits 长度。

常量
7.1 (req) 不应使用八进制常量(零除外)和八进制 escape 序列。

声明与定义
8.1 (req) 函数应当具有原型声明,且原型在函数的定义和调用范围内都是可见的。
8.2 (req) 不论何时声明或定义了一个对象或函数,它的类型都应显式声明。
8.3 (req) 函数的每个参数类型在声明和定义中必须是等同的,函数的返回类型也该
是等同的。
8.4 (req) 如果对象或函数被声明了多次,那么它们的类型应该是兼容的。
8.5 (req) 头文件中不应有对象或函数的定义。
8.6 (req) 函数应该声明为具有文件作用域。
8.7 (req) 如果对象的访问只是在单一的函数中,那么对象应该声明在块范围内声明。
8.8 (req) 外部对象或函数应该声明在唯一的文件中。
8.9 (req) 具有外部链接的标识符应该具有准确的外部定义。
8.10 (req) 在文件范围内声明和定义的所有对象或函数应该具有内部链接,除非是在
需要外部链接的情况下。
8.1l (req) static 存储类标识符具有内部链接的对象和函数的定义和声明。
8.12 (req) 当一个数组声明为具有外部链接,它的大小应该显式声明或者通过初始化
进行隐式定义。

初始化
9.1 (req) 所有自动变量在使用前都应被赋值。
9.2 (req) 应该使用大括号以指示和匹配数组和结构的非零初始化构造。
9.3 (req) 在枚举列表中,“=”不能显式用于除首元素之外的元素上,除非所有的元
素都是显式初始化的。

数值类型转换
10.1 (req) 下列条件成立时,整型表达式的值不应隐式转换为不同的基本类型:
a) 转换不是带符号的向更宽整数类型的转换,或者
b) 表达式是复杂表达式,或者
c) 表达式不是常量而是函数参数,或者
d) 表达式不是常量而是返回的表达式。
10.2 (req) 下列条件成立时,浮点类型表达式的值不应隐式转换为不同的类型:
a) 转换不是向更宽浮点类型的转换,或者
b) 表达式是复杂表达式,或者
c) 表达式是函数参数,或者
d) 表达式是返回表达式。
10.3 (req) 整型复杂表达式的值只能强制转换到更窄的类型且与表达式的基本类型具
有相同的符号。
10.4 (req) 浮点类型复杂表达式的值只能强制转换到更窄的浮点类型。
10.5 (req) 如果位运算符 ~ 和 << 应用在基本类型为 unsigned char 或 unsigned short
的操作数,结果应该立即强制转换为操作数的基本类型。
10.6 (req) 后缀“U”应该用在所有 unsigned 类型的常量上。

指针类型转换
11.1 (req) 转换不能发生在函数指针和其他除了整型之外的任何类型指针之间。
11.2 (req) 对象指针和其他除整型之外的任何类型指针之间、对象指针和其他类型对
象的指针之间、对象指针和 void 指针之间不能进行转换。
11.3 (adv) 不应在指针类型和整型之间进行强制转换
11.4 (adv) 不应在某类型对象指针和其他不同类型对象指针之间进行强制转换。
11.5 (req) 如果指针所指向的类型带有 const 或 volatile 限定符,那么移除限定符的强
制转换是不允许的。

表达式
12.1 (adv) 不要过分依赖 C 表达式中的运算符优先规则
12.2 (req) 表达式的值在标准所允许的任何运算次序下都应该是相同的。
12.3 (req) 不能在具有副作用的表达式中使用 sizeof 运算符。
12.4 (req) 逻辑运算符 && 或 | | 的右手操作数不能包含副作用。
12.5 (req) 逻辑 && 或 | | 的操作数应该是 primary-expressions。
12.6 (adv) 逻辑运算符(&&、| | 和 !)的操作数应该是有效的布尔数。有效布尔类型
的表达式不能用做非逻辑运算符(&&、| | 和 !)的操作数
12.7 (req) 位运算符不能用于基本类型(underlying type)是有符号的操作数上。
12.8 (req) 移位运算符的右手操作数应该位于零和某数之间,这个数要小于左手操作
数的基本类型的位宽。
12.9 (req) 一元减运算符不能用在基本类型无符号的表达式上。
12.10 (req) 不要使用逗号运算符。
12.11 (adv) 无符号整型常量表达式的计算不应产生折叠(wrap-around)。
12.12 (req) 不应使用浮点数的基本(underlying)的位表示法(bit representation)
12.13 (adv) 在一个表达式中,自增(++)和自减(- -)运算符不应同其他运算符混合
在一起。

控制语句表达式
13.1 (req) 赋值运算符不能使用在产生布尔值的表达式上。
13.2 (adv) 数的非零检测应该明确给出,除非操作数是有效的布尔类型。
13.3 (req) 浮点表达式不能做相等或不等的检测。
13.4 (req) for 语句的控制表达式不能包含任何浮点类型的对象。
13.5 (req) for 语句的三个表达式应该只关注循环控制。
13.6 (req) for 循环中用于迭代计数的数值变量不应在循环体中修改。
13.7 (req) 不允许进行结果不会改变的布尔运算。

控制流
14.1 (req) 不能有不可到达(unreachable)的代码。
14.2 (req) 所有非空语句(non-null statement)应该:
a) 不管怎样执行都至少有一个副作用(side-effect),或者
b) 可以引起控制流的转移
14.3 (req) 在预处理之前,空语句只能出现在一行上;其后可以跟有注释,假设紧跟
空语句的第一个字符是空格。
14.4 (req) 不应使用 goto 语句。
14.5 (req) 不应使用 continue 语句。
14.6 (req) 对任何迭代语句至多只应有一条 break 语句用于循环的结束。
14.7 (req) 一个函数在其结尾应该有单一的退出点。
14.8 (req) 组成 switch、while、do...while 或 for 结构体的语句应该是复合语句。
14.9 (req) if(表达式)结构应该跟随有复合语句。else 关键字应该跟随有复合语句或
者另外的 if 语句。
14.10 (req) 所有的 if ... else if 结构应该由 else 子句结束。

Switch 语句
15.1 (req) switch 标签只能用在当最紧密封闭(closely-enclosing)的复合语句是 switch
语句体的时候
15.2 (req) 无条件的 break 语句应该终止每个非空的 switch 子句。
15.3 (req) switch 语句的最后子句应该是 default 子句。
15.4 (req) switch 表达式不应是有效的布尔值。
15.5 (req) 每个 switch 语句至少应有一个 case 子句。

函数
16.1 (req) 函数定义不得带有可变数量的参数
16.2 (req) 函数不能调用自身,不管是直接还是间接的。
16.3 (req) 在函数的原型声明中应该为所有参数给出标识符
16.4 (req) 函数的声明和定义中使用的标识符应该一致
16.5 (req) 不带参数的函数应当声明为具有 void 类型的参数
16.6 (req) 传递给一个函数的参数应该与声明的参数匹配。
16.7 (adv) 函数原型中的指针参数如果不是用于修改所指向的对象,就应该声明为指
向 const 的指针。
16.8 (req) 带有 non-void 返回类型的函数其所有退出路径都应具有显式的带表达式的
return 语句。
16.9(req) 函数标识符的使用只能或者加前缀&,或者使用括起来的参数列表,列表可
以为空。
16.10 (req) 如果函数返回了错误信息,那么错误信息应该进行测试。

指针和数组
17.1 (req) 指针的数学运算只能用在指向数组或数组元素的指针上。
17.2 (req) 指针减法只能用在指向同一数组中元素的指针上。
17.3 (req) >、>=、<、<= 不应用在指针类型上,除非指针指向同一数组。
17.4 (req) 数组的索引应当是指针数学运算的唯一可允许的方式
17.5 (adv) 对象声明所包含的间接指针不得多于 2 级
17.6(req) 自动存储对象的地址不应赋值给其他的在第一个对象已经停止存在后仍然
保持的对象。

结构与联合
18.1 (req) 所有结构与联合的类型应该在转换单元(translation unit)的结尾是完善的。
18.2 (req) 对象不能赋值给重叠(overlapping)对象。
18.3 (req) 不能为了不相关的目的重用一块内存区域。
18.4 (req) 不要使用联合。

预处理指令
19.1 (adv) 文件中的#include 语句之前只能是其他预处理指令或注释。
19.2 (adv) #include 指令中的头文件名字里不能出现非标准字符。
19.3 (req) #include 预处理指令应该跟随<filename>或”filename”序列。
19.4 (req) C 的宏只能扩展为用大括号括起来的初始化、常量、小括号括起来的表达式、
类型限定符、存储类标识符或 do-while-zero 结构。
19.5 (req) 宏不能在块中进行 #define 和 #undef。
19.6 (req) 不要使用#undef。
19.7 (adv) 函数的使用优先选择函数宏(function-like macro)。
19.8 (req) 函数宏的调用不能缺少参数
19.9 (req) 传递给函数宏的参数不能包含看似预处理指令的标记。
19.10 (req) 在定义函数宏时,每个参数实例都应该以小括号括起来,除非它们做为#或
##的操作数。
19.11 (req) 预处理指令中所有宏标识符在使用前都应先定义,除了#ifdef 和#ifndef 指令
及 defined()操作符。
19.12 (req) 在单一的宏定义中最多可以出现一次 # 或 ## 预处理器操作符。
19.13 (adv) 不要使用# 或 ## 预处理器操作符。
19.14 (req) defined 预处理操作符只能使用两种标准形式之一。
19.15 (req) 应该采取防范措施以避免一个头文件的内容被包含两次。
19.16 (req) 预处理指令在句法上应该是有意义的,即使是在被预处理器排除的情况下。
19.17 (req) 所有的 #else、#elif 和 #endif 预处理指令应该同与它们相关的 #if 或
#ifdef 指令放在相同的文件中。

标准库
20.1 (req) 标准库中保留的标识符、宏和函数不能被定义、重定义或取消定义。
20.2 (req) 不能重用标准库中宏、对象和函数的名字。
20.3 (req) 传递给库函数的值必须检查其有效性。
20.4 (req) 不能使用动态堆的内存分配
20.5 (req) 不要使用错误指示 errno。
20.6 (req) 不应使用库<stddef.h>中的宏 offsetof。
20.7 (req) 不应使用 setjmp 宏和 longjmp 函数
20.8 (req) 不应使用信号处理工具<signal.h>
20.9 (req) 在产品代码中不应使用输入/输出库<stdio.h>。
20.10 (req) 不应使用库<stdlib.h>中的函数 atof、atoi 和 atol。
20.11 (req) 不应使用库<stdlib.h>中的函数 abort、exit、getenv 和 system。
20.12 (req) 不应使用库<time.h>中的时间处理函数。

运行时错误
21.1 (req) 最大限度降低运行时错误必须要确保至少使用了下列方法之一:
a) 静态分析工具/技术;
b) 动态分析工具/技术;
c) 显式的代码检测以处理运行时故障

9 ppendix B:

MISRA-C :1998

到 MISRA-C :2004 规则映射

10 Appendix C : 术语表

基本类型(Underlying type)

见 6.10.4 节“基本类型”

规则范围 MISRA

规则适用于工程项目中的所有源文件。即:

  • 由项目小组编写的文件
  • 所有由其他小组和副代理商提供的文件
  • 编译器库的头文件,但不是库文件,后者只做为目标代码
  • 所有第三方的库资源,特别是头文件

要认识到,在获得第三方资源符合规则标准时可能会有些问题,但许多工具和库的供应商为他们的代码采用 MISRA-C 做为标准。

布尔表达式

严格说来,C 当中没有布尔类型,但返回数值类型的表达式和返回布尔类型的表达式存在着概念上的差异。一个表达式表示了布尔值,或者是因为它出现在需要布尔值的地方,或者是因为它使用了产生布尔值的运算符。

在下面的上下文环境中希望布尔值:

  • if 语句的控制表达式
  • 迭代语句的控制表达式
  • 条件运算符 ? 的第一个操作数

每种环境需要一个“有效布尔”表达式,或者是“ Boolean-by-construct ”,或者是 “Boolean-by-construct”,定义如下:

Boolean-by-contruct 值由以下运算符产生:

  • 等值运算符(== 和 !=)
  • 逻辑运算符( !,&& 和 ||)
  • 关系运算符(<,>,<= 和 >=)

Boolean-by-enforcement 值可以使用工具实现一个特定的强制类型来产生。此时布尔类型与特定的 typedef 相关联,可以用于任何布尔对象。这可以带来许多好处,尤其是当具有检查工具的支持时,而且,在某些特殊情况下它能帮助避免逻辑运算和整数运算之间的混淆。

短整型(smaller integer type)

术语“短整型”用来描述那些服从整数提升(interger promotion)的整数类型。受整数提升影响的类型是 char,short,bit-field 和 enum。

posted @ 2024-08-29 11:56  QIYUEXIN  阅读(44)  评论(0编辑  收藏  举报