首页 › programming › 电脑里没有0.1:浮点只是二进制科学记数

电脑里没有0.1:浮点数只是二进制的科学记数法

导语:一个让所有程序员抓狂的瞬间

每个写代码的人,几乎都会经历同一个"信仰崩塌时刻"。你打开浏览器控制台,或者随便一个编程语言的 REPL,自信地敲下 0.1 + 0.2,按下回车,然后盯着屏幕愣住——返回的不是 0.3,而是一串让人怀疑人生的 0.30000000000000004。

这不是 bug,不是哪个语言偷懒,也不是你的电脑坏了。地球上每一台计算机,只要你用浮点数算这道题,都会给出同一个"错"答案。 它们达成了人类历史上最整齐的共识——只是这个共识恰好不等于 0.3。

Computerphile 这支《Floating Point Numbers》(浮点数)是解释这一现象最经典、被引用最多的入门视频之一。它没有甩一堆公式,而是用一个朴素到惊人的类比把这件事讲透了:浮点数根本不是什么神秘玩意儿,它就是我们中学学的科学记数法,只不过换成了二进制。 一旦你看穿这层窗户纸,0.1 + 0.2 ≠ 0.3 就从"玄学"变成"显然如此"。更关键的是——它还能救命。本文在拆解视频核心论证的同时,会补上视频没展开的 IEEE 754 精确结构、以及一个因为"二进制装不下 0.1"而付出 28 条人命的真实惨剧。

视频背景卡片

项目 内容
视频标题 Floating Point Numbers
频道 Computerphile(布拉迪·哈兰 Brady Haran 出品的计算机科普频道,订阅约 270 万)
发布日期 2014 年 1 月 22 日
时长 约 9–10 分钟
播放量 累计百万级(计算机基础概念长尾爆款,常年被课程引用)
核心主题 为什么浮点数算不准 0.3——它的本质是二进制科学记数法

核心内容拆解

1. 期待与现实:每个程序员的第一次"塌房"

视频一开场就点破了那种普遍的错觉。主讲人说得很直白:人是被计算器惯大的。 你从小按计算器,输入什么就精确给出什么,于是你理所当然地以为"计算机 = 绝对精确"。直到某一天,你在学编程时手贱敲了 0.1 + 0.2,电脑却甩给你一个 0.300000000...1(视频里这么口语化描述)。

主讲人把这一刻形容为"开始揪头发"(tear their hair out)的时刻——这几乎是每一个程序员的成人礼。视频的高明之处在于,它没有立刻教你"怎么解决",而是先承认:你的惊讶是合理的,但这种惊讶本身,恰恰暴露了你对"计算机怎么存数字"的根本误解。 真正的解药不是某个技巧,而是换个角度看浮点数到底是什么。

2. 浮点的真身:它就是科学记数法

接下来是全片最关键的一句定性:"Floating point numbers are essentially scientific notation."(浮点数本质上就是科学记数法。)视频用一个天体物理学的例子展开:你想用光速(约 3×10⁸,一个巨大的数)去乘一个极小的距离(0.00000015,即 1.5×10⁻⁷)。如果老老实实把所有零都写出来相乘,那是噩梦;但用科学记数法,你只需要 4.5 × 10¹ = 45——系数相乘,指数相加,搞定。

浮点数干的就是这件事,只不过它把"底数 10"换成了"底数 2"。一个浮点数 = 一段固定长度的数字(尾数/有效位) × 2 的若干次方(指数),再加上一个正负号。 所谓"floating point"(浮动的小数点),正是因为小数点的位置由那个指数决定,可以左右"浮动",从而用有限的存储位表达从原子尺度到宇宙尺度的数。视频反复强调:一旦你接受"浮点 = 二进制科学记数法",后面所有怪现象都有了着落。

3. 速度与效率:为什么要设计这么个"不精确"的东西

既然不精确,为什么不全用精确的整数或十进制小数?视频给出两个理由:速度(speed)和效率(efficiency)。

