确定浮点数中的小数位数

Determining the number of decimal digits in a floating number

我正在尝试编写一个程序来输出给定数字 (0.128) 的小数部分的位数。

我制作了以下程序:

#include <stdio.h>
#include <math.h>
int main(){

    float result = 0;
    int count = 0;
    int exp = 0;
    
    for(exp = 0; int(1+result) % 10 != 0; exp++)
    {
        result = 0.128 * pow(10, exp);
        count++;
    }
    printf("%d \n", count);
    printf("%f \n", result);

    return 0;
}

我的想法是 exp 一直递增,直到 int(1+result) % 10 输出 0。因此,例如当 result = 0.128 * pow(10,4) = 1280 时,结果 mod 10 (int(1+result) % 10) 将输出 0 并且循环将停止。

我知道,在更大的规模上,这种方法仍然效率低下,因为如果 result 是给定的输入,例如 1.1208,程序基本上会在比期望值少一位时停止;但是,我试图首先找出我面临当前问题的原因。

我的问题:循环不会仅仅停在1280;它一直循环直到它的值达到 128000000.000000.

这是我 运行 程序时的输出:

10 
128000000.000000 

抱歉,如果我的描述含糊不清,非常感谢任何给予的帮助。

I am trying to write a program that outputs the number of the digits in the decimal portion of a given number (0.128).

这个任务基本上是不可能的,因为在传统的(二进制)机器上这个目标没有意义。

如果我写

float f = 0.128;
printf("%f\n", f);

明白了

0.128000

我可能会得出结论,0.128 是三位数。 (别管三个 0。)

但是如果我再写

printf("%.15f\n", f);

明白了

0.128000006079674

等一下!这是怎么回事?现在它有多少位数?

习惯上说浮点数“不准确”或者说它们存在“舍入误差”。但事实上,浮点数以它们自己的方式非常准确 — 只是它们在 基数 中是准确的,而不是我们习惯于考虑的基数 10 .

令人惊讶的事实是大多数十进制(以 10 为基数)分数 不存在 作为有限二进制分数。这类似于数字 1/3 甚至不作为有限小数存在的方式。您可以将 1/3 近似为 0.333 或 0.3333333333 或 0.33333333333333333333,但如果没有无限多个 3,它只是一个近似值。同样,您可以将 2 底的 1/10 近似为 0b0.000110b0.0001100110b0.000110011001100110011001100110011,但如果没有无限数量的 0011,它也只是一个近似。 (最后一个版本,二进制小数点后 33 位,计算结果约为 0.0999999999767。)

你能想到的大多数小数都是一样的,包括0.128。所以当我写

float f = 0.128;

我在f中实际得到的是二进制数0b0.00100000110001001001101111,十进制正好是0.12800000607967376708984375。

一旦一个数字被存储为 float(或 double,就此而言)它就是它的样子:没有办法重新发现它最初是从“nice, round”小数,比如 0.128。如果您尝试“计算小数位数”,并且您的代码确实非常精确,您很可能会得到 26 的答案(即对应于数字“12800000607967376708984375”),而不是 3。


P.S。 如果您使用的是实现十进制浮点数的计算机硬件,那么这个问题的目标将是有意义的、可能的和易于处理的。并且确实存在十进制浮点数的实现。但是普通的 floatdouble 值中的任何一个都可能在当今任何常见的大众市场计算机上使用,它们总是会是二进制的(具体来说,符合 IEEE-754)。


P.P.S。我在上面写道,“我在 f 中实际得到的是二进制数 0b0.00100000110001001001101111”。如果你计算那里的有效位数——100000110001001001101111——你会得到 24,这根本不是巧合。您可以在 single precision floating-point format 上看到 float 的有效数字部分有 24 位(显式存储了 23 位),在这里,您看到了它的实际效果。

float 对比代码

二进制 float 不能准确编码 0.128,因为它不是 dyadic rational
相反,它取一个附近的值:0.12800000607967376708984375。 26位数字。

舍入误差

OP 的方法在 result = 0.128 * pow(10, exp); 中产生舍入误差。

需要扩展数学

目标很难。示例:FLT_TRUE_MIN 大约需要 149 个数字。

我们可以使用 doublelong double 来达到目的。
只需在每一步中将分数乘以 10.0。
d *= 10.0; 仍然会产生舍入误差,但比 OP 的方法少。

#include <stdio.h>
#include <math.h> int main(){
    int count = 0;

    float f =  0.128f;
    double d =  f - trunc(f);
    printf("%.30f\n", d);
    while (d) {
      d *= 10.0;
      double ipart = trunc(d);
      printf("%.0f", ipart);
      d -= ipart;
      count++;
    }
    printf("\n");
    printf("%d \n", count);
    return 0;
 }

输出

0.128000006079673767089843750000
12800000607967376708984375
26 

实用性

通常,超过 FLT_DECMAL_DIG (9) 或如此重要的小数位,OP 的目标通常没有那么有用。

正如其他人所说,十进制位数在使用二进制浮点数时没有意义。

但是你的终止条件也有缺陷。循环测试是 (int)(1+result) % 10 != 0 意味着只要我们遇到最后一位是 9 的整数,它就会停止。

这意味着0.90.990.9999都给出了2的结果。

我们还通过截断开始的 double 值来丢失精度,方法是将其存储到 float.

我们能做的最有用的事情是当剩余的小数部分小于所用类型的精度时终止。


建议的工作代码:

#include <math.h>
#include <float.h>
#include <stdio.h>

int main(void)
{
    double val = 0.128;
    double prec = DBL_EPSILON;
    double result;
    int count = 0;

    while (fabs(modf(val, &result)) > prec) {
        ++count;
        val *= 10;
        prec *= 10;
    }
    printf("%d digit(s): %0*.0f\n", count, count, result);
}

结果:

3 digit(s): 128