MISRA C 规范的学习

MISRA C 规范的学习


本篇文章仅用来整理并记录本人关于MISRA C学习的过程以及分享交流。

资料下载地址(包括2012英文版规范以及2004版中文规范,以及一些讲座资料等):
待上传。。。。

以下以2012版规范进行学习

规则

2 未使用的代码

总结:凡是在code中可以删除的,且不影响code正常运行的代码都被定义为unused code.
在MISRA c中规定,这部分代码需要被移除,否则QAC将会报错.
未使用的代码分有以下几种:

2.1. 项目不应包含不可达代码

int test_func(void)
{
	int i = 3;
	return i;
	i = 8; //这部分就被成为不可达code
}

2.2. 项目不应包含死代码

所谓的死代码就是指定义的未用变量或函数

extern int value;
extern char*p //假定p在别的函数中被应用
void test_func(void)
{
	int i;
	i = 3;//dead code,该函数没有传参,没有返回,对于变量的操作都可视为dead code,除非外部可用变量
	value >> 3;//dead code
	*p++;//error, misra c规定不允许指针地址做++
	(*p)++;//correct, 
}

2.3. 项目不应包含未使用的类型声明

规范对于该rule的解释是:如果代码中出现的类型没有被使用,那么就会造成阅读者的误解,因为他不清楚这个类型没有被使用是因为冗余的还是本身是错误的

int test_func(void)
{
	typedef int LOCAL_INT;//Non-compliant
	return 64;
}

2.4. 项目不应包含未使用的标签声明

这里的标签是指tag。比如说结构体定义时的tag,一般结构体的定义有多种形式,但是misra-c在这方面更严格些。

typedef struct student
{
	int key;
}stu;//这里的tag:student就属于 unused tag declaration
//misra-c中更建议下边的定义方式:
typedef struct
{
	int key;
}stu;

2.5. 项目不应包含未使用的宏声明

这里的标签是指tag。比如说结构体定义时的tag,一般结构体的定义有多种形式,但是misra-c在这方面更严格些。

int test_func(void)
{
	#define TEST_DATA1 3
	#define TEST_DATA2 5//Non-compliant
	return TEST_DATA1;
}

2.6. 项目不应包含未使用的跳转标签

int test_func(void)
{
	int i = 3;
label:			//Non-compliant
	return i;
}

2.7. 函数不应该包含无用的传参

int test_func(int value, 
			  int p[]) //Non-compliant
{
	value = 3;
	return value;
}

一般的为了避免出现unused parameter情况,可以在函数中增加如下代码作为检测。

/* NULL_PTR define with a void pointer to zero definition*/
# ifndef NULL_PTR
#  define NULL_PTR  ((void *)0)
# endif

#define UNUSED_PARAMETER(VariableName)          {if((VariableName) != 0U)\
                                                {/* Do Nothing */}}
int test_func(int value, 
			  void *p)
{
	value = 3;
	UNUSED_POINTER (p)
	UNUSED_PARAMETER(value )
	return value;
}

3.注释

本节主要讲注释符的使用的,略过。

4.字符集及词汇的约定

本节略过
比如'\141t'			//Non-compliant
	'\141\t' 		//compliant
再比如: "\x41g" 		//Non-compliant
		"\x41""g"	//compliant
		"\x41\x67"	//compliant

5.标识符

5.1标识符长度

C99与C90关于标识符长度具体要求是不一样的,C99更宽松一些。但是为了不必要的麻烦,长度应尽量控制。ISO标准要求变量,类型,函数和标签名的长度不超过31个字符

6.关于位域类型

6.1. 位域的声明

MISRA C中关于位域的声明做如下要求:
C90:仅允许有符号或者无符号int型,其他如enum,short,char等被视为未定义
C99:1.允许有符号无符号int 2.允许其他有符号或无符号的显式整数类型

注:MISRA C中关于位域的操作,对于纯int型是不被允许的。需要显示的表明是有符号还是无符号。

typedef unsigned int UINT_16;
unsigned int b1:2; //Compliant 
int b2:2; //Non-Compliant 
UINT_16 b3:2; //Compliant 
signed long b4:2;//Non-Compliant 

6.2. 单字节位域不应该是有符号类型

如果一个位域的值位宽仅有1,那么是不允许被定义为有符号类型的,因为这样会导致负数出现,而偏离程序员本身的期望。MISRA C中规定一个单字节有符号位域包括一个符号位和0值位
注:此规则不适用于未命名的位字段,因为他们的值不能被访问。

signed int f:1;  // Non-compliant; there's only room here for the sign
signed int:1; // unnamed, Compliant 
unsigned int f:1; //Compliant 
signed int f:2;//Compliant 

7. 文字和常量

7.1. 八进制常量不应该被使用

MISRA C中规定,开发者使用前导为0的常量是希望它们被作为十进制来使用,而不是八进制。当然,八进制的转义序列不在此规定中。

extern uint16_t code[10];
code[1] = 109; // compliant-----decimal 109
code[2] = 100; // compliant-----decimal 100
code[3] = 052; // Non-compliant-----decimal 42
code[4] = 071; // Non-compliant-----decimal 57

7.2. 无符号类型整数常量末尾带u

MISRA C 中规定无符号常量末尾必须带u。如果不带u且环境允许的话,该常量会被当做有符号对待。比如说0x8000(32768)在16位环境下为无符号类型,在32位情况下为有符号类型。

下面的示例假设机器具有16位int 类型和32位long 类型。

