news 2026/6/11 0:02:51

深入理解计算机系统:浮点数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解计算机系统:浮点数

浮点数是《深入理解计算机系统》(CSAPP)中公认的难点。看数学公式和抽象概念很容易绕晕,真正理解它的唯一捷径就是直接去算比特位。

本教程抛弃模糊的比喻,直接切入 32 位物理内存,带你手推二进制位模式。我们将通过实际的运算例子,一步步拆解

第一部分:前置知识 —— 二进制小数的局限与浮点数的诞生

要理解复杂的浮点数标准(IEEE 754),我们必须先搞清楚两件事:二进制如何表示小数,以及为什么我们不能直接固定小数部分的位置

1.1 二进制小数的表示

在日常使用的十进制中,小数点左边的数字权重是等,而小数点右边的数字权重是等(即)。

二进制小数的逻辑完全一样,只是基数从变成了

小数点左边的权重是

小数点右边的权重是

具体对应的值如下:

实例计算:将二进制101.11转换为十进制

我们将每一位乘以它的权重,然后相加:

  1. 整数部分 (101)5

  2. 小数部分 (.11)0.75

  3. 组合结果5.75

反向实例:将十进制 5.75 转换为二进制

你可以将其拆解为能用的幂次表示的数之和:

对应的二进制位就是:101.11

核心认知:二进制小数只能精确表示那些能够被写成形式的数。例如十进制的0.2(即)无法被精确拆解为 1/2, 1/4, 1/8 的有限组合,它在二进制中是一个无限循环小数。这就注定了浮点数天生带有“精度丢失”的基因。

1.2 定点数的局限性(为什么需要浮点数?)

既然我们可以用101.11这样的方式表示小数,为什么计算机不直接把 32 位内存分成两半:前 16 位存整数,后 16 位存小数?

这种表示方法被称为定点数(Fixed-Point)。它的“小数点”位置是固定死不动的。定点数非常简单直接,但它在计算机科学中存在致命的局限性:

1. 无法表示极大的数

如果前 16 位用来表示整数,哪怕是无符号数,它的最大值也只能到65535。在处理天文学数据(如光速、星系距离)或现代游戏引擎的宏大坐标时,连都装不下,这显然是不行的。

2. 无法表示极小的数

后 16 位表示小数,它能表示的最小正数是(大约是)。如果你要计算电子的质量(约千克),定点数直接会将其视作0.0

3. 极端的空间浪费

假设我们要表示。如果你用定点数,你需要先写出二进制的101,然后在后面跟着写 100 个0。这就意味着,为了表示一个有效信息只有两三位的数字,你浪费了整整 100 位的存储空间去记录“小数点的位置”。

破局之道:科学记数法

为了解决这些问题,计算机科学家引入了十进制中“科学记数法”的二进制版本。

在十进制中,我们不写 150000000,而是写成

在二进制中,我们也可以将任何数表示为:

小数点不再是固定的,而是随着指数(E)的变化而“浮动”。这就是浮点数(Floating-Point)名称的由来。IEEE 754 标准正是基于这个数学公式,用极其巧妙的方式将符号(s)、尾数(M)和指数(E)压缩进 32 位或 64 位的内存中。

第二部分:核心基石 —— IEEE 754 标准与通用结构

2.1 核心数学公式

在 IEEE 754 标准中,无论是单精度还是双精度,任何一个浮点数都可以用下面这个极其优美的数学公式来表示:

这个公式包含三个核心维度:

  1. s(Sign,符号):决定这个数是正数还是负数。s=0 时为正,s=1时为负。

  2. M(Significand/Mantissa,尾数):一个二进制小数,它包含了数字的有效位信息。

  3. E (Exponent,阶码):2 的幂次,它的作用是对浮点数进行加权,决定了二进制小数点的位置(向左还是向右“浮动”)。

极其重要的认知:

很多初学者会在这里栽跟头。请务必记住:内存里存的并不是 s, M, E 的直接数值。内存中存储的二进制串只是用来推导出 s, M, E 的线索。

2.2 内存布局(Bit 级别的解剖)

为了在内存中存储上述的三个元素,IEEE 754 将一段连续的比特(Bits)划分成了三个独立的字段:

  • 符号位s:占 1 位。直接对应公式中的 s。

  • 阶码字段exp:占 k 位。它用来推导公式中的 E(注意:exp不等于 E)。

  • 小数字段frac:占 n 位。它用来推导公式中的 M(注意:frac不等于 M)。

我们在 C 语言中最常用的两种浮点类型,其位分配如下表:

