当前位置: 首页 > news >正文

3.3 在代码中验证与避免误差

一、绕开二进制精度限制,用工程方法解决问题

既然误差是硬件底层的必然结果,我们就不能改变它,但可以规避它。下面是《程序是怎样运行的》中推荐的、在实际开发中最常用的 4 种解决方案:

方案 1:避免直接用==比较浮点数

问题:浮点数是近似值,直接比较相等几乎一定会出错。解决:用「差值是否小于极小阈值」来判断:

#include <stdio.h> #include <math.h> // 用于fabs函数(求绝对值) #define EPS 1e-6 // 定义极小阈值(0.000001) int main() { float a = 0.1; float b = 0.1; if (fabs((a + b) - 0.2) < EPS) { printf("近似相等\n"); } else { printf("不相等\n"); } return 0; }

✅ 适用场景:所有需要判断浮点数相等 / 大小的场景。

方案 2:使用高精度小数库

问题:float(32 位)、double(64 位)的精度依然有限,金融、科学计算需要更高精度。解决:使用语言自带的高精度库,用字符串存储小数,避免二进制转换误差:

  • Python:decimal.Decimal
  • Java:java.math.BigDecimal
  • C++:boost::multiprecision::cpp_dec_float

Python 示例

from decimal import Decimal, getcontext getcontext().prec = 20 # 设置精度为20位 a = Decimal('0.1') b = Decimal('0.1') print(a + b) # 输出:0.10000000000000000000 print(a + b == Decimal('0.2')) # 输出:True

✅ 适用场景:金融、会计、高精度科学计算。

方案 3:转换为整数运算

问题:整数可以用二进制精确表示,没有精度误差。解决:把小数放大为整数(比如把 “元” 变成 “分”),运算后再缩小:

#include <stdio.h> int main() { // 0.1元 = 10分,用整数存储 int sum = 0; for (int i = 0; i < 100; i++) { sum += 10; // 每次加10分(即0.1元) } printf("总金额:%d元%d分\n", sum / 100, sum % 100); return 0; }

运行结果总金额:10元0分(完全精确)✅ 适用场景:货币计算、小数位数固定的场景。

方案 4:减少运算次数,避免误差累积

问题:多次运算会让微小误差不断放大(比如 0.1 累加 100 次)。解决:尽量合并计算,用公式代替循环:

#include <stdio.h> int main() { // 直接用乘法:0.1 * 100 = 10,误差远小于循环累加 float sum = 0.1 * 100; printf("%f\n", sum); // 输出:10.000000(近似精确) return 0; }

✅ 适用场景:大量重复运算的场景。

核心认知

这些方案的本质,都是绕开 “二进制小数无法精确存储” 的硬件限制

  • 用整数 / 字符串存储 → 避免二进制转换误差;
  • 用阈值比较 → 接受近似值,避免绝对相等的执念;
  • 减少运算次数 → 降低误差累积的概率。这正是《程序是怎样运行的》中 “程序要适配硬件特性” 的最佳实践。

二、 二进制数和十六进制数

为什么要学十六进制?