常量类型合规
32767signed intComplicant
0x7fffsigned intCompliant
32768signed longCompliant
32768uunsigned intCompliant
0x8000unsigned intNon-compliant
0x8000uunsigned intCompliant

7.3. 小写字母"l"不能用于文字后缀

MISRA C规定:在声明文字时,使用大写后缀"L"消除了”1“和"l"之间的潜在歧义。

const int64_t a = 1L;  		// compliant
const int64_t a = 1l;  		// Non-compliant
const uint64_t c = 1Lu;  	// compliant
const uint64_t d = 1lu;		// Non-compliant
const uint64_t e = 1ULL;  	// compliant
const uint64_t f = 1Ull;	// Non-compliant
const int128_t g = 1LL;  	// compliant
const int128_t h = 1ll;		// Non-compliant
const float128_t m = 1.2L;	// compliant
const float128_t n = 2.4l;	// Non-compliant

7.4. 除非对象的类型为“指向const限定字符的指针”,否则字符串值不能赋值给对象。

MISRA C中描述:任何修改字符串值的操作都会导致未定义的行为。例如:当字符串值存储在只读内存中,在这种情况下,修改字符串值的操作将失败还可能导致异常或崩溃。

下边的示例显示了试图修改字符串值的操作:
"0123456789"[0] = '*'; // Non-compliant

下边的示例显示了如何防止字符串的修改:
char *s = "string";// Non-compliant
const volatile char *p = "string";// compliant

extern void func1(char *s1);
extern void func2(const char *s2);
void test(void)
{
	func1("string"); // Non-compliant
	func2("string"); // compliant
}

char *func3(void)
{
	return ("MISRA");// Non-compliant
}
const char *func3(void)
{
	return ("MISRA");// compliant
}

8. 声明和定义

8.1 数据类型应该明确规定

MISRA C中指出,在C90标准中允许在某种情况下省略类型,在这种情况下隐式指定类型为int类型。
但是不推荐这种操作,因为省略显示表达会导致一定的混淆。

示例如下:
extern void test(char c, const k);
这里k其实是const int型,但是实际的期望可能是const char类型。

关于一些合规或不合规的表达示例如下:

first sample_对象声明:

extern int16_t tmp;  // compliant
extern 		   tmp; // Non-compliant,隐式int类型
const  int16_t tmp;  // compliant
const 		   tmp; // Non-compliant,隐式int类型

second sample_函数类型声明:

extern func(void);// Non-compliant,隐式int类型
extern int16_t func(void);// compliant
extern func(char c, const k);// Non-compliant,隐式int类型
extern int16_t func(char c, const int16_t k);// compliant

third sample_类型定义:

typedef (*pfi) (void); // Non-compliant,隐式int返回类型
typedef int16_t (*pfi) (void); // compliant
typedef void (*pfv) (const x); // Non-compliant,参数x为隐式int类型
typedef void (*pfv) (int16_t x); // compliant

fourth sample_结构体成员声明:

struct str
{
	int16_t x; // compliant
	const   y; // Non-compliant,成员y为隐式int类型
}s;

8.2 关于函数传参的规定

MISRA C中表示,参数和形参的数量,类型与函数的预期和实际返回类型之间的不匹配可能会导致未定义的行为,该规则与规则8.1和规则8.4的目的是通过要求显式指定参数类型和函数返回类型来避免这种未定义的行为。规则17.3确保该信息在函数调用时可用,因此要求编译器诊断检测到的任何不匹配。

该规则还要求在声明中为所有参数指定名称。参数名可以提供有关函数接口的有用信息,声明和定义之间的不匹配可能表明编程错误

注:空的参数列表在原型中时无效的。如果一个函数类型没有参数,它的原型形式应使用关键字void。

下边示例展示了函数的声明以及相应的定义是否合规:

extern int16_t func1(int16_t n);  // compliant
extern void func2(int16_t ); // Non-compliant,没有具体的参数名
static  int16_t func3();  // Non-compliant,没有入参
static  int16_t func4(void); // compliant

/* compliant */
int16_t func1(int16_t n)
{
	return n;
}
/* Non-compliant */
static int16_t func3(vec, n)
int16_t *vec;
int16_t n;
{
	return vec[n-1];
}
下边示例展示了函数其他应用方面是否合规:
int16_t (*pf1) ();// Non-compliant
int16_t (*pf1) (void);// compliant
typedef int16_t (*pf2_t) (int16_t);// Non-compliant
typedef int16_t (*pf3_t) (int16_t n);// compliant

8.3 所有函数或对象的声明都应该有相同的名字和类型

声明或引用函数时,要考虑参数类型,以及参数名的一致性即函数定义及其声明的接口一致性。

示例如下:

extern void f(signed int tmp);
	   void f(int tmp); // compliant
extern void g(int *const);
	   void g(int*); // Non-compliant

extern int16_T func(int16_t num, int16_t tmp);
// Non-compliant 参数名字不匹配
int 16_t func(int16_t tmp, int16_t num)
{
	return num/tmp;
}
下边的示例中即使width_t与height_h实际的基本类型是一样的,但是也不允许如下的示例操作:
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) // Non-compliant
{
	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;
}

8.4 当定义了具有外部链接的对象或函数时,合规声明应该是可见的

