汽车制造之C语言编码标准(MISRA-C)

软 件 编 程 规 范

目  录

一     环境

二     语言扩展

三     文档

四     字符集

五     标识符

六     类型

七     常量

八     声明与定义

九     初始化

十     数值类型转换

十一   指针类型转换

十二   表达式

十三   控制语句表达式

十四   控制流

十五   switch语句

十六   函数

十七   指针和数组

十八   结构与联合

十九   预处理指令

二十   标准库

二十一 运行时错误

一 环境

规则1.1(强制): 所有代码都必须遵照ISO 9899:1990 Programming languages - C”,由ISO/IEC 9899/COR1:1995ISO/IEC 9899/AMD1:1995,和ISO/IEC9899/COR2:1996 修订。

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

这项规则要求任何对未定义行为或未指定行为的依赖,除非在其他规则中做了特殊说明,都应该避免。如果其他某项规则中声明了某个特殊行为,那么就只有这项特定规则在其需要时给出背离性。

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

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

规则1.4(强制): 编译器/链接器要确保31 个有效字符和大小写敏感能被外部标识符支持。

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

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

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

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

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

/* IEEE 754 single-precision floating-point */

typedef float float32_t;

二 语言扩展

规则2.1(强制): 汇编语言应该被封装并隔离。

在需要使用汇编指令的地方,建议以如下方式封装并隔离这些指令:(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

<>

Perform_Critical_Safety_Function (X);

/* this comment is not compliant */

在检查包含函数调用的页中,假设它是可执行代码。因为可能会省略掉注释的结束标记,那么对安全关键函数的调用将不会被执行。

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

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

三 文档

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

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

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

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

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

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

这项规则为本文档的使用者提供了产生其应用中使用的任何pragma 的要求。每个pragma的含义要写成文档,文档中应当包含完全可理解的对pragma 行为及其在应用中之含义的充分描述。应当尽量减少任何pragma 的使用,尽可能地把它们本地化和封装成专门的函数。

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

这是在使用了规则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 建议的。

四 字符集

规则4.1(强制): 只能使用ISO C 标准中定义的escape 序列。

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

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

三字母词由2 个问号序列后跟1 个确定字符组成(如,??- 代表“~”(非)符号,而??)代表“]”符号)。它们可能会对2 个问号标记的其他使用造成意外的混淆,例如字符串“(Date should be in the form ??-??-??)”将不会表现为预期的那样,实际上它被编译器解释为

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

五 标识符

规则5.1(强制): 标识符(内部的和外部的)的有效字符不能多于31

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.1(强制): 单纯的char 类型应该只用做存储和使用字符值。

规则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 类型。

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

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

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

七 常量

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

任何以“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(做为单个数字书写的)严格说来是八进制常量,然而在此规则下它也是允许的。

八 声明与定义

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

的。

