指针赋予了C编程最大的灵活性;结构体使得C程序整齐而紧凑;联合体在某些要求注重效率的场合有精彩的表现,这三个要素是C语言的精华。
然而,精华并不意味着完美,C语言在赋予程序员足够灵活性的同时,也给了程序员很多犯错误的机会。所以有必要关注指针、结构体和联合体的实现细节,从而保障程序的安全性。
在此.第一部分介绍《MISRA—C:2004》中与指针相关的部分规则,第二部分讲解结构体和联合体的操作规范。下文中凡是未加特殊说明的都是强制(required)规则,个别推荐(advisory)规则加了“推荐”标示。
1 指针的安全规范
《MISRA—C:2004》关于指针的规范主要分为三个部分:指针的类型转换规则、指针运算的规则和指针的有效性规则。
1.1 指针的类型转换
指针类型转换是个高风险的操作,所以应该尽量避免进行这个操作。MISRA—C对其中可能造成严重错误的情况作了严格的限定,选择其中两条作简要分析。
规则11.4(推荐):指向不同数据类型的指针之间不能相互转换。
思考如下程序:
uint8_t*pl;
uint32)_t*p2;
p2=(uint32_t*)pl;
/*注:uint8_t表示8位无符号整型,uint3_t表示32位无符号整型。*/
程序员希望将从p1单元开始的4个字节组成一个32付的整型来参与运算。
如果CPU允许各种数据对象存放在任意的存储单元,则以上转换没有问题。但某些CPU对某种(些)数据类型加强了对齐限制,要求这些数据对象占用一定的地址空间,比如某些字节寻址的CPU会要求32位(4字节)整型存放在4的整倍数地址上。在这个前提下.思考程序中的指针转换:假设pl一开始指向的是0x00O3单元(对uint8_t型的整型没有对齐要求),则执行最后一行强制转换后,p2到底指向哪个单元就无法预料了。
规则1 1.5:指针转换过程中不允许丢失指针的const、volatile属性。按如下定义指针:
uIntl6一t x;
uint16_t*const cpi=&x; /*const指针*/
uintl6_t*const *pcpi; /*指向const指针的指针*/
const uintl6_t* *ppci; /*指向const整型指针的指针*/
uIntl6_t* *ppi ;
const uint16_t *pci; /*指向const整型的指针*/
volatik uint16_t *pvi; /*指向volatile整型的指针*/
uintl6_t *pi;
则以下指针转换是允许的:
pl=cpi;
以下指针转换是不允许的:
pi=(umtl6_t*)pci;
pi=(uintl6_t*)pvil
ppi=(uintl6_t* *)pcpi;
ppi=(uintl6_I**)ppci+
以上非法指针类型转换将会丢失const或者volatile类型。丢失const属性,将有可能导致在对只读内容进行写操作时,编译器不会发出警告,编译器将不对具有volatile属性的变量作优化;丢失volatile属性,编译器的优化可能导致程序员预先设计的硬件时序操作失效,这样的错误很难发现。关于const和volatile关键字的详细作用,读者可参考ISOC获取更多信息。
1.2 指针的运算
ISOC标准中,对指向数组成员的指针运算(包括算术运算、比较等)做了规范定义,除此以外的指针运算属于未定义(undefined)范围,具体实现有赖于具体编译器,其安全性无法得到保障,MISRA—C中对指针运算的合法范围做了如下限定。
规则17.1:只有指向数组的指针才允许进行算术运算①。
规则17 2:只有指向同一个数组的两个指针才允许相减 ②。
规则17 3:只有指向同一个数组的两个指针才允许用>,>=,<,<=等关系运算符进行比较。
为了尽最大可能减少直接进行指针运算带来的隐患,尤其是程序动态运行时可能发生的数组越界等问题,MISRA—C对指针运算作了更为严格的规定。规则17 4:只允许用数组索引做指针运算。按如下方式定义数组和指针:
uint8_t a[10];
uint8_t *p;
则*(p+5)=O是不允许的.而p[5]=O则是允许的,尽管就这段程序而言,二者等价。
以下给出一段程序,读者可参照相应程序行的注释,细细品味上述规则的含义。
void my_fn(uInt*_t*p1.uint8_t p2[]){
①其实此处的算术运算仅限定于指针加减某个整数.比如ppoint=point一5.ppoint++等。0两个指针可指向不同的散组成员。
uint8_t index=0;
uint8_t *p3
uint8_t *p4;
*pl=O;
p1++; /*不允许,pl不是指向数组的指针*/
p1=p1+5;/*不允许,pl不是指向数组的指针*/
pl[5]=O; /*不允许,p1不是指向数组的指针*/
p3=&p1[5];/*不允许,pl不是指向数组的指针*/
p2[0]=O;
index++;
index=index+5:
p2[index]=0; /*允许*/
*(p2+index)=O; /*不允许*/
p4=&p2[5]; /*允许*/
}
1.3 指针的有效性
下面介绍《MISRA—C:2004》中关于指针有效性的规则。
规则17 6:动态分配对象的地址不允许在本对象消亡后传给另外一个对象。
这条规则的实际意义是不允许将栈对象的地址传给外部作用域的对象。
请看以下这段程序:
#include″stdi0.h″
char*getm(void){
char p[]=″hello world″;
return p;
intmain(){
char* str=NULL;
str=getm();
printf(str);
程序员希望最后的输出结果是″hello world″这个字符串,然而实际运行时,却出现乱码(具体内容依赖于编译环境)。
简单分析一下,由于chat p[]=″hell0 world″这条语句是在栈中分配空间存储″hell0 world″这个字符串,当函数getm()返回的时候,已分配的空间将会被释放(但内容并不会被销毁),而priM(str)涉及系统调用,有数据压栈,会修改从前分配给数组p[]存储空间的内容,导致程序无法得到预期的效果。
倘若将getm()函数体中的char p[]=″hell0 world″程序行改成char*q=″hello world″,则执行main( )的时候可以正确输出″hello world″,这是由于q指向的是静态数据区,而非栈中的某个单元。
所以,数组名是指针不假,但在实现细节上还是有很大的差异,程序员在使用指针的时候必须慎之又慎。