MISRA C中指出如果一个对象或函数的声明在定义该对象或函数时可见,编译器必须检查该声明和定义是否合规。在存在函数原型的情况下,如规则8.2所要求的,检查扩展到函数参数的数量和类型。
使用外部链接实现对象和函数声明的推荐方法是在头文件中声明它们,然后将头文件包含在所有需要它们的代码文件中,包括定义它们的代码文件(参见规则8.5).

extern int16_t count;
	   int16_t count = 0; // compliant
extern uint16_t speed = 6000u; // Non-compliant 在此定义之前没有声明

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)
{
	// compliant;
}
void func2(int16_t x, int16_t y)
{
	//compliant
}
void func3(int16_t x, uint16_t y)
{
	//Non-compliant 参数类型不一致
}
void func4(void)
{
	//Non-compliant 在此定义前没有func4的声明
}
static void func5(void)
{
	//compliant 该规则不适用于带内部链接的对象或函数
}

8.5 一个外部对象或函数应该在一个且只能一个的文件中被声明一次

NISRA C中指出, 通常,单个声明将在头文件中进行,头文件将包含在定义或使用标识符的任何翻译单元中。这确保了以下之间的一致性:

  1. 声明和定义;
  2. 不同翻译单元的声明

注:一个项目中可能有多个头文件,但每个外部对象或函数只能在一个头文件中声明
示例如下:

/* featureX.h */
extern int16_t a; // Declare a

/* file.c */
int16_t a = 0; // Define a

8.6 具有外部链接的标识符应当只有一个外部定义

该规范指出,如果使用的标识符存在多个定义(在不同的文件中)或根本不存在定义,则该行为是未定义的。该规则不允许在不同的文件中有多个定义,即使这些定义是相同的。如果声明不同,或将标识符初始化为不同的值,则为未定义的行为。

下边的示例中对象i被定义了两次:
/* file.c */
int16_t i = 10;
/* file1.c */
int16_t i = 20; //Non-compliant, 两次定义 i
下边的示例中对象j有一个临时定义和外部定义
/* file3.c */
int16_t j;//临时定义
int16_t j = 0; //compliant 外部定义
下边的示例中是不合规的,应为file4.c中的临时定义在翻译单元结束时会转变为外部定义,导致存在两个外部定义对象k
/* file4.c */
int16_t k;//临时定义-》变为外部定义

/* file5.c */
int16_t k = 0;//外部定义

8.7 如果函数和对象仅在一个翻译单元中被引用,那么它们不应该被外部链接定义

规范中指出通过赋予对象内部链接或没有链接来限制对象的可见性,可以减少无意中访问该对象的机会。类似地,通过赋予函数内部链接来降低函数的可见性,可以减少无意中调用函数的可能性。

遵循此规则还可以避免标识符与另一个翻译单元或库中的相同标识符之间的混淆。

8.8 静态存储类说明符应该被使用在所有内部链接的对象和函数的声明中

规则指出,当一个对象或函数被静态说明符修饰后,则对象或函数不能作为外部链接使用。

static int32_t x = 0; //内部链接,compliant
extern int32_t x;// Non-compliant
static int32_t f (void)//内部链接,compliant
int32_t f (void)// Non-compliant
{
	return 1;
}
static int32_t g (void);//内部链接,compliant
extern int32_t g (void)// Non-compliant
{
	return 1;
}

8.9 如果一个对象的标识符只出现在一个函数中,那么它应该在块作用域中定义

对于标题块作用域,个人理解的是局部变量。仅在单一函数中出现的。

下边合规的示例中, 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;
}

8.10 内联函数应该用静态存储类说明符声明

如果使用外部链接声明了内联函数,但没有在同一翻译单元中定义,则该行为是未定义的。
对使用了外部链接声明的内联函数的调用可以调用函数的外部定义,也可以使用内联定义。尽管这不会影响被调用函数的行为,但它可能会影响执行时间,从而对实时程序产生影响。

注:一个内联函数可以被多个翻译单元使用,只要把它的定义放在头文件中。

8.11 数组被外部链接引用时必须有一个确定的size

虽然可以声明不完全类型的数组并访问其元素,但当数组的大小可以显式地确定时,这样会更安全。为每个声明提供大小信息可以检查它们的一致性,以及还允许静态检查器执行一些数组边界分析。

extern int32_t array1[10];//compliant
extern int32_t array2[];//Non-compliant

8.12 在枚举数列表中,隐式指定的枚举常量的值应该是唯一的

规范中描述:隐式指定的枚举常量的值比前一个值大1。如果隐式指定了第一个枚举常量,则其值为0。
显式指定的枚举常量具有关联常量表达式的值。

如果隐式指定和显式指定的常量在枚举列表中混合使用,则可以复制值。这种复制可能是无意的,并可能导致意外行为。

该规则要求显式地复制枚举常量,这样就明确了意图.

enum colour {red = 3, blue, green, yellow = 5};//Non-compliant, yellow and green
enum colour {red = 3, blue, green = 5, yellow = 5};//compliant

8.13 只要有可能,指针应该指向const类型

指针应该指向const限定类型,除非:
1.用于修改对象 或
2.通过以下两种方式将其复制到另一个指向非const限定类型的指针:任务/内存移动或复制函数。
为了简单起见,该规则是根据指针及其所指向的类型编写的。
但是,它同样适用于数组及其包含的元素类型。数组应该有const限定类型的元素,除非:
1.数组中的任何元素被修改或者
2.通过上述方法将其复制到指向非const限定类的指针。
该规则通过确保指针不会无意中被用于修改对象来鼓励最佳实践。概念上,它等价于最初声明:
1.所有具有const限定类型元素的数组
2.所有指向const限定类型的指针。
然后只在需要符合语言标准约束的地方删除const限定

