等级:必要
分析:可判定,单一编译单元
适用:C90,C99
原理:C90标准允许在某些情况下省略类型,在这种情况下隐式指定int类型。可以使用隐式int类型的情况示例如下:
•对象声明;
•参数声明;
•成员声明;
•typedef声明;
•函数返回类型
省略显式类型声明可能会导致混淆。 例如,在声明中:
extern void g(char c, const k);
k 的类型隐式定义为 const int,而实际可能期望为 const char
示例:
下面的例子显示了兼容和不兼容的对象声明:
extern x; /* 违规 - 隐式int类型 */
extern int16_t x; /* 合规 - 显式类型 */
const y; /* 违规 - 隐式int类型 */
const int16_t y; /* 合规 - 显式类型 */
以下示例显示了合规和违规的函数类型声明:
extern f ( void ); /* 违规 - 返回类型隐式声明为int型 */
extern int16_t f ( void ); /* 合规 */
extern void g ( char c, const k ); /* 违规 - 形参k隐式声明为int型 */
extern void g ( char c, const int16_t k ); /* 合规 */
以下示例显示了合规和违规的类型定义:
typedef ( *pfi ) ( void ); /* 违规 - 返回类型隐式声明为int */
typedef int16_t ( *pfi ) ( void ); /* 合规 */
typedef void ( *pfv ) ( const x ); /* 违规 - 形参x隐式声明为int */
typedef void ( *pfv ) ( int16_t x ); /* 合规 */
以下示例显示了合规和违规的成员声明:
struct str
{
int16_t x; /* 合规
*/
const y; /* 违规 - 成员y隐式声明为int */
} s;
不管是变量还是函数,声明与定义都应该确定数据类型
等级:必要
分析:可判定,单一编译单元
适用:C90,C99
原理:早期的 C 版本,通常称为K&R C [30],没有提供一种机制来检查形参的数量或类型。对象或函数的类
型不必在 K&R C 中声明,因为对象的默认类型和函数的默认返回类型都为 int。
C90 标准引入了函数原型,这是一种声明了形参类型的函数声明形式。这使得 C90 允许(编译环境)根据声明的形参类型进行形参类型检查。同样的,除非函数原型指定可变数量的形参,它也允许检查形参数量。由于代码向后兼容的需要,C90 可以不使用函数原型。出于相同的原因,它也允许省略类型,在这种情况下, 类型将隐式声明为 int。
C99 标准移除了默认的 int 形参,但它通过可设置的选项提供了对旧式的 K&R 样式的函数的支持。
实参和形参的数量、类型和函数的预期返回类型、实际返回类型之间的不匹配,为潜在的未定义行为提供了可能。本规则与 Rule 8.1 和 Rule 8.4 的目的是通过要求明确指定形参类型和函数返回类型来避免这种未定义行为。Rule17.3 则保证了在调用函数时可以获取这些信息,从而可以一起要求编译器诊断出任何的不匹配情况。
本规则还要求为声明中的所有形参指定名称。形参名称可以提供相关功能接口的有用信息,声明与定义间的不匹配可能预示着编程错误。
注意:空参数列表在原型中无效。如果函数类型没有参数,它的原型形式使用关键字void
示例:第一个示例显示了一些函数的声明以及其中一些函数的相应定义。
/* 合规 */
extern int16_t func1 ( int16_t n );
/* 违规 - 未指定形参名称 */
extern void func2 ( int16_t );
/* 违规 - 不是有效的原型形式(空形参须明确填入"void") */
static int16_t func3 ( );
/* 合规 - 原型明确指定了无形参 */
static int16_t func4 ( void );
/* 合规 */
int16_t func1 ( int16_t n )
{
return n;
}
/* 违规 - 旧式的标识符和声明列表 */
static int16_t func3 ( vec, n )
int16_t *vec;
int16_t n;
{
return vec[ n - 1 ];
}
下面的示例描述了此规则在函数类型和函数声明和定义之外的应用:
/* 违规 - 无效的原型形式(空形参须明确填入"void") */
int16_t ( *pf1 ) ( );
/* 合规 - 原型明确指定了无形参 */
int16_t ( *pf1 ) ( void );
/* 违规 - 未指定形参名称 */
typedef int16_t ( *pf2_t ) ( int16_t );
/* 合规 */
typedef int16_t ( *pf3_t ) ( int16_t n );
等级:必要
分析:可判定,系统范围
适用:C90,C99
展开:存储类说明符不在此规则的限定范围内。
原理:同一对象或函数的声明使用一致类型和限定符会使代码更强壮。
在函数原型中指定形参名称可以检查函数定义及其声明的接口一致性。
例外:相同基本类型的兼容版本可以互换使用。 例如,int,signed 和 signed int 都是等效的。
示例:
extern void f(signed int);
void f(int); /* 合规 - 符合例外情况 */
extern void g(int * const);
void g(int *); /* 违规 - 类型限定词不一致 */
注意:以上都不符合Dir 4.6。
extern int16_t func ( int16_t num, int16_t den );
/* 违规 - 形参命名不一致 */
int16_t func ( int16_t den, int16_t num )
{
return num / den;
}
在本例中,area的定义为参数h使用了不同于声明中使用的类型名。即使width_t和height_t是相同的基本类型,这也不符合规则。
typedef uint16_t width_t;
typedef uint16_t height_t;
typedef uint32_t area_t;
extern area_t area ( width_t w, height_t h );
area_t area ( width_t w, width_t h )
{
return ( area_t ) w * h;
}
此规则不要求函数指针声明使用与函数声明相同的名称。 因此,以下示例是合规的。
extern void f1(int16_t x);
extern void f2(int16_t y);
void f(bool_t b)
{
void (*fp1)(int16_t z) = b ? f1 : f2;
}
等级:必要
分析:可判定,单一编译单元
适用:C90,C99
展开:兼容声明是为要定义的对象或函数描述其兼容类型的声明。
原理:如果在定义对象或函数时其声明已可见,则编译器必须检查该声明和定义是否合规。
根据规则 8.2 的要求,在功能原型存在的情况下,检查应扩展到形参的数量和类型。
给全局对象和函数添加声明的建议方法是:在头文件中声明它们,然后将头文件包含在所有需要它们的代码文件中,包括定义它们的代码文件(参见 Rule 8.5)。
示例:在这些示例中,除了代码中的声明或定义之外,没有对象或函数的声明或定义
extern int16_t count;
int16_t count = 0; /* 合规 */
extern uint16_t speed = 6000u; /* 违规 - 定义前未声明 */
uint8_t pressure = 101u; /* N违规 - 定义前未声明 */
extern void func1 ( void );
extern void func2 ( int16_t x, int16_t y );
extern void func3 ( int16_t x, int16_t y );
void func1 ( void )
{
/* 合规 */
}
下面的func3的不兼容定义也违反了规则8.3。
void func2 ( int16_t x, int16_t y )
{
/* Compliant */
}
void func3 ( int16_t x, uint16_t y )
{
/* 违规 - 形参类型不一致, 违反了 Rule 8.3, 即虽有声明但声明违规 */
}
void func4 ( void )
{
/* 违规 - 定义前未声明 */
}
static void func5 ( void )
{
/* 合规 - 此规则不适用于局部全局(internal linkage)的对象和函数 */
}
所有的全局变量和全局函数都应该在头文件中声明。
等级:必要
分析:可判定,系统范围
适用:C90,C99
展开:此规则仅适用于非定义声明
原理:将单一的声明放在头文件中,该头文件再被任何需要调用该标识符的编译单元包含,这是种通常做法。这种做法可以确保:
•声明和定义一致
•不同编译单元中的声明一致
注意:一个项目中可以有多个头文件,但是每个外部对象或函数只能在一个头文件中声明。
示例:
/* featureX.h */
extern int16_t a; /* 声明 a */
/* file.c */
#include "featureX.h"
int16_t a = 0; /* 定义 a */
等级:必要
分析:可判定,系统范围
适用:C90,C99
原理:同一个变量或函数多处定义,编译就会报错
示例:下面的示例中,对象“i”被定义了两次。
/* file1.c */
int16_t i = 10;
/* file2.c */
int16_t i = 20; /* 违规 - 全局变量 i 有两处定义 */
下面的示例中,对象“j”有一个临时定义和一个全局定义
/* file3.c */
int16_t j; /* 暂时定义 */
int16_t j = 1; /* 兼容-外部定义 */
下面的示例不兼容,因为对象k有两个外部定义。file4.c中的暂定定义在编译单元的末尾变成了一个外部定义。
/* file4.c */
int16_t k; /* Tentative definition - becomes external */
/* file5.c */
int16_t k = 0; /* External definition */
等级:必要
分析:可判定,系统范围
适用:C90,C99
原理:将对象定义为局部全局(internal linkage)或临时(no linkage)属性,可以限制对象的可见范围,从而减少无意中访问该对象的机会。同理,赋予函数局部属性,也可降低其可见性,从而减少无意中调用该函数的机会。
遵守此规则还避免了不同编译单元或库的同名标识符之间出现混淆的可能。
等级:必要
分析:可判定,单一编译单元
适用:C90,C99
展开:此规则同时适用于声明和定义。
原理:该标准规定,如果一个对象或函数是用外部标准类说明符声明的,并且该对象或函数的另一个声明已经可见,则链接是由先前的声明指定的。这可能会令人困惑,因为外部存储类指定符可能会创建外部链接。
因此,静态存储类说明符“static”应始终应用于局部全局对象和局部函数(具有内部链接的对象和功能)。
示例:
static int32_t x = 0; /* 定义: 局部全局变量 internal linkage */
extern int32_t x; /* 违规 */
static int32_t f(void); /* 声明: 局部函数 internal linkage */
int32_t f(void) /* 违规 */
{
return 1;
}
static int32_t g(void); /* 声明: 局部函数 internal linkage */
extern int32_t g(void) /* 违规 */
{
return 1;
}
局部函数和变量的声明,也需要加上static修饰
等级:必要
分析:可判定,单一编译单元
适用:C90,C99
原理:在块范围内定义对象可减少无意间访问该对象的可能性,并明确表示出“不应在其他位置访问该对象” 的意图。
在一个函数中,对象是定义在最外层还是最内层很大程度上取决于个人风格。
人们认识到,在某些情况下可能无法遵守本规则。例如,在块范围内声明的具有静态存储属性的对象不能直接从块的外部访问。如果不使用对对象的间接访问,就不可能设置和检查单元测试用例的结果。在这种情况下,某些项目可能更倾向于不应用此规则。
示例:在下面的合规示例中,i 被声明在块范围内,因为它是循环计数。相同文件中的其他函数无法将其用作任何其他目的。
void func ( void )
{
int32_t i;
for ( i = 0; i < N; ++i )
{
}
}
在下面的合规示例中,函数计数跟踪被调用的次数并返回该数字。 其他函数不需要知道 count 实现的详细信息,因此调用计数器是使用块作用域定义的。
uint32_t count(void)
{
static uint32_t call_count = 0;
++call_count;
return call_count;
}
等级:必要
分析:可判定,单一编译单元
适用:C90,C99
原理:如果一个内联函数是用外部链接声明的,但在同一编译单元中没有定义,则该行为是未定义的
对使用外部链接声明的内联函数的调用可以调用该函数的外部定义,也可以使用内联定义。虽然这不会影响被调用函数的行为,但它可能会影响执行时间,从而对实时程序产生影响
注意:通过将内联函数的定义放在头文件中,可以使多个编译单元可用
等级:建议
分析:可判定,单一编译单元
适用:C90,C99
展开:此规则仅适用于声明,不适用于定义。 定义数组的同时,通过初始化来隐式的指定其大小是被允许的。
原理:尽管标准 C 允许使用不完整类型声明数组并访问其元素,但显式的确定数组大小更安全。为每个声明提供其大小信息,可以检查它们的一致性。它还可以允许静态检查器在无需分析多个编译单元的情况下执行某些数组边界分析。
示例:
extern int32_t array1[10]; /* 合规 */
extern int32_t array2[]; /* 违规 */
等级:必要
分析:可判定,单一编译单元
适用:C90,C99
原理:隐式指定的枚举常量的值比其前一个值大 1。如果第一个枚举常量也是隐式指定的,则其值为 0。
显式指定的枚举常量的值为其指定的常量表达式的值。如果枚举列表中混合使用隐式和显式指定常量,则可能会出现重复值。这种重复值可能无意义,并可能导致意外的行为。
本规则要求明确枚举常量的重复值,从而明确其意图。
示例:在下面示例中,枚举常量 yellow 和 green 的值相同
/* 违规 - 隐式的指定 yellow 与 green 的值相同 */
enum colour { red = 3, blue, green, yellow = 5 };
/* 合规 - yellow 与 green 以显式的方式指定其值相同 */
enum colour { red = 3, blue, green = 5, yellow = 5 };
等级:必要
分析:可判定,系统范围
适用:C90,C99
展开:除下述两种情况外,都应将指针限定为指向 const 限定类型:
•用于修改对象,或
•通过以下两种方式将其复制到指向非const限定类型的另一个指针:
-内存分配
-内存移动或复制函数。
为简单起见,此规则是根据指针及其指向的类型编写的。但是,它同样适用于数组及其包含的元素类型。除下述情况外,数组应有 const 限定类型的元素:
•数组的任何元素被修改,或者
•它被复制到一个指针,该指针指向的类型不是通过上述方法限定的常量
原理:此规则是确保不会无意间使用指针来修改对象的最佳推荐。从概念讲,它等效于声明了:
•所有数组的元素类型为const- qualied,和
•所有指向const限定类型的指针。
然后,仅在有必要遵守语言标准的约束时,才移除 const 限定。
示例:下面的违规示例中,指针p 不用于修改对象,但它指向的类型没有 const 限定。
uint16_t f(uint16_t *p)
{
return *p;
}
此函数以下面方式定义,那它就是合规的:
uint16_t f(const uint16_t *p)
下面的示例则违背了C 语言的限制,它使用指向 const 限定类型的指针来修改对象。
void h(const uint16_t *p)
{
*p = 0;
}
下面的示例中,指针本身是 const 限定的,但其指向的类型不是,而指针 s 不用于修改对象,因此违规。
#include
char last_char(char * const s)
{
return s[strlen(s) - 1u];
}
若函数以下面方式定义,则此段代码合规:
char last_char(const char * const s)
在下面违规示例中,数组a 的所有元素均未被修改,但元素类型没有const 限定。
uint16_t first(uint16_t a[5])
{
return a[0];
}
将函数以下面方式定义后,此段代码合规:
uint16_t first(const uint16_t a[5])
不被修改的指针或数组都需要加上const属性!
等级:必要
分析:可判定,系统范围
适用:C90,C99
原理:谨慎的使用“restrict”类型限定符可以提高编译器编译代码的效率。它还可以优化静态分析。但是,要使用“restrict”类型限定符,程序员必须确保用两个或多个指针操作的内存区域不会重叠。
如果未正确使用“restrict”,则会存在很大的编译器生成与预期不符的代码的风险。
示例:MISRA C 准则不适用于标准库函数,故下面示例合规。但是,程序员必须确保由 p、q、和 n 定义的区域不重叠。
#include
void f(void)
{
/* memcpy 具有“restrict”类型的形参 */
memcpy(p, q, n);
}
下面的示例违规,它使用了“restrict”类型限定符。
void user_copy(void * restrict p, void * restrict q, size_t n)
{
}
这个类型限定符用的也很少。基本不会遇到。
等级:必要
分析:可判定,系统范围
适用:C90,C99
展开:就本规则而言,数组元素或结构成员应被视为离散对象
原理:根据标准,除非显式初始化,否则具有静态存储时间的对象(静态变量)将自动初始化为零。具有自动存储持续时间的对象(临时变量)不会自动初始化,因此可能具有不确定的值
注意:有时自动对象的显式初始化可能会被忽略。当使用goto或switch语句跳转到标签“绕过”对象的声明时,就会发生这种情况;该对象将按预期声明,但任何显式初始化都将被忽略。
示例
void f ( bool_t b, uint16_t *p )
{
if ( b )
{
*p = 3U;
}
}
void g ( void )
{
uint16_t u;
f ( false, &u );
if ( u == 3U )
{
/* * 违规 - u 尚未赋值 */
}
}
在下面的不符合 C99 的示例中,goto 语句跳过了x 的初始化。
注意:这个例子也不符合规则15.1。
{
goto L1;
uint16_t x = 10u;
L1:
x = x + 1u; /* 违规 - x 尚未赋值 */
}
等级:必要
分析:可判定,单一编译单元
适用:C90,C99
展开:此规则适用于对象和子对象的初始化。
形式为{0}的初始化程序将所有值都设置为 0,可用于初始化无嵌套括号的子对象。
注意:此规则本身不需要显式初始化对象或子对象。
原理:使用大括号表示子对象的初始化可以提高代码的清晰度,并迫使程序员考虑复杂数据结构(如多维数组或结构数组)中元素的初始化。
例外
int16_t y[ 3 ][ 2 ] = { 1, 2, 0, 0, 5, 6 }; /* 违规 */
int16_t y[ 3 ][ 2 ] = { { 1, 2 }, { 0 }, { 5, 6 } }; /* 合规 */
int16_t y[ 3 ][ 2 ] = { { 1, 2 }, { 0, 0 }, { 5, 6 } }; /* 合规 */
下面的示例中,使用了指定初始化来初始化子对象 z1[1],通过例外 3,z1 初始化合规。基于相同的理由,z2 的初始化亦合规。但是,z3 的初始化违规,因为子对象 z3[1]的一部分使用了指定初始化方式,但并未用花括号括起来。对 z4 的初始化是合规的,它使用了指定初始化来初始化子对象 z4[0],并且将子对象z4[1]的初始化用花括号括了起来。
int16_t z1[ 2 ][ 2 ] = { { 0 }, [ 1 ][ 1 ] = 1 }; /* 合规 */
int16_t z2[ 2 ][ 2 ] = { { 0 },
[ 1 ][ 1 ] = 1, [ 1 ][ 0 ] = 0
}; /* 合规 */
int16_t z3[ 2 ][ 2 ] = { { 0 }, [ 1 ][ 0 ] = 0, 1 }; /* 违规 */
int16_t z4[ 2 ][ 2 ] = { [ 0 ][ 1 ] = 0, { 0, 1 } }; /* 合规 */
下例中的第一行在不使用嵌套括号的情况下初始化了 3 个子数组。 第二和第三行显示了编写相同初始化程序的等效方法。
float32_t a[ 3 ][ 2 ] = { 0 }; /* 合规 */
float32_t a[ 3 ][ 2 ] = { { 0 }, { 0 }, { 0 } }; /* 合规 */
float32_t a[ 3 ][ 2 ] = { { 0.0f, 0.0f },
{ 0.0f, 0.0f },
{ 0.0f, 0.0f }
}; /* 合规 */
union u1 {
int16_t i;
float32_t f;
} u = { 0 }; /* 合规 */
struct s1 {
uint16_t len;
char buf[ 8 ];
} s[ 3 ] = {
{ 5u, { 'a', 'b', 'c', 'd', 'e', '\0', '\0', '\0' } },
{ 2u, { 0 } },
{ .len = 0u } /* 合规 - 成员buf被隐式初始化 */
}; /* 合规 - s[]完全初始化 */
等级:必要
分析:可判定,单一编译单元
适用:C90,C99
展开:如果数组对象或子对象的任何元素被显式初始化,则整个对象或子对象都应显式初始化
原理:为数组的每个元素提供一个显式的初始化,可以清晰地考虑到每个元素。
例外:
1.{0}形式的初始化可用于显式初始化数组对象或子对象的所有元素。
2.可以使用仅由指定的初始化组成的数组初始化形式,例如执行稀疏初始化。
3.使用字符串文字初始化的数组不需要为每个元素都初始化。
示例
/* 合规 */
int32_t x[ 3 ] = { 0, 1, 2 };
/* 违规 - y[2] 被隐式初始化 */
int32_t y[ 3 ] = { 0, 1 };
/* 违规 - t[0] 和 t[3] 被隐式初始化 */
float32_t t[ 4 ] = { [ 1 ] = 1.0f, 2.0f };
/* 合规 - 以指定初始化方式进行矩阵的稀疏初始化 */
float32_t z[ 50 ] = { [ 1 ] = 1.0f, [ 25 ] = 2.0f };
下面的合规示例中,数组 arr 的每个元素均被显式初始化:
float32_t arr[3][2] =
{
{ 0.0f, 0.0f },
{ PI / 4.0f, -PI / 4.0f },
{ 0 } /* 子对象arr[2]的所有元素均被初始化 */
};
下面示例中,数组元素 6 到 9 被隐式初始化为‘\0’:
char h[10] = "Hello"; /* 合规, 符合例外 3 */
等级:必要
分析:可判定,单一编译单元
适用:C90,C99
展开:此规则适用于对象和子对象的初始化。
通过在 C99 中提供的指定初始化,可以在初始化程序列表中初始化集合(结构或数组)或联合的组件的命名,并允许通过指定数组索引或结构以任何顺序初始化对象的元素,它们应用于的成员名称(不具有初始化值的元素采用未初始化对象的默认值)。
原理:使用指定初始化的方式进行初始化要格外小心,对象元素的初始化可能会不经意的重复,从而导致先前初始化的元素被覆盖。C99 标准并未指定是否覆盖重写的初始化副作用,且附件 J 中未列出。
为了允许稀疏的数组和结构,仅初始化应用程序所需的那些(元素或成员)是可接受的。
示例:
数组初始化:
/*
* 使用位置初始化的必须行为
* 合规 - a1 被初始化为 -5, -4, -3, -2, -1
*/
int16_t a1[ 5 ] = { -5, -4, -3, -2, -1 };
/*
* 使用指定初始化的类似行为
* 合规 - a2 被初始化为 -5, -4, -3, -2, -1
*/
int16_t a2[ 5 ] = { [ 0 ] = -5, [ 1 ] = -4, [ 2 ] = -3,[ 3 ] = -2, [ 4 ] = -1 };
/*
* 重复的指定初始化器元素值将覆盖先前的值
* 违规 - a3 被初始化为 -5, -4, -2, 0, -1
*/
int16_t a3[ 5 ] = { [ 0 ] = -5, [ 1 ] = -4, [ 2 ] = -3,[ 2 ] = -2, [ 4 ] = -1 };
在下面的违规示例中,不确定是否会产生副作用:
uint16_t *p;
void f(void)
{
uint16_t a[2] = { [0] = *p++, [0] = 1 };
}
结构体初始化:
struct mystruct
{
int32_t a;
int32_t b;
int32_t c;
int32_t d;
};
/*
* 使用位置初始化的必须行为
* 合规 - s1 为 100, -1, 42, 999
*/
struct mystruct s1 = { 100, -1, 42, 999 };
/*
* 使用指定初始化的类似行为
* 合规 - s2 为 100, -1, 42, 999
*/
struct mystruct s2 = { .a = 100, .b = -1, .c = 42, .d = 999 };
/*
* 重复的指定初始化器元素值将覆盖先前的值
* 违规 - s3 为 42, -1, 0, 999
*/
struct mystruct s3 = { .a = 100, .b = -1, .a = 42, .d = 999 };
等级:必要
分析:可判定,单一编译单元
适用:C90,C99
展开:该规则同样适用于作为灵活数组成员的数组子对象。
原理:如果一个数组未明确指定大小,则其大小由初始化元素的最高索引确定。当使用指定初始化时,可能并不总是清楚哪个初始化具有最高的索引,尤其是在初始化包含大量元素的情况下。
为了清晰明确设计意图,应明确声明数组大小。这么操作也给程序开发过程中更改初始化数组元素索引提供了保护,因为它违反了限制(参见 C99 第 6.7.8 节),在数组范围外初始化元素。
示例
/* 违规 - 可能无意中定义了一个只有一个元素的数组 */
int a1[] = { [0] = 1 };
/* 合规 */
int a2[10] = { [0] = 1 };