资源描述
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) puts("> 6") : puts("<= 6");
}
C语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是“>6”。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。如果你答错了这个问题,你也就到了得不到这份工作的边缘
在汇编语言层面,声明变量的时候,没有 signed 和 unsignde 之分,汇编器统统,将你输入的整数字面量当作有符号数处理成补码存入到计算机中,只有这一个标准!汇编器不会区分有符号还是无符号然后用两个标准来处理,它统统当作有符号的!并且统统汇编成补码!也就是说,db -20 汇编后为:EC ,而 db 236 汇编后也为 EC 。这里有一个小问题,思考深入的朋友会发现,db 是分配一个字节,那么一个字节能表示的有符号整数范围是:-128 ~ +127 ,那么 db 236 超过了这一范围,怎么可以?是的,+236 的补码的确超出了一个字节的表示范围,那么拿两个字节(当然更多的字节更好了)是可以装下的,应为:00 EC,也就是说 +236的补码应该是00 EC,一个字节装不下,但是,别忘了“截断”这个概念,就是说最后汇编的结果被截断了,00 EC 是两个字节,被截断成 EC ,所以,这是个“美丽的错误”,为什么这么说?因为,当你把 236 当作无符号数时,它汇编后的结果正好也是 EC ,这下皆大欢喜了,虽然汇编器只用一个标准来处理,但是借用了“截断”这个美丽的错误后,得到的结果是符合两个标准的!也就是说,给你一个字节,你想输入有符号的数,比如 -20 那么汇编后的结果是符合有符号数的;如果你输入 236 那么你肯定当作无符号数来处理了(因为236不在一个字节能表示的有符号数的范围内啊),得到的结果是符合无符号数的。于是给大家一个错觉:汇编器有两套标准,会区分有符号和无符号,然后分别汇编。其实,你们被骗了。:-)
3.第一点说明汇编器只用一个方法把整数字面量汇编成真正的机器数。但并不是说计算机不区分有符号数和无符号数,相反,计算机对有符号和无符号数区分的十分清晰,因为计算机进行某些同样功能的处理时有两套指令作为后备,这就是分别为有符号和无符号数准备的。但是,这里要强调一点,一个数到底是有符号数还是无符号数,计算机并不知道,这是由你来决定的,当你认为你要处理的数是有符号的,那么你就用那一套处理有符号数的指令,当你认为你要处理的数是无符号的,那就用处理无符号数的那一套指令。加减法只有一套指令,因为这一套指令同时适用于有符号和无符号。下面这些指令:mul div movzx … 是处理无符号数的,而这些:imul idiv movsx … 是处理有符号的。
举例来说:
内存里有 一个字节x 为:0x EC ,一个字节 y 为:0x 02 。当把x,y当作有符号数来看时,x = -20 ,y = +2 。当作无符号数看时,x = 236 ,y = 2 。下面进行加运算,用 add 指令,得到的结果为:0x EE ,那么这个 0x EE 当作有符号数就是:-18 ,无符号数就是 238 。所以,add 一个指令可以适用有符号和无符号两种情况。(呵呵,其实为什么要补码啊,就是为了这个呗,:-))
乘法运算就不行了,必须用两套指令,有符号的情况下用imul 得到的结果是:0x FF D8 就是 -40 。无符号的情况下用 mul ,得到:0x 01 D8 就是 472 。
4.为什么又扯到 c 了?因为大多数遇到有符号还是无符号问题的朋友,都是c里面的 signed 和 unsigned 声明引起的,那为什么开头是从汇编讲起呢?因为我们现在用的c编译器,无论gcc 也好,vc6 的cl 也好,都是将c语言代码编译成汇编语言代码,然后再用汇编器汇编成机器码的。搞清楚了汇编,就相当于从根本上明白了c,而且,用机器的思维去考虑问题,必须用汇编。(我一般遇到什么奇怪的c语言的问题都是把它编译成汇编来看。)
C 是可爱的,因为c符合kiss 原则,对机器的抽象程度刚刚好,让我们即提高了思维层面(比汇编的机器层面人性化多了),又不至于离机器太远(像c# ,java之类就太远了)。当初K&R 版的c就是高级一点的汇编,C又是可怕的,因为它把机器层面的所有的东西都反应了出来,像这个有没有符号的问题就是一例(java就不存在这个问题,因为它被设计成所有的整数都是有符号的)。为了说明它的可怕特举一例:
#include <stdio.h>
#include <string.h>
int main()
{
int x = 2;
char * str = "abcd";
int y = (x - strlen(str) ) / 2;
printf("%d\n",y);
}
结果应该是 -1 但是却得到:2147483647 。为什么?因为strlen的返回值,类型是size_t,也就是unsigned int ,与 int 混合计算时有符号类型被自动转换成了无符号类型,结果自然出乎意料。。。
观察编译后的代码,除法指令为 div ,意味无符号除法。
解决办法就是强制转换,变成 int y = (int)(x - strlen(str) ) / 2; 强制向有符号方向转换(编译器默认正好相反),这样一来,除法指令编译成 idiv 了。
我们知道,就是同样状态的两个内存单位,用有符号处理指令 imul ,idiv 等得到的结果,与用无符号处理指令mul,div等得到的结果,是截然不同的!所以牵扯到有符号无符号计算的问题,特别是存在讨厌的自动转换时,要倍加小心
为了避免这些错误,建议,凡是在运算的时候,确保你的变量都是 signed 的。
定义 INT16S ltr1 = 0;
ltr1 ++;
if(ltr1 = = 32767)
{
ltr1 += 1;
ltr1 += 2;
}
当ltr1 等于32767时,到了有符号整数的最大值,加1就是-32768(0X8000)
再加2就是-32766(0X8002)
6. INT16U i;
INT16S *k;
INT32S mbx;
k = pmes + 7;
mbx=0;
for(i=0;i<COUNTOFCALSAMP;i++) /*32*4计算的周波*/
{
mbx += *k;
k +=NUMOFCAL; /*存放的是8个量*/
}
ltr = labs(mbx); //整型量的值有可能出现负值时,当需要提取负值时需要用 labs取绝对值来反映其实际大小。
ltr = (INT16U)(mbx); //整型量的值有可能出现负值时,当mbx = - 6时,即mbx = 0xfffffffa,这种强制转换并不能得到我们想要的值,相反它的结果很大,是fffa。
7. INT16S ltr1;
INT16U ltr2,ltr3;
INT8U a;
INT16S b;
a= 6;
b = -20;
ltr1 = a + b;
ltr2 = (INT16S)(a + b);
c = 5;
d = 15;
ltr3 = c - d; //这个值会得到5到15之间的距离。
结果是:ltr1 = -14 ltr2 = 65522 ltr3 = 65526
当一个整型变量赋值给一个无符号的整型时,数据格式不变,直接复制。
ltr1 本身是整型,ltr1 = a + b = 0xfff2 = - 14;
Ltr2 本身是无符号整型,ltr2 = (INT16S)(a + b) 0xfff2 =65522 ;可见它们的16进制数值是一样的,这取决于赋值号左边的数据类型,这个法则只适合加减运算。
Ltr3 当它是无符号整型时,值为65526(0xfff6),当它为整型时,值为-10(0xfff6);
其16进制表示是一致的,区别是要看赋值号左边的数据类型。这个法则只适合加减运算
总结:
1.当运算符两边一个是有符号整型,一个是无符号整型时,有符号整型转成无符号整型 ,然后再运算。
这样问题就来了,当运算结果赋值给整型时是没有问题的,当运算结果赋值给无符号整型时,问题就有了,结果为负值的时候将得到一个很大数值,这个值用于运算时会出错。
可见,整型量与无符号整型在16进制的写法上没有什么区别,当整型量的负数部分从b1111 1111 1111 1111表示-1,b1000 0000 0000 0000表示-32768,这样的好处是便于加减法运算,可以将减法运算转变为加法运算,如 7 - 3 = 4,将3变为补码为:b1111111111111101 + b111 = b100;可见这是一种技法。
8. INT8U a;
INT16S b;
INT16S ltr1;
INT16S ltr1;
INT16S acqbuf[256];
b = -12368;
a = acqbuf[222];
ltr1 = b/a;
Ltr2 = b/a;
当acqbuf[222] = 30 时,ltr1 = 1772,ltr2 = 1772而不是我们要得到的负值。因为变量a是无符号整型,变量b的补码形式0xcfb0,运算时编译器将变量b当作无符号整型来运算,将最高位认为是数据,而不是符号,使用I$$UDIV函数来运算,即0xcfb0 / 30 = 1772
如果这样写ltr1 = b/acqbuf[222] = -412;就对了,因为他们都是整型量,编译器使用I$$DIV函数来运算,将最高位认为是符号位。
INT16U ltr1;
INT16S ltr2;
INT16S b;
INT16S acqbuf[256];
b = -6;
ltr1 = -6; //ltr1是无符号整型 所有ltr1 = 65530
ltr2 = acqbuf[224]/b; //都为整型,且负数除以负数等于正数,所有ltr2 = 1。
10 INT16S ltr1;
INT16S ltr2;
INT8U a,c,d;
INT16S b;
INT16S acqbuf[256];
a = 635;
b = -635;
ltr1 = b/acqbuf[222]; //acqbuf[222] = 29时,ltr1 = -21。
ltr2 = a/acqbuf[224]; //acqbuf[224] = -6时,ltr2 = 0。
11.
INT16S ltr1;
INT16S ltr2;
INT16U ltr3;
INT16S acqbuf[256];
ltr1 = acqbuf[222]/-2; //acqbuf[222] = 30时,ltr1 = -15。
ltr2 = acqbuf[224]/2; //acqbuf[224] = -7时,ltr2 = -3。
ltr3 = acqbuf[224]/2; //acqbuf[224] = -7时,ltr2 = 0xfffd = 65533。
以上除法编译的汇编语言中调用有符号的除法指令,I$$DIV函数。
再次总结:
混合运算时,加减乘除的表达式中,运算符两边一个是有符号整型,一个是无符号整型时,有符号整型转成无符号整型 ,然后再运算。
二进制运算以补码形式进行,不管加减乘除都会转化为这个形式,补码的好处就是将减法运算变为加法运算,这样的好处是可以省去硬件的减法电路。这样就出现了以下情况:在加减法运算时,处理器不区分变量是有符号的还是无符号的,全部以补码方式运算。
处理器的除法提供两种运算指令,一个是有符号的I$$DIV,一个是无符号的I$$UDIV,混合运算时就要慎重了,最好统一所有参与运算的变量,且都为有符号的整型变量,以免因转化带来不预期的运算结果。
那么乘法呢,来看一个有意思的问题
INT8U a;
INT16S b;
INT16S acqbuf[256];
INT16S ltr1;
INT16S ltr2;
b = -123;
a = acqbuf[222];
ltr1 = b*a;
// ltr2 = b*acqbuf[222];
这里的ltr1= -3690 ,ltr2 = -3690。
先解析ltr1的值,整型变量b = -123,补码为 0xff85,由于变量a是无符号的,b*a在运算时将变量b转化为无符号数0xff85,将认为最高位为数据位。如果a = acqbuf[222] = 30 ,乘法可以转变为加法运算,即30个 0xff85相加,相加结果产生溢出,截取16位值为0xF196 ,这个值就有意思了,当看着是整型量时,它等于 -3690;当看着是无符号的整型量时,它等于61846。
ltr2 = b*acqbuf[222];由于都是整型量,运算时乘法转变为加法运算,即30个 0xff85相加,相加结果产生溢出,截取16位值为0xF196 ,在结果上这两种算法没有区别。
有了上面的分析,现在来重新看除法运算,在TMS320LF24系列的DSP中,除法运算能不能用以下的方式来理解呢?
INT16S a,c;
INT16U b,f;
INT16S acqbuf[256];
INT16S ltr1;
INT16S ltr2;
a = -123;
b = acqbuf[222];
ltr1 = a/b;
f = 123;
c = acqbuf[224];
ltr2 = f/c;
这里的ltr1= 2180 ,ltr2 = 0。运算发生符号转换,编译器使用I$$UDIV除法函数。
先解析ltr1的值,整型变量a = -123,补码为 0xff85,由于变量b 是无符号的,a/b在运算时将变量a 转化为无符号数0xff85,将认为最高位为数据位。如果b = acqbuf[222] = 30 ,除法可以转变为减法运算(补码算法还是加法),使用I$$UDIV除法函数。 a/b = 0xff85/30是不是可以这样解释,I$$UDIV除法可以转变为减法运算(补码算法还是加法),a/b转化为0xff85-30 = = 0xff85+0xFFE2,如果每次将加法运算的结果赋值给变量a,那么每加法运算一次,a值减小一次,加2180次后f值将接近负数,由于是无符号除法运算,运算结果没有符号,2180就是a/b的结果。
ltr2 =f/c;整型变量c = acqbuf[224] = -6;补码为 0xfffa,123/0xfffa = 0。
现在改一个变量类型,把INT16U b,f;改为INT16S b,f;
INT16S a,c;
INT16S b,f;
INT16S acqbuf[256];
INT16S ltr1;
INT16S ltr2;
a = -123;
b = acqbuf[222];
ltr1 = a/b;
f = 123;
c = acqbuf[224];
ltr2 = f/c;
这里的ltr1= -4 ,ltr2 = -20。运算没有发生符号转换,编译器使用I$$DIV除法函数。
先解析ltr1的值,整型变量a = -123,补码为 0xff85,由于变量b 也是整型,如果b = acqbuf[222] = 30 ,除法可以转变为减法运算(补码算法还是加法),使用I$$DIV除法函数。 a/b = 0xff85/30是不是可以这样解释,I$$DIV除法可以转变为减法运算(补码算法还是加法),a/b转化为0xff85 + 30,如果每次将加法运算的结果赋值给变量a,那么每加法运算一次,a值增加一次,加4次后f值将接近正数,带上负号,那么-4就是a/b的结果。
ltr2 =f/c;如果整型变量c = acqbuf[224] = -6;补码为 0xfffa,除法可以转变为减法运算(补码算法还是加法),使用I$$DIV除法函数。 f/c = 123/0xfffa 是不是可以这样解释,I$$DIV除法可以转变为减法运算(补码算法还是加法),那么f/c转化为123 + 0xfffa ,如果每次将加法运算的结果赋值给变量f,考虑正常溢出的话,每加运算一次,f值减小一次,加20次后f值将接近负数,那么带上负号-20就是f/c的结果。
附录:
1.计算机对有符号整数的表示只采取一套编码方式,不存在正数用原码,负数用补码这用两套编码之说,大多数计算机内部的有符号整数都是用补码,就是说无论正负,这个计算机内部只用补码来编码!!!只不过正数和0的补码跟他原码在形式上相同,负数的补码在形式上与其绝对值的原码取反加一相同。
2-4有符号数的转换
从
到
方法
char
short
符号位扩展
char
long
符号位扩展
char
unsigned char
最高位失去符号位意义,变为数据位
char
unsigned short
符号位扩展到short;然后从short转到 unsigned short
char
unsigned long
符号位扩展到long; 然后从long 转到unsigned long
char
float
符号位扩展到long; 然后从long 转到float
char
double
符号位扩展到long; 然后从long 转到double
char
long double
符号位扩展到long; 然后从long 转到long double
short
char
保留低位字节
short
long
符号位扩展
short
unsigned char
保留低位字节
short
unsigned short
最高位失去符号位意义,变为数据位
short
unsigned long
符号位扩展到long; 然后从long转到unsigned double
short
float
符号位扩展到long; 然后从long 转到float
short
double
符号位扩展到long; 然后从long 转到double
short
long double
符号位扩展到long; 然后从long 转到double
long
char
保留低位字节
long
short
保留低位字节
long
unsigned char
保留低位字节
long
unsigned short
保留低位字节
long
unsigned long
最高位失去符号位意义,变为数据位
long
Float
使用单精度浮点数表示。可能丢失精度。
long
double
使用双精度浮点数表示。可能丢失精度。
long
long double
使用双精度浮点数表示。可能丢失精度。
2-5无符号数的转换
从
到
方法
unsigned char
char
最高位作为符号位
unsigned char
short
0扩展
unsigned char
long
0扩展
unsigned char
unsigned short
0扩展
unsigned char
unsigned long
0扩展
unsigned char
float
转换到long; 再从 long 转换到float
unsigned char
double
转换到long; 再从 long 转换到double
unsigned char
long double
转换到long; 再从 long 转换到double
unsigned short
char
保留低位字节
unsigned short
short
最高位作为符号位
unsigned short
long
0扩展
unsigned short
unsigned char
保留低位字节
unsigned short
unsigned long
0扩展
unsigned short
float
转换到long; 再从 long 转换到float
unsigned short
double
转换到long; 再从 long 转换到double
unsigned short
long double
转换到long; 再从 long 转换到double
unsigned long
char
保留低位字节
unsigned long
short
保留低位字节
unsigned long
long
最高位作为符号位
unsigned long
unsigned char
保留低位字节
unsigned long
unsigned short
保留低位字节
unsigned long
float
转换到long; 再从 long 转换到float
unsigned long
double
Convert directly to double
unsigned long
long double
转换到long; 再从 long 转换到double
展开阅读全文