浮点类型是如何存储的

计算机如何存储字节

计算机中最小的存储单位是bit只能保存0和1,整数在内存中如何存储我们都知道,将要存储的数字转成2进制即可

用windows自带的计数器可以方便的查看整数对应的2进制值
如:
byte类型(单字节)

十进制 二进制
8 0000 1000
9 0000 1001
100 0110 0100
-5 1111 1011
-8 1111 1000

第一位为符号位,负数等于正数取反 +1

那浮点类型是如何用这么少的字节(如float 4字节)表示这么大(float 最大 3.4028235E38)的数字呢?

浮点类型是如何存储的

浮点数

浮点数,是属于有理数中某特定子集的数的数字表示,在计算机中用以近似表示任意某个实数。具体的说,这个实数由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到,这种表示方法类似于基数为10的科学计数法。

科学计数法

科学计数法是一种记数的方法。把一个数表示成a与10的n次幂相乘的形式(1≤|a|<10,a不为分数形式,n为整数),这种记数法叫做科学计数法。当我们要标记或运算某个较大或较小且位数较多时,用科学计数法免去浪费很多空间和时间。

java中的浮点数字遵循 IEEE 754 标准

这也是一种目前最常用的浮点数标准!为许多CPU与浮点运算器所采用。

简单的说就是将一个浮点数字拆成3个部分(符号部分、指数部分、小数部分) 存储在连续的bit中,类似科学计数法。

用 {S,E,M}来表示一个数 V 的,即 V =(-1)S × M × 2E,如下:

S(符号位) E(指数位) M(有效数字位)

其中:

  • 符号位 s(Sign)决定数是正数(s=0)还是负数(s=1),而对于数值 0 的符号位解释则作为特殊情况处理。
  • 有效数字位 M(Significand)是二进制小数,它的取值范围为 0~1 。它也被称为尾数位(Mantissa)、系数位(Coefficient),甚至还被称作“小数”。
  • 指数位 E(Exponent)是 2 的幂(可能是负数,为了表示负数实际指数需要加一个偏移量),它的作用是对浮点数加权。

IEEE 754浮点数规范

+- d.ddd...d * β^e   (0 <= di < β)  

其中d.dd...d 为有效数字,β为基数,e 为指数

有效数字中 数字的个数 称为精度,我们可以用 p 来表示,即可称为 p 位有效数字精度。
每个数字 d 介于 0 和基数 β 之间,包括 0。

十进制表示

对十进制的浮点数,即基数 β 等于 10 的浮点数而言,上面的表达式非常容易理解。
如 12.34,我们可以根据上面的表达式表达为:
1×101 + 2×100 + 3×10-1 + 4×10-2
其规范的浮点数表达为:1.234×101

二进制表示

但对二进制来说,上面的表达式同样可以简单地表达。
唯一不同之处在于:二进制的 β 等于 2,而每个数字 d 只能在 0 和 1 之间取值。

如二进制数 1001.101,我们可以根据上面的表达式表达为:
1×23 + 0×22 + 0×21 + 1×20 + 1×2-1 + 0×2-2 + 1×2-3
其规范浮点数表达为:1.001101×23

二进制转换为十进制

二进制数 1001.101 转成十进制如下:

= 1 × 23 + 0 × 22 + 0 × 21 + 1 × 20 + 1 × 2-1 + 0 × 2-2 + 1×2-3
= 8 + 0 + 0 + 1 + 1/2 + 0 + 1/8
= 9又1/8 (9又8分之1)
= 9.625

由上面的等式,我们可以得出:
向左移动二进制小数点一位相当于这个数除以 2,而向右移动二进制小数点一位相当于这个数乘以 2。
如 101.11 = 5又3/4 (5.75),向左移动一位,得到 10.111 = 2又7/8 (2.875)。

除此之外,我们还可以得到这样一个基本规律:
一个十进制小数要能用浮点数精确地表示,最后一位必须是 5(当然这是必要条件,并非充分条件)。
如下面的示例所示:

