如何在 C 语言中直接计算 float 的取值范围?
实际编程时,可以用 <float.h> 中的宏获取当前实现的 float 范围。做练习时,也可以根据浮点表示的底数、有效位数和指数范围推导边界。无符号整数反复乘 2、等待回绕的方法,不能直接用于浮点数。
先明确“最小值”指什么
浮点数有几个容易混淆的边界:
| 边界 | 含义 |
|---|---|
FLT_MAX | 最大有限正数 |
-FLT_MAX | 最负的有限值 |
FLT_MIN | 最小正规正数 |
FLT_TRUE_MIN | 最小非零正数,C11 起提供 |
有限值的上下界是 [-FLT_MAX, FLT_MAX],但区间内只有离散的数能被精确表示。注意,FLT_MIN 是最小正规正数,不是最负的 float。
支持 subnormal(非正规数)时,FLT_TRUE_MIN 小于 FLT_MIN;没有非正规数时,两者相等。
#include <float.h>
#include <stdio.h>
int main(void)
{
printf("lowest finite float: %.*g\n", DECIMAL_DIG, -(double)FLT_MAX);
printf("largest finite float: %.*g\n", DECIMAL_DIG, (double)FLT_MAX);
printf("smallest positive normal: %.*g\n", DECIMAL_DIG, (double)FLT_MIN);
#ifdef FLT_TRUE_MIN
printf("smallest positive value: %.*g\n", DECIMAL_DIG, (double)FLT_TRUE_MIN);
#endif
return 0;
}
示例使用了 C99 起提供的 DECIMAL_DIG。通过 ... 传给 printf 时,float 本来就会提升为 double,这里的转换只是把类型写清楚。
根据浮点表示计算边界
C 不要求所有实现都把 float 定义成 IEEE754 binary32,因此不能只凭类型名称就假定它有 32 位。
对于 C 的浮点模型,设:
b = FLT_RADIX:表示使用的底数。p = FLT_MANT_DIG:有效数字位数,按底数b计。emin = FLT_MIN_EXP。emax = FLT_MAX_EXP。
边界公式为:
[ \mathrm{FLT_MAX}=(1-b^{-p})b^{emax} ]
[ \mathrm{FLT_MIN}=b^{emin-1} ]
如果实现支持该格式的渐进下溢,最小非正规正数为:
[ b^{emin-p} ]
这些公式可用于推导边界。在程序中直接计算时,要留意中间结果的类型。例如,先计算 b^emax,结果可能已经超出 float 的有限范围。
IEEE754 binary32 的具体结果
如果明确采用 IEEE754 binary32,正规数有 24 位二进制有效数字,包括隐含的最高位;正规数的无偏指数范围是 -126 到 127。
最大有限值由最大的有效数字和最大的有限指数得到:
[ \mathrm{FLT_MAX}=(2-2^{-23})2^{127} \approx 3.402823466\times10^{38} ]
最小正规正数为:
[ \mathrm{FLT_MIN}=2^{-126} \approx 1.175494351\times10^{-38} ]
最小非正规正数为:
[ \mathrm{FLT_TRUE_MIN}=2^{-149} \approx 1.401298464\times10^{-45} ]
在通常使用 binary32 的 float 和 binary64 的 double、并保留非正规数的环境中,可以这样演示计算:
#include <math.h>
#include <stdio.h>
int main(void)
{
float largest = (float)ldexp(2.0 - ldexp(1.0, -23), 127);
float smallest_normal = (float)ldexp(1.0, -126);
float smallest_positive = (float)ldexp(1.0, -149);
printf("largest: %.9g\n", (double)largest);
printf("smallest normal: %.9g\n", (double)smallest_normal);
printf("smallest positive: %.9g\n", (double)smallest_positive);
return 0;
}
ldexp(x, n) 计算 x * 2^n。这段代码根据已知的表示参数计算边界,其中的常量不能直接套用到其他浮点格式。
为什么不建议循环乘 2,直到浮点数溢出
无符号整数遵循模运算规则,达到边界后可以回绕。浮点数不按这套规则运行。
在常见 IEEE754 环境中,反复乘 2 可能最终得到正无穷,但这种溢出探测方法并不适用于所有 C 实现,结果还可能受到浮点异常和编译选项的影响。而且,反复乘 2 只能生成 2 的整数次幂,得不到有效位全为 1 的最大有限值。
如果练习要求不用 <float.h>,应明确允许假设哪一种浮点表示。已知采用 IEEE754 binary32 时,可以按上述公式计算;如果要求适用于所有 C 实现,就不能直接写死这些参数。
ui_max + 1 为什么会回绕
当 ui_max 是 unsigned int 时:
ul_max = ui_max + 1;
加法先按 unsigned int 的规则计算,结果再赋给 ul_max。若 ui_max == UINT_MAX,加法结果已经回绕为 0,后面的赋值无法恢复它。
应在加法之前转换到能够容纳结果的类型。例如,只有在 unsigned long 的范围确实更大时,下面的写法才成立:
#include <limits.h>
#if ULONG_MAX > UINT_MAX
unsigned long next_after_uint = (unsigned long)UINT_MAX + 1UL;
#endif
原代码的 (long)ui_max + 1 也有前提:long 必须能够表示转换值以及加法结果。C 不保证 long 的范围一定大于 unsigned int。
short 部分表现不同,通常与 integer promotions 有关:unsigned short 在运算前可能提升为 int,而 unsigned int 不会这样提升。具体行为取决于各类型的范围。另外,将超出目标有符号类型范围的无符号值赋给有符号变量,也不能作为通用的范围推导方法。核对整数边界时,应使用 <limits.h> 中对应的宏。
备注:内容仅供参考。
