尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
深入理解整数与浮点数在内存中的存储:从补码到IEEE 754精度陷阱
我印象挺深的一件事是有一次排查一个对账程序订单总价怎么都对不上数据库里字段用的 FLOAT差几分钱。一开始我以为舍入顺序有问题后来把几个中间变量的内存二进制打出来才发现根本原因FLOAT 存 0.1 这个数的时候就没存准。这个经历让我下决心把整数和浮点数在内存中怎么存储这件事彻底过了一遍。后来再遇到类似的精度问题、溢出问题、类型转换问题都是先看内存布局而不是在业务逻辑里瞎猜。这篇就把我梳理过的知识按实际使用顺序写出来适合刚开始学编程、准备技术面试、以及写过不少代码但没系统看过底层的人。1. 为什么整数负数不用直觉方案从原码到补码的必然选择1.1 原码的先天缺陷硬件加法器的视角先把问题拉回起点一个整数在内存里到底怎么放最直觉的想法是“符号位 绝对值”。比如 8 位二进制下用最高位表示正负剩下 7 位表示大小。5 就是00000101-5 就是10000101。这种编码方式叫原码很多教材会提一嘴然后告诉你“现在不用了”。但很少有人把“为什么不用”说透。问题出在加法电路上。如果计算机用原码做1 (-1)直接把两个二进制数按位相加会得到00000001 10000001 10000010这个结果按原码读是 -2不是 0。要想得到正确结果硬件必须先判断两个数的符号是否相同再比较绝对值谁大谁小最后决定是做加法还是做减法再补符号位。一套流程下来同一个加法器要兼顾符号判断、绝对值比较、减法运算电路复杂度肉眼可见地上升。这还只是第二层问题。第一层更基本原码里 0 有两种编码00000000和10000000分别代表 0 和 -0。计算机内部判断两个数“是否相等”时被迫把这两串二进制都当成 0 处理多一层额外判断。所以原码作为人类纸面上的记数方式没问题放进计算机硬件里处处别扭。1.2 模运算与钟面模型“取反加一”不是口诀而是结论补码为什么能解决这个问题核心思想是“模”。可以拿钟面来类比。假设现在是 3 点想把它拨到 2 点。你既可以逆时针回拨 1 小时也可以顺时针往前拨 11 小时。在模 12 的体系里-1 和 11 是同一个东西因为 3 11 14除以 12 取余数得到 2。往前拨 11 小时和往回拨 1 小时效果完全相同。n 位二进制整数天然就是一个模 2^n 的钟。8 位整数总共 256 种编码计算超过 255 的部分会自动溢出丢弃剩下的部分恰好等价于“对 256 取模”。于是如果想用“加上某个正数”来实现“减去一个负数”这个正数应该满足x y ≡ 0 (mod 2^n)。这个 y 就是 -x 的补数。问题变成怎么快速求这个补数。观察一下一个二进制数和它的反码逐位相加每一位都是 1得到一个全 1 的数。全 1 再加 1高位溢出后所有位归 0。这就解释了为什么“取反加一”能够求出负数x 的反码 1是唯一一个能让 x (反码 1) 恰好等于 2^n 的数。在模 2^n 的世界里这个数就是 -x 本身。所以补码不是谁拍脑袋定下的规则。它是在“加法器想统一处理正负数”这个工程诉求下被模运算逻辑逼出来的必然结果。1.3 补码的隐性福利统一加减、消灭正负零补码一旦被采用带来几个连锁好处。第一个好处是减法直接消失。5 - 3变成5 (-3)-3 用补码表示后和 5 相加结果天然就是 2。CPU 里只需要保留一个加法器加法指令可以完成一切加减运算硬件设计大幅简化。第二个好处是 0 的表示唯一。补码体系里 0 和 -0 都对应00000000不存在歧义。第三个好处是可表示范围略大一格。原码和反码在 8 位下能表示 -127 到 127补码却能表示 -128 到 127。因为10000000这个编码不再被 -0 占据它被释出来表示 -128。于是就有了一个经典现象127 1在 signed char 里变成了 -128。我经常用这个小代码演示#include stdio.h void print_bits(signed char x) { for (int i 7; i 0; i--) { putchar((x i) 1 ? 1 : 0); } putchar(\n); } int main(void) { signed char a 5; signed char b -5; print_bits(a); // 00000101 print_bits(b); // 11111011 return 0; }-5的内存形态不是直觉上的10000101而是11111011。看到这串二进制你应当马上意识到它存的是 -5 的补码。2. 整数边界和内存解读方式溢出、符号扩展与大小端2.1 同一串二进制不同解读有符号与无符号的差异同样的内存字节按有符号解读和按无符号解读结果可能完全不同。0xFF在 unsigned char 里是 255在 signed char 里是 -1。内存里那 8 个 bit 一模一样差别完全来自程序怎么解释它。这个差异在实际项目里最容易踩坑的地方是符号扩展。当一个 signed char 转成 int 时编译器不只拷贝 8 个 bit还会把最高位符号位复制到新增的所有高位上。于是signed char sc -1; unsigned char uc 0xFF; int a sc; // a -1内存 0xFFFFFFFF int b uc; // b 255内存 0x000000FF两段代码看起来都是“把一个字节变成 int”结果完全不同。很多同学在拼网络协议、写文件解析器时把字节抠出来再转成 int 参与计算一不注意就把 0xFF 的字节变成了 -1随后运算结果全是负数调半天也找不到原因。前面聊到127 1 -128这也是补码体系下整数回绕wraparound的典型表现。int 类型的最大值2147483647加上 1在 Java 里会变成-2147483648在 C 和 C 里这属于未定义行为但绝大多数平台也是回绕。这种问题很难靠单元测试覆盖因为只有在边界值附近才会出现建议处理计数器、序列号、时间戳增量这类逻辑时提前想清楚溢出策略。2.2 字节序大端和小端如何左右协议与文件解析另一个整数存储的经典问题是多字节数据在内存中的排列顺序。假设一个 32 位整数0x12345678它的四个字节从高到低是 12、34、56、78。大端模式会按 12 34 56 78 的顺序存到内存低地址到高地址小端模式则会反过来按 78 56 34 12 的顺序存。两种模式没有谁绝对更好只是历史选择。我平时用的个人电脑基本都是小端这是因为主流的 x86、ARM 处理器选择了小端。但网络协议里很多标准字节序用的是大端比如 TCP/IP 协议头里的端口号、IP 地址等。于是程序员经常要做字节序转换本机读出来的多字节数小端要先翻转才能拼出协议要求的数值。通常我会让团队记住一句话没有绝对的对错只有你处理的数据是不是按你预期的字节序排的。写序列化代码、解析二进制文件之前一定要先确认文件/协议的字节序约定。判断本机字节序的经典代码也很简单#include stdio.h int main(void) { unsigned short x 0x0001; unsigned char *p (unsigned char *)x; printf(%s\n, p[0] 1 ? little-endian : big-endian); return 0; }x的低字节是 01如果它落在低地址就是小端如果落在高地址就是大端。这个方法在嵌入式调试、内存 dump 检查时非常实用。3. 小数转二进制0.1精度损失的真正源头3.1 乘2取整小数的二进制展开方法整数转二进制用“除 2 取余”小数则反过来用“乘 2 取整”。拿一个干净的例子说0.625。按下述步骤0.625 * 2 1.25整数部分取 1剩余 0.25 0.25 * 2 0.5整数部分取 0剩余 0.5 0.5 * 2 1.0整数部分取 1剩余 0所以0.625 0.101。这个二进制小数可以精确写出来。理解了这个方法再看9.75这种数整数部分 9 转成1001小数部分 0.75 转成.11合起来就是1001.11。这些东西和十进制小数不同不需要近似。3.2 无限循环与近似存储0.1在内存里的“真实身份”问题在 0.1 这样的十进制小数上来了。按乘 2 取整一步步展开0.1 * 2 0.2整数部分取 0 0.2 * 2 0.4整数部分取 0 0.4 * 2 0.8整数部分取 0 0.8 * 2 1.6整数部分取 1剩余 0.6 0.6 * 2 1.2整数部分取 1剩余 0.2 0.2 * 2 0.4整数部分取 0 0.4 * 2 0.8整数部分取 0 0.8 * 2 1.6整数部分取 1剩余 0.6 ……可以看到 0011 开始无限重复0.1的二进制是0.000110011001100110011...一个无限循环小数。这不是程序 bug而是数学上的必然十进制下的有限小数在二进制下可能根本没有有限表示。所以不管用什么编程语言只要用二进制浮点数类型存 0.1存的都是一个近似值。在 float 里0.1f的真实值其实是0.100000001490116119384765625在 double 里0.1的真实值是0.1000000000000000055511151231257827021181583404541015625。打印到屏幕上时因为默认小数位少看起来是 0.1但在内存里那个数就不是精确的 0.1。4. IEEE 754浮点布局用一份十六进制反推内存结构4.1 符号、指数、尾数为什么指数要用偏移量浮点数在内存里的标准格式由 IEEE 754 定义。拿最常见的 float 来说32 位划分成三块第 31 位符号位0 正 1 负第 23 到 30 位指数位8 位采用偏移量表示第 0 到 22 位尾数位23 位double 则是 1 位符号、11 位指数、52 位尾数。指数为什么要用“偏移量”而不是直接存正负号因为指数本身代表科学计数法里的那个次方数它可能为负。如果直接给这 8 位再加符号位整个排序比较会变得麻烦。IEEE 754 采用的做法是把指数整体偏移float 偏移 127double 偏移 1023。也就是说真实指数 0 存成 127真实指数 -2 存成 125真实指数 3 存成 130。这样一来指数位在内存里天然就是一个无符号数比较两个浮点数大小时可以先把符号位、指数位按整数规则比较大多数情况下就能判断大小硬件实现很顺手。4.2 9.75的内存拆解从十进制到二进制的完整推导理论说完了一定要上手推一个具体数字我常用 9.75 做演示因为它的二进制形态足够干净。先把 9.75 转成二进制小数9 10010.75 .11合起来是1001.11写成二进制科学计数法小数点左移到第一位左边1001.11 1.00111 × 2³于是三个字段分别确定符号位0真实指数3加偏移 127得到 130转成二进制是10000010尾数小数点后的00111后面补零到 23 位拼在一起0 10000010 00111000000000000000000把这一串按 4 bit 分组0100 0001 0001 1100 0000 0000 0000 0000就是十六进制0x411C0000。我用代码验证过很多次#include stdio.h #include stdint.h #include string.h void print_float_layout(float f) { uint32_t bits; memcpy(bits, f, 4); for (int i 31; i 0; i--) { putchar((bits i) 1 ? 1 : 0); if (i 31 || i 23) { putchar( ); } } putchar(\n); } int main(void) { print_float_layout(9.75f); return 0; }输出0 10000010 00111000000000000000000和手推完全一致。你有任何诡异的浮点数转换结果都能用这个思路拆开检查。4.3 隐藏位机制23位尾数为何代表24位精度学 IEEE 754 时很多人会困惑float 尾数明明只有 23 位为什么资料上说它有 24 位有效二进制精度答案在“归一化”这件事上。二进制的科学计数法里只要数字不是 0归一化后小数位前面那个整数位一定是 1这是个确定值。比如9.75归一化后必然是1.00111 × 2³前面的整数位永远写成 1不需要额外占用存储空间。IEEE 754 约定这个 bit 被隐含掉存储时只保存它后面的 23 位所以实际上 float 能表达 24 位精度的有效数字double 则是 53 位。这也是为什么 float 大约对应 6 到 7 位十进制有效数字double 对应 15 到 16 位。拿 float 做金额累计时几位有效数字根本不够业务用这就是我开头那个对账 bug 的根源之一数据库里用 FLOAT 存金额累计一万条后精度误差就攒出来了四舍五入函数救不回来。5. 浮点数比较与特殊值那些教科书没写的坑5.1 为什么不要用比较浮点数以及正确的比较姿势既然浮点数是近似值直接比较就等于在问“这两个近似值是否完全相等”绝大多数情况会得到 false。经典案例不用多解释在几乎所有主流语言里0.1 0.2都不等于0.3。我用 C 打印过真实结果#include stdio.h int main(void) { double a 0.1; double b 0.2; printf(%.17g\n, a b); // 0.30000000000000004 return 0; }为什么不是正好 0.3因为 0.1 和 0.2 的二进制近似值相加再把二进制近似结果转回十进制额外多了一点误差。这属于用二进制小数表达十进制有限小数的数学代价。业务上处理金额我个人的经验是不要用浮点数存钱。在数据库里用整型存最小单位分或者用专门的高精度十进制类型。浮点数适合做物理计算、图形渲染、机器学习这类对“近似”不敏感的领域。如果某些算法确实需要用浮点并要判断是否接近常见的做法是设定一个误差阈值#include math.h int nearly_equal(double a, double b, double eps) { return fabs(a - b) eps; }这里的 eps 不能拍脑袋取。绝对值阈值适合比较接近 0 的数但比较 1 亿和 1 亿零 0.5 时同样的 eps 就不合适相对误差判断会更通用。我最常用的是fabs(a - b) max(fabs(a), fabs(b)) * eps配合一个较小的eps能覆盖大多数工程场景。5.2 特殊值的自洽规则-0.0、Infinity与NaNIEEE 754 在有限表示之外留了几个特殊编码很多人直到踩坑才发现它们的存在指数位全 1、尾数位全 0表示 Inf也就是无穷大符号位区分 Inf 和 -Inf。指数位全 1、尾数位不全为 0表示 NaN也就是“非数值”。指数位全 0、尾数位全 0表示 0同样有 0 和 -0。指数位全 0、尾数位不全为 0表示次正规数用于填补下溢附近的一段精度区间。举几个反直觉现象1.0 / 0.0得 Inf1.0 / -0.0得 -Inf0.0 -0.0判断为真但1.0 / 0.0与1.0 / -0.0不相等。In C0.0 / 0.0产生 NaN而且NaN ! NaN成立所以不能靠x x判断 x 是否合法要专门用isnan()。这些特殊值在数据处理里经常突然冒出来。比如用户输入了除数为 0没做保护公式结果变成 Inf后续排序、比较、JSON 序列化可能全部乱掉。又比如某些统计模型在计算过程中产生 NaNdebug 时第一眼看到的是“输出 NaN”根本想不到是输入数据里有个 -0.0 或 0.0 引发的。我处理这类问题时的一般套路是先查输入数据中是否有 0 或负值异常再在关键计算步骤对结果做一次有限性检查。比如 C 里有isfinite()可以一次同时过滤 Inf 和 NaN。养成这个习惯后由特殊值引发的隐性 bug 能少很多。把整数和浮点数在内存中存储这件事彻底过一遍是我过去几年里收益比较高的技术投入之一。现在回答数值相关的问题第一反应都是“先看它的二进制形态”再讨论业务逻辑。写代码时也会下意识想清楚存储单元是几字节、字节序是什么、符号会不会扩展、浮点比较是否用了容差。这些东西在日常开发里不会天天用到但一旦用到就是那种查通宵也未必定位的顽固问题。如果你还在初学阶段建议找个周末拿自己熟悉的语言把整数负数、大小端、0.1 的内存形态都打印一遍。这类基础一旦彻底打通后面看很多奇怪的现象都会豁然开朗。
RELATED