下边不合规示例中,p不用于修改对象,但对象所指向的类型不是const限定的
uint16_t f (uint16_t *p)
{
	return *p;
}
上述示例的合规写法应为:
uint16_t f (const uint16_t *p)

void h (const uint16_t *p)
{
	*p = 0; // Non-compliant,试图修改const限定的指针
}

uint6_t first (uint16_t a[4]) // Non-compliant
{
	return a[0];
}
uint16_t first (const uint16_t a[5]) //compliant

8.14 restrict 型限定符不应被使用

当使用restrict型限定符时必须要小心,使用它不仅可以提高编译器生成代码的效率,还可以允许改进静态分析。但是使用restrict限定符必须确保两个或多个指针操作的内存区域不重叠。
如果不正确的使用restrict,编译器将生成行为和预期不一致的代码,这是一个风险。

下边示例是合规的,因为MISRA c指南不被应用于库函数。但是编程人员需要确保p q and n没有重叠:
void f (void)
{
	memcpy(p, q, n);
}
下边示例是不合规的,因为参数被restrict定义:
void user_copy(void * restrict p, void * restrict q, size_t n)
{
}

9. 初始化

9.1 非静态变量的值在未设置前不能读取

根据标准,具有静态特性的对象将自动初始化为0,除非显式初始化。非静态属性的对象不会自动初始化,因此可能具有不确定的值。
注:有时可能忽略非静态属性对象的显示初始化。当使用goto或switch语句跳转到标签时,会绕过对象的声明,该对象将按预期声明,但将忽略任何显式初始化。

void f(bool_t b, uint16_t *p)
{
	if(b)
	{
		*p = 6U;
	}
}
void g(void)
{
	uint16_t u;
	f(false, &u);
	if(u == 3U)
	{
		//Non-compliant u has not been assigned a value
	}
}
下边的示例不合规的在C99,goto语句跳过了x的初始化
{
	goto L1;
	uint16_t x = 10u;
L1:
	x = x + 1u;//Non-compliant u has not been assigned a value
}	

9.2 聚合或联合的初始化应该用大括号括起来

使用大括号表示子对象的初始化提高了代码的清晰度,并迫使程序员考虑复杂数据结构(如多维数组或结构数组)中元素的初始化。

标准允许以下三种初始化的形式并且是等价的。但是该规则不允许第一种形式,因为它没有使用大括号显式的显示子对象的初始化:
int16_t array[3][2] = {1,2,0,0,5,6}//Non-compliant
int16_t array[3][2] = {{1,2},{0,0},{5,6}}//compliant
int16_t array[3][2] = {{1,2},{0},{5,6}}//compliant
下边示例通过子对象初始化来初始化数组:
int16_t test[2][2] = {{0},[1][1]=1};//compliant,打印结果:0,0,0,1
int16_t test[2][2] = {{0},[1][1]=1,[1][0]=1};//compliant,打印结果:0,0,1,1
int16_t test[2][2] = {[0][1]=0,{0,1}};//compliant,打印结果:0,0,0,1
int16_t test[2][2] = {{0},[1][0]=1,1};//Non-compliant,test[1][1]与test[1][0]

下边都是些合规的示例:
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}
};

9.3 数组不能部分初始化

规范指出,对于数组的每个元素都应有个清晰的显式初始化值。

int32_t x[3] = {0,1,2};
int32_t y[3] = {0,1};//Non-compliant
float32_t t[4] = {[1]=1.0f,2.0f};//Non-compliant t[0]/t[3]被隐式初始化
char h[10] = "hello";//compliant 数组元素6->9被隐式初始化为'\0'

9.4 一个对象的元素不能被初始化超过1次

示例如下:

关于数组的初始化:
int16_t a1[5] = {1,2,3,4,5};//compliant
int16_t a2[5] = {[0]=1,[1]=2,[2]=3,[3]=4,[4]=5};//compliant
/* 下边示例是Non-compliant, 重复初始化导致元素值被覆盖,会引发预料外的结果。1,2,4,0,5 */
int16_t a3[5] = {[0]=1,[1]=2,[2]=3,[2]=4,[4]=5};
/* 下边的违规示例中,未说明是否存在副作用 */
uint16_t *p;
void test(void)
{
	uint16_t array[2] = {[0] = *p++, [0] = 1};
}
关于结构体的初始化:
struct mystruct
{
	int32_t a;
	int32_t b;
	int32_t c;
	int32_t d;
};
struct mystruct s1 = {100,1,43,999};//compliant
struct mystruct s2 = {.a=100, .b=1, .c=43, .d=999};//compliant
struct mystruct s3 = {.a=100, .b=1, .a=43, .d=999};//Non-compliant,s3=43,1,0,999

9.5 指定的初始化器是用来初始化数组对象的数组的大小应明确指定

为了使意图明确,数组大小应该显式的声明出来。在程序开发期间,如果初始化元素的索引发生变化,超出了数组边界,这样违反了规范(C99第6/7/8节)而报错误信息,这就提供了某种保护。

int a1[] = {[0] = 1};//Non-compliant
int a1[10] = {[0] = 1};//compliant

10 基本类型model

原理