速度上,经过几十年硬件迭代,CPU 对浮点运算(FPU)已经优化到了"闪电般快",甚至现代芯片有专门的向量指令一次算一串浮点数。效率上,浮点能用同样的字节数,既装得下"宇宙大小"的数,也装得下"原子大小"的数。主讲人算了一笔账:如果你想让一个格式同时精确表示宇宙尺度和原子尺度,你要么得为每一个数存上兆字节的一堆零,要么得让 CPU 玩一套极其别扭的进位逻辑——这两条路都是灾难。浮点数用"科学记数法 + 有效数字"的折中,换来了惊人的表达范围。代价,就是精度上的妥协。

4. 致命关键:二进制里根本没有"0.1"这个数

这是整段论证的引爆点。视频对比了十进制和二进制:人类用十进制,有十分位、百分位、千分位,所以"0.1"写得理所当然、天经地义。但计算机用二进制,它的小数位是二分之一、四分之一、八分之一、十六分之一…… 你能在这一列里凑出 0.5、0.25、0.125、0.75,却永远凑不出一个干净的 0.1。

主讲人接着抛出那个决定性的事实:0.1 用二进制写,是 0.0001100110011...,那个 0011 会无限循环下去,直到无穷。 这就像你想用十进制精确写 1/3,只能写出 0.3333... 一样。问题是——浮点数的存储空间是有限的,它不可能把一个无限循环的小数完整存下来。于是它只能"截断",截断就必然产生误差。 这就是 0.1 在计算机里从一开始就不"干净"的根本原因:不是算错了,是它压根没被完整记住过。

5. 有效数字与截断:1/3 的悲剧,在二进制里重演

为了让"截断误差"更直观,视频用了一个绝妙的十进制类比。人知道 1/3 + 1/3 + 1/3 = 1,因为我们能心照不宣地理解 0.333... 是无限循环的。但浮点数不懂"循环",它只懂"有效数字"——存到第 N 位就到此为止。 所以在它的世界里,0.333...(截断后)+ 0.333...(截断后)+ 0.333...(截断后)= 0.999...,而不是 1。这就是浮点舍入误差(rounding error)的本质。

把同一套逻辑搬回二进制:1/10 + 2/10 就是 0.000110011...(循环)加上 0.001100110...(循环)。视频说,32 位浮点只存约 23 位有效数字(这里主讲人措辞略有含糊,见下文我延伸的精确版),存到第 23 位就被砍掉,于是 1/10 + 2/10 在二进制里"并不恰好"等于 3/10——也就是你看到的 0.1 + 0.2 ≠ 0.3。主讲人强调:对绝大多数场景(比如 3D 游戏里某个像素偏了十万分之一)这点误差毫无影响;但一旦涉及到"人能一眼看出来的钱",它就当场翻车。

6.【延伸分析】IEEE 754 的真实结构:纠正视频里的两处含糊

视频把原理讲透了,但在"位数"这个具体数字上说得比较口语化,容易被误解。这里我把 IEEE 754 标准的真实结构补全——这部分是我的延伸,而非视频原话。

视频反复提到"32 位存 23 位有效数字""64 位大约 56 位",这里有两处需要精确化。第一,IEEE 754 单精度(binary32)共 32 位,结构是 1 位符号(sign)+ 8 位指数(exponent)+ 23 位尾数(fraction)。但那 23 位是显式存储的"小数部分",由于规格化数字隐含一个最高位的 1(称为 hidden bit / implicit leading 1),实际有效精度是 24 位,换算成十进制大约只有 6~7 位有效数字,不是 23 位。第二,双精度(binary64,即 64 位)是 1 + 11 + 52 结构,有效精度 53 位,约 15~17 位十进制有效数字——不是"56 位"。这意味着 float 只能可靠表示约 7 位十进制数,double 约 15~17 位;超过这个位数,后面的数字都是"幻觉"。

顺带补充几个视频没提的彩蛋:IEEE 754 里还有 +0 和 -0 两个零、正负无穷(Infinity)、以及大名鼎鼎的 NaN(Not a Number,比如 0/0 的结果)。还有个反直觉的现象叫"灾难性抵消"(catastrophic cancellation)——两个接近的大数相减,有效数字会瞬间崩塌。这些不在视频里,但都是同一个"有限位数装无限实数"困境的延伸症状。