相关推荐

ROS2自定义消息:从msg定义到Python/C++双端调用实战

ROS2自定义消息:从msg定义到Python/C++双端调用实战

简介:ROS2消息自定义与使用完整示例包,面向需要掌握消息机制并实现自定义节点通信的ROS2学习者。资料以可编译运行的工作空间为主体,完整呈现从创建msg消息包、编写.msg字段定义、生成IDL接口,到通过colcon构建安装并在发布订阅节…

📅 2026/10/21 22:05:19
PowerShell脚本化Windows计划任务:从创建到批量部署指南

PowerShell脚本化Windows计划任务:从创建到批量部署指南

说起Windows上的定时任务,很多人第一反应是打开任务计划程序,点几下鼠标,填一个时间,选一个脚本,完事。但真到了需要给一批电脑统一部署计划任务、或者因为业务调整要整体更换触发时间的时候,图形界面就开始…

📅 2026/10/21 22:05:19
MATLAB量子计算实战:从量子比特建模到Grover搜索与QFT

MATLAB量子计算实战:从量子比特建模到Grover搜索与QFT

简介:量子计算算法的MATLAB实现资源,聚焦量子计算入门与算法验证场景,面向需要快速上手量子编程的初学者、相关课程学生及科研人员,帮助解决理论算法到可运行代码的转化问题。压缩包共5个文件,包含4个MATLAB脚本与1个M…