二进制小数 2的多少次方 十进制的小数
0.1 2-1 0.5
0.01 2-2 0.25
0.001 2-3 0.125
0.0001 2-4 0.0625
0.00001 2-5 0.03125
0.000001 2-6 0.015625
... ... ...

十进制转换为二进制

基本换算方法:
将10进制的数拆分成整数和小数两个部分
整数部分除以2,取余数;小数部分乘以2,取整数位。

示例:
将十进制 1.1 转成 二进制

整数部分:1
1

小数部分:0.1

0.1 *2 = 0.2 -> 0
0.2 *2 = 0.4 -> 0
0.4 *2 = 0.8 -> 0
0.8 *2 = 1.6 -> 1
0.6 *2 = 1.2 -> 1
0.2 *2 = 0.4 -> 0
0.4 *2 = 0.8 -> 0
0.8 *2 = 1.6 -> 1
0.6 *2 = 1.2 -> 1
0.2 *2 = 0.4 -> 0
0.4 *2 = 0.8 -> 0
0.8 *2 = 1.6 -> 1
0.6 *2 = 1.2 -> 1
0.2 *2 = 0.4 -> 0
0.4 *2 = 0.8 -> 0
0.8 *2 = 1.6 -> 1
......

二进制形式表示为:
1.000110011001100110011...

再将上面的数换算成10进制

1 + 1/16 + 1/32 + 1/256 + 1/512  + ...
约等于  
(32 + 16 + 2 + 1)/512    
约等于  
  51/512  
约等于  
  0.099609375

再加上整数1,约等于:
1.099609375

计算的位数越多越精确

注意:
二进制小数不像整数一样,只要位数足够,它就可以表示所有整数。
在有限长度的编码中,二进制小数一般无法精确的表示任意小数,比如十进制小数0.2,我们并不能将其准确的表示为一个二进制数,只能增加二进制长度提高表示的精度。

十进制的0.2 转换成二进制为:0.00110011001100110011001100110011001100110011001100110......


java API 中对Double 和 Float 类型的描述

Double 双精度浮点数 64bit (8byte)

根据 IEEE 754 浮点“双精度格式”位布局。

  • 第 63 位(掩码 0x8000000000000000L 选定的位)表示浮点数的符号。
  • 第 62-52 位(掩码 0x7ff0000000000000L 选定的位)表示指数。
  • 第 51-0 位(掩码 0x000fffffffffffffL 选定的位)表示浮点数的有效数字(有时也称为尾数)。

如果参数是正无穷大,则结果为 0x7ff0000000000000L。
如果参数是负无穷大,则结果为 0xfff0000000000000L。
如果参数是 NaN,则结果为 0x7ff8000000000000L。

Float 单精度浮点数 32bit (4byte)

根据 IEEE 754 浮点“单一格式”位布局。

  • 第 31 位(掩码 0x80000000 选定的位)表示浮点数的符号。
  • 第 30-23 位(掩码 0x7f800000 选定的位)表示指数。
  • 第 22-0 位(掩码 0x007fffff 选定的位)表示浮点数的有效位数(有时也称为尾数)。

如果参数为正无穷大,则结果为 0x7f800000。
如果参数为负无穷大,则结果为 0xff800000。
如果参数为 NaN,则结果为 0x7fc00000。

掩码位说明

这里以 double类型说明

  • 第 63 位(掩码 0x8000000000000000L 选定的位)表示浮点数的符号。
    转成二进制
    1000000000000000000000000000000000000000000000000000000000000000

  • 第 62-52 位(掩码 0x7ff0000000000000L 选定的位)表示指数。
    转成二进制
    0111111111110000000000000000000000000000000000000000000000000000

  • 第 51-0 位(掩码 0x000fffffffffffffL 选定的位)表示浮点数的有效数字(有时也称为尾数)。
    转成二进制
    0000000000001111111111111111111111111111111111111111111111111111

将一个浮点数与上面的掩码进行与运算,即可得到对应的 符号位、指数位、尾数位 的值。

这里的多少多少位是从右往左数的,当转成2进制不够64位时在前面补零即可