十六进制是查看内存数据的窗口—— 二进制太长,十进制不直观,十六进制是二进制的 “简写版”:

  • 1 位十六进制 = 4 位二进制(0xF=1111
  • 2 位十六进制 = 1 字节(8 位二进制)
  • 32 位浮点数 = 8 位十六进制

二进制 ↔ 十六进制 转换规则

二进制十六进制二进制十六进制
0000010008
0001110019
001021010A
001131011B
010041100C
010151101D
011061110E
011171111F

示例:二进制1011 0011→ 十六进制0xB3

📖 浮点数的十六进制表示

我们可以用十六进制直接查看浮点数在内存中的样子(以 C 语言为例):

#include <stdio.h> int main() { float a = 0.1; // 把float的地址强制转为char*,逐字节打印十六进制 unsigned char *p = (unsigned char *)&a; printf("0.1 的内存十六进制:"); for (int i = 0; i < 4; i++) { printf("%02X ", p[i]); } printf("\n"); return 0; }

运行结果(小端序):CD CC 4C 3E✅ 这串十六进制就是0.1在内存中的真实存储,对应我们之前拆解的 IEEE 754 格式。

底层逻辑

十六进制是连接 “人类可读” 和 “机器存储” 的桥梁

  • 程序员可以通过十六进制,直接查看内存中的浮点数、整数、字符串;
  • 理解十六进制,才能真正看懂《程序是怎样运行的》中 “内存布局”“数据存储” 的核心内容。

三、本部分核心结论

  1. 实践验证:用代码可以直观看到0.1是近似值,直接比较浮点数相等会出错;
  2. 误差规避:通过「阈值比较、高精度库、整数运算、减少运算次数」,可以有效避免小数运算误差;
  3. 底层认知:十六进制是查看内存数据的工具,帮助我们理解浮点数的真实存储格式;
  4. 最终升华:计算机小数运算 “出错” 不是 bug,是硬件限制的必然结果 ——理解规则,才能利用规则写出可靠的程序
http://www.jsqmd.com/news/514121/

相关文章:

  • Lumerical INTERCONNECT实战:5分钟搞定自相位调制(SPM)仿真(附参数配置截图)
  • Qwen-Image定制镜像部署案例:RTX4090D支撑Qwen-VL与Stable Diffusion联动实现图文互生
  • 云容笔谈应用场景:独立设计师用AI生成苏绣/缂丝/云锦纹样设计初稿
  • STM32HAL库驱动DHT11温湿度传感器:从零开始的避坑实战(附完整代码)
  • OpenClaw语音交互扩展:Qwen3-32B对接Whisper实现语音指令控制
  • Pixel Dimension Fissioner惊艳效果展示:10组高创意文案裂变真实对比图
  • 用PID控制器模拟房间温度控制:MATLAB与Simulink的奇妙之旅
  • 灵毓秀-造相Z-Turbo案例展示:快速生成精美古风插画
  • 解决 GPT-5.4 废话多的问题
  • 用STM32F103C8T6复刻开源手表WATCHX-NWATCH:从B站视频到桌面摆件的DIY全记录
  • Qwen-Image效果展示:商品图→属性提取→文案生成全流程惊艳效果实录
  • 真的太省时间!当红之选的降AI率软件 —— 千笔·专业降AI率智能体
  • LongCat-Image-Editn效果展示:中英双语一句话改图,原图非编辑区域纹丝不动
  • 别再手动写API了!用Flask+ngrok快速给MySQL做个Dify专用接口(附完整代码)
  • Linux nc命令实战:5个网络工程师常用的Netcat技巧(附真实案例)
  • OWL ADVENTURE环境配置详解:Anaconda虚拟环境下的依赖管理
  • 提示工程架构师必读:研发效能提升的6大关键点
  • Pixel Dimension Fissioner智能助手:客服话术动态裂变与风格适配应用
  • 数字货币做市避坑指南:Avellaneda模型在7*24市场的5个调参技巧
  • Keil5+C++玩转STM32:从点灯到串口通信的完整实战指南(附避坑技巧)
  • 基于STM32的汽修厂多参数环境监测与智能联动系统
  • 空间认知成为核心生产力:智慧仓储的下一代发展路径
  • CVE-2016-4437 Apache Shiro反序列化漏洞复现
  • Linux 下 IDEA 开发环境一站式部署与疑难排解
  • 企业内网搞定Kubeflow v1.8:从镜像拉取到Harbor仓库配置的完整避坑记录
  • Neeshck-Z-lmage_LYX_v2创意应用:用不同LoRA风格为你的故事配图
  • 解决HTML内容精准导出难题:HtmlToWord的高效文档转换实现
  • 揭秘提示工程架构师动态上下文适配架构设计的关键环节
  • AltiumDesigner新手必看:如何快速测量两个芯片间的布线长度(附常见错误排查)
  • 救命!运维深夜守跑批?金仓并行DML封神,亿级数据写入从几小时缩至2分钟