我们来讨论C语言关于浮点型强制转换的一个特性
比如
#include<stdio.h>
void main()
{
int I = (1<<31)-1,II;//这个是有符数的最大值Tmax
float f = 0;
int a,b,c;
a = (int)(float)I;
b =(float)I;
f = (float)I;
c = (int)f;
printf("%d,%d,%d\n", a, b, c);
}
运行结果如何?
在(2)章里面我们主要是讨论的超级单精度浮点,而32位机器使用的是单精度浮点和双精度浮点,单精度浮点k=8,n=23。而双精度浮点k=11,n=52。
那么对于单精度类型float来说,有效位只有23,不可能把31位int正整数表示完,于是在运算的时候就可能产生问题。
在老的C编译器里,可能上例中a、b、c最后结果都为-2147483648,因为(int)(float)I运算操作,先是把I转换成float,明显它不能表示Tmax,于是它会被四舍五入,用一个近似的float值代替Tmax,比如Tmax+1……你没有看错,float能表示的数的范围肯定比int大,Tmax = 2^31-1=2147483647而已,而Fmax=(1+2^23-1) * 2^127 = 1.4272476927059598810582859694495 * 10^45,恐怖吧?只是说数值越大精度越差,float能精确描述Tmax吗?用(2)章里的方法瞬间就知道,Tmax可是需要31位有效数的,float只有23位,爱莫能助,于是float就挑了个离Tmax最近的2的整次方数2^31=Tmax+1。
由于(float)I已经把Tmax转换成了2^31,(int)(float)I操作再回到int时,那就是把2^31转换成int,有符数补码其值就是-2^31 = -2147483648。
然而,当实际用gcc和vc测试发现,a、b都是2147483647,只有c是-2147483648,从这里可以看出,新的编译器对这个问题进行了优化,使得精度保留得更完整,但这样做可能就会产生新的问题。比如以前的老代码,就是希望通过强制转换将整数进行修改,那么用在新编译器上就一定会导致问题。
另外一个需要关注的特性涉及到寄存器,我这里还是贴出那段经典的代码:
#include<stdio.h>
double recip(int denom)
{
return 1.0/(double) denom;
}
void do_thinklife()
{
}
void main()
{
int denom=10;
double r1,r2;
int t1,t2,t3;
r1 = recip(denom);
r2 = recip(denom);
t1 = r1==r2; //比较1
do_thinklife();
t2 = r1==r2;//比较2
t3 = r1 == 1.0/(double) denom;//比较3
printf("t1:%d,t2:%d, t3:%d\n", t1, t2, t3);
}
用vc++ 运行, 1,1,1——符合预期
用dev c++运行, 1,1,0——(⊙o⊙)…
用redhat gcc运行,0,1,0——⊙﹏⊙
到底是肿么回事?
为了让redhat的结果看起来靠谱些,我用gcc -O2 参数来编译,出来结果是1,1,0——估计dev c++也是加了-O2编译参数优化的。
其实造成这个的原因还是精度问题,具体罪魁祸首是0.1,这个数即便用双精度也不可能精确表示!通过读取内存数据,我发现float f=0.1;f的四个
字节存的是0x3d cc cc cd,就是111101110011001100110011001101,首位补上00,我们来分析一下:
0 01111011 10011001100110011001101
e = 01111011 =D (123)
E=e-Bias=123-127 = -4……
Bias具体算法参见教材
M=1+f = 1+m*2^(-23) =1+0.60000002384185791015625 = 1.60000002384185791015625
V = (-1)^s * M * 2^E = 1.60000002384185791015625 * (1/16) = 0.100000001490116119384765625
……╮(╯▽╰)╭OMG
看到了吧,一个小小的0.1,居然需要曲线就轨到这种程度才能用float近似表示,没办法,谁让计算机只认二进制呢:(,同理即便代码利用的是double,也不可能有办法精确表示0.1,只是最终数值比上面float的表示更接近0.1而已。从这里我们还发现,虽然单精度有23个有效位来贡献精度,但真正能精确表示的十进制小数的位数很有限,因为很多位数都牺牲在从十进制转二进制的道路上了。
事实上,当r2 = recip(denom)执行完时,r2的值是存在寄存器里的,此时被认为是0.1;而由于多跑了r2赋值这段语句,r1的值已经写入存储器了(这是相对于寄存器的叫法而言,不是指硬盘),也就是双精度方式存储,就是类似上面我们算出来的值,因此比较1式子结果为0。然而当我们用do_thinklife()思考过人生后,r2的值终于也写入双精度存储,因此比较2的结果为1。同理,比较3中,由于该语句在执行完之前就进行比较,因此1.0/(double) denom是直接被当成0.1来处理的,自然结果为0。
有符号数的转换
如果从较低类型转换到较高类型,将进行符号扩展,例如一个值从short int(16位)转换到long类型,如果这个数是正数,则最高位为0,从16位扩展到32位时,扩展的高16位用0填充,即将符号位0进行扩展,这样扩展后的32位整数和原来的整数值是一样的。如果该数为负数,则最高位为1,从16位扩展到32位时,扩展的高16位用1填充,即将符号位1进行扩展,这样扩展后的32位整数和原来的整数值是也是一样的。
原文链接:https://blog.csdn.net/u013471946/article/details/23354913