按照浮点数计算规范要求:(划重点)

  • 符号为 1表示负数,0表示正数
  • 指数 =(为了表示负指数,实际的指数需要加上一个值,双精度为 2e10-1 = 1023)= 实际指数 + 1023
  • 有效数字省略了最高的一位1,去掉小数点左侧的 1,并用 0 在右侧补齐。
故前面十进制数(1.1)的二进制形式:

1.000110011001100110011...

用浮点类型表示:
  • 符号位:0
  • 指数为:0 + 1023 = 1111111111 = (不够11位前面补0) = 01111111111
  • 有效位:000110011001100110011...

所以存为:
0 01111111111 000110011001100110011...

用java代码输出进行验证

System.out.println(Long.toBinaryString(Double.doubleToLongBits(1.1)));
//11111111110001100110011001100110011001100110011001100110011010
//这种情况前面要补两个0
//0011111111110001100110011001100110011001100110011001100110011010

System.out.println(Long.toBinaryString(Double.doubleToLongBits(-1.1)));
//1011111111110001100110011001100110011001100110011001100110011010

浮点数精度问题

根据 IEEE 754 规范

  • Float 单精度浮点数,有效位数只有23 bit位
  • Double 双精度浮点数,有效位数只有52 bit位

在二进制,第一个有效数字必定是“1”,因此这个“1”并不会存储。
单精和双精浮点数的有效数字分别是有存储的23和52个位,加上最左边没有存储的第1个位,即是24和53个位。

通过计算其能表示的最大值,换十进制来看其精度:

  • Float
    二进制的24个1 => (二进制) 111111111111111111111111 => (计算) 2^24 - 1 => (十进制) 16777215 => (Float 精度最大8位数)

  • Double
    二进制的53个1 => (二进制) 11111111111111111111111111111111111111111111111111111 => (计算)2^53 - 1 => (十进制)9007199254740991 => (Double 精度最大16位数)

为什么会丢失精度

浮点运算很少是精确的,只要是超过精度能表示的范围就会产生误差。而往往产生误差不是因为数的大小,而是因为数的精度。

我自己理解为分两种情况(这个不一定是对)

  1. 当有小数时,浮点数本身就不能精确记录其数值,只记了一个近似值,此时进行计算就很可能不对
  2. 有效位数不够用了,导致舍入

1、不能精确记录其数值

通过上面的转换示例,我们知道小数的二进制表示一般都不是精确的,在有限的精度下只能尽量的表示近似值

值本身就不是精确的,再进行计算就很可能产生误差

0.1+0.2=0.30000000000000004
示例代码:
double d1 = 0.1;
double d2 = 0.2;
double d3 = d1 + d2;
System.out.println(d3);
System.out.println("######################################");
System.out.println(new BigDecimal(d1));
System.out.println(new BigDecimal(d2));
System.out.println(new BigDecimal(d3));

System.out.println("######################################");
System.out.println(Long.toBinaryString(Double.doubleToLongBits(d1)));
System.out.println(Long.toBinaryString(Double.doubleToLongBits(d2)));
System.out.println(Long.toBinaryString(Double.doubleToLongBits(d3)));

输出:

0.30000000000000004
######################################
0.1000000000000000055511151231257827021181583404541015625
0.200000000000000011102230246251565404236316680908203125
0.3000000000000000444089209850062616169452667236328125
######################################
11111110111001100110011001100110011001100110011001100110011010
11111111001001100110011001100110011001100110011001100110011010
11111111010011001100110011001100110011001100110011001100110100

0.1
原始值: 0 01111111011 1001100110011001100110011001100110011001100110011010
指数:1019 -1023 = -4
二进制形式:
0.00011001100110011001100110011001100110011001100110011010

0.2
原始值:0 01111111100 1001100110011001100110011001100110011001100110011010
指数:1020 -1023 = -3
二进制形式:
0.001001100110011001100110011001100110011001100110011010

0.3
原始值:0 01111111101 0011001100110011001100110011001100110011001100110100
指数:1021 = -2
二进制形式:
0.010011001100110011001100110011001100110011001100110100