原型的使用使得编译器能够检查函数定义和调用的完整性。如果没有原型,就不会迫使编译器检查出函数调用当中的一定错误(比如,函数体具有不同的参数数目,调用和定义之间参数类型的不匹配)。事实证明,函数接口是相当多问题的肇因,因此本规则是相当重要的。对外部函数来说,我们建议采用如下方法,在头文件中声明函数(亦即给出其原型),并在所有需要该函数原型的代码文件中包含这个头文件(见规则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(强制): 函数的每个参数类型在声明和定义中必须是等同的,函数的返回类型也该是等同的。

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

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

兼容类型的定义是冗长复杂的(详见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(强制): 函数应该声明为具有文件作用域。

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

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

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

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

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

extern int16_t a ;

然后对a 进行定义:

#include

int16_t a = 0 ;

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

规则8.9(强制): 具有外部链接的标识符应该具有准确的外部定义。

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

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

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

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

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

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

int array1[10] ; /* Compliant */

extern int array2[] ; /* Not compliant */

int array2[] = { 0, 10, 15 }; /* Compliant */

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

九 初始化

规则9.1(强制): 所有自动变量在使用前都应被赋值。

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

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

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

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

十 数值类型转换

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

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

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

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

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

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

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

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

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

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

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

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

有符号和无符号之间没有隐式转换

整型和浮点类型之间没有隐式转换

没有从宽类型向窄类型的隐式转换

函数参数没有隐式转换

函数的返回表达式没有隐式转换

复杂表达式没有隐式转换

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

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

进行

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

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

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

规则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”后缀将有助于避免混淆。当用在较大数值上时,后缀也许是多余的(在某种意义上它不会影响常量的类型);然而后缀的存在对代码的清晰性是种有价值的帮助。

十一 指针类型转换

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

对象指针

函数指针

void 指针

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

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

转换发生在对象指针和void 指针之间,而且目标类型承载了源类型的所有类型标识符

当空指针常量(void*)被赋值给任何类型的指针或与其做等值比较时,空指针常量被自动转化为特定的指针类型

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

规则11.1(强制): 转换不能发生在函数指针和其他除了整型之外的任何类型指针之间。

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

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

这些转换未经定义。

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

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

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

十二 表达式

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

括号的使用除了可以覆盖缺省的运算符优先级以外,还可以用来强调所使用的运算符。

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

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

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

x = a + b; /* acceptable */

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

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

x = a * -1; /* acceptable */

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

否则,二元和三元运算符的操作数应该是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 ) */

即使所有运算符都是相同的,也可以使用括号控制运算的次序。某些运算符(如,加法和乘法)在代数学上结合律的,而在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(强制): 表达式的值在标准所允许的任何运算次序下都应该是相同的。

除了少数运算符(特别地,函数调用运算符 ( )、&&、| |、? : 和 , (逗号) )之外,子表达式所依据的运算次序是未指定的并会随时更改。这意味着不能信任子表达式的运算次序,特别不能信任可能会发生副作用(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(建议): 逻辑运算符(&&| | !)的操作数应该是有效的布尔数。有效布尔类型的表达式不能用做非逻辑运算符(&&| | !)的操作数

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

expressions”。

规则12.7(强制): 位运算符不能用于基本类型(underlying type)是有符号的操作数上。

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

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

例如,如果左移或右移运算的左手操作数是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

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

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

混合在一起。

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

它显著削弱了代码的可读性

它为语句引入了其他的副作用,可能存在未定义的行为把这些操作同其他算术操作隔离开是比较安全的。

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

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

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

++u8b;

u8a = u8b + u8c;

u8c --;

十三 控制语句表达式

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

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

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

见术语表中的“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 */

{

}

}

十四 控制流

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

本规则是针对那些在任何环境中都不能到达的代码,这些代码在编译时就能被标识出不可到达。规则排除了可以到达但永远不会执行的代码(如,保护性编程代码(defensiveprogramming) )。如果从相关的入口到某部分代码之间不存在控制流路径,那么这部分代码就是不可到达的。例如,在无条件控制转移代码后的未标记代码就是不可到达的:

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(强制): 组成switchwhiledo...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 */

十五 switch语句

规则15.1(强制): switch 标签只能用在当最紧密封闭(closely-enclosing)的复合语句是switch 语句体的时候case default 标签的范围应该是做为switch 语句体的复合语句。所有case 子句和default子句应该具有相同的范围。

规则15.2(强制): 无条件的break 语句应该终止每个非空的switch 子句。

每个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 子句。

十六 函数

规则16.1(强制): 函数定义不得带有可变数量的参数

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

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

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

规则16.3(强制): 在函数的原型声明中应该为所有参数给出标识符出于兼容性、清晰性和可维护性的原因,应该在函数的原型声明中为所有参数给出名字。

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

规则16.5(强制): 不带参数的函数应当声明为具有void 类型的参数

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

void myfunc ( void );

规则16.6(强制): 传递给一个函数的参数应该与声明的参数匹配。

这个问题可以通过使用函数原型完全避免——见规则8.1。本规则被保留是因为编译器可

能不会标记这样的约束错误。

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

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

例如:

51

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 语句。

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

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

如果程序员写为:

if (f) /* not compliant – gives a constant non-zero value which is the address of f –

use

either f () or &f */

{

/* … */

}

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

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

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

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

十七 指针和数组

规则17.1(强制): 指针的数学运算只能用在指向数组或数组元素的指针上。

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

规则17.2(强制): 指针减法只能用在指向同一数组中元素的指针上。

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

规则17.3(强制): >>=<<= 不应用在指针类型上,除非指针指向同一数组。

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

规则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 级的间接指针会严重削弱对代码行为的理解,因此应该避免。

规则17.6(强制): 自动存储对象的地址不应赋值给其他的在第一个对象已经停止存在后仍然保持的对象。

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

例如:

int8_t * foobar (void)

{

int8_t local_auto;

return (&local_auto); /* not compliant */

}

十八 结构与联合

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

结构或联合的完整声明应该包含在任何涉及该结构的转换单元之内。见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)对象。

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

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

