08 - 负数和小数在计算机里长什么样
第 01 章说过,整数可以直接用二进制表示。但计算机里只有 0 和 1,没有「负号」、也没有「小数点」这种符号。那 -5 和 3.14 是怎么存的?这一章讲清楚思路,你会顺便理解一个困扰无数程序员的问题:为什么 0.1 + 0.2 ≠ 0.3?
负数:用一个 bit 当符号,但没那么简单
最朴素的想法:拿最高位当「符号位」,0 表示正、1 表示负。这叫「原码」。听起来完美,但有个麻烦:这样一来 +0 和 -0 变成两个不同的编码,而且做减法时电路要额外判断符号,很别扭。
计算机实际用的是一种更聪明的方案,叫补码(Two’s Complement)。你不需要会手算补码,只要理解它为什么值得:
补码的妙处在于——用它表示负数后,减法可以直接当加法来做,加减法用同一套电路就能搞定,不用为减法单独设计电路。
举个直觉例子:时钟是「模 12」的系统。把时针从 3 点往回拨 4 小时(减 4),结果是 11 点;而往前拨 8 小时(加 8),结果也是 11 点。所以在时钟这个系统里,「减 4」和「加 8」效果一样。补码就是利用类似的原理,把「减一个数」变成「加另一个数」。
- 好处:硬件大大简化(一套加法器通吃加减法),也没有了
+0/-0的尴尬。 - 你只要记住结论:计算机里的负整数用「补码」存储,目的是让减法能复用加法电路。
小数:用「浮点数」,像科学计数法
小数更麻烦,因为「小数点」在哪是浮动的。计算机借用了科学计数法的思路。
回忆科学计数法:314.159 可以写成 3.14159 × 10²。它由三部分组成:符号、有效数字(尾数)、指数(小数点该往哪挪、挪几位)。
计算机里的**浮点数(Floating Point)**就是这个思路的二进制版,一个小数被拆成三部分存起来:
符号位 指数部分 尾数部分
(正还是负) (小数点挪多少位) (有效数字)
「浮点」这个名字就来自「小数点的位置是浮动的」。常见的 float(32 位)、double(64 位),区别就是给这三部分分配的 bit 多少不同——位数越多,能表示的范围越大、精度越高。
为什么 0.1 + 0.2 ≠ 0.3
这是浮点数最著名的坑。根本原因是:
很多十进制小数,用二进制根本无法精确表示,只能存一个「非常接近但不完全相等」的近似值。
类比一下:十进制里,1/3 = 0.33333… 除不尽,你只能写有限位的近似值 0.3333。同样地,0.1 换算成二进制是一个无限循环的小数,计算机位数有限,只能截断存一个近似值。
于是:
- 存进去的
0.1其实是0.1000000000000000055… - 存进去的
0.2也有微小误差 - 两者相加,误差累积,结果就成了
0.30000000000000004,不等于精确的0.3。
这不是哪个语言的 bug,是浮点数表示方式的固有特性,几乎所有语言都这样。
实践建议(记住即可):
- 别用
==直接比较两个浮点数是否相等,而应判断它们的差是否小于一个极小值。 - 涉及钱的计算,不要用浮点数!要么用「整数分」来算(12.34 元存成 1234 分),要么用专门的高精度十进制类型(如 Python 的
Decimal)。
小结
- 计算机里没有负号和小数点,负数和小数都得靠「约定的编码方式」表示。
- 负整数用补码:核心好处是让减法能复用加法电路,硬件更简单(类比时钟「减 4 = 加 8」)。
- 小数用浮点数:借鉴科学计数法,拆成「符号 + 指数 + 尾数」三部分,小数点位置浮动。
- 很多十进制小数(如 0.1)在二进制里无限循环,只能存近似值,所以
0.1 + 0.2 ≠ 0.3——这是固有特性。 - 实践:别用
==比浮点数;算钱要用整数或高精度十进制类型。
下一章 → 09 - 文字和图片是怎么变成 0/1 的 | 回到 README 目录