本节中的规则共同定义了基本类型模型,并对C类型系统进行了限制,以便:

  1. 支持更强大的类型检查系统
  2. 为定义控制隐式和显式类型转换使用的规则提供合理的基础
  3. 推广可移植的编码实践
  4. 解决在ISO C中发现的一些类型转换异常
    基本类型模型通过将基本类型分配给那些ISO C认为是算术类型的对象和表达式来实现这一点。例如,将int添加到char中所得到的结果本质上是字符类型,而不是整数提升实际产生的int类型。

基本类型

一个对象或表达式的基本类型是由其基本类型类别和大小定义的。
表达式的基本类型类别反映了其基本行为,可以是:

  1. Essentially Boolean
  2. Essentially character
  3. Essentially enum
  4. Essentially signed
  5. Essentially unsigned
  6. Essentially floating

下表显示了标准整数类型如何映射到基本类型类别。

Booleancharactersignedunsignedenumfloating
_Boolcharsigned char
signed short
signed int
signed long
signed long long
unsigned char
unigned short
unsigned int
unsigned long
unsigned long long
named enumfloat
double
long double

10.1 操作数不能是不适当的基本类型

在下表中,单元格中的数字表示在何处应用限制,以限制将基本类型作为操作符的操作数使用。这些数字与下文中的数字一一对应,并表明为什么实行每一项限制。

operatoroperandBooleancharacterenumsignedunsignedfloating
[ ]integer341
+345
-3458
+ -either35
* /either345
%either3451
< > <= >=either3
== !=either
! && 或any22222
<< >>left345,661
<< >>right34771
~ & ^any345,661
?:1st22222
?:2nd and 3rd

Rationale:

  1. 对这些操作数使用基本为float类型的表达式违反了约束条件
  2. 当操作数被解释为布尔值时,应该始终使用本质上为布尔类型的表达式
  3. 当操作数被解释为数值时,不应使用本质上为布尔类型的操作数
  4. 当操作数被解释为数值时,不应使用本质上为字符类型的操作数。字符数据的数值是实现定义的
  5. 实质上为枚举类型的操作数不应用于算术操作,因为枚举对象使用实现定义的整数类型。因此,涉及enum对象的操作可能会产生意外类型的结果。注意:来自匿名枚举的枚举常量本质上具有signed类型
  6. 移位和按位操作应该只对本质上为无符号类型的操作数执行。在本质上有符号的类型上使用它们所产生的数值是由实现定义的
  7. 移位操作符的右操作数本质上应该是无符号类型,以确保负移位不会导致未定义的行为
  8. 实质上无符号类型的操作数不应用作一元减操作符的操作数,因为结果的有符号性由实现的int大小决定

下边是本节的一些示例说明

enum enuma { a1, a2, a3 } ena, enb; //本质上枚举类型
enum {K1=1, K2=2}; //本质上有符号类型
下面的示例是不合规的,注释指的是导致不合规的编号的基本原理项
f32a & 3U //Rationale 1,违背约束条件
f32a << 2 //Rationale 1,违背约束条件

cha && bla //Rationale 2,char类型用作boolean值
ena ? a1 : a2 //Rationale 2,enum类型用作boolean值
s8a && bla //Rationale 2,signed类型用作boolean值
u8a ? a1 : a2 //Rationale 2,unsigned类型用作boolean值
f32a && bla //Rationale 2,float类型用作boolean值

bla * blb // Rationale 3,boolean类型用作数值使用
bla > blb // Rationale 3,boolean类型用作数值使用

cha & chb // Rationale 4,char类型用作数值使用
cha << 1 // Rationale 4,char类型用作数值使用

ena-- // Rationale 5,enum类型用作算术操作
ena * a1 // Rationale 5,enum类型用作算术操作

s8a & 2 // Rationale 6,signed类型的位操作
50 << 3U // Rationale 6,signed类型的移位操作

u8a << s8a // Rationale 7,移位幅度使用有符号类型
u8a << -1 // Rationale 7,移位幅度使用有符号类型

-u8a // Rationale 8,无符号类型的一元减操作
/* 不合规操作,违反此规则以及10.3 */
ena += a1 // Rationale 5,enum类型使用算术操作
/* 下边示例是合规的 */
bla && blb
bla ? u8a : u8b
cha - chb
cha > chb
ena > a1
K1 * s8a
s8a + s16b
-(s8a) * s8b
s8a > 0
--s16b
U8a + u16b
u8a & 2U
u8a > 0u
u8a << 2U
f32a + f32b
f32a > 0.0

10.2 在进行加减法运算时,不得不当使用本质上是字符型

规则表明,本质上具有字符类型(字符数据)的表达式不应用于算术,因为数据不代表数值
上面的使用是允许的,因为它们允许对字符数据进行可能合理的操作。
例如:

  1. 两个本质上是字符类型的操作数相减可用于在‘0’到‘9’范围内的数字与相应的序数值之间进行转换
  2. 本质上的字符类型和本质上的无符号类型的相加可用于将序数值转换为‘0’到‘9’范围内的相应数字
  3. 从本质字符类型减去本质无符号类型可用于将字符从小写转换为大写
下边是合规的示例:
'0' + u8a  //转换u8a到数字
s8a + '0'  //转换s8a到数字
cha - '0'  //转换cha到序数
'0' - s8a  //转换-s8a到数字

下边是不合规的示例:
s16a - 'a'
'0' + f32a
cha + ':'
cha - ena

10.3 表达式的值不应分配给具有较窄的基本类型或不同的基本类型类别的对象

