实数的两种类型

计算机做加法、减法、比较这样的操作时首先要对齐小数点,计算机中处理小数点位置有浮点和定点两种,定点就是小数点永远在固定的位置上,比如说我们约定一种32位无符号定点数,它的小数点永远在第5位后面,这样最大能表示的数就是11111.111111111111111111111111111。

定点数的优点是很简单,大部分运算实现起来和整数一样或者略有变化,但是缺点则是表示范围,比如我们刚才的例子中,最大只能表示32;而且在表示很小的数的时候,大部分位都是0,精度很差,不能充分运用存储单元。

浮点数

浮点格式的组成则包括符号、尾数、基数和指数,通过这四部分来表示一个小数。由于计算机内部是二进制的,因此基数自然而然是2(就如十进制的基数是10一样)。因此计算机在数据中往往无需记录基数(因为总是2),而是只用符号、尾数、指数这三部分来表示。

image.png

十进制到二进制就是将十进制转换为 (1~1.9)* 2^x 的公式

  1. 5
  2. ->5.0
  3. ->5.0 * 2^0
  4. ->2.5 * 2^1
  5. ->1.25 * 2^2
  6. 0.2
  7. ->0.4 * 2^-1
  8. ->0.8 * 2^-2
  9. ->1.6 * 2^-3

指数可能是正数,也可能是负数,即指数是有符号的整数,而有符号整数的计算是比无符号整数麻烦的,所以为了减少不必要的麻烦,在实际存储指数的时候,需要把指数转换成无符号整数,float 的指数部分是 8 位,IEEE 标准规定单精度浮点的指数取值范围是 -127 ~ +128,于是为了把指数转换成无符号整数,就要加个偏移量,比如 float 的指数偏移量是 127,这样指数就不会出现负数了。

IEEE754

浮点格式的表示形式有很多,而在C中遵循的是IEEE 754标准:

  • float单精度浮点数为32位。32位的构造为:符号部分1bit、指数部分8bit以及尾数部分23bit。

  • double双精度浮点数为64位。64位的构造为:符号部分1bit、指数部分11bit以及尾数部分52bit。

取整误差

从右向左分别是0次幂、1次幂、2次幂以此递增,因此小数点前的二进制换算为十进制便是:

1 * 8 + 1 * 4 + 1 * 2 + 0 = 14

而在小数点之后的位权,相应的从左向右分别是-1次幂、-2次幂依次递减。因此小数点之后的二进制转换为十进制便是:

1 * 0.5 + 1 * 0.25 + 0 * 0.125 + 1 * 0.0625 = 0.8125

因此1110.1101这个二进制小数转换为十进制便是14.8125。

小数点之后的二进制并不能表示所有的十进制数,换言之有一些十进制数是无法转换成二进制的,因为在二进制表示的十进制是不连续的。

十进制中的0.1,在二进制中是一个接近十进制0.1的二进制浮点数。这是因为无论小数点之后有多少位二进制的数,2的负数次幂都无法相加得到0.1这个结果,因此0.1这个十进制数在二进制中会变成一个无限小数。

精度和准确度

我们知道1/3如果换算成小数0.3333….是无穷尽的,那么它在五位精度的情况下可以写成 0.3333,而在七位的情况下就又变成了0.333333。

举一个简单的例子,如果在计算中我们使用的是一位精度。那么整个计算可能就变成了下面的这种情况:

  1. 0.5 * 0.5 + 0.5 * 0.5
  2. = 0.25 + 0.25
  3. = 0.2 + 0.2
  4. = 0.4

而如果我们使用的是两位精度,那么计算过程又会变成下面的情况。

  1. 0.5 * 0.5 + 0.5*0.5
  2. = 0.25 + 0.25
  3. = 0.5

BCD编码

亦称二进码十进数或二-十进制代码。用4位二进制数来表示1位十进制数中的0~9这10个数。一般用于高精度计算。比如会计制度经常需要对很长的数字串作准确的计算。相对于一般的浮点式记数法,采用BCD码,既可保存数值的精确度,又可免去使电脑作浮点运算时所耗费的时间。

浮点数字面量

浮点数的字面量在编译器眼里被视为 double 类型,但是可以在浮点数后面加一个字母 f 表示这个字面量是以 float 类型存储的。

浮点数的运算

在我们常用的电脑上的x86或者x64架构上,单个浮点数变量或者两个浮点数之间的运算是在一个双精度的浮点数栈上进行的,因此单精度浮点数在运算前会先被转换成双精度浮点数。

“计算” 本身并不是影响速度的最大原因。假设需要的数据已经在寄存器中了,那么由于现代CPU的微观并行化,跑一条 float 指令和跑两条 double 的 SSE 指令几乎没有太大速度区别。真正的问题在于内存吞吐量。float数据的读写显然的比 double 数据的读写省去了一半的数据量。 在CPU速度和内存速度不成比例的今天,这个差距在数据量大时非常明显。

浮点数转整型

当浮点数和整型的值互相转换时,计算机都会重新计算位模式,因为它们的表达完全不一样,但是如果用指针的方式来进行转换,那么位模式不会进行计算,而是会直接进行内存复制,如下代码

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int a = 36;
  5. float b = *(float*)&a;
  6. printf("%e",b);
  7. return 0;
  8. }
  1. double a = 0.001 + 0.002;
  2. int b = (int)(a * 1000); // 3

这样做可以避免浮点数运算中的精度问题,但是会造成超过 3 位数之后的数都会被舍去,所以我们可以乘以 10 的 7 次方,正好把 float 类型不准的精度去掉。

0.1,0.3用二进制表示是循环小数,浮点数精度有限,最后会做舍入处理,所以偶尔会大一点,偶尔会小一点,乘以10之后也就比相应整数大一点或者小一点。(0.3 != 0.2 + 0.1)

C 里浮点数转整形是向0取整,于是比3小一点点也会转成2。

用浮点数表示整数

JavaScript 的 Number 类型是遵循 IEEE 754 规范表示的,这就意味着 JavaScript 能精确表示的数字是有限的,所以 js 会有大数相加的问题(表示上限2的53次方)。

IEEE 754中的浮点数是对实数的近似表示,其取值密度在越远离0的地方越稀疏,int (float i) 会有精度缺失。

https://airguanz.github.io/2019/11/20/float-to-int-exact-range.html