7.【延伸分析】真实世界的血泪:因为"装不下 0.1",死了 28 个人

如果说 0.1 + 0.2 只是让人揪头发,那它的"军用加强版"则夺走了人命。这是我补充的真实案例,视频没有提及,但最能说明"二进制装不下 0.1"这件事的分量。

1991 年 2 月 25 日,海湾战争期间,一枚伊拉克"飞毛腿"(Scud)导弹击中了沙特阿拉伯宰赫兰(Dhahran)附近的美军兵营,造成 28 名美军士兵死亡。事后美国审计总署(GAO)的调查报告指出,罪魁祸首正是浮点/定点舍入误差:"爱国者"防空导弹系统的软件,用"十分之一秒"(0.1 秒)为单位来累计运行时间,再用它来预测来袭导弹的位置。 而 0.1 秒在二进制里正是那个无限循环的 0.000110011...,系统用一个 24 位定点寄存器把它截断了,每一次累计都丢掉一丁点。系统连续运行约 100 小时后,这枚"丢掉的一丁点"累积成了约 0.36 秒 的巨大时间偏差——而飞毛腿导弹速度极快,0.36 秒足以让它飞出数百米,于是拦截窗口完全错开,导弹直接砸进兵营。

技术上严格说,爱国者用的是定点(fixed-point)而非纯浮点,但病根完全一样:二进制无法精确表示 0.1。这正是 Computerphile 视频那句话最沉重的注脚——"在大多数情况下误差无所谓,可一旦它有所谓,就是大事。"这一课,是用 28 条人命写进教科书的。

8. 货币怎么办:工程界的两条铁律

回到程序员日常最痛的场景——钱。视频最后给出了两条业界通行、极其务实的解法。

第一,用专门的十进制类型(decimal type)。 Java 的 BigDecimal、C# 的 decimal、Python 的 decimal.Decimal,这类类型内部用十进制(或任意精度)来存数字,从根上避开了"0.1 装不下"的问题。第二,也是最朴素、最稳的办法——把所有金额按最小单位存成整数。 也就是不要存 0.10 元、0.20 元,而是存 10 分、20 分(整数永远精确),所有运算都在整数上做,最后要显示给人看时再除以 100。

主讲人总结得很妙:浮点数之所以让那么多人困惑,是因为人们把它当成了"会算错数的数学";可一旦你意识到它只是"二进制科学记数法 + 有效数字",那些怪事就都顺理成章了。它不是在"犯错",它是在"用有限的位数,忠实地表达一个它注定无法完整说出的数"。

知识结构图

flowchart TD
    A["浮点数 Floating Point<br/>本质:二进制科学记数法"] --> B["真身:科学记数法"]
    A --> C["结构:IEEE 754 标准布局"]
    A --> D["核心限制"]
    A --> E["真实后果与解法"]

    B --> B1["十进制版: 3×10⁸ (光速)"]
    B --> B2["二进制版: 1.x × 2ⁿ"]
    B --> B3["只有 ½ ¼ ⅛ …<br/>二进制里压根没有 0.1"]

    C --> C1["符号位 1 bit"]
    C --> C2["指数 8 / 11 bit"]
    C --> C3["尾数 23 / 52 bit<br/>≈ 7 / 15–17 位十进制"]

    D --> D1["0.1 = 0.000110011…<br/>无限循环"]
    D --> D2["存储有限 → 截断 → 舍入误差"]
    D --> D3["1/3 类比: 截断后<br/>0.333…×3 = 0.999… ≠ 1"]

    E --> E1["0.1+0.2 = 0.30000000000000004"]
    E --> E2["货币翻车 / 爱国者导弹 28 人"]
    E --> E3["解法: 整数存分 或 decimal 类型"]

实战启示:知道哪里"不能信任"浮点