C语言允许程序员有相当大的自由度,并允许自动执行不同算术类型之间的赋值。但是,使用这些隐式转换可能会导致意想不到的结果,可能会导致值,符号或精度的损失。关于C类型系统的更多细节可以在附录C中找到
MISRA基本类型模型强制使用更强的类型,减少了这些问题发生的可能性。
另:

  1. 本质上有符号类型的非负整数常量表达式可以赋值给本质上无符号类型的对象
  2. 初始化式(0)可用于初始化聚合类型或联合类型
如下示例是合规的:
uint8_t u8a = 0;
bool_t flag = (bool_t)0;
bool_t ser = true;
bool_t get = (u8b > u8c);
u8a = 2;
cha += 1;

u8a = u8b + u8c + u8d;
u8a = (uint8_t) s8a;

u32a = u16a;
u32a = 2U + 125U;

下边示例是不合规的,因为他们有不同的基本类型:
uint8_t u8a = 1.0f; //无符号和浮点型
bool_t bla = 0; //布尔和有符号型
cha = 7; //字符和浮点型
u8a = 'a'; //无符号和字符型
u8b = 1-2; //无符号和有符号型
u8c += 'a'; // u8c = u8c + 'a' 将字符分配给无符号
下边示例是不合规的,由于它们包含对较窄的基本类型的赋值:
s8a = K2;// 常量值不适合
u16a = u32a;// uint32_t to uint16_t
uint8_t fool (uint16_t x)
{
	return x; // uint16_t to uint8_t
}

10.4 通常进行算术转换的运算符的两个操作数应具有相同的基本类型类别

以下允许使用通用形式的字符操作:

  1. 二元运算符+和+=可以有一个操作数本质上是字符类型,另一个操作数本质上是有符号或无符号类型
  2. 二元运算符-和-=的左操作数可以是本质上的字符类型,右操作数可以是本质上有符号或无符号类型
下边的示例是合规的,由于它们是同样的基本类型:
ena > A1
u8a + u16b
下边的示例是合规的,由于字符操作1:
cha += u8a
下边的示例是不合规的:
s8a += u8a //signed and unsigned
u8b + 2 //unsigned and signed
enb > A1 //enum<enumb> and enum<enuma>
u8a += cha //unsigned and char

10.5 不应将表达式的值转换为不适当的基本类型

示例如下:

(bool_t) false //compliant
(int32_t) 3U //compliant
(bool_t) 0 //compliant
(bool_t) 3U //Non-compliant

(int32_t) ena //compliant
(enum enuma) 3 //Non-compliant
(char) enc //compliant

10.6 复合表达式的值不能赋给具有更广泛基本类型的对象

示例如下

u16c = u16a + u16b; // compliant
u32a = (uint32_t) u16a + u16b //compliant  cast causes addition in uint32_t
以下是不合规的示例:
u32a = u16a + u16b; 赋值上的隐式转换
use_uint32(u16a + u16b); //参数的隐式转换

10.7 如果一个符合表达式用作执行通常算术转换的操作符的一个操作数,则另一个操作数不应具有更广泛的基本类型

示例如下:

以下示例是合规的
u32a * u16a + u16b;
(u32a*u16a) + u16b
u32a * ((uint32_t) u16a + u16b)
以下示例是不合规的:
u32a * (u16a + u16b)
u32a + (u16a + u16b)

10.8 复合表达式的值不能被转换为不同的基本类型类别或更广泛的基本类型

示例如下:

(uint16_t) (u32a + u32b)
(uint16_t) (s32a + s32b) // Non-cmpliant 不同的基本类型
(uint16_t) s32a // compliant s32a不是复合表达式
(uint32_t) (u16a + u16b) //Non-compliant 不能转换为更宽的基本类型

11 指针类型转换

指针类型可以分为以下几种:

  • 指针指向的对象
  • 指针函数
  • 不完全类型指针
  • 指向void型指针
  • 一个空指针常量,即值0,可选的转换为void *

涉及指针的唯一被标准允许的转换是:

  • 指针类型到void类型的转换
  • 从指针类型到算术类型的转换
  • 从算术类型到指针类型的转换
  • 从一个指针类型到另一个指针类型的转换

尽管语言约束允许,指针和除整数类型外的任何算术类型之间的转换都是未定义的
以下允许的指针转换不需要显式转换:

  • 从指针类型到_Bool的转换(仅C99)
  • 从空指针常量到指针类型的转换
  • 从指针类型到兼容指针类型的转换,前提是目标类型具有源类型的所有类型限定符
  • 指向对象或不完整类型的指针与void *或其限定版本之间的转换,前提是目标类型具有源类型的所有类型限定符

指针类型和整数类型之间的转换是由实现定义的

11.1 函数指针与任何其他类型的指针之间不得进行转换

基本原理概述:
将指向函数的指针转换为:

  • 指向对象的指针
  • 不完整指针
  • void *指针
    导致未定义的行为

如果函数是通过与被调用函数类型不兼容的指针调用的,则该行为是未定义的。标准允许将指向函数的指针转换为指向不同类型函数的指针。标准也允许将整数转换为指向函数的指针。然而,为了避免由于使用不兼容的指针类型调用函数而导致的未定义行为,这两条规则都是禁止的。

例外的情况:

  1. 空指针常量可以转换为指向函数的指针
  2. 指向函数的指针可以转换为空指针
  3. 函数类型可以隐式转换为指向该函数类型的指针

示例如下:

