1.废话可略过
面试时书面遇到一道题:
#include<stdio.h>
void main()
{
float a=0x12345678;
printf("a=%f",a);
return 0;
}
输出什么?浮点数很久没用过了,记得好像有什么精度缺失什么的,但是想不起来了,我当时拿手机立马把这个十六进制换成十进制就写上了,还补上了六个0,面试官问我浮点型在计算机怎么存储,给我又问懵了。他给了我十分钟查一下,然后再来做这道题,并给他讲清楚,我找了半天,找到的资料竟然漏了个关键点,给我卖了,找到的资料要么太长要么太简洁,坑死个人。
2.快速掌握怎么使用
2.1 IEEE754标准
按照一定格式把一个十进制的数存成二进制数,IEEE754标准就是那个格式规则。
格式是:一位符号位(0是正,1是负),阶码(指数+偏移),尾数(类似科学计数法看小数点后的有效数字部分)
float偏移是127,double偏移是1023,即 2^(n-1)-1 。(先记着,为什么这么算看3.2部分)。
float 的32位划分与double 的64位划分如下图:
2.2 操作步骤示例
把十进制数换成二进制,然后按照三部分格式填进去就行了。(下例按float型演示)
例1:原值十进制浮点型20.5
二进制=10100.1
符号:正数为0,指数为4,阶码=4+127(换成二进制填入),尾数是标蓝部分,不够就补0
存储值=0 10000011 0100 1000 0000 0000 0000 000
(为什么1.0100 1,只要小数点后面作为有效部分,因为二进制写成科学计数法这种形式首位肯定是1啊,所以隐藏了,看起来是23位,实际是24位)
可使用(IEEE 754 浮点数转换 - 锤子在线工具 (toolhelper.cn))自行验证。
例2:原值 十六进制浮点型整数 0x12345678=305419896(十进制)
二进制=0001 0010 0011 0100 0101 0110 0111 1000
符号:正数为0,从右往左数,指数为28,阶码就是28+127,尾数为标蓝的23位
存储值 0 10011011 0010 0011 0100 0101 0110 100
这个存储值和原值不一样了,因为进行了舍入(个人理解,尾数后跟的第一个二进制是1就进位,是0就舍去,3.3有解释)
3. 原理与解释
3.1为什么要用IEEE754标准存储浮点数
类比科学计数法,0.000000000000000001或13400000000000这种很长的数,有效部分却很短,我们可以用科学计数法来表示成1x10^(-18),和1.34x10^13。这样是不是就很简便了,我们提取出来的关键信息就是有效数字,和指数两部分。
同理,IEEE754标准就是用,符号位,阶码,尾数,三部分来表示一个数
但是这两个计数方法都有缺陷,前者想必大家都知道(对于200000000005这种用科学计数法也没有多简洁),后者坑就更大了,科学计数法只是用起来不方便了,IEEE754标准在某些情况下会让你的原值与存储值不相等,再用%f输出(默认六位小数四舍五入),连输出值和存储值也不一定一样了。
3.2 阶码与偏移
为什么float的偏移量是127,double偏移量是1023?
因为前者我们用了8位表示指数属性,后者用了11位表示指数属性,当一个浮点数是个很小的数时,指数会是负的,所以我们要用8位(或11)位来表示正负两种情况,2^8=256,一半正一半负,当然我们会想到用一位符号位,七位表示指数,符号位用0表示负,1表示正的,不知道为什么IEEE754用的是加偏移量的方法。可能是怕和前面那个符号位表示方式相反怕记混淆了?(具体取值区间的问题可以看章末另一篇文章,更细致)
3.3 精度问题
3.3.1原值,存储值,输出值
原值:我们要存到计算机里的自然值,是连续的,现实生活中0-1之间会有无穷个值,但是计算机内的值是离散的,一个个的。
存储值:存入计算机的值,按照固定规则的,以float为例,它的精度只有24位,隐藏的一位和23位尾数,这23位二进制数只能有2^23种变化,却要来表示远超他的范围,必然会导致精度缺失。
输出值:我们输出浮点数时候,%f输出的是存储值,但是%f默认六位小数,四舍五入等规则,会让输出值和存储值也不一定完全一样,要注意。
更新订正:%f的输出在截取时候的规则不是四舍五入,而是四舍六入五取偶,(也称银行家舍入,最近舍入,向偶数舍入,谁知道为啥他又这么多别名,见到了知道是什么就行)
四舍六入五取偶就是四舍六入五考虑,五后非空就进一,五后为空看奇偶,五前为偶应舍去,五前为奇要进一。例如保留一位小数输出0.15和0.25都是0.2,5后面没了前一位为奇数进位,偶数舍去。保留一位小数输出0.151和0.251会变成0.2和0.3(纸面上演示,不是上机,上机输出0.15保留一位小数变成0.2是因为存储值的第三位小数后面还有没显示出的部分);
针对的是输出时候(存储值到输出值)的舍入规则,不是IEEE754存入时候(原值到存储值)的舍入规则。因为我们在输出时候不会把一个浮点型的存储值完全输出,保留n位小数时,第n+1是5时,其后面可能还有数没显示出来的数,导致这个特性一般很难看出来,表现得跟四舍五入一样。实际你找一个标准的蓝值用%f输出下试试就知道了。
3.3.2 舍入规则
首先计算机的存储值的有效数字一定是23位二进制能表示的一种。
即 00000000000000000000000 到 11111111111111111111111。
整数分析:305419896(10进制)
二进制=0001 0010 0011 0100 0101 0110 011 1 1000
因为计算机内只能存这23位可以表示的蓝值,实际的数已经超出了五位,我们要存的值处于两个蓝值区间内, 更接近谁就存谁,我们可以看到两个实际可以存的蓝点值之间的分度值现在是2^5=32(超出了五位,可见精度丢失的严重),而5位二进制数首位为1最小有一定大于和首位为0最大的情况(10000>01111),可以直接根据后面第一位是1还是0进行进位或舍去,所以现在存储值是0001 0010 0011 0100 0101 0110 100 0 0000=305419904(十进制)比原值还大了。(注意:存储的是IEEE754格式的0 10011011 00100011010001010110100,我只是把它写成了标准二进制形式方便复制过去转换验证)
带小数的分析:123456.1(10进制)
二进制=0001 1110 0010 0100 0000.0001 100 1 1001
同理,有效部分超过23位溢出了,第一位是1,进位,存储值=0001 1110 0010 0100 0000.0001 101 0 0000,所以我们要存123456.1结果存进去变成了123456.1015625,不过我们要保证输出时和输入值一样的话,记着float只能有七位精度就行了,输出时只输出七位有效数,小数点后只保留一位就行了。
3.3.3 精度分析
计算机中,整数转换成2进制,是2的正次方数累加(1,2,4,8……可组成任意整数)
小数转换成2进制,是2的负次方累加(0.5,0.25,0.125……有所遗漏,有的数转换成2进制会很长)
尾数的23位二进制可以表示2^23个值,而指数划定了值在哪个范围。我们知道指数的取值是[-126,127],举例来说:
[2^(-1),2^0)即[0.5,0.999),因为整数部分为0,所以实际上我们有24位来表示有效数,分度值是2^(-24),小数嘛。(我理解的是有效位肯定是1的嘛,我们把一位小数移到了隐藏那一位,所以有就是24位来表示小数了)
[2^(0),2^1)即[1,2),23位有效数,整数部分占1位,分度值是2^(-23),
[2^(1),2^2)即[2,4),23位有效数,整数部分占2位,分度值是2^(-22),
[2^(2),2^3)即[4,8),23位有效数,整数部分占3位,分度值是2^(-21),
[2^(3),2^4)即[8,16),23位有效数,整数部分占4位,分度值是2^(-20),
…………
[2^23,2^24)即[8388608,16777216),整数刚好23位,分度值是2^0
[2^23,2^24)即[16777216,33554432),整数24位,溢出一位,分度值是2^1
有一张表可以查间隔
间隔(即分度值)越来越大了,从这能看出IEEE754能存储的值就是坐标轴上的刻度,只能取到刻度上而不能处于间隔之间,是离散的不是连续的,而离坐标原点越远,刻度间的间隔越大,精度越低,如下图。(是不是困惑为什么会有正无穷负无穷,不是超范围了吗?这又涉及到几种特殊情况了,篇末)
3.4 使用建议
读到这里我们知道,当你想存一个浮点数时候,可能在存时候就不对了(缺失精度),取出时也可能不对(%f输出规则限制),但实际常见的浮点数还是可以正常使用的,float可以精确表示7位十进制有效数字(科学计数法那种有效数字,整数加小数),double可以表示16位。
还有就是在计算机中浮点数进行运算时,是用的存储值,但存储值和原值不一定相等啊,这样算出来不是会出错嘛。是会出错,但是在精度范围内是可以正确加法运算的,因为在7位精度范围内我们可以保证是对的明说,第八位一定是舍去了,四舍五入,他小于5,那么两个小于5的数相加不会进位。减法呢就不一定了,还没找到更简单的理解方法。乘除就更别说了。有哪位大佬知道欢迎指点。
更详细的解释可以看下这篇,很长很长,第四部分规格数,取值范围等概念。
这一篇,里面有一点关于浮点数运算部分,不过太简略了