浮点数的迷雾:比较、异常值与精度陷阱
浮点数这东西,看着简单,用起来全是坑。我入行第三年就吃过一次大亏——一个电机控制算法,在实验室跑得好好的,到了客户现场就间歇性抽风。查了三天,最后发现是浮点数比较出了问题。从那以后,我对浮点数就多了一份敬畏。
今天咱们就把浮点数的几个核心陷阱掰开揉碎讲清楚。说白了就三件事:怎么比、怎么处理特殊值、怎么控制精度。
一、浮点数比较:别直接用 ==
很多新手会这么写:
float a = 0.1f;
float b = 0.1f;
if (a == b) {
// 你以为会进来?
}
嗯,这个例子可能能过。但换个场景试试:
float a = 0.1f;
float b = 0.1f + 0.2f - 0.2f; // 你觉得结果是多少?
if (a == b) {
printf("相等\n");
} else {
printf("不相等\n");
}
结果大概率是「不相等」。为什么?因为浮点数的二进制表示天生就不精确。0.1 在二进制里是个无限循环小数,存进去的时候就已经被截断了。
那怎么比?用「绝对误差」或「相对误差」:
#include <math.h>
#include <float.h>
// 绝对误差比较
int float_eq_abs(float a, float b, float epsilon) {
return fabsf(a - b) < epsilon;
}
// 相对误差比较(推荐)
int float_eq_rel(float a, float b, float epsilon) {
float diff = fabsf(a - b);
float max_val = fmaxf(fabsf(a), fabsf(b));
return diff <= max_val * epsilon;
}
// 使用示例
if (float_eq_rel(a, b, 1e-6f)) {
// 认为相等
}
我个人习惯:在嵌入式场景下,如果数值范围已知,我更喜欢用绝对误差。比如电压值在 0~3.3V 之间,epsilon 取 0.001 就够用了。相对误差在数值接近零时会出问题,要注意。
二、NaN 和 Infinity:你代码里的隐形炸弹
NaN(Not a Number)和 Infinity 是浮点数的两个特殊值。它们不是错误,是标准规定的合法值。但如果你不处理,它们会像病毒一样传播。
先看怎么产生的:
float inf = 1.0f / 0.0f; // +Infinity
float nan = 0.0f / 0.0f; // NaN
float neg_inf = -1.0f / 0.0f; // -Infinity
NaN 有个特别坑的特性——它不等于任何值,包括它自己:
float x = nan;
if (x == x) {
// 这行永远不会执行!
printf("这不可能\n");
}
我在项目中遇到过一个问题:传感器偶尔返回 NaN,然后整个控制链路全乱了。因为 NaN 参与任何运算结果都是 NaN,一路传下去,最后执行器直接飞车。
正确的做法是:在关键计算节点做检查。
#include <math.h>
int is_safe_float(float x) {
// 检查是否有限数(不是 NaN 也不是 Infinity)
return isfinite(x);
}
// 使用示例
float sensor_val = read_sensor();
if (!is_safe_float(sensor_val)) {
// 用上一次有效值代替,或者报错
sensor_val = last_valid_value;
log_error("传感器返回异常值");
}
避坑指南:我曾经在一个 PID 控制器里忘了检查积分项的 NaN,结果积分项变成 NaN 后,整个输出也变成 NaN,电机直接满速运转。嗯,那次差点把机械臂干报废。从那以后,我所有浮点运算的输入输出都会加 isfinite 检查。
三、精度问题:你以为的精度不是你以为的
float 只有 23 位尾数,大约 7 位十进制有效数字。double 有 52 位尾数,大约 15 位有效数字。很多人知道这个数字,但没意识到这意味着什么。
看个例子:
float a = 16777216.0f; // 2^24
float b = a + 1.0f;
printf("a = %f\n", a); // 16777216.000000
printf("b = %f\n", b); // 16777216.000000 —— 没变!
printf("a == b: %d\n", a == b); // 1(相等!)
为什么?因为 float 的精度不够,16777217 无法精确表示,被舍入回了 16777216。这不是 bug,是浮点数的物理限制。
精度损失的常见场景:
| 场景 | 问题 | 建议 |
|---|---|---|
| 大数加小数 | 小数被吃掉 | 先排序,从小加到大 |
| 相近数相减 | 有效数字丢失 | 用数学变换避免直接相减 |
| 累加大量小值 | 误差累积 | 用 Kahan 求和算法 |
| 循环迭代 | 误差逐级放大 | 定期重置或使用更高精度 |
Kahan 求和算法是个好东西,我经常在需要累加大量浮点数的场景用:
float kahan_sum(float *data, int n) {
float sum = 0.0f;
float c = 0.0f; // 补偿项
for (int i = 0; i < n; i++) {
float y = data[i] - c;
float t = sum + y;
c = (t - sum) - y; // 计算丢失的低位
sum = t;
}
return sum;
}
我的经验:在嵌入式系统里,能用 int 就别用 float。比如电压采集,ADC 出来是 12 位整数,直接做整数运算,最后再转浮点显示。这样既快又准。你想想看,为了省那点开发时间,后面花三天查浮点 bug,不值当。
四、知识体系总览
下面这张图把浮点数的核心陷阱串起来了。我建议你把它存下来,写代码前瞄一眼:
浮点数不是数学里的实数,它是有限精度的近似表示。接受这个事实,你的代码就会少很多莫名其妙的 bug。记住三句话:比较用误差、运算前检查、精度要心里有数。
公众号:蓝海资料掘金营,微信deep3321