嵌入式软件C语言可靠性设计问题汇总精品.docx
《嵌入式软件C语言可靠性设计问题汇总精品.docx》由会员分享,可在线阅读,更多相关《嵌入式软件C语言可靠性设计问题汇总精品.docx(24页珍藏版)》请在冰点文库上搜索。
嵌入式软件C语言可靠性设计问题汇总精品
【关键字】设计、建议、情况、条件、空间、文件、运行、传统、地方、问题、系统、机制、有效、合理、良好、快速、配合、执行、保持、提升、发现、了解、位置、关键、局面、安全、网络、意识、需要、环境、能力、需求、方式、作用、办法、标准、增量、结构、主体、速度、力度、设置、检验、分析、简化、保护、严格、纠正、保证、优先、确保、服务、支持、优化、取决于、实现、提高、规范、不改变
嵌入式软件可靠性设计问题集锦
1、程序员理解错误
1.1、英文标点被误写成中文标点;
比较运算符==误写成赋值运算符=,代码if(x=5){…}
本意是比较变量x是否等于常量5,但是误将’==’写成了’=’,if语句恒为真。
如果在逻辑判断表达式中出现赋值运算符,现在的大多数编译器会给出警告信息。
并非所有程序员都会注意到这类警告,因此有经验的程序员使用下面的代码来避免此类错误:
if(5==x){…}
将常量放在变量x的左边,即使程序员误将’==’写成了’=’,编译器会产生一个任谁也不能无视的语法错误信息:
不可给常量赋值!
1.2、+=与=+、-=与=-容易混
复合运算符会给程序带来隐含Bug,如下所示代码:
tmp=+1;
该代码本意是想表达tmp=tmp+1,但是将复合赋值运算符+=误写成=+:
将正整数常量1赋值给变量tmp。
编译器会欣然接受这类代码,连警告都不会产生。
-=与=-同理。
类似的逻辑与&&、位与&、逻辑或||、位或|、逻辑非!
、位取反~等。
字母l和数字1、字母o和数字0也易混淆,这种情况可借助编译器来纠正。
1.3、程序员输入错误
阿拉伯数值输入,宏变量宏定义方式实现。
防止多次录入数字出现手误。
1.4、数组问题
C语言数组下标从0开始。
定义inta[30],是从a[0]~a[29]。
1.5、switch…case语句中的 break关键字
switch…case语句可以很方便的实现多分支结构。
漏加break,引起顺序执行多个case语句;break关键字用于跳出最近的那层循环语句或者switch语句。
network code()
{
switch(line) {
case THING1:
doit1();
break;
case THING2:
if(x==STUFF) {
do_first_stuff();
if(y==OTHER_STUFF)
do_later_stuff();
}
initialize_modes_pointer();
default:
processing();
use_modes_pointer();
1.5、变量赋值
int a=34
intb=034
变量a和b相等吗?
No。
以’0x’为前缀的16进制常量,10进制常量不需要前缀,数字’0’为前缀的8进制。
误用8进制的例子,最后一个数组元素赋值错误:
1.a[0]=106; /*十进制数106*/
2.a[1]=112; /*十进制数112*/
3.a[2]=052;
1.6、指针的加减运算
下面的代码运行在32位ARM架构上,执行后,a和p的值?
int a=1;
int *p=(int*)0x00001000;
a=a+1;
p=p+1;
a=2,但是p的结果是0x00001004。
指针p加1后,p的值增加了4。
原因是指针做加减运算时是以指针的数据类型为单位。
p+1实际上是p+1*sizeof(int)。
不理解这一点,在使用指针直接操作数据时极易犯错。
比如下面对连续RAM初始化零操作代码:
unsigned int *pRAMaddr; //定义地址指针变量
for(pRAMaddr=StartAddr;pRAMaddr{ *pRAMaddr=0x00000000; //指定RAM地址清零 } 由于pRAMaddr是一个指针变量,所以pRAMaddr+=4代码其实使pRAMaddr偏移了4*sizeof(int)=16个字节,所以每执行一次for循环,会使变量pRAMaddr偏移16个字节空间,但只有4字节空间被初始化为零。其它的12字节数据的内容,在大多数架构处理器中都会是随机数。对于sizeof(),这里强调两点,第一它是一个关键字,而不是函数,并且它默认返回无符号整型数据(无符号!!);第二,使用sizeof获取数组长度时,不要对指针应用sizeof操作符,比如下面的例子:void ClearRAM(char array[]) { int i ; for(i=0;i/*for(i=0;i { array[i]=0x00; } } int main(void) { char Fle[20]; ClearRAM(Fle); //只能清除数组Fle中的前四个元素 } 对于数组array[20],使用代码sizeof(array[20])/sizeof(array[0])可以获得数组的元素(这里为20),但数组名和指针容易混淆,有且只有一种情况下可以当做指针,就是数组名作为函数形参时,数组名被认为是指针。同时不能再兼任数组名。只有这种情况,数组名才可当做指针,但容易引发风险。在ClearRAM函数内,作为形参的array[]不再是数组名了,而成了指针。sizeof(array)相当于求指针变量占用的字节数,在32位系统下,该值为4,sizeof(array)/sizeof(array[0])的运算结果也为4。所以在main函数中调用ClearRAM(Fle),也只能清除数组Fle中的前四个元素了。1.7、增量运算符++和减量运算符--既可以做前缀也可以做后缀。前缀和后缀的区别在于值的增加或减少这一动作发生的时间是不同的。作为前缀是先自加或自减然后做别的运算,作为后缀时,是先做运算,之后再自加或自减。下面的例子可以很好的解释前缀和后缀的区别。1.int a=8,b=2,y; 2.y=a+++--b; 代码执行后,y的值是多少? y=(a++)+(--b); 当赋值给变量y时,a的值为8,b的值为1,所以变量y的值为9;赋值完成后,变量a自加,a的值变为9,千万不要以为y的值为10。分解成两条语句:1.y=a+(--b); 2.a=a+1; 2、编译器语义检查萝卜快了不洗泥。C语言足够灵活且几乎不进行任何运行时检查,比如数组越界、指针是否合法、运算结果是否溢出等。C语言足够灵活,对于一个数组a[30],它允许使用像a[-1]这样的形式来快速获取数组首元素所在地址前面的数据;2.1、数据类型问题下面的两个例子的问题是什么?unsignedchar i; for(i=0;i<256;i++) {…} unsignedchari; for(i=10;i>=0;i--){…}无符号char类型,范围为0~255,所以无符号char类型变量i永远小于256(第一个for循环无限执行),永远大于等于0(第二个for循环无限执行)。2.2、误加标点符号1.if(a>b); //这里误加了一个分号 2.a=b; 2.3、编译器忽略掉多余的空格符和换行符1.if(n<3) 2.return //这里少加了一个分号 3.logrec.data=x[0]; 4.logrec.time=x[1]; 5.logrec.code=x[2]; 这段代码的本意是n<3时程序直接返回,由于程序员的失误,return少了一个结束分号。编译器将它翻译成返回表达式logrec.data=x[0]的结果,return后面即使是一个表达式也是C语言允许的。这样当n>=3时,表达式logrec.data=x[0];就不会被执行,给程序埋下了隐患。2.4、数组越界。代码在硬件上运行,一段时间后LCD显示屏上的一个数字不正常的被改变。经过一段时间的调试,问题被定位到下面的一段代码中:int SensorData[30]; ......for(i=30;i>0;i--) { SensorData[i]=...; ...... } 很多编译器会对上述代码产生警告:赋值超出数组界限。但并非所有程序员都对编译器警告保持足够敏感,而且编译器也并不能检查出数组越界的所有情况。2.5、数组声明具有外部链接时大小应显式声明模块A中定义数组:int SensorData[30];在模块B中引用该数组,但由于你引用代码并不规范,这里没有显式声明数组大小,但编译器也允许这么做:externint SensorData[]; 如果在模块B中存在和上面一样的代码:for(i=30;i>0;i--) { SensorData[i]=…; ......} 这次,编译器不会给出警告信息,因为编译器压根就不知道数组的元素个数。所以,当一个数组声明为具有外部链接,它的大小应该显式声明。2.6、编译器检查不出数组越界函数func()的形参是一个数组形式,函数代码简化如下所示:char * func(char SensorData[30]) { unsignedint i; for(i=30;i>0;i--) { SensorData[i]=…; … } } 这个给SensorData[30]赋初值的语句,编译器也是不给任何警告的。实际上,编译器是将数组名SensorData隐含的转化为指向数组第一个元素的指针,函数体是使用指针的形式来访问数组的,它当然也不会知道数组元素的个数了。造成这种局面的原因之一是C编译器的作者们认为指针代替数组可以提高程序效率,而且,还可以简化编译器的复杂度。指针和数组容易给程序造成混乱,如何区分?可以将数组名等同于指针的情况有且只有一处,就是数组作为函数形参时。其它时候,数组名是数组名,指针是指针。另一个例子:编译器同样检查不出数组越界。用数组来缓存通讯中的一帧数据。在通讯中断中将接收的数据保存到数组中,直到一帧数据完全接收后再进行处理。即使定义的数组长度足够长,接收数据的过程中也可能发生数组越界,特别是干扰严重时。这是由于外界的干扰破坏了数据帧的某些位,对一帧的数据长度判断错误,接收的数据超出数组范围,多余的数据会改写数组相邻的变量,造成系统崩溃。由于中断事件的异步性,这类数组越界编译器无法检查到。如果局部数组越界,可能引发ARM架构硬件异常。一设备用于接收无线传感器的数据,一次升级后,发现接收设备工作一段时间后会死机。调试表明ARM7处理器发生了硬件异常,异常处理代码是一段死循环(死机的直接原因)。接收设备有一个硬件模块用于接收无线传感器的整包数据并存在自己的硬件缓冲区中,当一帧数据接收完成后,使用外部中断通知设备取数据,外部中断服务程序精简后如下所示:__irq ExintHandler(void) { Unsignedchar DataBuf[50]; GetData(DataBug); //从硬件缓冲区取一帧数据 … } 由于存在多个无线传感器近乎同时发送数据的可能,加之GetData()函数保护力度不够,数组DataBuf在取数据过程中发生越界。由于数组DataBuf为局部变量,被分配在堆栈中,同在此堆栈中的还有中断发生时的运行环境以及中断返回地址。溢出的数据将这些数据破坏掉,中断返回时PC指针可能变成一个不合法值,硬件异常由此产生。但如果被利用,则可作病毒。1988年,第一个网络蠕虫在一天之内感染了2000到6000台计算机,利用的正是一个标准输入库函数的数组越界Bug。起因是一个标准输入输出库函数gets(),原来设计为从数据流中获取一段文本,遗憾的是,gets()函数没有规定输入文本的长度。gets()函数内部定义了一个500字节的数组,攻击者发送了大于500字节的数据,利用溢出的数据修改了堆栈中的PC指针,从而获取了系统权限。2.7、编译器与volatile限定符源文件定义变量unsignedint a;volatileunsignedint a;头文件声明变量externunsignedlong a;externunsignedint a;编译器提示语法错误:变量‘a’声明类型不一致编译器:不给错误信息(或仅一条警告)volatile属于类型限定符,另一个常见的类型限定符是const关键字。限定符volatile在嵌入式软件中至关重要,用来告诉编译器不要优化它修饰的变量。模块A源文件中定义变量:volatileunsignedint TimerCount=0;该变量用来在一个定时器服务程序中进行软件计时: TimerCount++; //读取IO端口1的值在模块A的头文件中,声明变量:externunsignedint TimerCount; //这里漏掉了类型限定符volatile模块B中,要使用TimerCount变量进行精确的软件延时:#include “...A.h” //首先包含模块A的头文件 … TimerCount=0; while(TimerCount>=TIMER_VALUE); //延时一段时间 … 这是一个死循环。在模块B中,变量TimerCount是被当作unsignedint类型变量。由于寄存器速度远快于RAM,编译器在使用非volatile限定变量时,先将变量从RAM中拷贝到寄存器中,如果同一个代码块再次用到该变量,就不再从RAM中拷贝数据而是直接使用之前寄存器备份值。代码while(TimerCount>=TIMER_VALUE)中,变量TimerCount仅第一次执行时被使用,之后都是使用的寄存器备份值,而这个寄存器值一直为0,所以程序无限循环。下面的流程图说明了程序使用限定符volatile和不使用volatile的执行过程使用了volatile会怎样?2.8、定义为volatile的变量的作用过程优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面左侧函数有什么问题:代码目的是用来返指针*ptr指向值的平方intsquare(volatileint*ptr){return*ptr**ptr;}编译器将产生类似下面的代码intsquare(volatileint*ptr){inta,b;a=*ptr;b=*ptr;returna*b;}正确代码:longsquare(volatileint*ptr){inta;a=*ptr;returna*a;}2.9、局部变量必须显式初始化●例1:unsigned intGetTempValue(void) { unsigned int sum; //定义局部变量,保存总值。但其初值并不一定为0 for(i=0;i<10;i++) { sum+=CollectTemp(); //函数CollectTemp可以得到当前的温度值 } return (sum/10); } ●例2:char * GetData(void) { char buffer[100];//局部数组 … return buffer; } 3、不合理的优先级C语言有32个关键字,却有34个运算符。要记住所有运算符的优先级是困难的。不合理的#define会加重优先级问题,让问题变得更加隐蔽。#define READSDA IO0PIN&(1<<11) //定义宏,读IO口p0.11的端口状态 //目的是判断端口p0.11是否为高电平 if(READSDA==(1<<11)) { … } 编译器在编译后将宏带入,原if语句变为:if(IO0PIN&(1<<11) ==(1<<11)) { … } 3.1、常规使用可能引起误会的运算符常被误会的优先级表达式误认为真实结果取值运算符*、自增运算符++,优先级相同,但自右向左结合*p++(*p)++*(p++)成员选择运算符.高于取值运算符**p.f(*p).f*(p.f)数组下标运算符[]优先级,高于取值运算符*int*ap[]int(*ap)[]ap为数组指针int*(ap[])ap为指针数组函数()优先级高于取值运算符*int*fp()int(*fp)()fp为函数指针int*(fp())fp为函数,返回指针等于==和不等于!=运算符优先级高于位操作运算符&、^和|val&mask!=0(val&mask)!=0val&(mask!=0)等于==和不等于!=运算符高于赋值运算符=c=getchar()!=EOF(c=getchar())!=EOFc=(getchar()!=EOF)算数运算符+和-优先级高于移位运算符<<和>>msb<<4+lsb(msb<<4)+lsbmsb<<(4+lsb)4、隐式转换和强制转换4.1、有符号和无符号char和short类型自动转换表达式中的有符号和无符号char和short类型都自动被转换为int类型.在需要的情况下,将自动被转换为unsignedint(在short和int具有相同大小时)。这称为类型提升。提升在算数运算中通常不会有什么大的坏处,但如果位运算符~和<<,应用在基本类型为unsignedchar或unsignedshort的操作数,结果应该立即强制转换为unsignedchar或者unsignedshort类型(取决于操作时使用的类型)。uint8_t port =0xa5U; /*typedefunsignedcharuint8_t*/uint8_t result_8; result_8= (~port) >> 4; 如果不了解表达式里的类型提升,认为在运算过程中变量port一直是unsignedchar类型的。期望的运算过程:~port结果为0xa5,0xa5>>4结果为0x0a。实际上,result_8的结果却是0xfa。在ARM结构下,int类型为32位。变量port在运算前被提升为int类型:~port结果为0xffffffa5,0xa5>>4结果为0x0ffffffa,赋值给变量result_8,发生类型截断(隐性转换),result_8=0xfa。正确表达式语句应该为: result_8=(unsignedchar)(~port)>>4; /*强制转换*/4.2、混合数据类型运算中会转换成较高级别数据类型数据类型的级别从高到低的顺序:longdouble、double、float、unsignedlonglong、longlong、unsignedlong、long、unsignedint、int。这种类型提升通常都是件好事,但往往有很多程序员不能真正理解这句话,从而做一些想当然的事情,比如下面的例子,int类型表示16位。uint16_t u16a = 40000; /* 16位无符号变量*/ uint16_t u16b= 30000; /*16位无符号变量*/ uint32_t u32x; /*32位无符号变量 */ uint32_t u32y; u32x = u16a +u16b; /* u32x = 70000还是4464 ? */ u32y =(uint32_t)(u16a + u16b); /* u32y=70000还是4464?*/ u32x和u32y的结果都是4464(16位值范围0-65536,超出值域范围部分4464)。不要认为表达式中有一个高类别uint32_t类型变量,编译器都会帮你把所有其他低类别都提升到uint32_t类型。正确的书写方式:u32x = (uint32_t)u16a +(uint32_t)u16b;或者: u32x = (uint32_t)u16a + u16b; 后一种写法在本表达式中是正确的,但是在其它表达式中不一定正确,比如:uint16_t u16a,u16b,u16c; uint32_t u32x; u32x=u16a+u16b + (uint32_t)u16c;/*错误写法,u16a+u16b仍可能溢出*/4.3、
*pRAMaddr=0x00000000; //指定RAM地址清零
由于pRAMaddr是一个指针变量,所以pRAMaddr+=4代码其实使pRAMaddr偏移了4*sizeof(int)=16个字节,所以每执行一次for循环,会使变量pRAMaddr偏移16个字节空间,但只有4字节空间被初始化为零。
其它的12字节数据的内容,在大多数架构处理器中都会是随机数。
对于sizeof(),这里强调两点,第一它是一个关键字,而不是函数,并且它默认返回无符号整型数据(无符号!
!
);第二,使用sizeof获取数组长度时,不要对指针应用sizeof操作符,比如下面的例子:
void ClearRAM(char array[])
int i ;
for(i=0;i/*for(i=0;i { array[i]=0x00; } } int main(void) { char Fle[20]; ClearRAM(Fle); //只能清除数组Fle中的前四个元素 } 对于数组array[20],使用代码sizeof(array[20])/sizeof(array[0])可以获得数组的元素(这里为20),但数组名和指针容易混淆,有且只有一种情况下可以当做指针,就是数组名作为函数形参时,数组名被认为是指针。同时不能再兼任数组名。只有这种情况,数组名才可当做指针,但容易引发风险。在ClearRAM函数内,作为形参的array[]不再是数组名了,而成了指针。sizeof(array)相当于求指针变量占用的字节数,在32位系统下,该值为4,sizeof(array)/sizeof(array[0])的运算结果也为4。所以在main函数中调用ClearRAM(Fle),也只能清除数组Fle中的前四个元素了。1.7、增量运算符++和减量运算符--既可以做前缀也可以做后缀。前缀和后缀的区别在于值的增加或减少这一动作发生的时间是不同的。作为前缀是先自加或自减然后做别的运算,作为后缀时,是先做运算,之后再自加或自减。下面的例子可以很好的解释前缀和后缀的区别。1.int a=8,b=2,y; 2.y=a+++--b; 代码执行后,y的值是多少? y=(a++)+(--b); 当赋值给变量y时,a的值为8,b的值为1,所以变量y的值为9;赋值完成后,变量a自加,a的值变为9,千万不要以为y的值为10。分解成两条语句:1.y=a+(--b); 2.a=a+1; 2、编译器语义检查萝卜快了不洗泥。C语言足够灵活且几乎不进行任何运行时检查,比如数组越界、指针是否合法、运算结果是否溢出等。C语言足够灵活,对于一个数组a[30],它允许使用像a[-1]这样的形式来快速获取数组首元素所在地址前面的数据;2.1、数据类型问题下面的两个例子的问题是什么?unsignedchar i; for(i=0;i<256;i++) {…} unsignedchari; for(i=10;i>=0;i--){…}无符号char类型,范围为0~255,所以无符号char类型变量i永远小于256(第一个for循环无限执行),永远大于等于0(第二个for循环无限执行)。2.2、误加标点符号1.if(a>b); //这里误加了一个分号 2.a=b; 2.3、编译器忽略掉多余的空格符和换行符1.if(n<3) 2.return //这里少加了一个分号 3.logrec.data=x[0]; 4.logrec.time=x[1]; 5.logrec.code=x[2]; 这段代码的本意是n<3时程序直接返回,由于程序员的失误,return少了一个结束分号。编译器将它翻译成返回表达式logrec.data=x[0]的结果,return后面即使是一个表达式也是C语言允许的。这样当n>=3时,表达式logrec.data=x[0];就不会被执行,给程序埋下了隐患。2.4、数组越界。代码在硬件上运行,一段时间后LCD显示屏上的一个数字不正常的被改变。经过一段时间的调试,问题被定位到下面的一段代码中:int SensorData[30]; ......for(i=30;i>0;i--) { SensorData[i]=...; ...... } 很多编译器会对上述代码产生警告:赋值超出数组界限。但并非所有程序员都对编译器警告保持足够敏感,而且编译器也并不能检查出数组越界的所有情况。2.5、数组声明具有外部链接时大小应显式声明模块A中定义数组:int SensorData[30];在模块B中引用该数组,但由于你引用代码并不规范,这里没有显式声明数组大小,但编译器也允许这么做:externint SensorData[]; 如果在模块B中存在和上面一样的代码:for(i=30;i>0;i--) { SensorData[i]=…; ......} 这次,编译器不会给出警告信息,因为编译器压根就不知道数组的元素个数。所以,当一个数组声明为具有外部链接,它的大小应该显式声明。2.6、编译器检查不出数组越界函数func()的形参是一个数组形式,函数代码简化如下所示:char * func(char SensorData[30]) { unsignedint i; for(i=30;i>0;i--) { SensorData[i]=…; … } } 这个给SensorData[30]赋初值的语句,编译器也是不给任何警告的。实际上,编译器是将数组名SensorData隐含的转化为指向数组第一个元素的指针,函数体是使用指针的形式来访问数组的,它当然也不会知道数组元素的个数了。造成这种局面的原因之一是C编译器的作者们认为指针代替数组可以提高程序效率,而且,还可以简化编译器的复杂度。指针和数组容易给程序造成混乱,如何区分?可以将数组名等同于指针的情况有且只有一处,就是数组作为函数形参时。其它时候,数组名是数组名,指针是指针。另一个例子:编译器同样检查不出数组越界。用数组来缓存通讯中的一帧数据。在通讯中断中将接收的数据保存到数组中,直到一帧数据完全接收后再进行处理。即使定义的数组长度足够长,接收数据的过程中也可能发生数组越界,特别是干扰严重时。这是由于外界的干扰破坏了数据帧的某些位,对一帧的数据长度判断错误,接收的数据超出数组范围,多余的数据会改写数组相邻的变量,造成系统崩溃。由于中断事件的异步性,这类数组越界编译器无法检查到。如果局部数组越界,可能引发ARM架构硬件异常。一设备用于接收无线传感器的数据,一次升级后,发现接收设备工作一段时间后会死机。调试表明ARM7处理器发生了硬件异常,异常处理代码是一段死循环(死机的直接原因)。接收设备有一个硬件模块用于接收无线传感器的整包数据并存在自己的硬件缓冲区中,当一帧数据接收完成后,使用外部中断通知设备取数据,外部中断服务程序精简后如下所示:__irq ExintHandler(void) { Unsignedchar DataBuf[50]; GetData(DataBug); //从硬件缓冲区取一帧数据 … } 由于存在多个无线传感器近乎同时发送数据的可能,加之GetData()函数保护力度不够,数组DataBuf在取数据过程中发生越界。由于数组DataBuf为局部变量,被分配在堆栈中,同在此堆栈中的还有中断发生时的运行环境以及中断返回地址。溢出的数据将这些数据破坏掉,中断返回时PC指针可能变成一个不合法值,硬件异常由此产生。但如果被利用,则可作病毒。1988年,第一个网络蠕虫在一天之内感染了2000到6000台计算机,利用的正是一个标准输入库函数的数组越界Bug。起因是一个标准输入输出库函数gets(),原来设计为从数据流中获取一段文本,遗憾的是,gets()函数没有规定输入文本的长度。gets()函数内部定义了一个500字节的数组,攻击者发送了大于500字节的数据,利用溢出的数据修改了堆栈中的PC指针,从而获取了系统权限。2.7、编译器与volatile限定符源文件定义变量unsignedint a;volatileunsignedint a;头文件声明变量externunsignedlong a;externunsignedint a;编译器提示语法错误:变量‘a’声明类型不一致编译器:不给错误信息(或仅一条警告)volatile属于类型限定符,另一个常见的类型限定符是const关键字。限定符volatile在嵌入式软件中至关重要,用来告诉编译器不要优化它修饰的变量。模块A源文件中定义变量:volatileunsignedint TimerCount=0;该变量用来在一个定时器服务程序中进行软件计时: TimerCount++; //读取IO端口1的值在模块A的头文件中,声明变量:externunsignedint TimerCount; //这里漏掉了类型限定符volatile模块B中,要使用TimerCount变量进行精确的软件延时:#include “...A.h” //首先包含模块A的头文件 … TimerCount=0; while(TimerCount>=TIMER_VALUE); //延时一段时间 … 这是一个死循环。在模块B中,变量TimerCount是被当作unsignedint类型变量。由于寄存器速度远快于RAM,编译器在使用非volatile限定变量时,先将变量从RAM中拷贝到寄存器中,如果同一个代码块再次用到该变量,就不再从RAM中拷贝数据而是直接使用之前寄存器备份值。代码while(TimerCount>=TIMER_VALUE)中,变量TimerCount仅第一次执行时被使用,之后都是使用的寄存器备份值,而这个寄存器值一直为0,所以程序无限循环。下面的流程图说明了程序使用限定符volatile和不使用volatile的执行过程使用了volatile会怎样?2.8、定义为volatile的变量的作用过程优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面左侧函数有什么问题:代码目的是用来返指针*ptr指向值的平方intsquare(volatileint*ptr){return*ptr**ptr;}编译器将产生类似下面的代码intsquare(volatileint*ptr){inta,b;a=*ptr;b=*ptr;returna*b;}正确代码:longsquare(volatileint*ptr){inta;a=*ptr;returna*a;}2.9、局部变量必须显式初始化●例1:unsigned intGetTempValue(void) { unsigned int sum; //定义局部变量,保存总值。但其初值并不一定为0 for(i=0;i<10;i++) { sum+=CollectTemp(); //函数CollectTemp可以得到当前的温度值 } return (sum/10); } ●例2:char * GetData(void) { char buffer[100];//局部数组 … return buffer; } 3、不合理的优先级C语言有32个关键字,却有34个运算符。要记住所有运算符的优先级是困难的。不合理的#define会加重优先级问题,让问题变得更加隐蔽。#define READSDA IO0PIN&(1<<11) //定义宏,读IO口p0.11的端口状态 //目的是判断端口p0.11是否为高电平 if(READSDA==(1<<11)) { … } 编译器在编译后将宏带入,原if语句变为:if(IO0PIN&(1<<11) ==(1<<11)) { … } 3.1、常规使用可能引起误会的运算符常被误会的优先级表达式误认为真实结果取值运算符*、自增运算符++,优先级相同,但自右向左结合*p++(*p)++*(p++)成员选择运算符.高于取值运算符**p.f(*p).f*(p.f)数组下标运算符[]优先级,高于取值运算符*int*ap[]int(*ap)[]ap为数组指针int*(ap[])ap为指针数组函数()优先级高于取值运算符*int*fp()int(*fp)()fp为函数指针int*(fp())fp为函数,返回指针等于==和不等于!=运算符优先级高于位操作运算符&、^和|val&mask!=0(val&mask)!=0val&(mask!=0)等于==和不等于!=运算符高于赋值运算符=c=getchar()!=EOF(c=getchar())!=EOFc=(getchar()!=EOF)算数运算符+和-优先级高于移位运算符<<和>>msb<<4+lsb(msb<<4)+lsbmsb<<(4+lsb)4、隐式转换和强制转换4.1、有符号和无符号char和short类型自动转换表达式中的有符号和无符号char和short类型都自动被转换为int类型.在需要的情况下,将自动被转换为unsignedint(在short和int具有相同大小时)。这称为类型提升。提升在算数运算中通常不会有什么大的坏处,但如果位运算符~和<<,应用在基本类型为unsignedchar或unsignedshort的操作数,结果应该立即强制转换为unsignedchar或者unsignedshort类型(取决于操作时使用的类型)。uint8_t port =0xa5U; /*typedefunsignedcharuint8_t*/uint8_t result_8; result_8= (~port) >> 4; 如果不了解表达式里的类型提升,认为在运算过程中变量port一直是unsignedchar类型的。期望的运算过程:~port结果为0xa5,0xa5>>4结果为0x0a。实际上,result_8的结果却是0xfa。在ARM结构下,int类型为32位。变量port在运算前被提升为int类型:~port结果为0xffffffa5,0xa5>>4结果为0x0ffffffa,赋值给变量result_8,发生类型截断(隐性转换),result_8=0xfa。正确表达式语句应该为: result_8=(unsignedchar)(~port)>>4; /*强制转换*/4.2、混合数据类型运算中会转换成较高级别数据类型数据类型的级别从高到低的顺序:longdouble、double、float、unsignedlonglong、longlong、unsignedlong、long、unsignedint、int。这种类型提升通常都是件好事,但往往有很多程序员不能真正理解这句话,从而做一些想当然的事情,比如下面的例子,int类型表示16位。uint16_t u16a = 40000; /* 16位无符号变量*/ uint16_t u16b= 30000; /*16位无符号变量*/ uint32_t u32x; /*32位无符号变量 */ uint32_t u32y; u32x = u16a +u16b; /* u32x = 70000还是4464 ? */ u32y =(uint32_t)(u16a + u16b); /* u32y=70000还是4464?*/ u32x和u32y的结果都是4464(16位值范围0-65536,超出值域范围部分4464)。不要认为表达式中有一个高类别uint32_t类型变量,编译器都会帮你把所有其他低类别都提升到uint32_t类型。正确的书写方式:u32x = (uint32_t)u16a +(uint32_t)u16b;或者: u32x = (uint32_t)u16a + u16b; 后一种写法在本表达式中是正确的,但是在其它表达式中不一定正确,比如:uint16_t u16a,u16b,u16c; uint32_t u32x; u32x=u16a+u16b + (uint32_t)u16c;/*错误写法,u16a+u16b仍可能溢出*/4.3、
/*for(i=0;i { array[i]=0x00; } } int main(void) { char Fle[20]; ClearRAM(Fle); //只能清除数组Fle中的前四个元素 } 对于数组array[20],使用代码sizeof(array[20])/sizeof(array[0])可以获得数组的元素(这里为20),但数组名和指针容易混淆,有且只有一种情况下可以当做指针,就是数组名作为函数形参时,数组名被认为是指针。同时不能再兼任数组名。只有这种情况,数组名才可当做指针,但容易引发风险。在ClearRAM函数内,作为形参的array[]不再是数组名了,而成了指针。sizeof(array)相当于求指针变量占用的字节数,在32位系统下,该值为4,sizeof(array)/sizeof(array[0])的运算结果也为4。所以在main函数中调用ClearRAM(Fle),也只能清除数组Fle中的前四个元素了。1.7、增量运算符++和减量运算符--既可以做前缀也可以做后缀。前缀和后缀的区别在于值的增加或减少这一动作发生的时间是不同的。作为前缀是先自加或自减然后做别的运算,作为后缀时,是先做运算,之后再自加或自减。下面的例子可以很好的解释前缀和后缀的区别。1.int a=8,b=2,y; 2.y=a+++--b; 代码执行后,y的值是多少? y=(a++)+(--b); 当赋值给变量y时,a的值为8,b的值为1,所以变量y的值为9;赋值完成后,变量a自加,a的值变为9,千万不要以为y的值为10。分解成两条语句:1.y=a+(--b); 2.a=a+1; 2、编译器语义检查萝卜快了不洗泥。C语言足够灵活且几乎不进行任何运行时检查,比如数组越界、指针是否合法、运算结果是否溢出等。C语言足够灵活,对于一个数组a[30],它允许使用像a[-1]这样的形式来快速获取数组首元素所在地址前面的数据;2.1、数据类型问题下面的两个例子的问题是什么?unsignedchar i; for(i=0;i<256;i++) {…} unsignedchari; for(i=10;i>=0;i--){…}无符号char类型,范围为0~255,所以无符号char类型变量i永远小于256(第一个for循环无限执行),永远大于等于0(第二个for循环无限执行)。2.2、误加标点符号1.if(a>b); //这里误加了一个分号 2.a=b; 2.3、编译器忽略掉多余的空格符和换行符1.if(n<3) 2.return //这里少加了一个分号 3.logrec.data=x[0]; 4.logrec.time=x[1]; 5.logrec.code=x[2]; 这段代码的本意是n<3时程序直接返回,由于程序员的失误,return少了一个结束分号。编译器将它翻译成返回表达式logrec.data=x[0]的结果,return后面即使是一个表达式也是C语言允许的。这样当n>=3时,表达式logrec.data=x[0];就不会被执行,给程序埋下了隐患。2.4、数组越界。代码在硬件上运行,一段时间后LCD显示屏上的一个数字不正常的被改变。经过一段时间的调试,问题被定位到下面的一段代码中:int SensorData[30]; ......for(i=30;i>0;i--) { SensorData[i]=...; ...... } 很多编译器会对上述代码产生警告:赋值超出数组界限。但并非所有程序员都对编译器警告保持足够敏感,而且编译器也并不能检查出数组越界的所有情况。2.5、数组声明具有外部链接时大小应显式声明模块A中定义数组:int SensorData[30];在模块B中引用该数组,但由于你引用代码并不规范,这里没有显式声明数组大小,但编译器也允许这么做:externint SensorData[]; 如果在模块B中存在和上面一样的代码:for(i=30;i>0;i--) { SensorData[i]=…; ......} 这次,编译器不会给出警告信息,因为编译器压根就不知道数组的元素个数。所以,当一个数组声明为具有外部链接,它的大小应该显式声明。2.6、编译器检查不出数组越界函数func()的形参是一个数组形式,函数代码简化如下所示:char * func(char SensorData[30]) { unsignedint i; for(i=30;i>0;i--) { SensorData[i]=…; … } } 这个给SensorData[30]赋初值的语句,编译器也是不给任何警告的。实际上,编译器是将数组名SensorData隐含的转化为指向数组第一个元素的指针,函数体是使用指针的形式来访问数组的,它当然也不会知道数组元素的个数了。造成这种局面的原因之一是C编译器的作者们认为指针代替数组可以提高程序效率,而且,还可以简化编译器的复杂度。指针和数组容易给程序造成混乱,如何区分?可以将数组名等同于指针的情况有且只有一处,就是数组作为函数形参时。其它时候,数组名是数组名,指针是指针。另一个例子:编译器同样检查不出数组越界。用数组来缓存通讯中的一帧数据。在通讯中断中将接收的数据保存到数组中,直到一帧数据完全接收后再进行处理。即使定义的数组长度足够长,接收数据的过程中也可能发生数组越界,特别是干扰严重时。这是由于外界的干扰破坏了数据帧的某些位,对一帧的数据长度判断错误,接收的数据超出数组范围,多余的数据会改写数组相邻的变量,造成系统崩溃。由于中断事件的异步性,这类数组越界编译器无法检查到。如果局部数组越界,可能引发ARM架构硬件异常。一设备用于接收无线传感器的数据,一次升级后,发现接收设备工作一段时间后会死机。调试表明ARM7处理器发生了硬件异常,异常处理代码是一段死循环(死机的直接原因)。接收设备有一个硬件模块用于接收无线传感器的整包数据并存在自己的硬件缓冲区中,当一帧数据接收完成后,使用外部中断通知设备取数据,外部中断服务程序精简后如下所示:__irq ExintHandler(void) { Unsignedchar DataBuf[50]; GetData(DataBug); //从硬件缓冲区取一帧数据 … } 由于存在多个无线传感器近乎同时发送数据的可能,加之GetData()函数保护力度不够,数组DataBuf在取数据过程中发生越界。由于数组DataBuf为局部变量,被分配在堆栈中,同在此堆栈中的还有中断发生时的运行环境以及中断返回地址。溢出的数据将这些数据破坏掉,中断返回时PC指针可能变成一个不合法值,硬件异常由此产生。但如果被利用,则可作病毒。1988年,第一个网络蠕虫在一天之内感染了2000到6000台计算机,利用的正是一个标准输入库函数的数组越界Bug。起因是一个标准输入输出库函数gets(),原来设计为从数据流中获取一段文本,遗憾的是,gets()函数没有规定输入文本的长度。gets()函数内部定义了一个500字节的数组,攻击者发送了大于500字节的数据,利用溢出的数据修改了堆栈中的PC指针,从而获取了系统权限。2.7、编译器与volatile限定符源文件定义变量unsignedint a;volatileunsignedint a;头文件声明变量externunsignedlong a;externunsignedint a;编译器提示语法错误:变量‘a’声明类型不一致编译器:不给错误信息(或仅一条警告)volatile属于类型限定符,另一个常见的类型限定符是const关键字。限定符volatile在嵌入式软件中至关重要,用来告诉编译器不要优化它修饰的变量。模块A源文件中定义变量:volatileunsignedint TimerCount=0;该变量用来在一个定时器服务程序中进行软件计时: TimerCount++; //读取IO端口1的值在模块A的头文件中,声明变量:externunsignedint TimerCount; //这里漏掉了类型限定符volatile模块B中,要使用TimerCount变量进行精确的软件延时:#include “...A.h” //首先包含模块A的头文件 … TimerCount=0; while(TimerCount>=TIMER_VALUE); //延时一段时间 … 这是一个死循环。在模块B中,变量TimerCount是被当作unsignedint类型变量。由于寄存器速度远快于RAM,编译器在使用非volatile限定变量时,先将变量从RAM中拷贝到寄存器中,如果同一个代码块再次用到该变量,就不再从RAM中拷贝数据而是直接使用之前寄存器备份值。代码while(TimerCount>=TIMER_VALUE)中,变量TimerCount仅第一次执行时被使用,之后都是使用的寄存器备份值,而这个寄存器值一直为0,所以程序无限循环。下面的流程图说明了程序使用限定符volatile和不使用volatile的执行过程使用了volatile会怎样?2.8、定义为volatile的变量的作用过程优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面左侧函数有什么问题:代码目的是用来返指针*ptr指向值的平方intsquare(volatileint*ptr){return*ptr**ptr;}编译器将产生类似下面的代码intsquare(volatileint*ptr){inta,b;a=*ptr;b=*ptr;returna*b;}正确代码:longsquare(volatileint*ptr){inta;a=*ptr;returna*a;}2.9、局部变量必须显式初始化●例1:unsigned intGetTempValue(void) { unsigned int sum; //定义局部变量,保存总值。但其初值并不一定为0 for(i=0;i<10;i++) { sum+=CollectTemp(); //函数CollectTemp可以得到当前的温度值 } return (sum/10); } ●例2:char * GetData(void) { char buffer[100];//局部数组 … return buffer; } 3、不合理的优先级C语言有32个关键字,却有34个运算符。要记住所有运算符的优先级是困难的。不合理的#define会加重优先级问题,让问题变得更加隐蔽。#define READSDA IO0PIN&(1<<11) //定义宏,读IO口p0.11的端口状态 //目的是判断端口p0.11是否为高电平 if(READSDA==(1<<11)) { … } 编译器在编译后将宏带入,原if语句变为:if(IO0PIN&(1<<11) ==(1<<11)) { … } 3.1、常规使用可能引起误会的运算符常被误会的优先级表达式误认为真实结果取值运算符*、自增运算符++,优先级相同,但自右向左结合*p++(*p)++*(p++)成员选择运算符.高于取值运算符**p.f(*p).f*(p.f)数组下标运算符[]优先级,高于取值运算符*int*ap[]int(*ap)[]ap为数组指针int*(ap[])ap为指针数组函数()优先级高于取值运算符*int*fp()int(*fp)()fp为函数指针int*(fp())fp为函数,返回指针等于==和不等于!=运算符优先级高于位操作运算符&、^和|val&mask!=0(val&mask)!=0val&(mask!=0)等于==和不等于!=运算符高于赋值运算符=c=getchar()!=EOF(c=getchar())!=EOFc=(getchar()!=EOF)算数运算符+和-优先级高于移位运算符<<和>>msb<<4+lsb(msb<<4)+lsbmsb<<(4+lsb)4、隐式转换和强制转换4.1、有符号和无符号char和short类型自动转换表达式中的有符号和无符号char和short类型都自动被转换为int类型.在需要的情况下,将自动被转换为unsignedint(在short和int具有相同大小时)。这称为类型提升。提升在算数运算中通常不会有什么大的坏处,但如果位运算符~和<<,应用在基本类型为unsignedchar或unsignedshort的操作数,结果应该立即强制转换为unsignedchar或者unsignedshort类型(取决于操作时使用的类型)。uint8_t port =0xa5U; /*typedefunsignedcharuint8_t*/uint8_t result_8; result_8= (~port) >> 4; 如果不了解表达式里的类型提升,认为在运算过程中变量port一直是unsignedchar类型的。期望的运算过程:~port结果为0xa5,0xa5>>4结果为0x0a。实际上,result_8的结果却是0xfa。在ARM结构下,int类型为32位。变量port在运算前被提升为int类型:~port结果为0xffffffa5,0xa5>>4结果为0x0ffffffa,赋值给变量result_8,发生类型截断(隐性转换),result_8=0xfa。正确表达式语句应该为: result_8=(unsignedchar)(~port)>>4; /*强制转换*/4.2、混合数据类型运算中会转换成较高级别数据类型数据类型的级别从高到低的顺序:longdouble、double、float、unsignedlonglong、longlong、unsignedlong、long、unsignedint、int。这种类型提升通常都是件好事,但往往有很多程序员不能真正理解这句话,从而做一些想当然的事情,比如下面的例子,int类型表示16位。uint16_t u16a = 40000; /* 16位无符号变量*/ uint16_t u16b= 30000; /*16位无符号变量*/ uint32_t u32x; /*32位无符号变量 */ uint32_t u32y; u32x = u16a +u16b; /* u32x = 70000还是4464 ? */ u32y =(uint32_t)(u16a + u16b); /* u32y=70000还是4464?*/ u32x和u32y的结果都是4464(16位值范围0-65536,超出值域范围部分4464)。不要认为表达式中有一个高类别uint32_t类型变量,编译器都会帮你把所有其他低类别都提升到uint32_t类型。正确的书写方式:u32x = (uint32_t)u16a +(uint32_t)u16b;或者: u32x = (uint32_t)u16a + u16b; 后一种写法在本表达式中是正确的,但是在其它表达式中不一定正确,比如:uint16_t u16a,u16b,u16c; uint32_t u32x; u32x=u16a+u16b + (uint32_t)u16c;/*错误写法,u16a+u16b仍可能溢出*/4.3、
array[i]=0x00;
int main(void)
char Fle[20];
ClearRAM(Fle); //只能清除数组Fle中的前四个元素
对于数组array[20],使用代码sizeof(array[20])/sizeof(array[0])可以获得数组的元素(这里为20),但数组名和指针容易混淆,有且只有一种情况下可以当做指针,就是数组名作为函数形参时,数组名被认为是指针。
同时不能再兼任数组名。
只有这种情况,数组名才可当做指针,但容易引发风险。
在ClearRAM函数内,作为形参的array[]不再是数组名了,而成了指针。
sizeof(array)相当于求指针变量占用的字节数,在32位系统下,该值为4,sizeof(array)/sizeof(array[0])的运算结果也为4。
所以在main函数中调用ClearRAM(Fle),也只能清除数组Fle中的前四个元素了。
1.7、增量运算符++和减量运算符--
既可以做前缀也可以做后缀。
前缀和后缀的区别在于值的增加或减少这一动作发生的时间是不同的。
作为前缀是先自加或自减然后做别的运算,作为后缀时,是先做运算,之后再自加或自减。
下面的例子可以很好的解释前缀和后缀的区别。
1.int a=8,b=2,y;
2.y=a+++--b;
代码执行后,y的值是多少?
y=(a++)+(--b);
当赋值给变量y时,a的值为8,b的值为1,所以变量y的值为9;赋值完成后,变量a自加,a的值变为9,千万不要以为y的值为10。
分解成两条语句:
1.y=a+(--b);
2.a=a+1;
2、编译器语义检查
萝卜快了不洗泥。
C语言足够灵活且几乎不进行任何运行时检查,比如数组越界、指针是否合法、运算结果是否溢出等。
C语言足够灵活,对于一个数组a[30],它允许使用像a[-1]这样的形式来快速获取数组首元素所在地址前面的数据;
2.1、数据类型问题
下面的两个例子的问题是什么?
unsignedchar i;
for(i=0;i<256;i++) {…}
unsignedchari;
for(i=10;i>=0;i--){…}
无符号char类型,范围为0~255,所以无符号char类型变量i永远小于256(第一个for循环无限执行),永远大于等于0(第二个for循环无限执行)。
2.2、误加标点符号
1.if(a>b); //这里误加了一个分号
2.a=b;
2.3、编译器忽略掉多余的空格符和换行符
1.if(n<3)
2.return //这里少加了一个分号
3.logrec.data=x[0];
4.logrec.time=x[1];
5.logrec.code=x[2];
这段代码的本意是n<3时程序直接返回,由于程序员的失误,return少了一个结束分号。
编译器将它翻译成返回表达式logrec.data=x[0]的结果,return后面即使是一个表达式也是C语言允许的。
这样当n>=3时,表达式logrec.data=x[0];就不会被执行,给程序埋下了隐患。
2.4、数组越界。
代码在硬件上运行,一段时间后LCD显示屏上的一个数字不正常的被改变。
经过一段时间的调试,问题被定位到下面的一段代码中:
int SensorData[30];
......
for(i=30;i>0;i--)
SensorData[i]=...;
很多编译器会对上述代码产生警告:
赋值超出数组界限。
但并非所有程序员都对编译器警告保持足够敏感,而且编译器也并不能检查出数组越界的所有情况。
2.5、数组声明具有外部链接时大小应显式声明
模块A中定义数组:
在模块B中引用该数组,但由于你引用代码并不规范,这里没有显式声明数组大小,但编译器也允许这么做:
externint SensorData[];
如果在模块B中存在和上面一样的代码:
SensorData[i]=…;
这次,编译器不会给出警告信息,因为编译器压根就不知道数组的元素个数。
所以,当一个数组声明为具有外部链接,它的大小应该显式声明。
2.6、编译器检查不出数组越界
函数func()的形参是一个数组形式,函数代码简化如下所示:
char * func(char SensorData[30])
unsignedint i;
…
这个给SensorData[30]赋初值的语句,编译器也是不给任何警告的。
实际上,编译器是将数组名SensorData隐含的转化为指向数组第一个元素的指针,函数体是使用指针的形式来访问数组的,它当然也不会知道数组元素的个数了。
造成这种局面的原因之一是C编译器的作者们认为指针代替数组可以提高程序效率,而且,还可以简化编译器的复杂度。
指针和数组容易给程序造成混乱,如何区分?
可以将数组名等同于指针的情况有且只有一处,就是数组作为函数形参时。
其它时候,数组名是数组名,指针是指针。
另一个例子:
编译器同样检查不出数组越界。
用数组来缓存通讯中的一帧数据。
在通讯中断中将接收的数据保存到数组中,直到一帧数据完全接收后再进行处理。
即使定义的数组长度足够长,接收数据的过程中也可能发生数组越界,特别是干扰严重时。
这是由于外界的干扰破坏了数据帧的某些位,对一帧的数据长度判断错误,接收的数据超出数组范围,多余的数据会改写数组相邻的变量,造成系统崩溃。
由于中断事件的异步性,这类数组越界编译器无法检查到。
如果局部数组越界,可能引发ARM架构硬件异常。
一设备用于接收无线传感器的数据,一次升级后,发现接收设备工作一段时间后会死机。
调试表明ARM7处理器发生了硬件异常,异常处理代码是一段死循环(死机的直接原因)。
接收设备有一个硬件模块用于接收无线传感器的整包数据并存在自己的硬件缓冲区中,当一帧数据接收完成后,使用外部中断通知设备取数据,外部中断服务程序精简后如下所示:
__irq ExintHandler(void)
Unsignedchar DataBuf[50];
GetData(DataBug); //从硬件缓冲区取一帧数据
由于存在多个无线传感器近乎同时发送数据的可能,加之GetData()函数保护力度不够,数组DataBuf在取数据过程中发生越界。
由于数组DataBuf为局部变量,被分配在堆栈中,同在此堆栈中的还有中断发生时的运行环境以及中断返回地址。
溢出的数据将这些数据破坏掉,中断返回时PC指针可能变成一个不合法值,硬件异常由此产生。
但如果被利用,则可作病毒。
1988年,第一个网络蠕虫在一天之内感染了2000到6000台计算机,利用的正是一个标准输入库函数的数组越界Bug。
起因是一个标准输入输出库函数gets(),原来设计为从数据流中获取一段文本,遗憾的是,gets()函数没有规定输入文本的长度。
gets()函数内部定义了一个500字节的数组,攻击者发送了大于500字节的数据,利用溢出的数据修改了堆栈中的PC指针,从而获取了系统权限。
2.7、编译器与volatile限定符
源文件定义变量
unsignedint a;
volatileunsignedint a;
头文件声明变量
externunsignedlong a;
externunsignedint a;
编译器
提示语法错误:
变量‘a’声明类型不一致
编译器:
不给错误信息(或仅一条警告)
volatile属于类型限定符,另一个常见的类型限定符是const关键字。
限定符volatile在嵌入式软件中至关重要,用来告诉编译器不要优化它修饰的变量。
模块A源文件中定义变量:
volatileunsignedint TimerCount=0;
该变量用来在一个定时器服务程序中进行软件计时:
TimerCount++; //读取IO端口1的值
在模块A的头文件中,声明变量:
externunsignedint TimerCount; //这里漏掉了类型限定符volatile
模块B中,要使用TimerCount变量进行精确的软件延时:
#include “...A.h” //首先包含模块A的头文件
TimerCount=0;
while(TimerCount>=TIMER_VALUE); //延时一段时间
这是一个死循环。
在模块B中,变量TimerCount是被当作unsignedint类型变量。
由于寄存器速度远快于RAM,编译器在使用非volatile限定变量时,先将变量从RAM中拷贝到寄存器中,如果同一个代码块再次用到该变量,就不再从RAM中拷贝数据而是直接使用之前寄存器备份值。
代码while(TimerCount>=TIMER_VALUE)中,变量TimerCount仅第一次执行时被使用,之后都是使用的寄存器备份值,而这个寄存器值一直为0,所以程序无限循环。
下面的流程图说明了程序使用限定符volatile和不使用volatile的执行过程
使用了volatile会怎样?
2.8、定义为volatile的变量的作用过程
优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
下面左侧函数有什么问题:
代码目的是用来返指针*ptr指向值的平方
intsquare(volatileint*ptr){
return*ptr**ptr;
编译器将产生类似下面的代码
inta,b;
a=*ptr;
b=*ptr;
returna*b;
正确代码:
longsquare(volatileint*ptr){
inta;
returna*a;
2.9、局部变量必须显式初始化
●例1:
unsigned intGetTempValue(void)
unsigned int sum; //定义局部变量,保存总值。
但其初值并不一定为0
for(i=0;i<10;i++)
sum+=CollectTemp(); //函数CollectTemp可以得到当前的温度值
return (sum/10);
●例2:
char * GetData(void)
char buffer[100];//局部数组
return buffer;
3、不合理的优先级
C语言有32个关键字,却有34个运算符。
要记住所有运算符的优先级是困难的。
不合理的#define会加重优先级问题,让问题变得更加隐蔽。
#define READSDA IO0PIN&(1<<11) //定义宏,读IO口p0.11的端口状态
//目的是判断端口p0.11是否为高电平
if(READSDA==(1<<11))
编译器在编译后将宏带入,原if语句变为:
if(IO0PIN&(1<<11) ==(1<<11))
3.1、常规使用可能引起误会的运算符
常被误会的优先级
表达式
误认为
真实结果
取值运算符*、自增运算符++,
优先级相同,但自右向左结合
*p++
(*p)++
*(p++)
成员选择运算符.
高于取值运算符*
*p.f
(*p).f
*(p.f)
数组下标运算符[]优先级,
int*ap[]
int(*ap)[]
ap为数组指针
int*(ap[])
ap为指针数组
函数()优先级高于取值运算符*
int*fp()
int(*fp)()
fp为函数指针
int*(fp())
fp为函数,返回指针
等于==和不等于!
=运算符优先级
高于位操作运算符&、^和|
val&mask!
=0
(val&mask)!
val&(mask!
=0)
=运算符
高于赋值运算符=
c=getchar()!
=EOF
(c=getchar())!
c=(getchar()!
=EOF)
算数运算符+和-优先级
高于移位运算符<<和>>
msb<<4+lsb
(msb<<4)+lsb
msb<<(4+lsb)
4、隐式转换和强制转换
4.1、有符号和无符号char和short类型自动转换
表达式中的有符号和无符号char和short类型都自动被转换为int类型.
在需要的情况下,将自动被转换为unsignedint(在short和int具有相同大小时)。
这称为类型提升。
提升在算数运算中通常不会有什么大的坏处,但如果位运算符~和<<,应用在基本类型为unsignedchar或unsignedshort的操作数,结果应该立即强制转换为unsignedchar或者unsignedshort类型(取决于操作时使用的类型)。
uint8_t port =0xa5U; /*typedefunsignedcharuint8_t*/
uint8_t result_8;
result_8= (~port) >> 4;
如果不了解表达式里的类型提升,认为在运算过程中变量port一直是unsignedchar类型的。
期望的运算过程:
~port结果为0xa5,0xa5>>4结果为0x0a。
实际上,result_8的结果却是0xfa。
在ARM结构下,int类型为32位。
变量port在运算前被提升为int类型:
~port结果为0xffffffa5,0xa5>>4结果为0x0ffffffa,赋值给变量result_8,发生类型截断(隐性转换),result_8=0xfa。
正确表达式语句应该为:
result_8=(unsignedchar)(~port)>>4; /*强制转换*/
4.2、混合数据类型运算中会转换成较高级别数据类型
数据类型的级别从高到低的顺序:
longdouble、double、float、unsignedlonglong、longlong、unsignedlong、long、unsignedint、int。
这种类型提升通常都是件好事,但往往有很多程序员不能真正理解这句话,从而做一些想当然的事情,比如下面的例子,int类型表示16位。
uint16_t u16a = 40000; /* 16位无符号变量*/
uint16_t u16b= 30000; /*16位无符号变量*/
uint32_t u32x; /*32位无符号变量 */
uint32_t u32y;
u32x = u16a +u16b; /* u32x = 70000还是4464 ?
*/
u32y =(uint32_t)(u16a + u16b); /* u32y=70000还是4464?
u32x和u32y的结果都是4464(16位值范围0-65536,超出值域范围部分4464)。
不要认为表达式中有一个高类别uint32_t类型变量,编译器都会帮你把所有其他低类别都提升到uint32_t类型。
正确的书写方式:
u32x = (uint32_t)u16a +(uint32_t)u16b;
或者:
u32x = (uint32_t)u16a + u16b;
后一种写法在本表达式中是正确的,但是在其它表达式中不一定正确,比如:
uint16_t u16a,u16b,u16c;
uint32_t u32x;
u32x=u16a+u16b + (uint32_t)u16c;/*错误写法,u16a+u16b仍可能溢出*/
4.3、
copyright@ 2008-2023 冰点文库 网站版权所有
经营许可证编号:鄂ICP备19020893号-2