C 语言类型精度总位数符号位 (s)阶码位 (exp) 宽度 k小数位 (frac) 宽度 n
float单精度321823
double双精度6411152

对于 32 位的float,它的位模式看起来像这样:[s] [exp: 8位] [frac: 23位]

2.3 偏置常数(Bias):为什么不直接用补码?

接下来我们要解决一个极其硬核的工程问题:阶码 E必须能表示负数。

因为要表示像 0.0001 这样极小的数,我们需要负的指数(比如)。

既然需要负数,按照《深入理解计算机系统》整数部分的常识,我们应该用补码(Two's Complement)来存储exp对吧?

但是,IEEE 754 的设计者极其巧妙地拒绝了补码,而是采用了“偏置(Biased)”编码。

为什么这么干?为了性能。计算机 CPU 在比较两个浮点数大小(比如a > b)时,如果用补码,硬件电路会非常复杂,因为浮点数的符号位在最前面,中间还夹着阶码。 为了让浮点数的比较能够直接复用整数比较的硬件电路,设计者决定:让exp字段永远是一个无符号整数(Unsigned Int)。无符号数越大,表示的真实阶码 E就越大。

如何做到?引入偏置常数(Bias):

我们给exp的无符号值减去一个固定的常数(Bias),就能得到真实的、有正有负的 E。

Bias 的计算公式是:

(其中 k 是exp的位数)

  • 单精度(k=8)的 Bias127

  • 双精度(k=11)的 Bias1023

实例推演(以单精度为例):

真实阶码的公式为:

  • 如果内存中的exp位全是0,它的无符号值是 0。真实阶码 E = 0 - 127 = -127。(实际情况略有不同,这是第三部分非规格化值的特殊规则,先留个悬念)。

  • 如果内存中的exp1000 0000,则无符号值是 128。真实阶码 E = 128 - 127 = 1。

  • 如果内存中的exp0111 1110,则无符号值是 126。真实阶码 E = 126 - 127 = -1。

通过这种“减去一个常数”的做法,原本只能表示的 8 位无符号数,被完美地平移成了表示负数和正数的阶码范围。

第三部分:深水区 —— 浮点数的三大形态(重点)

3.1 第一形态:规格化值(Normalized Values)

这是最常见的情况。

触发条件:当exp的位模式既不全为0,也不全为1时(即无符号值在之间)。

在这种形态下,有两个极其重要的硬核规则:

1. 真实的阶码

这在我们上一部分的结尾已经推导过了。

2. “隐藏的 1”:真实尾数

这是计算机科学中极为经典的一个优化设计。想想我们第一部分提到的科学记数法,在二进制中,任何非零数字的科学记数法表示,小数点左边必然是 1(例如),因为二进制只有 0 和 1,既然是非零数,首位肯定不能是 0。

既然这个1永远存在,设计者想:为什么我们要在极其宝贵的内存里去存一个永远是 1 的数字呢?

所以,IEEE 754 规定:这个1是免费送的,不需要存储!内存里的frac字段(也就是数学公式里的 f),只存储小数点右边的部分。

硬核推演:将 5.75 存入 32 位 float 内存

在第一部分我们算过,的二进制是101.11

步骤 1:转为二进制科学记数法

步骤 2:确定符号位s

正数,所以s = 0

步骤 3:计算exp

真实阶码。根据公式,得出

的 8 位无符号二进制是1000 0001

步骤 4:确定frac

尾数 M 是。去掉隐藏的 1,剩下的小数部分0111

我们将它填入 23 位的frac字段中,尾部补 0:011 1000 0000 0000 0000 0000

最终内存中的 32 位真实布局

0 10000001 01110000000000000000000

3.2 第二形态:非规格化值(Denormalized Values)

触发条件:当exp的所有位全为0时。

为什么要有这种形态?假设没有它,规格化数能表示的最小正数是。比这个数更小的数值(极靠近 0 的数)就只能直接变成 0,这会导致计算精度的突然断崖式下降。

非规格化值就是为了表示0以及极度接近 0 的数字而存在的。

它的规则发生了变化:

1. 没有隐藏的 1:真实尾数

既然是为了表示极小的数,首位就不能再是 1 了,而是

2. 特殊的阶码计算:真实阶码

注意,这里不是。这是为了实现一种叫做“逐渐下溢(Gradual Underflow)”的平滑过渡特性,使得最大非规格化数和最小规格化数之间能够完美衔接(这里只需记住公式,这是 IEEE 754 为了数学平滑性做的特殊硬编码)。

特殊现象:+0.0 和 -0.0

exp全为 0,且frac也全为 0 时,根据公式算出来的值就是 0。

但由于符号位s的存在,浮点数在底层实际上有两种 0:符号位为 0 的+0.0和符号位为 1 的-0.0。在 C 语言中,它们在数值比较(==)时是相等的,但在某些极其特殊的数学库运算中会有不同的行为。

3.3 第三形态:特殊值(Special Values)

触发条件:当exp的所有位全为1时(即无符号值为 255)。

这是一种错误处理机制,用来表示数学上无法处理的结果:

  • 无穷大(Infinity):如果frac全为0,则表示无穷大。当s=0时是正无穷,s=1时是负无穷。这通常发生在两个极大的数相乘导致溢出,或者进行除以 0 的操作时(例如1.0 / 0.0在 C 语言里不会崩溃,而是得到正无穷)。

  • 不是一个数(NaN - Not a Number):如果frac不全为0,则表示 NaN。这通常发生在非法数学运算时,比如计算,或者

第四部分:规则与陷阱 —— 舍入机制(Rounding)

4.1 为什么必须舍入?

在十进制中,有些分数是无法用有限小数表示的,比如

在二进制中,情况更加严峻。比如我们最常用的十进制(即 1/10),它的分母 10 并不是 2 的幂次。如果你尝试把它转成二进制,会得到一个无限循环小数:

但是,在单精度float中,我们的frac字段只有 23 位!这就意味着,计算机必须在某一位强行“一刀切”,把后面的比特全部丢掉。这种为了适应有限内存而丢弃精度的操作,就是舍入

4.2 IEEE 754 的默认规则:向偶数舍入(Round-to-Even)

从小我们学到的舍入规则叫“四舍五入”。比如

但“四舍五入”在计算机科学里存在一个致命缺陷:统计偏差

想一想,如果一个数字正好处在两个整数的绝对中间(比如 $1.5$、$2.5$、$3.5$),按照四舍五入的规则,它们全都会被向上进位。如果在一个包含数百万次计算的金融系统中,所有处在正中间的数都往上进位,最终的计算结果会不可避免地偏大

为了解决这个问题,IEEE 754 默认采用向偶数舍入

  • 规则:当一个数字在正中间时,它离谁近就舍入给谁(这跟普通的舍入一样)。

  • 核心差异:当一个数字恰好在正中间时,我们不固定向上或向下,而是看哪个结果是偶数,就舍入给谁。

比如在十进制中模拟这个规则:

  • 在正中间,舍入后是偶数,所以变成2

  • 在正中间,舍入后是偶数,所以变成2(注意,不是3!)。

    由于数字末尾是奇数和偶数的概率各占 50%,这种“有时向上、有时向下”的机制,完美抵消了统计偏差。

4.3 二进制的“向偶数舍入”实战

在二进制中,什么叫“偶数”?二进制的最低有效位是0就是偶数,是1就是奇数。

什么叫“恰好在正中间”?在二进制里,被舍弃的部分如果恰好是1000...00(即首位是 1,后面全 0),就是恰好一半。

实例演练:我们要将以下数字舍入到小数点后两位(即保留XX.YY的格式)

  1. 不到一半(向下截断)

    10.00 | 011竖线后面的011小于一半(100),直接丢弃。结果是10.00

  2. 超过一半(向上进位)

    10.00 | 110竖线后面的110大于一半(100),向上进位。结果是10.01

  3. 恰好一半(向偶数舍入 - 末位为 0)

    10.10 | 100竖线后面是100,恰好是一半!此时看保留部分的最后一位,是0(已经是偶数了)。所以直接丢弃。结果是10.10

  4. 恰好一半(向偶数舍入 - 末位为 1)

    10.11 | 100竖线后面是100,恰好一半!此时保留部分的最后一位是1(奇数)。为了变成偶数,必须向上进 1。10.11 + 0.01 = 11.00。结果是11.00

第五部分:实战与反直觉现象 —— 浮点运算与 C 语言特性

当你掌握了底层的位模式和舍入规则,C 语言里那些曾经让你百思不得其解的 Bug 就会豁然开朗。

5.1 浮点数加法丧失结合律(大数吃小数)

在纯数学中,(A + B) + C = A + (B + C) 永远成立。

但在 C 语言的浮点数中,这不成立

代码现象:

float a = (3.14 + 1e20) - 1e20; float b = 3.14 + (1e20 - 1e20);

如果你打印这两个变量,你会发现a的值是0.0,而b的值是3.14

底层原理解密(灾难性抵消):

当我们计算3.14 + 1e20时,由于两个数字相差极大,CPU 在做加法前必须先“对齐阶码”。为了和对齐,3.14的阶码必须疯狂增大,而它的尾数则必须疯狂右移。

在单精度的 23 位尾数限制下,右移的过程中,3.14的有效位早就被全部舍入丢弃了。所以3.14 + 1e20在计算机眼里的结果依然是1e20。接着再减去1e20,结果自然就变成了 0.0。

5.2 C 语言中的类型转换强制规则

面试经常问:在intfloatdouble之间互相转换会发生什么?

  • int转换成float:不会溢出,但可能丢失精度。

    • 原因:C 语言的int是 32 位的,它的有效位可以占满 31 位。但是float的尾数frac只有 23 位(加上隐藏的 1 共 24 位)。如果你的int数字非常大,需要超过 24 个二进制位来精确表示时,多出来的低位就会被“向偶数舍入”无情切掉。

  • int转换成double:绝对安全。

    • 原因double的尾数有高达 52 位,把一个 32 位的int完整塞进去绰绰有余。

  • floatdouble转换成int:向零舍入(直接截断)。

    • 原因:不看偶数还是奇数,直接把小数部分粗暴砍掉。如果浮点数太大,超出了int的最大范围,C 语言标准并未定义具体行为,通常在现代系统上会变成一个极大的负数(比如-2147483648)。

结语

至此,CSAPP 浮点数章节已经全部拆解完毕。从为什么会有浮点数,到偏置常数与三大形态的解析,再到向偶数舍入的逻辑,最后回归到C语言的代码现象。这套标准虽然看似反人类,但在“性能、存储范围、数学平滑性”这个不可能三角之间,IEEE 754 已经做到了极其优雅的平衡。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/14 14:22:07

IT 运维10个工具来回切,一半工作时间全浪费

你的 IT 团队,是不是正陷在这样的恶性循环里:补丁管理、远程支持、状态监控、资产盘点各用一套独立系统,每天8小时工作,近半时间都耗在平台切换、数据拼凑、流程对齐上?这不是团队能力问题,而是工具蔓延正在…

作者头像 李华
网站建设 2026/4/14 14:20:53

如何轻松退出Windows Insider计划?OfflineInsiderEnroll终极解决方案

如何轻松退出Windows Insider计划?OfflineInsiderEnroll终极解决方案 【免费下载链接】offlineinsiderenroll OfflineInsiderEnroll - A script to enable access to the Windows Insider Program on machines not signed in with Microsoft Account 项目地址: ht…

作者头像 李华
网站建设 2026/4/14 14:19:23

YOLOv8融合VMamba:目标检测性能跃升实战解析

1. 环境配置与依赖安装 在开始YOLOv8与VMamba的融合实验之前,我们需要先搭建好开发环境。这里我推荐使用Ubuntu 22.04系统配合Anaconda进行环境管理,实测下来这个组合最稳定。如果你用的是Windows系统,建议通过WSL2来运行Ubuntu环境&#xff…

作者头像 李华
网站建设 2026/4/14 14:15:13

校招简历-HR筛选简历只看这5点:大厂前HR教你写满一页A4纸

HR筛选简历只看这5点:大厂前HR教你写满一页A4纸 结合截至 2026 年 4 月的公开校招简历建议、ATS 解析规则与技术岗筛选逻辑整理|适用对象:26届、27届计算机类专业校招生 很多简历,死得很安静。 投出去。 没消息。 你以为是竞争太…

作者头像 李华
网站建设 2026/4/14 14:15:12

秋招0 Offer后,我靠这4个动作在春招把局面拉回来了

秋招0 Offer后,我靠这4个动作在春招把局面拉回来了 结合 2026 届公开招聘信息、企业官网岗位页与公开求职复盘整理|更新时间:2026 年 4 月 标题里的“我”,不是某一个具体的人。 更像这半年里,那些秋招失利、又在春招…

作者头像 李华
网站建设 2026/4/14 14:12:06

3个维度解析Shadcn-Vue:如何构建专属Vue组件库?

3个维度解析Shadcn-Vue:如何构建专属Vue组件库? 【免费下载链接】shadcn-vue Vue port of shadcn-ui 项目地址: https://gitcode.com/gh_mirrors/sh/shadcn-vue 您是否曾因传统UI组件库的限制而感到束手束脚?当设计需求与预制组件不匹…

作者头像 李华