本文章是基于Misra C coding stardand写的,因为原文是技术书,所以我在这里用我的理解给大家解释一下。什么是MISRA C编程规范呢,其实就是为了保证代码的规范而定义的一套c语言的规则而已。广泛应用于汽车领域中。
rule2.2 源代码应该使用/*...*/类型的注释。良好的习惯,虽然很多编译器也都支持//注释,但是不同的编译器可能会造成很多不同的麻烦。
rule2.3 不要在/*...*/中再次进行/*...*/注释了,会造成编译器的误解,因为不支持嵌套的注释。
rule3.5 在嵌入式系统中,因为很多变量不是0就是1或者2, 3,4,这些值都不是很大,为了节约嵌入式中的可怜的空间,我们就使用位域的方法,这个问题是很有意思的问题,这个问题就能描述清楚数据在嵌入式系统中的资源分配问题。
struct message{ unsigned int isCarRunning:1 unsigned int isEngineStop:1 unsigned int lightNumber:4 }上面是什么意思呢,就是在内存中isOpen怎么分配的问题,其实在这里也有很多讲究:有兴趣的童鞋可以查看编译器的资料,主要有两点需要大家注意:
1, 就是大小端问题,迷惑了很多学者的问题,说白了就是一个字节中是从低位往高位排还是高位往低位排的问题。就像老师让学生排队,谁排在第一位都无所谓,但是我们在设计的时候我们要清楚谁站在了第一位,简单写个程序测一下就知道啦或者看你用的哪个编译器看一下就知道啦
2.还有就是一个很蛋疼的问题,就是位域是否重叠,这个如果清楚的话就会知道整个结构体的大小了,不然你还是迷迷糊糊,举个例子我们都知道一个字节占8位,假如现在让你顺序存储一个6位的数和4位的数,当你把6位的数存储结束之后,后面的四位的这个数是从剩下的两位开始存储还是另起炉灶从下个字节开始存储。这个问题想要搞清楚,具体的编译器有具体的做法。这样你就自己会计算出来你这个结构体的大小了。
我自己觉得这一条还是很重要的,对于内存的分配问题尤为重要,希望大家能够理解里面的精髓。
rule5.2
int i; { int i;/*由于c语言中域的概念,这个i会隐藏掉全局变量(上面一个变量)*/ i = 5;/*这个地方给后面的读者造成了灾难,当代码量大的时候,鬼知道你要往哪里赋值*/ }在这个函数内,就相当于隐藏了第一行的i,那么这条规则你就不符合了,所以良好的编程习惯和命名规则还是要好点的。
rule5.3 typedef应当是唯一的标识符,什么意思呢,其实也很简单,比方说
typedef unsigned char uint8_t typedef unsigned char uint8_t/*这样做是违法的,同一个人uint8_t,你不能把人家搞两次*/ unsigned char uint8_t/*这也是违法的,应该没有人这样做吧,哪有这么命名变量的吧*/大型项目当中,应该有个专门的文件来管理这些东西,为了这些烦人的事情发生,把时间浪费在这个上面是毫无意义的。
rule5.4 标签(tag)名称必须是唯一的标识符,比方说
struct stag { uint16_t a; uint16_t b;}; struct stag temp = {0, 0}/*这样做是合法的,目前是没有问题的,那么看下一行有没有什么惊喜*/ union stag temp2 = {0, 0}/*不合法的,怎么可能这样搞,stag没有这么渣吧,一下是搞不了两个的,这样编译器就会不知道你是干嘛的*/也就是说尽量使用清晰明了的标识符,这样既增加后面人的可读性,又不会出现各种麻烦。
rule6.1&6.2 好好见识一下什么是char,你可能并不了解,有单纯的char(这个东东和asii码有着千丝万缕的联系,如果不用它,就尽量别去惹,说不准会很麻烦),signed char,unsigned char, 后面两个一般用于数字型数据,char用于字符型数据,char类型的变量所能接受的只有赋值和等于操作符(=,==,!=)别乱搞什么>=这些不等式,这样会造成麻烦,但是unsigned char, signed char这两个是可以的。
rule6.3 应该使用指示了大小和符号的typedef以代替基本的数据类型,就是说不要用unsigned char temp;这样定义变量,尽量使用typedef unsigned char uint8_t; uint8_t temp;这样来定义变量。原因就是简单。后面人看起来也不费劲。
rule6.4这条规则说我们在定义位域的时候不能enum, short,或者char类型,只能用int型的,原文中说其他类型的行为未定义,那好吧,我们以后乖乖的用int型的就好,少惹麻烦,其实我觉的行,我也不太理解为什么MISRA就觉的不行了,可能是有些编译器的问题吧。
rule6.5 这条规则也很有意思,它说unsigned int 类型的位域至少为2bit长度。还说1bit长度的有符号位域是无用的。这个确实没什么用,符号位都占一位,还能有什么用呢,当然我们设计的时候一般还是超过2bit的长度的。
rule7.1这条是针对8进制数据的,一般也不会用到吧,因为没人喜欢给自己找麻烦,除了考试的时候老师喜欢给你弄点麻烦之外,就是针对8进制的数据不要用就行了,容易造成误解,052是八进制的,十进制的42你说麻烦不麻烦。
rule8.1函数应当具有原型声明,且原型在函数的定义和调用范围内都是可见的,这样做的原因就是可以让编译器能够检查出函数定义和调用的完整性,如果没有原型的话,编译器就会默认,也许就会造成你的代码有bug存在,这也是浪费生命的事,(比如,函数体具有不同的参数数目,调用和定义之间的参数类型不匹配,函数的返回值不匹配等一些问题)这一条看似很普通,但是往往被很多人忽略,最后造成致命的打击。
rule8.2 无论何时声明或定义一个对象或函数, 它的类型都应该是显式声明。
extern x; /* 缺东西了,发现了没有 */ extern int16_t x ; /* 这样才完美 */ const y ; /* 缺东西了,发现了没有 */ const int16_t y ; /* 这样才完美 */ static foo (void) ; /* 缺东西了,发现了没有 */ static int16_t foo (void) ; /* 这样才完美 所以无论变量或是函数,一定要把衣服穿对了,不要玩裸奔 */rule8.3 函数的每个参数类型在声明和定义中必须是等同的,函数的返回类型也该是等同的。这种东西应该都知道,我就不必再废话了
rule8.5头文件中不应该有对象或函数的定义。头文件应该用于声明对象,函数,typedef和宏,而不应该包含或生成占据存储空间的对象或函数的定义(如果被其他文件引用的话会很麻烦,造成对内存致命性的打击,因为每一次引用头文件都会造成内存分配的问题。尤其嵌入式,可怜巴巴就那么一点)这样就清晰地划分了只有C文件才能包含可执行的源代码,而头文件就是用来交流的,只包含声明,是个外交官。
rule8.7 如果对象的访问只是在单一的函数中,那么对象应该在块范围内声明,在不必要的情况下避免使用全局标识符。通常这意味着在一个头文件中声明一个外部标识符,而在定义或使用该标识符的任何文件中包含这个头文件
extern uint16_t a;/* 假设在featureX.h中声明a*/ /*然后对a进行定义 包含这个头文件*/ #include <featureX.h> int16_t a = 0;项目中存在的头文件可能是一个或多个,但是任何一个外部对象或函数都只能在一个头文件中声明。
rule8.9 一个标识符如果存在多个定义(在不同的文件中)或者甚至没有定义,那么这些行为谁也不知道会怎么样,也许就会带来灾难性的麻烦,如果被在不同的文件中被定义两次,而且初始值都不一样,这时候带来的结果就是想哭也不知道找谁。
rule8.11 static 存储类标识符应该用于具有内部链接的对象和函数的定义和声明,要用static好好保护好你的资源,不要让别人找到你,我自己干我自己的事就好。static要常用。除非是在需要外部链接的情况下。static 声明的东西只有在本文件中可见。
rule8.12当一个数组为具有外部链接,它的大小应该是显式声明或者通过初始化进行隐式定义。
int array1[10];/*这个数组的大小就是显示声明*/ extern int array2[];/*鬼知道你这个多大啊,尽管可以在数组不完善时访问其元素,然而任然是在数组的大小可以显示的情况下,这样做才会更为安全。多一点安全,少扣点工资,也挺好*/ int array2[] = {0, 1, 3};/*这样的话就叫做初始化进行隐式定义*/rule9.1 所有自动变量在使用前都应该被赋值,虽然有缺省值为0,但是有一些编译器没有,你说气人不气人,所以就初始化,养成好习惯
rule9.2
应该使用大括号以指示和匹配数组和结构的非零初始化构造。举个例子 int16_t y[3][2] = {1,2,3,4,5,6};/*这么搞编译器不认识啊,这是二位数组啊,看清楚了*/ int16_t y[3][2] = {{1,2},{3,4},{5,6}};/*这么弄就对了*/rule9.3 我们来谈谈枚举enum
如果说你不给赋值,系统会默认从0开始,逐渐增加,但是要注意界限,别一下子搞几千个,那会发生不可预知的错误。 enum colour {red = 3, blue, gree, yellow = 5}/*你说这样合理吗?gree也是5*/ enum colour {red = 3, buue, gree = 5, yellow = 5}/*这么做是能通过编译的,但是有什么意义呢!*/我们来一起看看比较头疼的数值类型的转换,简单举个例子
uint16_t u16a = 40000; uint16_t u16b = 30000; uint32_t u32x; u32x = u16a + u16b;/*我们期望的是70000,但假如说这里int是16位的话,结果就不是我们想要的,很简单,放不下这么大的数字*/10.2&10.3 隐式转化类型:这个是很麻烦的,但又是很重要的,怎么办呢,还是看看比较好,记住的话会出了问题多一个思路。
下列条件成立时,整型表达式的值不应隐式转换为不同的基本类型:
a) 转换不是带符号的向更宽整数类型的转换,或者 b) 表达式是复杂表达式,或者
c) 表达式不是常量而是函数参数,或者
d) 表达式不是常量而是返回的表达式。
下列条件成立时,浮点类型表达式的值不应隐式转换为不同的类型: a) 转换不是向更宽浮点类型的转换,或者 b) 表达式是复杂表达式,或者 c) 表达式是函数参数,或者 d) 表达式是返回表达式。
这么看起来是不是很别扭,看起来有点头疼,那么记住下面几个规则其实也就搞定了。
? 有符号和无符号之间没有隐式转换 ? 整型和浮点类型之间没有隐式转换 ? 没有从宽类型向窄类型的隐式转换 ? 函数参数没有隐式转换 ? 函数的返回表达式没有隐式转换 ? 复杂表达式没有隐式转换
下面我们来稍微的看点例子简单的了解一下。
extern void fool (uint8_t x); int16_t t1(void) { fool(u8a);/*这个当然是可以的了*/ fool(u8a + u8b);/*这个就需要我们自己去计算一下了, 要保证没有超过*/ fool(2);/*这个是不行的,因为是有符号的*/ fool(2U);/*这个是可以的*/ fool((uint8_t)2)/*这个是可以的,强制转化了,看到没?*/ }把上面的规则了解清楚了就很简单能理解为什么要这么干了。
rule10.3&10.4显示转化
整型复杂表达式的值只能强制转换到更窄的类型且与表达式的基本类型具有相同的符号。浮点类型复杂表达式的值只能强制转换到更窄的浮点类型。什么意思呢,举个例子就明白了
(float32_t) (f64a + f64b)/*这就是更窄类型的变化,我估计一般没人这么干,谁愿意找这种事*/ (float64_t) (f32a + f32b)/*这就是规矩,就是不能强制类型转化*/ (float64_t) f32a/*这个可以,为什么,不是复杂表达式*/ (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 */其实按照上面的规则读两遍,也不复杂,理解了也就很简单了。
rule10.5 如果位运算符 ~ 和 << 应用在基本类型为unsigned char 或unsigned short 的操作数,结果应该立即强制转换为操作数的基本类型。
uint8_t port = 0x5aU; uint8_t result_8; uint16_t result_16; uint16_t mode; result_8 = (~port) >> 4;/*~port 的值在16 位机器上是0xffa5,而在32 位机器上是0xffffffa5。在每种情况下,result的值是0xfa,然而期望值可能是0x0a。这样的危险可以通过如下所示的强制转换来避免:*/ result_8 = ( ( uint8_t ) (~port ) ) >> 4; result_16 = ( ( uint16_t ) (~(uint16_t) port ) ) >> 4; 这两种方法可以完美的解决这个问题。rule10.6 后缀“U”应该用在所有unsigned 类型的常量上。例如常量40000在32位环境中是int类型,而在16位环境中则是long类型。值0x8000在16位环境中是unsigned int 类型,而在32位环境中则是signed int类型。
rule11.1 转换不能发生在函数指针和其他除了整型之外的任何类型指针之间。函数指针不能瞎搞啊,函数指针到不同类型指针的转换会导致未定义的行为。举个例子,这意味着一个函数指针不能转换成指向不同类型函数的指针。你想想你的a函数能转化成b函数吗?
rule11.2对象指针和其他除整型之外的任何类型指针之间、对象指针和其他类型对象的指针之间、对象指针和void 指针之间不能进行转换。为什么会有这些规定呢,因为没有人规定这些行为,谁也不知道会带来什么灾难性的结果。
rule11.3不应在指针类型和整型之间进行强制转换。c语言的魅力就是可以直接访问一些硬件中的东西,所以尽可能的情况下要避免指针和整型之间的转换,但是在访问内存映射寄存器或其他硬件特性时这是不可避免的。
rule11.4不应在某类型对象指针和其他不同类型对象指针之间进行强制转换。
uint8_t *p1; uint16_t *p2; p2 = (uint16_t *)p1;/*可以是可以,但是我们应该尽量去避免*/rule11.5如果指针所指向的类型带有const 或volatile 限定符,那么移除限定符的强制转换是不允许的。
uint16_t x; uint16_t * const cpi = &x; /* const pointer */ uint16_t * pi; pi = cpi; /* Compliant – no conversion no cast required */ pi = (uint16_t *)pci;/*你都定义为const的了,干嘛还要改它呢*/rule12.1 我们c语言有个运算优先级的说法,这些东西除了在考试的时候玩玩,项目中就不要玩了,没什么意思,不要吝啬一个括号,没意思,所以不要过分依赖C 表达式中的运算符优先规则
赋值运算符的右手操作数不需要使用括号,除非右手端本身包含了赋值表达式:x=a+b;就好了,别搞x=(a+b) 一元运算符的操作数不需要使用括号:x = a * -1这就可以了,没必要x = a*(-1)
rule12.2表达式的值在标准所允许的任何运算次序下都应该是相同的。x = b[i] + i++;这要是先算b[i]还是i++这结果很明显肯定就不一样了,x = func ( i++, i);根据函数的两个参数的运算次序不同,表达式会给出不同的结果。这个也很明显,所以我们要尽量去避免这些,这又不是考试
x = y = y = z / 3;x = y = y++;。这种东西也别出现,有什么好处呢。
#define MAX (a, b) ( ( (a) > (b) ) ? (a) : (b) ) z = MAX (i++, j);这么做会不会有点过分,如果条件成立,i就加两次,如果不成立呢,那不就加一次吗
rule12.3 不能在具有副作用的表达式中使用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 */这么干是不是有点不对啊,这个可能出现的情况不多rule12.4 逻辑运算符 && 或 | | 的右手操作数不能包含副作用。
if ( ishigh && ( x == i++ ) ) /* Not compliant */ if ( ishigh && ( x == f (x) ) ) /* Only acceptable if f(x) is known to have no side effects */ 为什么是这样的呢,假如说ishigh是不成立的,那么i就会参与运算,就会加1,如果不成立呢,i就不会加1 第二个也一样,ishigh的真假直接关系到后面f(x)是否执行的问题rule12.5逻辑 && 或 | | 的操作数应该是primary-expressions。
如果操作数不是单一的标识符或常量,那么它必须被括起来。在这种情况下,括号对于代码的可读性和确保预期的 行为都是非常重要的。如果表达式只由逻辑 && 序列组成或逻辑 | | 序列组成,就不需要使用括号。
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 */这个规则遵守的人可能比较少。但我们遵守的话,我们就养成了好习惯,这样就会更快的达到人生巅峰不是吗
rule12.6逻辑运算符(&&、| | 和 !)的操作数应该是有效的布尔数。有效布尔类型的表达式不能用做非逻辑运算符(&&、| | 和 !)的操作数
rule12.7 位运算符不能用于基本类型(underlying type)是有符号的操作数上。位运算(~、<<、>>、&、^ 和 | )对有符号整数通常是无意义的。比如,如果右移运算把符号位移动到数据位上或者左移运算把数据位移动到符号位上,就会产生问题。 signed char a; a>>4; 因为有个符号位,这个不注意尤其在循环的时候很容易造成伪循环,很容易造成麻烦。左移和右移也被广泛的应用,因为它比乘法和除法效率要高很多。尤其嵌入式中。想要设计出高效率的算法,这点是一个考虑的方向。
rule12.8 一元减运算符不能用在基本类型无符号的表达式上。很简单,容易减出毛病unsigned i = 0; i --;这后面成什么鬼了
rule12.9不要使用逗号运算符。为什么呢,读起来费劲。就考试考起来有意思。
rule12.11 无符号整型常量表达式的计算不应产生折叠。
#define START 0x8000 #define END 0xFFFF #define LEN 0x8000 #if ( ( ( END – START ) – LEN ) < 0 ) #error Buffer Overrun /* Not OK: subtraction result wraps around to 0xFFFFFFFF */是不是很麻烦。 #endifrule12.12不应使用浮点数的基本(underlying)的位表示法(bit representation),因为每一个编译器对它的存储有自己的规定。
rule 12.13 在一个表达式中,自增(++)和自减(- -)运算符不应同其他运算符混合在一起。没人看的懂,混在一起的话。u8a = ++u8b + u8c--; /* Not compliant */ 就像这种,谁受得了。
rule 13.1赋值运算符不能使用在产生布尔值的表达式上。if(x = y)这什么玩意儿。。。。。
rule13.3 浮点表达式不能做相等或不等的检测。就是因为浮点的存储的固有特性。例如if (x == 0.0f)鬼知道什么时候成立呢。
rule13.4 for 语句的控制表达式不能包含任何浮点类型的对象。'
rule 13.5 第一个表达式初始化循环计数器 (例子中的i) 第二个表达式应该包含对循环计数器(i)和其他可选的循环控制变量的测试 第三个表达式循环计数器(i)的递增或递减
rule 13.6 for 循环中用于迭代计数的数值变量不应在循环体中修改。这个应该不会有太大的问题,但是我还是说明一下比较好
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 */这个鬼是不行的 }rule13.7 不允许进行结果不会改变的布尔运算。if(u16a < 0)不可能的呀,因为u16a永远>=0的呀。