剖析浮点数

本文详细介绍了浮点数的存储原理,包括其在内存中的表现形式以及如何进行二进制与十六进制之间的转换。同时探讨了IEEE 754标准下float和double类型的具体实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面一篇我们讲了位运算后,基本C语言的大块都提及了,一些细节和用法暂时不再本模块介绍了。希望我
的心愿能够在我毕业之前给我的大学生涯划上一个圆满的句号。加油努力。
在本模块的第三篇就已经讲了基本的数据类型,其中把浮点数刻意留在了后面来介绍。我的理解是在我们理
解了内存,指针,位运算等后,再来介绍浮点这个 特殊而又普通的数据类型比较好理解。浮点数和基本类型
数据的存储差别比较大,这里不是说存储形式的差别,而是浮点数存放的时候是要经过运算后再转换成整数
的4字节或8字节的形式,然后再存放到内存里。因此,只通过16进制数是看不出来和整数有什么差别。
同样,浮点数具体是怎么存储的,在大学的课程上一般不 会细细讲解,一般是我们自己有兴趣再查阅资料。
包括本篇的内容,如果你不是一个自学者或者充满好奇心,你也不会看下去,也不会找到本篇的URL。因
此,包 括很多已经工作很多年的程序员都不知道浮点数具体是怎么运算然后存储的。就我来讲,认为还是非
常有必要了解这个常用的数据类型的换算过程,虽然我们个人来 讲很难去打破当前浮点数的计算规则以至于
将他的精度提高,但是了解下底层工作者们的辛苦,我们应该向他们真诚的致敬。因为有他们,我们便有了
大树可以乘 凉。
好了,废话不多说。本篇的目的就是为了让更多的人了解浮点数存储的基本原理,还是那句话,学习的同时
带着思考。同样这里不讨论浮点数的精度损失和数值的计算理论。直接讲实质的表现。
在计算机发展过程中,我们使用的小数和实数曾经提出过很多种的表示方法。典型的比如相对于浮点数的定
点数(Fixed Point Number)。在这种表达方式中,小数点固定的位于实数所有数字中间的某个位置。货币
的表达就可以使用这种方式,比如 88.22 或者 22.88 可以用于表达具有四位精度(Precision),小数点后
有两位的货币值。由于小数点位置固定,所以可以直接用四位数值来表达相应的数值。SQL 中的
NUMBER 数据类型就是利用定点数来定义的。还有一种提议的表达方式为有理数表达方式,即用两个整数
的比值来表达实数。
很显然,上面的定点数表示法有缺陷,不能表示很小的数或者很大的数。于是,为了解决这种问题,我们的
前辈们自然想到了科学技术法的形式来表示,即用 一个尾数(Mantissa ),一个基数(Base),一个指数
(Exponent)以及一个表示正负的符号来表达实数。比如 123.456 用十进制科学计数法可以表达为
1.23456 × 102 ,其中 1.23456 为尾数,10 为基数,2 为指数。浮点数利用指数达到了浮动小数点的效果,
从而可以灵活地表达更大范围的实数。
大约就在1985年,IEEE标准754的推出,它是一个仔细制定的表示浮点数及其运算的标准。这项工作是从
1976年Intel发起8087的设 计开始的,8087是一种为8086处理器提供浮点支持的芯片,他们雇佣了
William Kahan,加州大学伯克利分校的一位教授,作为帮助设计未来处理器浮点标准的顾问。他们支持
Kahan加入一个IEEE资助的制订工业标准的委员会。这 个委员会最终采纳了一个非常接近于Kahan为
Intel设计的标准。目前,实际上所有的计算机够支持这个后来被称为IEEE浮点(IEEE floating point)的
标准。这大大改善了科学应用程序在不同机器上的可移植性。所谓IEEE就是电器和电子工程师协会。
介绍完了历史,先来看看浮点数最直接的表示。在数学上:
12.341010 = 1*101 + 2*100 + 3*101
+ 4*102
= 12(34/100) (这里由于编辑器的原因,只能写
这么机械了)。
在比如二进制:
1
101.112 = 1*22 + 0*21 + 1*20 + 1*21
+ 1*22
= 4 + 0 + 1 + 1/2 + 1/4 = 5(3/4)。
上面简单的描述了在数学意义上的浮点数表示,但是在计算机中,我们存放在内存中的直观上
看16进制数,那么这些16进制数是怎么表示我们浮点数的二进制形式呢?
在 IEEE 标准中,浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数
域三个域,其中保存的值分别用于表示给定二进制浮点数中的符 号,指数和尾数。这样,通过尾数和可以调
节的指数(所以称为"浮点")就可以表达给定的数值了。具体的格式:
符号位 阶码 尾数 长度
float 1 8 23 32
double 1 11 52 64
我们都知道浮点数在32位机子上有两种 精度,float占32位,double占64位。很多朋友喜欢把
double用于8字节的数据存储。从这点我们应该不要特殊看到浮点数的内存存储形式, 他跟整
数没有什么区别,只是在这4字节或者8字节里有4个区域,整数有符号只有符号位及后面的
数值,之所以最高位表示有符号数的符号位。原因之一在于 0x7fffffff位最大整数,为整个32
位所能表示的最大无符号整数0xffffffff的一半减一,也就是:比如1字节:无符号是:0xff,
有符 号正数为:(0, 127],负数为[128,
0)。在8位有符号时,肯定内存值大于等于: 0x80。二进
制就是1000 0000,比他大,只会在低7位上变化,最高位已经是1了,变了就变小了。所以这
里也是一个比较巧用的地方,一举两得。
那么,我们先来看32位浮点数 的换算:
1. 从浮点数到16进制数
float var = 5.2f;
就这个浮点数,我们一步一步将它转换为16进制数。
首先,整数部分5,4位二进制表示为:0101。
其次,小数部分0.2,我们应该学了小数转换为二进制的计算方法,那么就是依次乘以2,取整数部分作为二
进制数,取小数部分继续乘以2,一直算到小数结果为0为止。那么对0.2进行计算:
0.2*2 = 0.4 * 2 = 0.8 * 2 = 1.6(0.6) * 2 = 1.2(0.2)*2 = 0.4 * 2 = 0.8 * 2 = 1.6(0.6) * 2 = 1.2 ... ...
0 0 1 1 0 0 1 1 ... ...
因此,这里把0.2的二进制就计算出来了,结果就为:0.00110011... ... 这里的省略号是你没有办法计算完。
二进制序列无限循环,没有到达结果为0的那一天。那么此时我们该怎么办?这里就得取到一定的二进制位
数后停止计算,然后 舍入。我们知道,float是32位,后面尾数的长度只能最大23位。因此,计算结束的
时候,整数部分加上小数部分的二进制一共23位二进制。因此5.2 的二进制表示就为:
101.00110011001100110011
一共23位。
此时,使用科学计数法表示,结果为:
1.0100110011001100110011 * 22
2
由于我们规定,使用二进制科学计数法后,小数点左边必须为1(肯定为1嘛,为0的话那不就是
0.xxxx*sxxx 了,这样没有什么意义),这里不能为0是有一个很大的好处的,为什么?因为规定为1,这
样这个1就不用存储了,我们在从16进制数换算到浮点数的时候加上 这个1就是了,因为我们知道这里应
该有个1,省略到这个1的目的是为了后面的小数部分能够多表示一位,精度就更高一些了哟。那么省略到
小数点前面的1后的 结果为:
.01001100110011001100110 * 22
这里后面蓝色的0就是补上的,这里不是随便补的一个0,而是0.2的二进制在这一位上本来就应该为0,如
果该为1,我们就得补上一个1.是不是这样多了一位后,实际上我们用23位表示了24位的数据量。有一个
位是隐藏了,固定为1的。我们不必记录它。
好了,尾数在这里就计算好了,他就是 01001100110011001100110 。
再来看阶数,这里我们知道是2^2次方,那么指数就是2。同样IEEE标准又规定了,因为中间的 阶码在
float中是占8位,而这个 阶码又是有符号的(意思就是说,可以有2^2
次方的形式)。
float 类型的 偏置量 Bias = 2k1
1
= 281
1
= 127 ,但还要补上刚才因为左移作为小数部分的 2
位(也就是科学技术法的指数),因此偏置量为 127 + 2=129 ,就是 IEEE 浮点数表示标准:
V = (1)
s × M × 2E
E = e Bias
中的 e ,此前计算 Bias=127 ,刚好验证了 E = 129 127
= 2 。
这里的阶码就是12910 ,二进制就是:1000 00012 。
因此,拼接起来后:
1000 0001 01001100110011001100110
| ← 8位 → | | ←23
位 →
|
一共就是31位了,这里还差一位,那就是符号位,我们定义的是5.2,正数。因此这里最高位是0,1表示
负数。
而后结果就是:
0 1000 0001 01001100110011001100110
1位 | ← 8位 → | | ←23
位 →
|
到这里,我们内存里面的十六进制数产生了,分开来看:
0 100 0000 1 010 0110 0110 0110 0110 0110
4 0 A 6 6 6 6 6
因此,我们看到的就是0x40A66666, 此就是5.2最终的整数形式。
2.从十六进制数到浮点数
我们还是可以用上面5.2的例子,再将0x40A66666换算回去,用同样一个例子,结果更直观,逆运算更好
理解。那我们就开始吧。
首先,要还原回去,必须将这个16进制用我们的计算器换算成二进制:
0 100 0000 1 010 0110 0110 0110 0110 011 0
我是COPY上面的。这里颜色已经很明显了,我划分成了3个区域 。
首先确定符号,这里是0,因此是正数。
其次看绿色的8位,换成10进制就是:12910
3
我们逆运算,知道这里需要129 127
= 2得到指数,得到了指数,我们便知道我们小数点是向哪个方向移
动了好多位。脑子里已经有了一个科学计数法的锥形。
再次把红色的23位提取出来,这里不把它换成10进制,因为我们指数是表示的二进制上移动了多少位,底
数是2,而不是10。
这里因为之前我们都知道有个固定的1给省略了,因此这里要给加上去。加上去之后:
1 010 0110 0110 0110 0110 011 0
这里是24位,我们先不管,小数点添进去:
1 . 010 0110 0110 0110 0110 011 0 * 22
然后将科学计数法变换成普通的二进制小数:
1 01 . 0 0110 0110 0110 0110 011 0
到这里,就真正可以把整数部分换成十进制了:
1 01 . 0 0110 0110 0110 0110 011 0
5. xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
我们知道了,整数部分是5,后面的小数部分再进行逆运算:
这里我们就应该想想小数到二进制数是乘法,这里逆运算就应该除以2,因此就可以表示为:
0 . 0 0110 0110 0110 0110 011 0
0 + 0*21
+ 0*22
+ 1*23
+ 1*24
+ 0*25
+ 0*26
+ 1*27
+ ... ... + 0*221
这样一个式子,我们算出结果
来,放在浮点数里:
5.1999998。
因此我们可以看到精度已经有损失了。
问题一:写写5.2
的16进制数?
再来看一个例子:
float var = 0.5, 算16进制数。
首先,0.5整数部分为0,这里就不处理了。
其次,0.5小数部分,二进制表示为:0.1
这里是0.1,将尾数补满23位则是:
0.10 0000 0000 0000 0000 0002
由于小数点左边是0,因此需要向右移动一位 ,因此:
1.0 0000 0000 0000 0000 00002 * 21
这里1又被省略掉,所以23位全部变成了0 ,因此:
.00 0000 0000 0000 0000 00002 * 21
4
然后,因为这里指数是1,
因此阶码就是:1
+ 127 = 126 = 0111 11102
这样一来,阶码就有了,由于又是正数,那么组合起来:
0 01111110 00000000000000000000000
这样一来,最终的16进制数则为:0x3f000000.
是不是很简单啊。
64位浮点数 的换算:
这里就不再具体说明怎么换算的了,只需要提到2个地方:
一是,中间的阶码在double中占有11位,因此就不是+127了,而是加上1023,因为11位能表示的最大无
符号数是2047,因此有符号范围[1024,
1023]。
二是,尾数是52位,因此精度更高,能表示的数也就越大。我们在换算5.2的时候,后面的小数二进制+前
面的5的二进制再省略一位后的总位数要填满52位。
好了,浮点数也没有太多要说的,就到这里吧,在用的时候注意精度和范围就可以了。
最后在提一个问题:
问题二:
float var0 = 5.2;
float var1 = 500.2;
float var2 = 50000.2;
float var3 = 5000000.2;
观察这几个数,加深一下那三个域的计算方式,并说出这些数据有什么规律?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值