二进制加法运算

0.00011001100110011001100110011001100110011001100110011010
+
0.001001100110011001100110011001100110011001100110011010
=
0.010011001100110011001100110011001100110011001100110100
其他示例:
double a = 0.03;
double b = 0.01;
System.out.println(a - b); 
//结果 0.019999999999999997

double x = 10.2;
double y = 10.03;
System.out.println(x + y); 
//结果 20.229999999999997

double dx = 1.099999999999999999999999999999d;
System.out.println(dx);
//结果 1.1

double dy = 1.1000000000000000000000000000001d;
System.out.println(dy);
//结果 1.1

2、有效位数不够用

这里用float验证,float最大的精度是8位数

// 有效位数不够
float f = 1.23456789f;
System.out.println(f);   // 1.2345679   最大只能有8位

System.out.println("=====================");
float f1 = 10000f;
System.out.println(f1);  // 10000.0
float f2 = 1.123456f;
System.out.println(f2);  // 1.123456

// 相加之后有效位数不够
float f3 = f1 + f2;
System.out.println(f3);  // 10001.123   位数不够,后面的被省略了

关于舍入

对于不能精确的表示的数,采取一种系统的方法:找到“最接近”的匹配值,它可以用期望的浮点形式表现出来,这就是舍入。

对于舍入,可以有很多种规则,可以向上舍入,向下舍入,向偶数舍入。如果我们只采用前两种中的一种,就会造成平均数过大或者过小,实际上这时候就是引入了统计偏差。如果是采用偶数舍入,则有一半的机会是向上舍入,一半的机会是向下舍入,这样子可以避免统计偏差。而 IEEE 754 就是采用向最近偶数舍入(round to nearest even)的规则。

(这段是网上抄的)


其他

大端 小端问题

这里以java语言示例,用大端的方式示例(网络序)

java中是以大端模式存储的,java对我们屏蔽了内部字节顺序的问题以实现跨平台!

实际在不同的cpu架构下,存储方式不同,我们常用的X86是以小端的模式存储的。

网络传输一般采用大端序,也被称之为网络字节序,或网络序。IP协议中定义大端序为网络字节序。


测试代码

二进制字符串转成Double

public static void main(String[] args) {
    // 4607632778762754458
    String s = "0011111111110001100110011001100110011001100110011001100110011010";
    System.out.println("二进制字符串 = " + s);
    long l = Long.parseLong(s, 2);
    System.out.println("转成Long = " + l);
    double d = Double.longBitsToDouble(l);
    System.out.println("再将Long转成Double = " + d);
}

输出:

二进制字符串 = 0011111111110001100110011001100110011001100110011001100110011010
转成Long = 4607632778762754458
再将Long转成Double = 1.1

写内存的方式转Double

/**
 * bit字符串转Double
 * 
 * @param bitStr 64位的01字符串
 * @throws Exception
 */
public static void bit2Double(String bitStr) throws Exception {
    String[] array = splitString(bitStr, 8);
    System.out.println(Arrays.toString(array));

    Unsafe unsafe = getUnsafe();
    long address = unsafe.allocateMemory(8L);

    for (int i = 0; i < array.length; i++) {
        String bits = array[i];
        byte bt = (byte) Integer.parseInt(bits, 2);
        // 因为实际上是小端模式存储的,所以这里从后面开始写入
        unsafe.putByte(address + (7 - i), (byte) bt);
    }

    long lVal = unsafe.getLong(address);
    System.out.println("对应的long值是:" + lVal);
    System.out.println(Long.toBinaryString(lVal));

    double dVal = unsafe.getDouble(address);
    System.out.println("转成Double 类型是:" + dVal);
    System.out.println(Long.toBinaryString(Double.doubleToLongBits(dVal)));

}

public static String[] splitString(String source, int length) {
    String[] array = new String[length];
    for (int i = 0; i < length; i++) {
        array[i] = source.substring(i * length, (i + 1) * length);
    }
    return array;
}

public static Unsafe getUnsafe() throws Exception {
    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe) f.get(null);
    return unsafe;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352