第1章 计算机基础
大道至简,盘古生其中。计算机的绚丽世界一切都是由0 与1 组成的。
追根究底的习惯是深度分析和解决问题、提升程序员素质的关键所在,有助于编写高质量的代码。基础知识的深度认知决定着知识上层建筑的延展性。试问,对于如下的基础知识,你的认知是否足够清晰呢?
-
- 位移运算可以快速地实现乘除运算,那位移时要注意什么?
- 浮点数的存储与计算为什么总会产生微小的误差?
- 乱码产生的根源是什么?
- 代码执行时,CPU是如何与内存配合完成程序使命的?
- 网络连接资源耗尽的问题本质是什么?
- 黑客攻击的通常套路是什么?如何有效地防止?
本章从编程的角度深度探讨计算机组成原理、计算机网络、信息安全等相关内容,与具体编程语言无关。本章并不会讨论内部硬件的工作原理、网络世界的协议和底层传输方式、安全领域的攻防类型等内容。
1.1 走进0 与1 的世界
简单地说,计算机就是晶体管、电路板组装起来的电子设备,无论是图形图像的渲染、网络远程共享,还是大数据计算,归根结底都是0 与1 的信号处理。信息存储和逻辑计算的元数据,只能是0 与1,但是它们在不同介质里的物理表现方式却是不一样的,如二极管的断电与通电、CPU 的低电平与高电平、磁盘的电荷左右方向。明确了0 与1 的物理表现方式后,设定基数为2,进位规则是“逢二进一”,借位规则是“借一当二”,所以称为二进制。那么如何表示日常生活中的十进制数值呢?二进制数位从右往左,每一位都是乘以2,如下示例为二进制数与十进制数的对应关系,阴影部分的数字为二进制数:
1=1,10=2,100=4,1000=8,11000=24,即20=1;21=2;22=4;23=8;24+23=24
设想有8 条电路,每条电路有低电平和高电平两种状态。根据数学排列组合,有8 个2 相乘,即28,能够表示256 种不同的信号。假设表示区间为0 ~ 255,最大数即为28-1,那么32 条电路能够表示的最大数为(232-1)=4,294,967,295。平时所说的32 位机器,就能够同时处理字长为32 位的电路信号。
如何表示负数呢?上面的8 条电路,最左侧的一条表示正负,0 表示正数,1 表示负数,不参与数值表示,其余的7 条电路表示实际数值。在二进制世界中,表示数的基本编码方式有原码、反码和补码三种。
原码:符号位和数字实际值的结合。正数数值部分是数值本身,符号位为0;负数数值部分是数值本身,符号位为1。8 位二进制数的表示范围是[-127,127]。
反码:正数数值部分是数值本身,符号位为0;负数的数值部分是在正数表示的基础上对各个位取反,符号位为1。8 位二进制数的表示范围是[-127,127]。
补码:正数数值部分是数值本身,符号位为0;负数的数值部分是在正数表示的基础上对各个位取反后加1,符号位为1。8 位二进制数的表示范围是[-128,127]。
三种编码方式对比如表1-1 所示。
表1-1 三种编码方式对比
既然原码的编码方式是最符合人类认知的,那为什么还会有反码和补码的表达方式呢?因为计算机的运作方式与人类的思维模式是不同的。为了加速计算机对加减乘除的运算速度,减少额外的识别成本,反码和补码应运而生。以减法计算为例,减去一个数等于加上这个数的负数,例如1-2=1+(-2)=-1。在计算机中延续这种计算思维,不需要额外做符号位的识别,使用原码计算的结果为1-2=1+(-2)=[00000001] 原+[1000 0010] 原=[1000 0011] 原= -3,这个结果显然是不正确的。为了解决这一问题,出现了反码的编码方式。使用反码计算,结果为1-2=1+(-2)= [0000 0001] 反+ [1111 1101] 反=[1111 1110] 反= -1,结果正确。但是在某些特殊情况下,使用反码存在认知方面的问题,例如2-2=2+(-2)=[0000 0010]反+[1111 1101]反=[1111 1111]反=-0,结果出现了-0,但实际上0 不存在+ 0 和-0 两种表达方式,它们对应的都是0。随着数字的编码表示的发展,补码诞生了,它解决了反码中+ 0 和-0 的问题。例如2-2=2+(-2)=[0000 0010] 补+ [1111 1110] 补=[0000 0000] 补= 0。补码的出现除解决运算的问题外,还带来一个额外的好处,即在占用相同位数的条件下,补码的表达区间比前两种编码的表达区间更大。例如,8 位二进制编码中,补码表示的范围增大到-128,其对应的补码为[1000 0000] 补。8 条电路的最大值为01111111 即127,表示范围因有正负之分而改变为-128 ~ 127,二进制整数最终都是以补码形式出现的。正数的补码与原码、反码是一样的,而负数的补码是反码加1 的结果。这样使减法运算可以使用加法器实现,符号位也参与运算。比如35 + (-35) 如图1-1(a)所示,35-37 如图1-1(b)所示。
图1-1 负数运算
加减法是高频运算,使用同一个运算器,可以减少中间变量存储的开销,这样也降低了CPU 内部的设计复杂度,使内部结构更加精简,计算更加高效,无论对于指令、寄存器,还是运算器都会减轻很大的负担。
如图1-1(c)所示,计算结果需要9 条电路来表示,用8 条电路来表达这个计算结果即溢出,即在数值运算过程中,超出规定的表示范围。一旦溢出,计算结果就是错误的。在各种编程语言中,均规定了不同数字类型的表示范围,有相应的最大值和最小值。
以上示例中的一条电路线在计算机中被称为1 位,即1 个bit,简写为B,中文翻译为字节。8 个bit 组成一个单位,称为一个字节,即1 个Byte,简写为B。1024个Byte,简写为KB;1024 个KB,简写为MB;1024 个MB,简写为GB,这些都是计算机中常用的存储计量单位。
除二进制的加减法外,还有一种大家既陌生又熟悉的操作:位移运算。陌生是指不易理解且不常用,熟悉是指“别人家的开发工程师”在代码中经常使用这种方式进行高低位的截取、哈希计算,甚至运用在乘除法运算中。向右移动1 位近似表示除以2(如表1-2 所示),十进制的奇数转化为二进制数后,在向右移时,最右边的1 将被直接抹去,说明向右移对于奇数并非完全相当于除以2。在左移<< 与右移>> 两种运算中,符号位均参与移动,除负数往右移动,高位补1 之外,其他情况均在空位处补0,红色是原有数据的符号位,绿色仅是标记,便于识别移动方向。
表1-2 带符号位移运算
左移运算由于符号位参与向左移动,在移动后的结果中,最左位可能是1 或者0,即正数向左移动的结果可能是正,也可能是负;负数向左移动的结果同样可能是正,也可能是负。所有结果均假想为单字节机器,而在实际程序运用中并非如此。
对于三个大于号的>>> 无符号向右移动(注意不存在<<< 无符号向左移动的运算方式),当向右移动时,正负数高位均补0,正数不断向右移动的最小值是0,而负数不断向右移动的最小值是1。无符号意即藐视符号位,符号位失去特权,必须像其他平常的数字位一起向右移动,高位直接补0,根本不关心是正数还是负数。此运算常用在高位转低位的场景中,如表1-3 所示分别表示向右移动1~3 位的结果,左侧空位均补0。
表1-3 无符号位移运算
为何负数不断地无符号向右移动的最小值是1 呢?在实际编程中,位移运算仅作用于整型(32 位)和长整型(64 位)数上,假如在整型数上移动的位数是字长的整数倍,无论是否带符号位以及移动方向,均为本身。因为移动的位数是一个mod 32 的结果,即35>>1 与35>>33 是一样的结果。如果是长整型,mod 64,即35<<1 与35<<65 的结果是一样的。负数在无符号往右移动63 位时,除最右边为1 外,左边均为0,达到最小值1,如果>>>64,则为其原数值本身。
位运算的其他操作比较好理解,包括按位取反(符号为~)、按位与(符号为&)、按位或(符号为|)、按位异或(符号为^)等运算。其中,按位与(&)运算典型的场景是获取网段值,IP 地址与掩码255.255.255.0 进行按位与运算得到高24 位,即为当前IP 的网段。按位运算的左右两边都是整型数,true&false 这样的方式也是合法的,因为boolean 底层表示也是0 与1。
按位与和逻辑与(符号为&&)运算都可以作用于条件表达式,但是后者有短路功能,表达如下所示:
boolean a = true;
boolean b = true;
boolean c = (a=(1==2)) && (b=(1==2));
因为&& 前边的条件表达式,即如上的红色代码部分的结果为false,触发短路,直接退出,最后a 的值为false,b 的值为true。假如把 && 修改为按位与&,则执行的结果为a 与b 都是false。
同样的逻辑,按位或对应的逻辑或运算(符号为||)也具有短路功能,当逻辑或|| 之前的条件表达式,即如下的红色代码部分的结果为true 时,直接退出:
boolean e = false;
boolean f = false;
boolean g = (e=(1==1)) || (f=(1==1));
最后e 的值为true,f 的值为false。假如把|| 修改为按位或符号|,执行的结果为e 与f 都是true。
逻辑或、逻辑与运算只能对布尔类型的条件表达式进行运算,7&&8 这种运算表达式是错误的。
异或运算没有短路功能,符号在键盘的数字6 上方,在哈希算法中用于离散哈希值,对应的位上不一样才是1,一样的都是0。比如,1^1=0 / 0^0=0 / 1^0=1 / true^true=false / true^false=true。
基于0 与1 的信号处理为我们带来了缤纷多彩的计算机世界,随着基础材料和信号处理技术的发展,未来计算机能够处理的基础信号将不仅仅是二进制信息。比如,三进制(高电平、低电平、断电),甚至十进制信息,届时计算机世界又会迎来一次全新的变革。
1.2 浮点数
计算机定义了两种小数,分别为定点数和浮点数。其中,定点数的小数点位置是固定的,在确定字长的系统中一旦指定小数点的位置后,它的整数部分和小数部分也随之确定。二者之间独立表示,互不干扰。由于小数点位置是固定的,所以定点数能够表示的范围非常有限。考虑到定点数相对简单,本节不再展开。下面重点介绍应用更广、更加复杂的浮点数。它是采用科学计数法来表示的,由符号位、有效数字、指数三部分组成。使用浮点数存储和计算的场景无处不在,若使用不当则容易造成计算值与理论值不一致,如下示例代码:
float a = 1f;
float b = 0.9f;
// 结果为:0.100000024
float f = a - b;
执行结果显示计算结果与预期存在明显的误差,本节将通过深入剖析造成这个误差的原因来介绍浮点数的构成与计算原理。由于浮点数是以科学计数法来表示的,所以我们先从科学计数法讲起。
1.2.1 科学计数法
浮点数在计算机中用以近似表示任意某个实数。在数学中,采用科学计数法来近似表示一个极大或极小且位数较多的数。如a × 10n,其中a 满足1 ≤ |a | < 10,10n 是以10 为底数,n 为指数的幂运算表达式。a × 10n 还可以表示成aen,如图1-2(a)中计算器的结果所示。 - 4.86e11 等价于 -4.86 × 1011,它们都表示真实值-486000000000,具体格式说明如图1-2(b)所示。
图1-2 科学计数法
科学计数法的有效数字为从第1 个非零数字开始的全部数字,指数决定小数点的位置,符号表示该数的正与负。值得注意的是,十进制科学计数法要求有效数字的整数部分必须在[1, 9] 区间内,即图1-2(b)中的“4”,满足这个要求的表示形式被称为“规格化”。科学计数法可以唯一地表示任何一个数,且所占用的存储空间会更少,计算机就是利用这一特性表示极大或极小的数值。例如,长整型能表示的最大值约为922 亿亿,想要表示更大量级的数值,必须使用浮点数才可以做到。
1.2.2 浮点数表示
浮点数表示就是如何用二进制数表示符号、指数和有效数字。当前业界流行的浮点数标准是IEEE754,该标准规定了4 种浮点数类型:单精度、双精度、延伸单精度、延伸双精度。前两种类型是最常用的,它们的取值范围如表1-4 所示。
表1-4 单精度和双精度
因为浮点数无法表示零值,所以取值范围分为两个区间:正数区间和负数区间。下面将着重分析单精度浮点数,而双精度浮点数与其相比只是位数不同而已,完全可以触类旁通,本节不再展开。以单精度类型为例,它被分配了4 个字节,总共32 位,具体格式如图1-3 所示。
图1-3 单精度浮点数格式
从数学世界的科学计数法映射到计算机世界的浮点数时,数制从十进制改为二进制,还要考虑内存硬件设备的实现方式。在规格化表示上存在差异,称谓有所改变,指数称为“阶码”,有效数字称为“尾数”,所以用于存储符号、阶码、尾数的二进制位分别称为符号位、阶码位、尾数位,下面详细阐述三个部分的编码格式。
1. 符号位
在最高二进制位上分配1 位表示浮点数的符号,0 表示正数,1 表示负数。
2. 阶码位
在符号位右侧分配8 位用来存储指数,IEEE754 标准规定阶码位存储的是指数对应的移码,而不是指数的原码或补码。根据计算机组成原理中对移码的定义可知,移码是将一个真值在数轴上正向平移一个偏移量之后得到的,即[x ] 移 = x + 2n -1 (n 为x 的二进制位数,含符号位)。移码的几何意义是把真值映射到一个正数域,其特点是可以直观地反映两个真值的大小,即移码大的真值也大。基于这个特点,对计算机来说用移码比较两个真值的大小非常简单,只要高位对齐后逐个比较即可,不用考虑负号的问题,这也是阶码会采用移码表示的原因所在。
由于阶码实际存储的是指数的移码,所以指数与阶码之间的换算关系就是指数与它的移码之间的换算关系。假设指数的真值为e ,阶码为E ,则有E = e + (2n -1 -1),其中2n -1 -1 是IEEE754 标准规定的偏移量,n =8 是阶码的二进制位数。
为什么偏移值为2n -1 -1 而不是2n -1 呢?因为8 个二进制位能表示指数的取值范围为[-128,127],现在将指数变成移码表示,即将区间[-128,127] 正向平移到正数域,区间里的每个数都需要加上128,从而得到阶码范围为[0,255]。由于计算机规定阶码全为0 或全为1 两种情况被当作特殊值处理(全0 被认为是机器零,全1 被认为是无穷大),去除这两个特殊值,阶码的取值范围变成了[1,254]。如果偏移量不变仍为128 的话,则根据换算关系公式[x ] 阶= x + 128 得到指数的范围变成[ -127,126],指数最大只能取到126,显然会缩小浮点数能表示的取值范围。所以IEEE754 标准规定单精度的阶码偏移量为2n - 1 -1(即127),这样能表示的指数范围为[ -126,127],指数最大值能取到127。
3. 尾数位
最右侧分配连续的23 位用来存储有效数字,IEEE754 标准规定尾数以原码表示。正指数和有效数字的最大值决定了32 位存储空间能够表示浮点数的十进制最大值。指数最大值为2127 ≈ 1.7 × 1038,而有效数字部分最大值是二进制的1.11…1(小数点后23 个1),是一个无限接近于2 的数字,所以得到最大的十进制数为2 × 1.7 × 1038,再加上最左1 位的符号,最终得到32 位浮点数最大值为3.4e+38。为了方便阅读,从右向左每4 位用短横线断开:
0111-1111-0111-1111-1111-1111-1111-1111
-
- 红色部分为符号位,值为0,表示正数。
- 绿色部分为阶码位即指数,值为2254 - 127 = 2127 ≈ 1.7 × 1038。
- 黄色部分为尾数位即有效数字,值为1.11111111111111111111111。
科学计数法进行规格化的目的是保证浮点数表示的唯一性。如同十进制规格化的要求1 ≤ |a | < 10,二进制数值规格化后的尾数形式为1.xyz,满足1 ≤ |a | < 2。为了节约存储空间,将符合规格化尾数的首个1 省略,所以尾数表面上是23 位,却表示了24 位二进制数,如图1-4 所示。
图1-4 尾数的规格化表示
常用浮点数的规格化表示如表1-5 所示。
表1-5 浮点数的规格化表示
注:①尾数部分的有效数字为1.00000101100110011001101,将其转换成十进制值为1.021875,然后乘以24 得到16.35000038。由此可见,计算机实际存储的值可能与真值是不一样的。补充说明,二进制小数转化为十进制小数,小数点后一位是2-1,依次累加即可,如 1.00000101 = 1+2-6+2-8= 1.01953125。
② 0.9 不能用有限二进制位进行精确表示,所以1-0.9 并不精确地等于0.1,实际结果是0.100000024,具体原因后面进行分析。
1.2.3 加减运算
在数学中,进行两个小数的加减运算时,首先要将小数点对齐,然后同位数进行加减运算。对两个采用科学计数法表示的数做加减法运算时,为了让小数点对齐就需要确保指数一样。当小数点对齐后,再将有效数字按照正常的数进行加减运算。
(1)零值检测。检查参加运算的两个数中是否存在为0 的数(0 在浮点数是一种规定,即阶码与尾数全为0),因为浮点数运算过程比较复杂,如果其中一个数为0,可以直接得出结果。
(2)对阶操作。通过比较阶码的大小判断小数点位置是否对齐。当阶码不相等时表示当前两个浮点数的小数点位置没有对齐,则需要通过移动尾数改变阶码的大小,使二者最终相等,这个过程便称为对阶。尾数向右移动1 位,则阶码值加1,反之减1。在移动尾数时,部分二进制位将会被移出,但向左移会使高位被移出,对结果造成的误差更大。所以,IEEE754 规定对阶的移动方向为向右移动,即选择阶码小的数进行操作。
(3)尾数求和。当对阶完成后,直接按位相加即可完成求和(如果是负数则需要先转换成补码再进行运算)。这个道理与十进制数加法相同,例如9.8 × 1038 与6.5 × 1037 进行求和,将指数小的进行升阶,即6.5 × 1037 变成0.65 × 1038,然后求和得到结果为10.45 × 1038。
(4)结果规格化。如果运算的结果仍然满足规格化形式,则无须处理,否则需要通过尾数位的向左或右移动调整达到规格化形式。尾数位向右移动称为右规,反之称为左规。如上面计算结果为10.45 × 1038,右规操作后为1.045 × 1039。
(5)结果舍入。在对阶过程或右规时,尾数需要右移,最右端被移出的位会被丢弃,从而导致结果精度的损失。为了减少这种精度的损失,先将移出的这部分数据保存起来,称为保护位,等到规格化后再根据保护位进行舍入处理。
了解了浮点数的加减运算过程后可以发现,阶码在加减运算过程中只是用来比较大小,从而决定是否需要进行对阶操作。所以,IEEE754 标准针对这一特性,将阶码采用移码表示,目的就是利用移码的特点来简化两个数的比较操作。下面针对前面例子从对阶、按位减法的角度分析为什么1.0-0.9 结果为0.100000024,而不是理论值0.1。1.0-0.9 等价于1.0 + (-0.9),首先分析1.0 与-0.9 的二进制编码:
1.0 的二进制为 0011-1111-1000-0000-0000-0000-0000-0000
-0.9 的二进制为 1011-1111-0110-0110-0110-0110-0110-0110
从上可以得出二者的符号、阶码、尾数三部分数据,如表1-6 所示。
表1-6 符号、阶码与尾数
由于尾数位的最左端存在一个隐藏位,所以实际尾数二进制分别为:1000-0000-0000-0000-0000-0000 和1110-0110-0110-0110-0110-0110,红色为隐藏位。下面运算都是基于实际的尾数位进行的,具体过程如下:
(1)对阶。1.0 的阶码为127,-0.9 的阶码为126,比较阶码大小时需要向右移动-0.9 尾数的补码,使其阶码变为127,同时高位需要补1,移动后的结果为1000-1100-1100-1100-1100-1101,最左的 1 是图1-4 介绍的隐藏灰色的1 补进的。注意,绿色的数字仅仅是为了方便阅读,更加清晰观察到数字位的对齐或整体移动方向。
(2)尾数求和。因为尾数都转换成补码,所以可以直接按位相加,注意符号位也要参与运算,如图1-5 所示。
图1-5 尾数求和示意
其中最左端为符号位,计算结果为0,尾数位计算结果为0000-1100-1100-1100-1100-1101。
(3)规格化。上一步计算的结果并不符合要求,尾数的最高位必须是1,所以需要将结果向左移动4 位,同时阶码需要减4。移动后阶码等于123(二进制为1111011),尾数为1100-1100-1100-1100-1101-0000。再隐藏尾数的最高位,进而变为100-1100-1100-1100-1101-0000,最右边的4 个0 是左移4 位后补上的。
综上所述,得出运算后结果的符号为0、阶码为1111011、尾数为100-1100-1100-1100-1101-0000,三部分组合起来就是 1.0-0.9 的结果,对应的十进制值0.100000024。至此,在本节开始处的减法悬案真相大白。
但是,浮点数的悬案并不止于此。如下示例代码为三种判断浮点数是否相等比较的方式,请大家思考运行结果是什么?
float g = 1.0f - 0.9f;
float h = 0.9f - 0.8f;
// 第一种,判断浮点数是否相等的方式
if (g == h) {
System.out.println("true");
} else {
System.out.println("false");
}
// 第二种,判断浮点数是否相等的方式
Float x = Float.valueOf(g);
Float y = Float.valueOf(h);
if (x.equals(y)) {
System.out.println("true");
} else {
System.out.println("false");
}
// 第三种,判断浮点数是否相等的方式
Float m = new Float(g);
Float n = new Float(h);
if (m.equals(n)) {
System.out.println("true");
} else {
System.out.println("false");
}
相信以上代码的运行结果会让人大跌眼镜, 输出结果为3 个false !1.0f-0.9f 与0.9f-0.8f 的结果理应都为0.1,但实际是不相等的。上面已经分析出1.0f-0.9f=0.100000024,那么0.9f-0.8f 的结果为多少呢? 0.9-0.8 等价于 0.9+(-0.8),首先分析 0.9 与-0.8 的二进制编码:
0.9 的二进制编码为 0011-1111-0110-0110-0110-0110-0110-0110
-0.8 的二进制编码为1011-1111-0100-1100-1100-1100-1100-1101
从上可以得出二者的符号、阶码、尾数三部分数据,如表1-7 所示。
表1-7 0.9 与-0.8 的符号、阶码与尾数
由于尾数位的最左端存在一个隐藏位,所以实际尾数二进制分别为: 1110-0110-0110-0110-0110-0110 和1100-1100-1100-1100-1100-1101,红色为隐藏位。下面运算都是基于实际的尾数位进行的,具体过程如下:
(1)对阶。0.9 和-0.8 的阶码都为 126,不需要进行移阶运算。
(2)尾数求和。因为尾数都转换成补码,所以可以直接按位相加,注意符号位也要参与运算,如图1-6 所示。
图1-6 尾数求和
其中最左端为符号位,计算结果为 0,尾数位计算结果为 0001-1001-1001-1001-1001-1001。
(3)规格化。上一步计算的结果并不符合要求,尾数的最高位必须是1,所以需要将结果向左移动 3 位,同时阶码需要减3。移动后阶码等于 123(二进制为1111011),尾数为 1100-1100-1100-1100-1100-1000。再隐藏尾数的最高位,进而变为100-1100-1100-1100-1100-1000。
综上所述,得出运算后结果的符号为 0、阶码为 1111011、尾数为 100-1100-1100-1100-1100-1000,三部分组合起来就是0.9f-0.8f 的结果,对应的十进制数值为0.099999964。 至此,又揭秘了一个悬案1.0f-0.9f 的结果与0.9f-0.8f 的结果不相等。因此在浮点数比较时正确的写法:
float g = 1.0f - 0.9f;
float h = 0.9f - 0.8f;
double diff = 1e-6;
if (Math.abs(g - h) < diff) {
System.out.println("true");
} else {
System.out.println("false");
}
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
if (x.compareTo(y) == 0) {
System.out.println("true");
} else {
System.out.println("false");
}
1.2.4 浮点数使用
在使用浮点数时推荐使用双精度,使用单精度由于表示区间的限制,计算结果会出现微小的误差,实例代码如下所示:
float ff = 0.9f;
double dd = 0.9d;
// 0.8999999761581421
System.out.println(ff/1.0);
// 0.9
System.out.println(dd/1.0);
在要求绝对精确表示的业务场景下,比如金融行业的货币表示,推荐使用整型存储其最小单位的值,展示时可以转换成该货币的常用单位,比如人民币使用分存储,美元使用美分存储。在要求精确表示小数点n 位的业务场景下,比如圆周率要求存储小数点后1000 位数字,使用单精度和双精度浮点数类型保存是难以做到的,这时推荐采用数组保存小数部分的数据。在比较浮点数时,由于存在误差,往往会出现意料之外的结果,所以禁止通过判断两个浮点数是否相等来控制某些业务流程。在数据库中保存小数时,推荐使用decimal 类型,禁止使用float 类型和double 类型。因为这两种类型在存储的时候,存在精度损失的问题。
综上所述,在要求绝对精度表示的业务场景中,在小数保存、计算、转型过程中都需要谨慎对待。