typedef void (*fp16) (int16_t n);
typedef void (*fp32) (int32_t n);
#include <stdlib.h>  //获取宏NULL
fp16 fp1 = NULL; //compliant  例外的情况1
fp32 fp2 = (fp32) fp1; // Non-compliant,函数指针转换到不同类型的函数指针
if (fp2 != NUll)  //compliant  例外的情况1
{
}
fp16 fp3 = (fp16) 0x8000; //Non-compliant  整型转换为函数指针
fp16 fp4 = (fp16) 1.0e6F; //Non-compliant  浮点型转换为函数指针

在下面的例子中,函数调用返回一个指向函数类型的指针。将返回值转换为void,符合此规则。
typedef fp16 (*pfp16) (void);
pfp16 pfp1;
(void) (*pfp1()); // compliant  例外的情况2 函数指针转换为void

下面的示例展示了从函数类型到指向该函数类型的指针的兼容隐式转换。
extern void f (int16_t n);
f(1); // compliant  例外的情况3
fp16 fp5 = f; // compliant  例外的情况3

11.2 指向不完全类型的指针与任何其他类型之间不得进行转换

基本原理概述:
转换到或从指向不完全类型的指针可能导致指针未正确对齐,从而导致未定义的行为
将指向不完全类型的指针转换或从浮点类型转换总是导致未定义的行为。
指向未完成类型的指针有时用于隐藏对象的表示形式。将指向未完成类型的指针转换为指向对象的指针将破坏这种封装。
例外的情况:

  1. 空指针常量可以转换为指向不完全类型的指针
  2. 指向不完全类型的指针可以转换为void
struct tmp1; //不完全类型
struct tmp2; //不完全类型
struct tmp1 *sp;
struct tmp2 *tp;
int16_t  *ip;
#include <stdlib.h> //获取宏NULL
ip = (int16_t *)sp; // Non-compliant
sp = (struct tmp1 *)1234; // Non-compliant
tp = (struct tmp2 *) sp;// Non-compliant
sp = NULL; // compliant 例外的情况1
struct tmp1 *f(void);
(void) f(); // compliant 例外的情况2

11.3 不应在指向对象类型的指针和指向不同对象类型的指针之间进行强制转换

基本原理概述:
将指向对象的指针转换为指向不同对象的指针可能导致指针未正确对齐,从而导致未定义的行为。
即使已知转换会产生正确对齐的指针,但如果该指针用于访问对象,则其行为可能是未定义的。例如,如果一个int类型的对象被作为short类型访问,即使int和short有相同的表示和对齐要求,其行为也是未定义的。
例外的情况:
允许将指向对象类型的指针转换为指向char,signed char 或 unsigned char类型之一的对象类型的指针。标准保证指向这些类型的指针可以用来访问对象的

示例如下:

uint8_t *p1;
uint32_t *p2;

p2 = (uint32_t *)  p1; //Non-compliant 可能导致指针未对齐

const short *p;
const volatile short *q;
q = (const volatile short *) p; // compliant

int * const * p;
const int * const *q;
q = (const int * const *) p; // Non-compliant 指针类型不同

11.4 不应在对象指针和整数类型之间进行转换

基本原理:
将整数转换为指向对象的指针可能导致指针未正确对齐,从而导致未定义行为。
将对象指针转换为整数可能会产生无法用所选整数类型表示的值,从而导致未定义的行为。

应该尽可能避免在指针和整数类型之间进行转换,但在寻址内映射寄存器或其他硬件特定特性时,可能需要进行类型转换。如果使用整数和指针之间的转换,应注意确保所产生的任何指针不会引发规则11.3种讨论的未定义行为。

例外的情况:
具有整型的空指针常量可以转换为指向对象的指针

示例如下:

uint8_t *PORTA = (uint8_t *) 0x0002; // Non-compliant
uint16_t *p;
int32_t addr = (int32_t) &p; // Non-compliant
uint8_t *q = (uint8_t *) addr; // Non-compliant
bool_t b = (bool_t) p; // Non-compliant
enum etag {A, B} e = (enum etag) p; // Non-compliant

11.5 不应该将指向void的指针转换为指向对象的指针

基本原理:
将指向void的指针转换为指向object的指针可能导致指针没有正确对齐,从而导致未定义的行为。在可能但没必要的情况下应该避免使用,例如在处理内存分配函数时,如果要将指向对象的指针转换为指向void的指针,应注意确保所产生的任何指针不会导致规则11.3中讨论的未定义行为
例外的情况:
指向void类型的空指针常量可以转换为指向对象的指针。
示例如下:

uint32_t *p32;
void *p;
uint16_t *p16;
p = p32; // compliant  指针类型由uint32_t 到 void
p16 = p; //Non-compliant
p = (void *) p16; //compliant
p32 = (uint32_t *) p; //Non-compliant

11.6 转换不能在指向void的指针和算术类型之间进行

基本原理:
将整数转换为指向void的指针可能导致指针没有正确对齐,从而导致未定义的行为。
将void指针转换为整数可能会产生无法用所选整数类型表示的值,从而导致未定义的行为

任何非整数算术类型与指向void的指针之间的转换都是未定义的

例外的情况:
值为0的整型常量表达式可以转换为指向void的指针
示例如下:

void *p
uint32_t u;
p = (void *) 0x1234u; // Non-compliant
p = (void *) 1024.0f; // Non-compliant
u = (uint32_t) p; // Non-compliant