规则18.4(强制): 不要使用联合。

十九 预处理指令

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

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

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

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

规则19.3(强制): #include 预处理指令应该跟随”filename”序列。

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

#include “ filename.h”

#include

#define FILE “filename.h”

#include FILE

规则19.4(强制): C的宏只能扩展为用大括号括起来的初始化、常量、小括号括起来的表达式、类型限定符、存储类标识符或do-while-zero 结构。

这些是宏当中所有可允许使用的形式。存储类标识符和类型限定符包括诸如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)。

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

规则19.8(强制): 函数宏的调用不能缺少参数

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

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

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

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

函数宏的定义中,参数应该用小括号括起来。例如一个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(强制): 在单一的宏定义中最多可以出现一次 # ## 预处理器操作符。

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

规则19.13(建议): 不要使用# ## 预处理器操作符。

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

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

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 指令(根据上下文内容)。如果其中一个被排除指令的组成形式不好(badlyformed),编译器忽略它时不会给出任何警告,这将带来不幸的后果。

本规则要求所有预处理指令在句法上是有效的,即使它们出现在被排除的代码块中。特别地,要确保 #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 应该放在同一个文件中。遵循本规则会保持良好的代码结构并能避免维护性问题。

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

相关的所有指令放在一个文件中即可。

二十 标准库

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

通常 #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(强制): 传递给库函数的值必须检查其有效性。

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

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

math.h 中的许多数学函数,比如:

负数不能传递给sqrt 或log 函数;

fmod 函数的第二个参数不能为零

toupper 和tolower:当传递给toupper 函数的参数不是小写字符时,某些实现能产生并非预期的结果(tolower 函数情况类似)

如果为ctype.h 中的字符测试函数传递无效的值时会给出未定义的行为

应用于大多数负整数的abs 函数给出未定义的行为

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

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

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

调用函数前检查输入值

设计深入函数内部的检查手段。这种方法尤其适应于实验室内开发的库,纵然它也可以用于买进的第三方库(如果第三方库的供应商声明他们已内置了检查的话)。

产生函数的“封装”(wrapped)版本,在该版本中首先检查输入,然后调用原始的函

数。

静态地声明输入参数永远不会采取无效的值。

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

规则20.4(强制): 不能使用动态堆的内存分配

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

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

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

规则20.5(强制): 不要使用错误指示errno

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

规则20.6(强制): 不应使用库中的宏offsetof

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

规则20.7(强制): 不应使用setjmp 宏和longjmp 函数 

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

规则20.8(强制): 不应使用信号处理工具

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

规则20.9(强制): 在产品代码中不应使用输入/输出库

这包含文件和I/O 函数fgetpos、fopen、ftell、gets、perror、remove、rename 和ungetc。流和文件I/O 具有大量未指定的、未定义的和实现定义的行为。本文档中假定正常情况下嵌入式系统的产品代码中不需要它们。

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

规则20.10(强制): 不应使用库中的函数atofatoi atol

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

规则20.11(强制): 不应使用库中的函数abortexitgetenv system

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

规则20.12(强制): 不应使用库中的时间处理函数。

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

二十一 运行时错误

规则21.1(强制): 最大限度降低运行时错误必须要确保至少使用了下列方法之一:

a) 静态分析工具/技术;

b) 动态分析工具/技术;

c) 显式的代码检测以处理运行时故障

运行时检测是个问题,它不专属于C,但C 程序员需要加以特别的注意。这是因为C 语言在提供任何运行时检测方面能力较弱。对于鲁棒的软件来说,动态检测是必需的,但C 的实现不需要。因此C 程序员需要谨慎考虑的问题是,在任何可能出现运行时错误的地方增加代码的动态检测。当表达式仅仅由处在良好定义范围内的值组成时,倘若可以声称,对于所有处在定义范围内的值来说不会发生异常的话,运行时检测就不是必需的。如果使用了这样的声明,它应该与它所依赖的假设一起组织成文档。然而如果使用了这种方法,一定要小心的是,后续的代码改变可能会使原先的假设无效,或者要小心出于某种原因而改变了原先的假设。

                                                                                                                         

你可能感兴趣的:(编程修养,c语言)