《深入理解计算机系统》——浮点数强制转换小议

我们来讨论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具体算法参见教材

bias=2^{(k-1)}-1

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值