1、无中生“友”

我有一个“朋友”,正在学习第二门语言时遇到这样一个现象

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	a := 0.1
	b := 0.2
	fmt.Println(a + b)  // 0.30000000000000004
	fmt.Printf("%d\n", unsafe.Sizeof(a))  // 8
}

没错,上述现象简单来说就是计算机计算的0.1+0.2并不等于0.3了,其实这个现象很常见,对别的语言来说也一样,下面通过一步步简要分析来解释这个现象

刚好在学习基础时再遇到,于是再花一点时间去拾遗下大学的基础知识,顺便记录一下(O_o)

2、浮点型数据介绍

日常程序开发并不只是用到整数,反而在多数情况下,我们用到的都是实数(有理数和无理数的集合)

实数之间的运算即浮点型运算,浮点运算不像整数运算,它的计算结果一般是不确定的。一块芯片上的浮点计算结果也许与另一块芯片上的不同

部分文字内容来源于大学时的计算机基础课程《计算机组成原理》

3、浮点数的表示形式

浮点型的科学计数法表示:N=M*rE

  • M称为浮点数的尾数,M取小数,可正可负
  • E称为浮点数的指数,也叫阶码,E取整数,可正可负
  • r称为浮点数的基数,计算机中r24816

浮点数在计算机中的表示,有一个IEEE的标准,它定义了两个基本的格式:

一个是用32比特表示单精度的浮点数,也就是我们常常说的float

另外一个是用64比特表示双精度的浮点数,也就是我们平时说的 double

在计算机中都是用二进制存储,因此不论是32位浮点数还是64位浮点数,由于基数2是固定常数,对每一个浮点数都一样,所以不必用显示的方式来表示它

go语言来说,分别是float32float64,这两种类型的二进制表示分别如下图

那么具体是怎么转换和存储的呢?

3.1 浮点数转换为二进制

以浮点数39.29为例

对于整数部分,直接转换为二进制,即:100111

对于小数部分,让小数一直乘2,小于1则用结果继续乘,大于1则结果减1继续乘,等于1则结束

0.29 * 2 = 0.58  // 小于1,则继续乘
0.58 * 2 = 1.16  // 大于1,则减1继续乘
0.16 * 2 = 0.32  // 小于1,则继续乘
0.32 * 2 = 0.64  // 小于1,则继续乘
0.64 * 2 = 1.28  // 大于1,则减1继续乘
0.28 * 2 = 0.56  // 小于1,则继续乘
0.56 * 2 = 1.12  // 大于1,则减1继续乘
0.12 * 2 = 0.24  // 小于1,则继续乘
0.24 * 2 = 0.48  // 小于1,则继续乘
0.48 * 2 = 0.96  // 小于1,则继续乘
0.96 * 2 = 1.92  // 大于1,则减1继续乘
0.92 * 2 = 1.84  // 大于1,则减1继续乘
0.84 * 2 = 1.68  // 大于1,则减1继续乘
0.68 * 2 = 1.36  // 大于1,则减1继续乘
0.36 * 2 = 0.72  // 小于1,则继续乘
0.72 * 2 = 1.44  // 大于1,则减1继续乘
0.44 * 2 = 0.88  // 小于1,则继续乘
0.88 * 2 = 1.76  // 大于1,则减1继续乘
0.76 * 2 = 1.52  // 大于1,则减1继续乘
0.52 * 2 = 1.04  // 大于1,则减1继续乘
0.04 * 2 = 0.08  // 小于1,则继续乘
0.08 * 2 = 0.16  // 小于1,则继续乘
0.16 * 2 = 0.32  // 小于1,则继续乘,结果与第三行相同,一直循环

最终,由于注定不会等于1,只能无限循环,将相乘之后的结果的整数部分拼接起来,所以0.29的二进制表示为:

01001010001111010111000......

因此,39.29的二进制就是100111.01001010001111010111000............

3.2 科学计数法表示二进制数

简单来说,科数计数法就是把一个数字变成1.x2的多少次方。向左移了几位就是2的几次方,如果向右移了几位就是2的负几次方

39.29的二进制位需要左移5位,因此用科学计数法表示为

1.0011101001010001111010111000... * 2^5

3.3 存储科学计数法表示的二进制

  • Float32,用32位的二进制来存储一个浮点数
  • Float64,用64位的二进制来存储一个浮点数

float32位为例进行表示

  • sign:用1位表示浮点型的正负,0表示正数,1表示负数

  • exponent(指数):存储科学技术法的指数部分的值(几次方),8位表示的数据范围可以是0~255,但由于指数部分可能为负数,因此exponent有8位的表示范围是-127 ~ 128,计算时,让指数加上127得到的值转换为二进制存储在此处,这里是5+127=132,转换乘二进制10000100存储到exponent

  • fraction(小数):用23位来表示二进制小数的科学计数法中的小数部分

最终,39.29在存储时的二进制为0 10000100 001110100101000111101,后面超出的直接丢弃,这就是浮点型可能无法精确表示的原因

4、如何精确的表示浮点数

go中使用decimal包可以解决浮点数精度丢失这个问题

package main

import (
	"fmt"
	"github.com/shopspring/decimal"
)

func main() {
	c := decimal.NewFromFloat(35.2922)
	fmt.Println(c.Round(1))  //保留小数点后1位自动四舍五入
	fmt.Println(c.Truncate(2))  //保留小数点后2位不需要四舍五入
}

See you ~