看完视频和这些延伸案例,落到工程师的日常工作里,有几条可以直接照做的行动指南:

1. 钱永远别用 float/double。 这是最硬的一条铁律。无论是电商、金融还是账单,一律用整数存最小单位(分/厘),或用 BigDecimal / decimal 这类十进制类型。视频里主讲人那句"每个程序员迟早都会撞上这一下"是真的——区别只是你是撞在测试环境,还是撞在生产环境的对账单上。

2. 永远不要用 == 比较两个浮点数。 因为 0.1 + 0.2 == 0.3 会返回 false。正确做法是判断两者之差的绝对值是否小于一个极小的容差(epsilon),比如 abs(a - b) < 1e-9。很多现代语言(如 Rust、测试框架)甚至会用"相对 + 绝对"混合容差。

3. 警惕累加放大误差。 把上百万个浮点数加在一起,误差会越积越大(爱国者导弹就是时间累积版)。如果必须做大量求和,考虑用 Kahan summation(卡汉求和) 算法,它专门用一个"补偿项"来追回每次舍入丢掉的那一点点,能把累计误差压到接近机器精度。

4. 选对精度。 别图省内存到处用 float(32 位),现代 CPU 上 double 往往同样快甚至更快(寄存器本就是 64/80 位)。只在显存吃紧的 GPU 训练、海量存储等场景才考虑 float16/bfloat16。

5. 把 0.30000000000000004.com 存进收藏夹。 这个网站专门收集各语言里 0.1 + 0.2 的输出,下次有人问你"是不是我们语言有 bug",直接甩链接——不是任何语言的 bug,是整个 IEEE 754 的设计取舍。

本质上,浮点数教会程序员的是一种核心思维:精确性不是免费的,它是一种可以用范围和速度去兑换的资源。 你要做的,是清楚知道在当前场景下,你换进来了什么、又放弃了什么。在那 23 位有效数字够用的地方(图形、物理模拟、AI),放手让它快;在差一分钱都不行的地方(金融、计时、计量),老老实实退回整数和十进制。

延伸学习路径

  1. What Every Computer Scientist Should Know About Floating-Point Arithmetic(David Goldberg, 1991)—— 浮点领域的"圣经级"综述,想真正啃透精度问题、舍入模式、误差分析的必读经典,ACM 发表,网上有免费全文。
  2. Floating Point Visually Explained(Fabien Sanglard, fabiensanglard.net)—— 用大量图解把 IEEE 754 的位布局、隐含位、指数偏置讲得极其直观,是补全本文第 6 节技术细节的最佳图文搭档。
  3. 0.30000000000000004.com —— 一页纸的网站,罗列几乎所有主流编程语言计算 0.1 + 0.2 的结果,方便即查即用、破除"语言 bug"迷思。
  4. Computerphile 频道(youtube.com/@Computerphile)—— 本文分析视频的出处,Brady Haran 出品,覆盖编译器、加密、图灵机、数据结构等大量计算机基础硬核话题,每条都是精品。
  5. Numberphile - Floating Point / 大数 相关视频(youtube.com/c/numberphile)—— 同一团队(Brady Haran)的数学频道,从纯数学角度讲大数、精度、格雷厄姆数等,与本文形成"数学侧 + 工程侧"互补。

一句话总结

浮点数不是"算不准的数学",而是"二进制科学记数法"——它用有限的位数装下了从原子到宇宙的尺度,代价是你必须清楚知道:在哪些地方可以信任它的"差不多",在哪些地方(钱、计时、计量)必须换回整数和十进制。

参考来源

  1. Floating Point Numbers — Computerphile — 本文深度解读的核心视频(含完整脚本)。
  2. Patriot Missile Software Problem — UNC / GAO Report — 1991 年爱国者导弹因 0.1 秒舍入误差导致 28 人死亡的案例依据(第 7 节)。
  3. Floating Point Visually Explained — Fabien Sanglard — IEEE 754 位布局的图解延伸阅读(第 6 节补充)。

    本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。

完成摘要(What was done)

下一步计划(What's next)

注意事项(If applicable)

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入