📅 2026/10/21 22:00:16
MORE NEWS

更多资讯

📰

脉冲神经网络:SNN的生物学原理与脉冲信息处理

这一章我们将从生物学的角度去剖析SNN的原理以及他如何处理脉冲信息。为什么神经元需要膜电位?为什么它不是直接把输入转成输出,而是先积累,再达到阈值,然后发放 Spike?这些设计并非凭空出现,而是受到真实生…

📰

SCI返修审稿人要求提供AIGC说明与检测单?2027国际期刊返修材料核验指南

SCI返修审稿人要求提供AIGC说明与检测单?2027国际期刊返修材料核验指南【返修核验要点】如编辑或审稿人询问生成式工具使用情况,先核对本刊当期指南和具体来信要求,逐项说明实际使用范围与人工核验过程。检测报告不能单独证明论文来源&#x…

📰

【Valhalla L3开源评测|GitHub热榜Top10】CCBaga|Rust+Tauri桌面工具静态源码审计

Valhalla L3静态源码审计|CCBaga桌面客户端项目工程尽调报告【Valhalla L3开源评测|GitHub热榜Top10】CCBaga|RustTauri桌面工具静态源码审计一、高管一页纸综述(产品/技术负责人 30秒决策结论)1.1 项目基础档案1.2 审…

📰

一边是搜索 API 开放,一边是被令从联邦系统移除:DeepSeek 团队该怎样读这份 DHS 采购禁令

一边是搜索 API 开放,一边是被令从联邦系统移除:DeepSeek 团队该怎样读这份 DHS 采购禁令 一条时间轴上并行的两件事 把两条新闻放在同一张时间轴上,画面会很有意思:一边是 DeepSeek 在国内开发者社区开放搜索 API,讨…

📰

AI Agent 学习路线|从零搭建智能体,小白 程序员收藏这份大模型实战教程

本文分享一套完整 AI Agent 学习路线,适合小白与程序员。从 Python 基础、大模型原理、提示词工程入手,依次学习 Agent 核心要素、LangChain/LangGraph 框架、RAG 向量检索,再到项目优化、工程部署。按路线学习 2~3 个月,可独立开…

📰

dm-mod.ko与dax.ko

apk add kmod-dmapk add kmod-daxapk info -L kmod-dmapk info -L kmod-daxxz -d 123.xz 解压文件,会删除原文件123.xztar -xvf 123.tar 解包文件,会覆盖同名文件mkdir -p 123/456/789 创建多层目录tar-cvf 123.tar 123 打包文件或目录,得到1…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