为什么只用 0 和 1
为什么只用 0 和 1
苏联人曾经造出过一台三进制计算机。1958 年,莫斯科国立大学的尼古拉·布鲁斯特尼茨夫(Nikolay Brusentsov)主持开发了一台名叫"Setun"的计算机,用的不是二进制的 0 和 1,而是三进制的 -1、0、1。这台机器运行良好,效率甚至在某些方面比同期的二进制计算机还高。
那为什么今天,地球上所有的电脑都在用 0 和 1?
这个问题比你想象的要深刻。答案不仅仅是"工程方便",背后有数学、物理、信息论多个维度的原因——以及一些历史的偶然。
Level 1:建立直觉
噪声是数字计算的天敌
想象你在打电话,对方说了个数字。如果对方说的是"一"或者"八",你很容易听错——这两个字发音相近,加上噪音可能分不清。
但如果对方只说"有"和"没有",清晰度就大不相同了。两个极端状态之间的区别,比多个中间状态之间的区别,要容易识别得多。
电路里也是同理。
任何电路都有噪声——来自温度、电磁干扰、供电波动。如果你用 10 个不同的电压等级来代表 10 个数字(比如 0V 代表 0,0.5V 代表 1,1V 代表 2,……4.5V 代表 9),那相邻等级之间只有 0.5V 的差距。这么小的间隔,噪声很容易让你的 3 误读成 2 或者 4。
而二进制只用两个状态:低电压(0)和高电压(1)。电路设计师可以把阈值设在中间,比如以 2.5V 为界,低于 2.5V 一律算 0,高于 2.5V 一律算 1。这样就有了 2.5V 的"错误容忍带"——噪声要超过这个量才会造成误判。
状态越少,每个状态的"领地"越大,抗噪声能力越强。
从"是否"到"如何多少"
一个开关给你一个比特——只能表示"是"或"否"。
但人类要处理的信息远不止是非题。我们需要数量、顺序、颜色、声音。
好消息是:任何信息都可以用"是/否"问题的组合来唯一确定。
这就是信息论的核心直觉。克劳德·香农(Claude Shannon,不是后面的 AI 公司,但同名挺有意思)在 1948 年发表的论文《通信的数学理论》证明了这一点,开创了信息论。
举个例子:猜一个 1 到 100 之间的数字,最优策略是每次都把剩余范围对半切:
- 大于 50 吗?
- 大于 75 吗?
- 大于 62 吗?
- ……
每个问题都是一个比特(是/否)。用 7 个是非问题,就可以唯一确定 1 到 128 之间的任何数(2⁷ = 128)。用 10 个问题,可以确定 1 到 1024 之间的任何数(2¹⁰ = 1024)。
任何 N 个可能的值,只需要 log₂(N) 个比特就能唯一表示。 这是整个二进制世界的数学基础。
0 和 1 的哲学:离散与连续
自然界大多数东西是连续的:温度、颜色、声音的响度……这些量在两个值之间可以有无穷多的中间值。
计算机选择用离散的比特来表示这些连续的量,这是一种近似——但这种近似非常强大。
用多少位来近似,决定了精度:
- 8位颜色通道(0-255):可以表示 256 种亮度等级
- 16位音频采样:可以区分 65,536 个音量等级
- 32位浮点数:可以以大约 7 位十进制数的精度表示实数
精度不够?加更多位。这是数字系统的一个优雅之处:精度是可以用硬件换来的,而不是物理上固定的限制。
Level 2:原理剖析
香农信息论:比特的数学定义
香农定义了信息量的度量方式:一个事件携带的信息量,等于让你"不再惊讶"所需的问题数。
更精确地说,一个概率为 p 的事件,携带的信息量是:
I = -log₂(p) 比特
比如:
- 抛一枚公平硬币,结果是正面(概率 0.5):-log₂(0.5) = 1 比特
- 掷一个公平骰子,结果是 3(概率 1/6):-log₂(1/6) ≈ 2.58 比特
- 明天太阳从东方升起(概率约 1):≈ 0 比特(没什么信息)
一段文字、一张图片、一段视频携带多少信息量?香农给了我们精确计算的工具。
这也解释了为什么压缩算法能把文件变小:文件里有大量冗余信息(比如英文中字母 'e' 出现频率远高于 'z'),压缩算法去掉这些冗余,用更少的比特表示同样的信息。
为什么不用三进制?
回到开头的问题。三进制在某些数学上确实有优势——比如,同样位数的数字,三进制可以表示更多的值(3 进制的 n 位 = 3ⁿ 个值,而二进制的 n 位 = 2ⁿ 个值,3ⁿ > 2ⁿ)。
苏联的 Setun 就利用了这一点,它的某些计算效率的确优于同期的二进制机器。
那为什么三进制没有流行起来?
原因一:物理实现太难
制造一个稳定的两态开关已经很困难了,三态更难:你需要三个不同的电压等级都能稳定保持,中间的"1"状态最容易受到噪声干扰向 0 或 2 偏移。
原因二:逻辑门复杂了三倍
二进制有简洁的逻辑门(AND/OR/NOT),三进制对应的逻辑运算要复杂得多。设计和制造这些电路的成本指数级上升。
原因三:历史惯性
1947 年晶体管被发明,早期电路天然地走向了二进制(高电压/低电压)。这条路越走越成熟,三进制就越来越难追上了。
不过,有意思的是:量子计算的"量子比特"(qubit)在某种意义上确实可以处于 0、1 以及两者叠加的状态——只是使用规则完全不同于经典三进制。
二进制算术:比你想象的简单
在二进制里进行加法,规则只有四条:
0 + 0 = 0 (无进位)
0 + 1 = 1 (无进位)
1 + 0 = 1 (无进位)
1 + 1 = 0,进位 1 (10 in binary)
来算一个例子:5 + 3 = ?
0101 (5的二进制)
+ 0011 (3的二进制)
------
1000 (8的二进制)
从最低位开始:
- 位0:1+1 = 10(写0,进位1)
- 位1:0+1+进位1 = 10(写0,进位1)
- 位2:1+0+进位1 = 10(写0,进位1)
- 位3:0+0+进位1 = 01(写1,无进位)
结果:1000 = 8。正确!
这四条规则就是加法器电路的全部——下一章我们会看到怎么用逻辑门实现这四条规则。
负数:补码的妙处
在十进制里,负数就是在数字前加个"-"号。但在计算机里,比特位没有地方放"负号"这个特殊符号,怎么表示负数?
解决方案叫做二补数(Two's Complement),非常聪明。
对于一个 n 位二进制数,负数 -k 被表示为 2ⁿ - k。
具体来说:对一个二进制数按位取反,然后加 1,就得到它的负数表示。
例子:用 8 位表示 -5
5 的二进制: 00000101
按位取反: 11111010
加 1: 11111011 ← 这就是 -5 的补码表示
验证:5 + (-5) 应该等于 0
00000101 (5)
+ 11111011 (-5)
----------
100000000 (9位,舍掉最高的溢出位)
= 00000000 (0)✓
二补数的精妙之处在于:加法电路完全不需要改动,就能正确处理有符号和无符号整数。 CPU 不需要知道它在做带符号运算还是无符号运算,同一套电路两种情况都 work。
浮点数:在有限比特里表示实数
整数好处理,但物理世界中充满了小数:3.14,0.001,1.234e10……
计算机用浮点数来表示这些实数。IEEE 754 标准(1985年定义,今天仍在用)规定了 32 位浮点数的格式:
1位符号 8位指数 23位尾数
┌───┬─────────┬───────────────────────┐
│ S │ EEEEEEEE│ MMMMMMMMMMMMMMMMMMMMMMM│
└───┴─────────┴───────────────────────┘
表示的值 = (-1)^S × 1.M × 2^(E-127)
例如,数字 0.75 在 32 位浮点数里是:
- 符号:0(正数)
- 0.75 = 0.11(二进制)= 1.1 × 2⁻¹
- 指数:-1 + 127 = 126 = 01111110
- 尾数:10000000000000000000000
完整表示:0 01111110 10000000000000000000000
这种设计有个重要特点:不是所有小数都能精确表示。比如 0.1 在二进制里是无限循环小数(就像十进制里的 1/3 = 0.333…),只能近似。
# Python里试试这个
>>> 0.1 + 0.2
0.30000000000000004 # 不是 0.3!
这不是 Python 的 bug,这是二进制浮点数的本质——它只能精确表示分母是 2 的幂次方的分数(1/2, 1/4, 1/8…),其他分数都是近似值。
Level 3 · 规范怎么定义的(资深)
IEEE 754:浮点数的"宪法"
二进制数字系统中最重要的工业标准是 IEEE 754(IEEE Standard for Floating-Point Arithmetic)。该标准于 1985 年首次发布,2008 年和 2019 年分别修订。它精确定义了 binary16(半精度)、binary32(单精度)、binary64(双精度)和 binary128(四精度)四种浮点格式,以及运算的舍入规则、特殊值(±∞、NaN、±0)的行为。
IEEE 754 要求所有基本运算(加减乘除和平方根)的结果必须是正确舍入的——即结果等同于先用无穷精度计算,再舍入到目标格式。这个看似简单的要求极大地简化了数值分析:程序员可以依赖确定性的误差界限,而不是面对各平台的随机差异。William Kahan 因设计这套标准获得了 1989 年图灵奖。
补码的形式化定义
有符号整数的补码表示也有严格定义。对于 n 位二进制补码,值 x 的编码定义为:若 x >= 0,编码为 x 的二进制表示;若 x < 0,编码为 2^n + x。这意味着 n 位补码的范围是 [-2^(n-1), 2^(n-1) - 1],且加法运算自然兼容有符号和无符号的二进制位模式——硬件不需要区分符号,同一个加法器即可处理。C 语言标准(ISO/IEC 9899:2018,即 C17)在 C23 之前允许原码、反码和补码三种表示,但 C23(ISO/IEC 9899:2024)终于规定补码是唯一合法的有符号整数表示,结束了数十年的歧义。
Level 4 · 边界与陷阱(所有人)
陷阱 1:0.1 + 0.2 != 0.3
这是最经典的浮点陷阱。在 IEEE 754 binary64 中,0.1 的精确值是 0.1000000000000000055511151231257827021181583404541015625,0.2 也有类似的微小偏差。两者相加后,结果是 0.30000000000000004 而非 0.3。几乎所有语言(Python、JavaScript、Java、C)都有这个问题。金融系统因此必须使用定点数或 decimal 类型,否则会出现分币级别的累积误差——2012 年骑士资本(Knight Capital)因为软件 bug 在 45 分钟内亏损 4.4 亿美元,虽然主因是部署错误,但浮点精度问题在金融行业是同类"致命小数"的常见根源。
陷阱 2:有符号整数溢出是未定义行为
在 C/C++ 中,有符号整数溢出是未定义行为(Undefined Behavior),编译器可以假设它永远不会发生,并据此进行优化。例如 if (x + 1 > x) 这个条件,编译器可能直接优化为 true,因为"有符号整数不会溢出"。这会导致安全检查被静默删除。Linux 内核曾多次因此引入漏洞(CVE-2009-1385 等)。GCC 提供 -fwrapv 选项强制有符号溢出使用补码回绕语义,而 Rust 在 debug 模式下默认检测溢出并 panic。
陷阱 3:大端与小端的字节序混淆
同样的 32 位整数 0x01020304,在大端机器上内存布局是 01 02 03 04,在小端机器上是 04 03 02 01。网络协议(TCP/IP)统一使用大端("网络字节序"),而 x86/ARM 默认使用小端。如果你直接把结构体 memcpy 到网络缓冲区而不做字节序转换(htonl/ntohl),对端收到的数据就是乱的。这个 bug 在跨平台网络编程中极其常见,且因为本机测试时两端字节序相同而不易发现。