• 作者:老汪软件技巧
  • 发表时间:2024-09-30 17:02
  • 浏览量:

数字在计算机中的表示怎么表示整数?

我们知道在计算机中,所有的数据都是以二进制的形式存储的,而我们日常使用的数都是十进制,所以数据存储时我们需要进行进制转换(进制转换的方法在数学上叫做除 k 取余法,科学计数法的逆运算),比如:

我们实际编写的变量是:9,那么在计算机中存储时,需要将十进制转换为二进制,也就是:1001。

怎么表示负数?

通过进制转换我们可以存储整数了,那么计算机又如何存储负数呢?

其实这个问题也简单,我只要定一个规定,规定某一个位为符号位即可(一般规定最高位是符号位),0 表示正数,1 表示负数,那么就可以表示负数了。

比如:-9,在计算机中存储时,需要将十进制转换为二进制,也就是:1001,然后再增加一位符号放在最高位,那么也就是:11001。

到这里矛盾就开始出现了,目前我们认为 11001 是 -9,那疑问来了,15 又该如何表示?

你可能会说,这个简单,用 011001 表示即可。这样咋一看是没有什么问题,但其实存在一个很大的问题,那就是位数不固定。看过一些黑客电脑图片的朋友可能会有感受,像这种满屏的 01 代码:

所以把 -9 和 15 放在一起,那就是 11001011001,你还能分得出来吗?

所以这里需要引出一个叫做字节的概念,也就是组(单元)的概念(八位一组)。所以假如采用一个字节来存储的话,那么 -9 和 15 就应该是 1001 1001 和 0001 1001,这样再放到一起 1001 1001 0001 1001,我只要八个八个地分,自然就能区分开了。

一个字节的缺陷也很明显,最大也就只能表示 0111 1111 127,至于 128、129 等更大的数无法表示了(数据溢出)。所以我们一般采用多个字节来表示一个数,但肯定也不是越多越好,多了浪费内存。

在 C 语言中,int 使用 4 个字节,long 使用 8 个字节(在 32 位操作系统上也可能是 4 个字节)。

怎么表示小数(分数)?

前面我们已经可以存储所有的正负整数了,那么还差小数,我们就能存储所有的实数(数轴上所有的点)。

其实在计算机中想表示小数也很简单,那就是实现科学计数法。

前面我们加了一位用来表示符号,接下来我们只需要在加几位来作为指数位即可。

既然采用科学计数法,那么就是整数和小数共同组成尾数部分,再通过指数来分隔整数和小数(指数也就是记录小数点的位置)。

那么由于尾数位置有限(JavaScript 中,尾数占 52 位),也就是意味着小数存储越多,整数就存储越少,反之亦然。

我们知道十进制中 0.5 表示 05/10,那么同样的在二进制中 0.1 表示 01/10,也就是 1/2(0.5)。同样的在二进制中 1.1 表示 11/10,也就是 3/2(1.5);1.01 表示 101/100,也就是 5/4(1.25)。

其实我们使用二进制来表示小数,分母都是 2 的幂,那么如果十进制中分母不是 2 的幂,在转化过程中就会出现无限循环小数。比如:十进制的 0.1 转化成二进制就会出现循环小数 0.000110011001100110011001100110011...,而 0.875 则不会,结果为 0.111。

那么结合有些数据进制无法完全转化导致无限循环或不循环小数,我们在运算时放任小数的位数不管(不控制保留几位),会不会出问题呢?这个可能要看具体语言怎么去实现(一般来说都是保留高位舍弃低位)。

JavaScript 中数字的表示

学过 C 语言的朋友应该知道在 C 语言中,数字从大体上分有两种:整型和浮点型。

根据我们可以发现:

The Number type has exactly values(2^64 - 2^53 + 3), representing the double-precision 64-bit format IEEE 754-2019 values as specified in the IEEE Standard for Binary Floating-Point Arithmetic.

也就是说 JavaScript 中,Number 采用的是 64 位的 IEEE 754 标准来表示数字的。其实 IEEE 754 标准也就是规定了:一个数字由符号位、指数位和尾数位三部分组成(科学计数法),然后根据你的存储内存大小,给你每个部分约定不同的字节长。

我们先看一下 IEEE 754 标准中规定的关于 32 位浮点型各个位的含义:

所以在 JavaScript 中,第一位表示符号位(0 正,1 负),接下来的 11 位表示指数,剩下的 52 位表示尾数(从高位到低位)。

值得一提的是:在 JavaScript 中,如果一个数很小很小,那么 JavaScript Number 会自动采用科学计数法表示。

了解了前面的数字表示,关于 JavaScript 中的这两个问题,我们就很好理解了。

数字安全问题(数据溢出)

在 JavaScript 中,我们知道最大安全整数是 Number.MAX_SAFE_INTEGER,9xx 16 位数字。只要总数(整数+小数)不超过这么多,就不会数据溢出。如果总数超过了这么多,JavaScript 选择的方式丢弃末尾小数(也就是我们常说的浮点数运算的精度丢失的问题)。

