以下等级一般分为三种,建议,必要,强制
建议:建议准则应该在合理可行的范围内遵循这些条目。建议准则不需要正式偏差授权
必要:声明符合本文件的 C 代码应符合所有必要准则,如果有不符合项,则应如第 5.4 节所述,有正式的偏差声明。
强制:声明符合本文档的C 代码应符合每项强制准则,且不允许有偏离。
所有源文件都应通过编译且没有任何编译错误
等级:该指令必要被执行
原理:有的编译器可能会尽管编译错误,但仍会生成目标模块。但是,执行生成的程序可能会产生意外的行为。
所有代码应可追溯到书面要求
等级:该指令必要被执行
原理:所有的程序代码都应该是项目功能需要的,例如开发员人debug时的代码应该在量产时被删除,否则可能导致输出异常,影响其他控制器。
追溯代码到文件化要求的方法应由项目决定。 一种实现可追溯性的方法是对照相应的已针对需求进行审核过的设计文档来审核代码。
运行时的故障,应尽量减少
等级:该指令必要被执行
原理:C 语言设计提供非常有限的内置的运行时检查。虽然这种方法可以生成紧凑而快速的可执行代码,但是它将运行时检查的负担放在了程序员身上。因此,为了达到预期的健壮性水平,程序员必须仔细考虑在可能发生运行时错误的地方添加动态检查。
运行时故障一般包括以下可能:
◆ 算术错误:这包括在表达式求值中出现的错误,例如上溢,下溢,被零除或因移位而丢失有效位。 在考虑整数溢出时,请注意,无符号整数计算不会严格溢出,而是会产生确定的值,但可能是意料之外的值。 应仔细考虑算术表达式中的值范围和运算顺序。该项在编写程序时时常需要注意
◆ 指针算术:确保在动态计算地址时,计算出的地址是合理的,并且指向有意义的地方。特别需要确保的是,如果一个指针指向数组中的某个点,那么当指针被增加或以其他方式改变时,它仍然指向同一个数组中的某个点。参见指针算法的限制—Rule 18.1、Rule 18.2 和 Rule 18.3。
◆ 数组超限错误:在使用数组索引对数组进行索引之前,请确保数组索引在数组大小的范围内—Rule 18.1。该项在编写程序时时常需要注意
◆ 函数参数:在将参数传递给库函数(Dir 4.11)之前,应检查其有效性
◆ 指针引用:除非指针已知为非 NULL,否则在引用指针之前应进行运行时检查。一旦进行了检查,在单个函数中推断指针是否已经更改以及是否需要另一个检查就相对简单了。跨函数边界进行推理要困难得多,特别是在调用其他源文件或库中定义的函数时。
◆ dynamic memory 动态存储:如果正在执行动态内存分配,则必须检查每个分配是否成功,并且设计并测试了适当的降级或恢复策略
汇编语言的所有运用都应记录在案
等级:该指令建议被执行
原理:应该记录使用汇编语言的基本原理以及 C 和汇编语言之间的接口连接机制。
汇编语言应被封装和隔离
等级:该指令必要被执行
原理:使用汇编语言说明的地方,应将它们封装和隔离在下述结构中:
◆ 汇编语言函数;
◆ C 语言函数(C99 首选内联函数);
◆ C 语言宏。
出于执行效率的考虑,有时必须嵌入简单的汇编指令,例如启用/禁用中断。若有必要(嵌入汇编指令), 则建议使用宏或内联函数(C99)来实现。
封装汇编语言的益处:
◆ 提高可读性;
◆ 封装宏或函数的名称和文档,清楚地说明了汇编指令的意图;
◆ 所有执行相同指定功能的汇编语言代码可以共享相同的封装,从而提高可维护性;
◆ 当实现方案或静态分析变动时,封装后的汇编语言可以轻松的被替换。
注意:内联汇编语言的使用是对标准C 的扩展,因此它违反了 Rule 1.2。
代码段不应被“注释掉”
等级:该指令建议被执行
原理:代码中的未参与编译的部分应该使用条件编译实现,例如:
#if 0
...
#endif
具有相同可见性的相同名称空间中的标识符在印刷/屏幕显示上应明确
等级:该指令建议被执行
原理:同一段代码中的相同名称空间下列字符不能同时存在于两个变量中:
大写与小写,如id2_abc与id2_ABC不要同时存在
带下划线与不带下划线,如 id1_a_b_c与id1_abc
字母“ O”和数字“ 0”,如id6_O和id6_0
字母“ I”和数字“ 1”,如id4_I和 id4_1
字母“ I”和字母“ l”,如id4_I和id4_l
字母“ l”和数字“ 1”,如id4_l和 id4_1
字母“ S”和数字“ 5”,如id4_S和 id4_5
字母“ Z”和数字“ 2”,如id5_Z和 id5_2
字母“ n”和字母“ h”,如id5_n和 id5_h
字母“ B”和数字“ 8”,如id7_B和 id7_8
字母序列“ rn”(“ r”后跟“ n”)和字母“ m”,如id8_rn和 id8_m
int32_t id9_rn;
struct
{int32_t id9_m; /* Compliant(这里合规,是因为它和上面的 id9_rn 不是同一命名空间) */
};
应使用指示大小和符号的 typedef 类型代替基本数字类型
等级:该指令建议被执行
基本数字类型(char,short,int,long,long long(C99),float,double 和long double)不应被使
用,而应使用由 typedef 定义的特定长度的数字类型。
原理:这个实际代码中使用的很多,autosar标准数据type中也有定义,如unsigned char被typedef为uint8
如果函数返回错误信息,则应测试该错误信息
等级:该指令必要被执行
使用:一个函数(无论它是标准库的一部分,第三方库还是用户定义的功能)都可以视为提供了一些指示错误发生的方法。 这可以通过错误标志,某些特殊的返回值或其他方式进行。 每当函数提供这种机制时,调用程序应在函数返回后立即检查错误指示。
但是,请注意,检查函数的输入值被认为是比在函数完成后尝试检测错误更可靠的错误预防方法(请参见Dir 4.11)。
如果指向结构体或联合的指针在转换单元中从未解引用,则应该隐藏该对象的实现
等级:该指令建议被执行
原理:在使用结构体或联合体时,如果没有进行解引用,即访问内部的数据定义,则应该通过指针操作.
示例:
/* Opaque.h */
#ifndef OPAQUE_H
#define OPAQUE_H
typedef struct OpaqueType *pOpaqueType;
#endif
/* Opaque.c */
#include "Opaque.h"
struct OpaqueType
{
/* Object implementation */
};
/* UseOpaque.c */
#include "Opaque.h"
void f ( void )
{
pOpaqueType pObject;
pObject = GetObject ( ); /* Get a handle to an OpaqueType object */
UseObject ( pObject ); /* Use it... */
}
在C文件中进行具体的结构体/联合体的定义,在头文件通过指针定义该数据类型。实际访问时不会访问具体的内部数据,保证内部数据不会被意外修改。
应该优先使用函数而不是类函数的宏,因为它们是可互换的
等级:该指令建议被执行
原理:在大多数情况下,应该使用函数而不是宏。函数执行参数类型检查并对其参数求值一次,从而避免了潜在的多重副作用问题。在许多调试系统中,步进执行函数比步进执行宏更容易。
尽管如此,宏在某些情况下可能是有用的。
在决定使用函数还是宏时应考虑下述因素:
•函数参数和结果类型检查的好处;
•C99中内联函数的可用性,尽管注意内联函数的作用程度是由实现定义的;
•代码大小和执行速度之间的权衡
•编译时求值的可能性是否重要:带有常量参数的宏更有可能在编译时求值,而不是相应的函数调用;
•参数是否对函数有效:宏参数是文本的,而函数参数是表达式;
•易于理解和可维护性
示例:
以下示例符合标准。 这些类似函数的宏不能用函数替换,因为它具有 C 操作符作为参数:
#define EVAL_BINOP( OP, L, R ) ( ( L ) OP ( R ) )
uint32_t x = EVAL_BINOP ( +, 1, 2 );
在下面的示例中,使用宏初始化具有静态存储持续时间的对象是合规的,因为此处不允许进行函数调用。
#define DIV2(X) ( ( X ) / 2 )
void f ( void )
{
static uint16_t x = DIV2 ( 10 ); /* 合规 - 此处不允许函数调用 */
uint16_t y = DIV2 ( 10 ); /* 违规 - 此处允许函数调用 */
}
应采取预防措施,以防止头文件的内容被包含多次
等级:该指令必要被执行
原理:这个也很常见,在头文件中需要“头文件卫士”
示例:
/* file.h */
#ifndef FILE_H
/* 违规 - 缺少 #define FILE_H */
#endif
为了便于检查,应使用下列两种形式之一,对头文件的内容进行保护,避免多次包含。
#if !defined ( identifier )
#define identifier
/* Contents of file */
#endif
#ifndef identifier
#define identifier
/* Contents of file */
#endif
应该检查传递给库函数的值的有效性
等级:该指令必要被执行
原理:标准库中的许多函数都不检查传递给它们的参数的有效性。而且,即使标准中要求进行检查,或编译器作者声称检查参数的情况下,也不能保证进行充分的检查。
类似地,其他库中的函数的接口描述可能不会指定这些函数执行的检查。还有一个风险是,指定的检查不一定充分执行。
编程人员应为所有具有限制输入域的库函数(标准库、第三方库和内部库)提供适当的输入值检查。
标准库中具有受限域并需要检查的函数示例如下:
•
•当函数 toupper 传递一个非小写字母的参数(tolower 也有一样的问题)时,某些实现可能会产生意外结果;
•如果传递无效值,
•应用于最负整数的abs函数给出了未定义的行为
尽管
编程人员应该识别任何领域约束,这些约束应该合理地应用于正在使用的函数(可能在接口描述中记录,也可能没有记录),并提供适当的检查,以确保输入值位于该域中。当然,如果需要,可以通过了解参数表示的内容以及参数值的合理范围来进一步限制该值
我们可以通过多种方式来满足本准则的要求,包括:
•在调用函数之前检查值;
•检查调用的库函数中的值——这尤其适用于内部设计的库,但如果供应商能够证明他们已经内置了检查,则可以适用于购买的库;
•生成执行检查的函数的“包装”版本,然后调用原始函数;
•静态地演示输入参数永远不能取无效值
不应使用动态内存分配
等级:该指令必要被执行
此规则适用于所有的动态内存分配的封装,包括:
◆ 标准库提供的内容;
◆ 第三方软件包。
原理:标准库的动态内存分配和释放例程可能导致规则21.3中描述的未定义行为。任何其他动态内存分配系统都可能表现出与标准库类似的未定义行为。
应检查第三方例程的规范,以确保动态内存分配没有被无意中使用
如果决定使用动态内存,应注意确保软件以可预测的方式运行。例如,存在以下风险:
•可用内存可能不足以满足请求-必须注意确保对分配失败有一个安全和适当的响应
•根据使用模式和产生的碎片程度,执行分配或重新分配所需的执行时间差异很大
示例:
为了方便起见,这些示例都是基于使用标准库的动态内存函数,因为它们的接口是众所周知的
在本例中,在第一次调用free之后,由于指针p的值变得不确定,因此该行为是未定义的。尽管在调用free之后存储在指针中的值保持不变,但在某些目标上,它所指向的内存可能不再存在,并且复制该指针的行为可能导致内存异常
#include
void f ( void )
{
char *p = ( char * ) malloc ( 10 );
char *q;
free ( p );
q = p; /* Undefined behaviour - value of p is indeterminate */
p = ( char * ) malloc ( 20 );
free ( p );
p = NULL; /* Assigning NULL to freed pointer makes it determinate */
}
如果要使用malloc,必须要保证释放后的指针为NULL
为对资源提供操作而设计的函数应该按适当的顺序调用
等级:该指令建议被执行
在资源上提供操作的一组函数通常有三种操作:
对于每一组这样的函数,对其操作的所有使用都应按适当的顺序进行。
原理:静态分析器工具能够提供路径分析检查,可以通过程序识别导致未调用序列的释放函数的路径。为了使这种自动检查的好处最大化,因此鼓励开发人员通过设计和声明静态分析器的平衡函数集来启用这些检查。
示例:
/* These functions are intended to be paired */
extern mutex_t mutex_lock ( void );
extern void mutex_unlock ( mutex_t m );
extern int16_t x;
void f ( void )
{
mutex_t m = mutex_lock ( );
if ( x > 0 )
{
mutex_unlock ( m );
}
else
{
/* Mutex not unlocked on this path */
}
}
加锁和解锁需要按顺序调用