11.7 对象指针和非整数算术类型之间不应执行强制转换

将本质上是布尔型,字符型或枚举型的类型转换为指向对象的指针可能导致指针没有正确对齐,从而导致未定义的行为。
将对象指针转换为本质上的布尔型,字符型或枚举型可能会产生无法用所选整数类型表示的值,从而导致未定义的行为。
将指向对象的指针转换为浮点型或从浮点型转换为浮点型导致未定义的行为。

示例如下:

int16_t *p;
float32_t f;
f = (float32_t) p; // Non-compliant
p = (int16_t *) f; // Non-compliant

12 表达式

12.1 表达式中操作符的优先级应该显式设置


2004版本:

数据类型的学习

先来再次了解下各个数据类型:
| 类型标识符 | 类型说明 | 字节数 | 范围 | 备注 |
|–|–|–|–|–|–|
| char | 字符型 | 1 | -128~127 | - |
| unsigned char | 无符号字符型 | 1 | 0~255 | - |
| short | 短整型 | 2 | -32768~32767 | - |
| unsigned short | 无符号短整型 | 2 | 0~65535 | - |
| int | 整型 | 4 | -2147483648~2147483647 | - |
| unsigned int | 无符号整型 | 4 | 0~4294967295 | - |
| float | 单精度 | 4 | - | 7位有效位 |
| double | 双精度 | 8 | - | 15位有效位 |
MISRA-C 中对于表达式中存在隐式数据类型转换的情况作了严格的限制:
规则10.1:以下情况,整型表达式不允许出现隐式转换

  • 整型操作数不是被扩充为更多位数的同符号整数
  • 表达式是复杂表达式
  • 表达式不是常数表达式且是函数的参数
  • 表达式不是常数表达式且是函数的返回表达式

规则10.2:以下情况,浮点数表达式不允许出现隐式转换

  • 浮点型操作数不是被扩充为更多位数的同符号整数
  • 表达式是复杂表达式
  • 表达式是函数的参数
  • 表达式是函数的返回表达式

举两个例子:
第一个是关于符号位的:

//incorrect
unsigned char a = 0x5a;//0101 1010, ~a->11111111 1010 0101
unsigned char b = (~a) >> 4;
printf("%d\r\n", b); //250

//correct
unsigned char a = 0x5a;//0101 1010, ~a->11111111 1010 0101
unsigned char b = ((unsigned char)(~a)) >> 4;
printf("%d\r\n", b); //100

第二个是关于数据范围溢出的:

//当编译器为16位时打印结果为5,5
//当编译器为32位时打印结果为5,65541
#include <stdio.h>
int main(void) { 
    unsigned short a = 10;
    unsigned short b = 65531;
    unsigned short test = a + b;
    unsigned int c = 0;
    unsigned int d;
    d = (a+b)+c;
    printf("%d-%d\r\n",test,d);//5,65541
	return 0;
}
//correct operate
#include <stdio.h>
int main(void) { 
    unsigned short a = 10;
    unsigned short b = 65531;
    unsigned short test = a + b;
    unsigned int c = 0;
    unsigned int d;
    d = (unsigned int)a + (unsigned int)b + c;
    printf("%d-%d\r\n",test,d);//5,65541
	return 0;
}
//有的人可能说将d = (a+b)+c;->d = a+(b+c);也可以避免出现
//溢出问题,但这并不是治本的方法,学习是为了了解本质,而
//不是只为了解决问题。只有明确了每一个操作数的
//实际数据类型,才能保障代码的安全

针对第一个例子,MISRA C中有明确规定:
规则10.5:如果位操作符~和移位操作符<< or >>联合作用于unsigned char
对于MISRA C规范来说,在编码的过程中与其去一一辨别隐式表达式是否在MISRA C规则中,还不如去用强制转换显式的标识出每个操作数的实际数据类型。

关于指针的运算

MISRA C中为了防止越界等问题,做了如下规定:

  1. 只有指向数组的指针才允许进行算数运算
  2. 只有指向同一数组的两个指针才允许加减

持续更新中。。。。。

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
MISRA-C-2012是一种针对C编程语言的软件开发规范,旨在帮助开发人员编写高质量、安全、可靠的嵌入式系统软件。该规范MISRA(Motor Industry Software Reliability Association)组织制定,是一份非常严格和规范的标准,包含了大量的编码规则和指南,旨在避免C语言程序中常见的错误和潜在的安全问题。 MISRA-C-2012规范对于内存管理、控制语句、表达式、数据类型、函数等方面都有详细的规则和建议,例如在使用指针时需要遵守的规则、如何使用控制语句、如何定义函数原型等内容。除此之外,该规范还对编码风格、变量名命名规范等方面进行了规定,以便使程序代码更易于理解和维护。 遵守MISRA-C-2012规范的好处是显而易见的,首先可以增加代码的可读性和可维护性,提高程序的质量和稳定性。其次,通过规范化的编码规则,可以有效地减少程序中的bug和错误,提高软件的可靠性和安全性。另外,MISRA-C-2012规范还有助于提高软件开发人员的编码水平和技术素养,促进团队之间的协作和沟通。 总的来说,MISRA-C-2012规范对于开发嵌入式系统软件的团队和个人来说是一份非常有价值的参考文档,它能够帮助开发人员编写出更加可靠和高质量的软件,符合行业标准和市场需求。因此,我们应该在软件开发过程中严格遵守MISRA-C-2012规范,以确保软件的质量和安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值