数字运算能力__数字运算的基本内涵

为什么说是不安全的,而不是直接说是错误的?

举个例子:

const a = Number.MAX_SAFE_INTEGER + 1; // 9007199254740992
const b = Number.MAX_SAFE_INTEGER + 2; // 9007199254740992
const c = Number.MAX_SAFE_INTEGER + 3; // 9007199254740994
// ...

我们看上面的例子,可以发现 a、c 的计算结果是正确的,而 b 的计算结果是错误的。接下了我们可以来分析一下:

我们知道 Number.MAX_SAFE_INTEGER(9007199254740991) 其实就是 11...(省略50个1,共52位),那么 +1 的结果就是:100...(省略50个0,共53位),此时位数超过了尾数能够存储的最大位数(52),JavaScript 进行尾数舍弃,并且进行指数 +1,所以此时最终的计算结果没有错误。

同理,b 的计算结果错误是因为结果为 100...(省略49个0,共53位)...1,也就是末尾是 1,而由于被舍弃了,自然运算结果是 9007199254740992。而 c 自然就是指数 +2 了。

所以当超出 Number.MAX_SAFE_INTEGER, 也就是尾数位数用尽,计算有可能正确,也可能不正确,也称之为不安全。

数字精度问题

产生精度丢失问题有两种情况:

小数转化成二进制出现了循环小数

0.1 + 0.2 !== 0.3

这个问题的成因在 怎么表示小数(分数)? 中有详细描述。

尾数(整数+小数)超过了 16 位,导致了末尾小数丢失

大数 + 小数可能会导致小数丢失,比如:

这个因为当尾数总位数超过 16 位时,JavaScript 会默认丢弃末尾多出的小数。

解决方案

清楚了精度问题的产生原因,那么解决思路自然就很清晰了,其实无外乎就两种:要么消除误差,要么接受误差。

消除误差

既然我们前面已经知道误差产生的根源来自于两个因素:进制转换 和 尾数位置有限。所有消除误差的思路也很简单,一方面是采用不定长的 String 类型存储,另一方面是实现十进制的模拟运算实现。

接受误差

在一定精度范围内,可以接受。这就跟我们购买商品的时喜欢抹零是一个道理。那么虽然进制转换会导致一定的数据误差,但是只要在一点误差范围内,我们就认为这个数据是一个有效的数据。

比如 0.1 + 0.2 === 0.3,如果让这个等式成立呢?那么我就设置一个精度,如果在这个精度之外的数值,我们就认为是误差值,通过去除误差值之后再进行比较。

为什么我们平时更喜欢第二种方案?

使用 number 运算只用算一次,而自己实现十进制运算则需要算多次。

工作中实际遇到的问题,以及我的解决策略消除小数设备数据处理相关

之前做机械设备数据采集时,遇到了数据溢出问题。但是由于底层设备(PLC)他们舍弃了小数(比如使用 cm,而不是 m 来作为物料长度单位),这样不会遇到精度问题,但是很容易遇到大数问题(也就是数据溢出问题)。

对于这个问题其实也很好解决,直接使用 JavaScript 的 即可。不过需要注意的是:在运算时,需要将相关操作数全部转换成 BigInt 类型,在展示和传输时,转换成 String 类型(像一些进程通信是需要 stringify 序列化)。

金额相关

我们知道跟钱打交道,如果系统只有一种币种,那么我们使用前面的方案(舍弃小数)是可行的,比如人民币最小单位是分,我们只需要全部用分表示,系统中就没有小数。

但是只要一旦涉及多币种,涉及到汇率,小数可能就不好消除了(理论上也可以将汇率拦截在系统对外接口处,这样就可以保证我们系统内部数据的纯净;如果有多币种原始数据,那就无法消除小数了,比如一个存款机,我既要往里面存入人民币,又要往里面存美元,这时候如果我们选择强行消除小数,这会导致原始数据丢失,比如我就是无法知道他到底存了多少美元)。

不过如果整数位数太多,比如达到了 15 位,那么这时即使我们想保留 2 位小数都无法保留两位小数。

NaN 怎么产生的(numeral 库的 BUG)?

numeral 库 这个库存在的一个 BUG,当我们使用科学计数法的时候,会与该库的取实现去除多余小数的方式有冲突,该库使用转化成科学计数法来实现小数点右移,再进行取整,最后小数点左移,实现保留 xx 位小数。其实直接使用乘以 power 实现小数点右移就能解决这个 BUG,如下:

总结

实际上在运算中加入 toFixed 操作对于数据运算没有任何帮助(无论是数据安全还是精度缺失),数据该溢出还是会溢出,而对于精度,只会导致精度进一步下降。实际上,toFixed 只能用于格式化一下数据,让数据在展示的时候看起来更好看,仅此而已。

运算结果总数字 超过了16 位数字(十进制,准确来说是二进制超过了 52 位),此时就会丢末尾数字。小数丢了我们一般称作精度缺失,而整数丢了我们一般称作数据溢出。

参考