c语言浮点变量储存0.1,0.1+0.2!==0.3引发的思考-浮点数的存储和计算

0.1+0.2结果不是0.3,而是0.30000000000000004,要弄清楚这个问题还要从计算机底层存储说起,首先先要回顾几个基本的概念及其运算

整数表示

整数主要分为有符号整数和无符号整数。

无符号整数编码

假设整数类型数据有w位,这些位可以表示成[xw-1,xw-2,...,x0],每位都是0或1,这样就获得了x的无符号表示,我们规定B2Uw表示二进制转无符号数,则:

1460000023346683

举个列子说明:

B2U4[0101]=0*23+1*22+0*21+1*20 = 5

B2U4[1011]=1*23+0*22+1*21+1*20 = 11

无符号数最小值是所有位取0,也就是整数0。最大值是所有位取1,也就是整数2w - 1。

有符号整数编码

最常见的有符号整数编码就是补码,将最高有效位充当符号位,0为正数,1为负数,具体定义如下,

二进制位表示为[xw-1,xw-2,...,x0],我们规定B2Tw表示二进制转补码,则:

1460000023346686

举个列子说明:

B2T4[0101]=-0*23+1*22+0*21+1*20 = 5

B2T4[1011]=-1*23+0*22+1*21+1*20 = -5

有符号数最小值是所有位取[1000....],也就是整数-2w-1,最大值是所有位取[01111...],也就是整数2w-1-1

需要注意的点:

一、补码的范围是不对称的: |TMin(补码最小值)|= |TMax(补码最大值)| + 1

二、无符号数最大值刚好比补码最大值的两倍大一 : UMax(无符号最大值) = 2 * TMax + 1

无符号数扩展

将w位无符号数扩展为w'位,我们主要是对高位进行0扩展,[xw-1,xw-2,...,x0]扩展后位[0,0,0,...,xw-1,xw-2,...,x0],其中w'>w,并且B2Uw(x)=B2Uw'(x')

有符号数(补码)扩展

将w位有符号数扩展为w'位,我们主要是对高位使用xw-1进行扩展,[xw-1,xw-2,...,x0]扩展后位[xw-1,xw-1,xw-1,...,xw-1,xw-2,...,x0],其中w'>w,并且B2Tw(x)=B2Tw'(x')

整数运算

无符号数加法:

对于满足0<=x,y<2w的所有x,y:

1460000023346684

补码加法:

对于满足-2w-1<=x,y<=2w-1-1的所有x,y:

1460000023346685

基础知识回顾完了,接下来进入我们的主题,关于浮点数的存储和计算逻辑,首先看一下浮点数在计算机中是怎么存储的。

浮点数的表示

直到20世纪80年代,每个计算机制造商都设计了自己的表示浮点数的规则,以及对浮点数计算细节的实现。另外,它们常常不会太关注运算的准确性,而把实现的速度和简便性看得比精确性更重要。

这些情况随着IEEE标准754的推出而改变了,这是一个仔细制定的表示浮点数及其计算的标准。

二进制小数

理解浮点数第一步是考虑含有小数值的二进制数字。首先让我们先看一下更熟悉的十进制表示法,十进制表示法如下:

1460000023346687

这个表达描述的数值d为:

1460000023346688

类似的考虑形如:

1460000023346689

的表示法,其中bi的取值为0或1。这种方式表达的数b为:

1460000023346690

点右边的数位的权是2的正幂,左边的数位的权是2的负幂。

例如:101.11 表示为:1*22+0*21+1*20+1*2-1+1*2-2=5.75 。

我们容易看出,小数点向左移动一位相当于被2除,小数点向右移动一位相当于乘2。

以101.11为例

小数点左移

二进制表示为10.111, 数值为1*21+0*20+1*2-1+1*2-2+1*2-3=2.875。

小数点右移

二进制表示为1011.1, 数值为1*23+0*22+1*21+1*20+1*2-1=11.5。

假设我们只考虑有限长度的编码,那么十进制表示法不能准确的表示1/3和5/7这样的数,同理,二进制小数只能表示那些能被写成x*2y,其他的值只能近似的表示。

例如1/5可以用十进制0.2精确表示,但是我们不能把它精确的表示为二进制小数。

增长二进制表达长度可以提高表示的精度。如下表:

1460000023346692

IEEE浮点数表示

表示方式

IEEE浮点标准使用

1460000023346691的形式来表示一个浮点数:

符号:s决定是负数(s=1)还是正数(s=0)

尾数:M是一个二进制小数,它的范围是1~2-⍷或者是0~1-⍷

阶码:E的作用是对浮点数加权,这个权重是2的E次幂(可能是负数)

将浮点数的位表示分为3个阶段:

一个单独的符号位s直接编码符号s

k位的阶码字段exp=ek-1ek-2...e0

n位的小数字段frac=fn-1fn-2...f0编码尾码M,但是编码出来的值也依赖于阶码的值是否为0。

以c语言的float和double为例:

1460000023346694

1460000023346693

根据exp的值不同,编码后的值被分为三种情况,情况对应如下:

1460000023346695

规格化数值表示

exp不全为0也不全为1的情况下:

阶码的值为e-Bias,其中e为无符号数,其位表示ek-1ek-2...e0,Bias值为2k-1-1。

尾码的值为1+f,其中f值为0.fn-1fn-2...f0,这种方式也称为隐含的以1开头的表示,因为总是可以通过阶码的移动使得尾数1<= M < 2,所以这样可以多获取一个精度的值。

非规格化数

exp全为0的的情况下:

阶码的值为1-Bias,尾码的值为f,其中Bias和f的取值方式和规格化数相同。

舍入规则

因为表示方法限制了浮点数的精度和范围,所以浮点数只能近似的表示实数运算。对于数值x,我们需要一个系统的方法,找到最接近的x的x'。

IEEE浮点格式定义了四种不同的舍入规则,默认的方法是找到最接近匹配的,其他三种用于计算上界和下界。举例来说明四种舍入方式:

1460000023346696

默认使用的是向偶数舍入的方式,它首先试图找到一个最接近的值,唯一的设计决策是确定两个可能的中间数的值,该方法是:将数字向上或向下舍入,使得结果的最低有效位是偶数。

``

为什么使用偏向取偶数,而不是向上或向下舍入,可以假想这样一个场景,使用向上或向下舍入一组值,会在计算这些平均值的时候引入统计误差,向偶数舍入会在大多数现实情况中避免这种统计误差

``

同理向偶数舍入也可以运用在二进制的小数上,我们将最低有效位的值0认为是偶数,值1认为是奇数,一般来说只有对形如:XXXX.YYYY1000的二进制的模式的数,这个舍入方式才会有效,最右边的Y是要被舍入的位置,只有这种位模式表示两个可能的值中间的值。

如下,我们考虑舍入到小数点后两位:

10.000112舍入后的值位10.002(向下舍入)

10.001102舍入后的值位10.012(向上舍入)

10.011002舍入后的值位10.102(向偶数舍入),这个值是两个可能值中间的值,所以倾向于使最低有效位为零。

浮点数计算

根据ECMA-262规范描述:

primitive value corresponding to a double-precision 64-bit binary format IEEE 754 value

翻译成中文为:

数值型原始值对应双精度64位二进制格式的IEEE 754值

以下计算以双精度浮点数为前提

浮点数计算遵循以下步骤

对阶,小阶向大阶对齐;

尾数运算;

规格化处理;

舍入处理;

溢出验证(上溢和下溢);

以标题中的0.1+0.2为例

0.1和0.2根据IEEE标准表示为双精度浮点数:

数值

s

exp

frac

0.1

0

01111111011

1001100110011001100110011001100110011001100110011010

0.2

0

01111111100

1001100110011001100110011001100110011001100110011010

0.1表示为(-1)0*1.6*2-4

0.2表示为(-1)0*1.6*2-3

对阶

0.1的阶码值:-4

0.2的阶码值:-3

所以将0.1的尾码右移1一位为:

0 01111111100 1100110011001100110011001100110011001100110011001101

尾数相加

由于0.1尾数右移了一位,整数位则默认补0。

x: 0.1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101

+ y: 1.1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010

-----------------------------------------------------------------------

10.0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111

规格化处理

由于尾数并不是规格化数,因为整数部分为10而不是1,需要对其进行规格化处理。

即尾数需要右移1位,阶码值加一。

10.0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0111

右移一位

1.00110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 011(1)

规格化后,尾数高位1,隐藏不显示

s

exp

frac

0

01111111101

0011001100110011001100110011001100110011001100110011(1)

舍入处理

尾数末位是1,需要利用上文介绍的舍入方法对其进行舍入,按最接近值舍入为(末尾加1)引入了误差

s

exp

frac

0

01111111101

0011001100110011001100110011001100110011001100110100

检查溢出

0 01111111101 0011001100110011001100110011001100110011001100110100,没右发生溢出,故0.1+0.2结果为:

s

exp

frac

0

01111111101

0011001100110011001100110011001100110011001100110100

结果

将上述表示按规格化数转化成十进制为

0.30000000000000004

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值