如何以完美的准确性打印浮点数值以供后续扫描?

6
假设我有一个浮点类型的floatdouble值(在典型机器上为32位或64位),我想将这个值打印为文本(例如到标准输出流),然后稍后,在某些其他进程中,使用fscanf()(如果我使用C)或可能是istream::operator>>()(如果我使用C ++),将其扫描回来。但是,我需要扫描的浮点数最终与原始值完全相同(直到等效表示相同的值)。此外,所打印的值应该对人类容易阅读,即我不想打印0x42355316并将其重新解释为32位浮点数。
如何做到这一点?我假设(C和C ++)的标准库不足以实现这一点,但我可能是错的。我认为足够数量的小数位数可能能够保证低于精度阈值的错误,但这与保证舍入/截断方式完全相同是不同的。
注意: - 扫描不必在扫描的值方面完全准确,只需与原始值相同即可。 - 如果这样更容易,您可以假设该值是数字且不是无穷大。 - 希望支持非规格化数,但不是必需的;仍然,如果我们得到非规格化数,则应该明显失败。

1
你是否考虑使用%a格式,它易于阅读且类似于浮点数?或者使用其C++ iostream等效形式? - Adrian Mole
1
@ShadiNaif 但是没有必要重新解释任何东西!使用scanf中的%a格式将会把值直接读入一个double变量。IEEE数字可以被以十六进制浮点格式准确地表示(始终如此),因为没有暗示的十进制转换/从中进行转换。 - Adrian Mole
是的@AdrianMole,但就我所理解的问题而言;它必须可读作为普通数字,而不是十六进制。如果我错了,请纠正我,einpoklum。 - Shadi Naif
1
@ShadiNaif 这就是我在第一条评论中提出问题的原因。 "易于阅读" 是指人类还是计算机? - Adrian Mole
1
已经有三个答案了 - 我们都没有等待您的澄清! - Adrian Mole
显示剩余4条评论
3个回答

4
首先,您应该使用fprintffscanf%a格式。这就是它的设计初衷,并且C标准要求如果实现使用二进制浮点,则必须起作用(重现原始数字)。
如果无法实现以上内容,您应该使用至少具有FLT_DECIMAL_DIG有效数字的float和至少具有DBL_DECIMAL_DIG有效数字的double进行打印。这些常量在<float.h>中定义,定义如下:

…十进制位数n,使得任何具有p个基数b数字的浮点数可以四舍五入为一个具有n个小数位的浮点数,然后再转换回去而不改变值……[b是浮点格式使用的基数,由FLT_RADIX定义,p是格式中基于b的位数。]

例如:
    printf("%.*g\n", FLT_DECIMAL_DIG, 1.f/3);

或:
#define QuoteHelper(x)  #x
#define Quote(x)        QuoteHelper(x)
…
    printf("%." Quote(FLT_DECIMAL_DIG) "g\n", 1.f/3);

在C++中,这些常量在<limits>中定义为std::numeric_limits<Type>::max_digits10,其中Typefloatdouble或另一种浮点类型。
请注意,C标准仅建议通过十进制数字进行此类往返操作;它并不要求这样做。例如,C 2018 5.2.4.2.2 15在“建议实践”标题下指出:
从(至少)double转换为带有DECIMAL_DIG位数的十进制数,然后再转回去应该是一个恒等函数。[DECIMAL_DIG相当于实现中支持的最宽浮点格式的FLT_DECIMAL_DIGDBL_DECIMAL_DIG]。
相反,如果使用%a,并且FLT_RADIX是2的幂(表示实现使用基数为2、16或另一个2的幂的浮点基数),那么C标准要求扫描使用%a产生的数字所得到的结果等于原始数字。

2
我需要扫描到的浮点数与原始值完全相同,可以通过使用%a格式说明符来实现。
此外,打印出的值应该容易被人类读取为浮点数,即我不想打印0x42355316并将其重新解释为32位浮点数。这更加棘手和主观。 %a生成的字符串的第一部分实际上是由十六进制数字组成的分数,因此输出如0x1.4p+3可能需要一些时间才能被人类读者解析为10
一个选项是打印所有表示浮点值所需的小数位数,但可能有很多位。例如,考虑值0.1,它作为64位浮点数的最接近表示可能是:
0x1.999999999999ap-4  ==  0.1000000000000000055511151231257827021181583404541015625

当执行printf("%.*lf\n", DBL_DECIMAL_DIG, 01);时(例如参考Eric的答案),会打印出

0.10000000000000001   // If DBL_DECIMAL_DIG == 17

我的建议介于两者之间。与%a类似,我们可以将任何以基数2表示为分数乘以2的整数幂的浮点数精确表示。我们可以将该分数转换为整数(相应地增加指数),并将其作为十进制值打印出来。

0x1.999999999999ap-4 --> 1.999999999999a16 * 2-4  --> 1999999999999a16 * 2-56 
                     --> 720575940379279410 * 2-56

这个整数有限的数字(小于2的53次方),但结果仍然是原始double值的精确表示。

以下代码片段是概念证明,没有检查角落情况。格式说明符%ap字符(如“...乘以2的Power次方...”)分隔尾数和指数,我将使用一个q代替,没有特别的原因,只是使用了不同的符号。

尾数的值也将被减少(并相应地提高指数),删除所有尾随的零位。想法是5q+1(解析为510* 21)应该更容易地被识别为10,而不是2814749767106560q-48

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void to_my_format(double x, char *str)
{
    int exponent;
    double mantissa = frexp(x, &exponent);
    long long m = 0;
    if ( mantissa ) {
        exponent -= 52;
        m = (long long)scalbn(mantissa, 52);
        // A reduced mantissa should be more readable
        while (m  &&  m % 2 == 0) {
            ++exponent;
            m /= 2;
        }
    }
    sprintf(str, "%lldq%+d", m, exponent);
    //                ^
    // Here 'q' is used to separate the mantissa from the exponent  
}

double from_my_format(char const *str)
{
    char *end;
    long long mantissa = strtoll(str, &end, 10);
    long exponent = strtol(str + (end - str + 1), &end, 10);
    return scalbn(mantissa, exponent);
}

int main(void)
{
    double tests[] = { 1, 0.5, 2, 10, -256, acos(-1), 1000000, 0.1, 0.125 };
    size_t n = (sizeof tests) / (sizeof *tests);
    
    char num[32];
    for ( size_t i = 0; i < n; ++i ) {
        to_my_format(tests[i], num);
        double x = from_my_format(num);
        printf("%22s%22a ", num, tests[i]);
        if ( tests[i] != x )
            printf(" *** %22a *** Round-trip failed\n", x);
        else
            printf("%58.55g\n", x);
    }
    return 0;
}

请点击这里进行可测试性的测试。

通常来说,阅读体验的改进可以说微乎其微,这当然是个人观点。


我猜测p代表的是“乘以2的幂次方”而不是e?但是,q又代表什么意思呢? - einpoklum
@einpoklum,我道歉,它除了成为不同的符号之外没有特定的含义。当然,如果有人考虑这种表示方式,最好想出一个更有意义的符号。 - Bob__
不需要道歉,只需编辑答案以说明pq的意义即可...我已经给了你我的+1。 - einpoklum

0

您可以使用%a格式说明符将值打印为十六进制浮点数。请注意,这与重新解释float为整数并打印整数值不同。

例如:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    float x;
    scanf("%f", &x);
    printf("x=%.7f\n", x);

    char str[20];
    sprintf(str, "%a", x);
    printf("str=%s\n", str);

    float y;
    sscanf(str, "%f", &y);
    printf("y=%.7f\n", y);
    printf("x==y: %d\n", (x == y));

    return 0;
}

输入为4时,输出结果为:

x=4.0000000
str=0x1p+2
y=4.0000000
x==y: 1

输入为3.3时,输出结果为:

x=3.3000000
str=0x1.a66666p+1
y=3.3000000
x==y: 1

从输出结果可以看出,%a格式说明符以指数格式打印,其中尾数为十六进制,指数为十进制。通过等式检查,可以直接将此格式转换回完全相同的值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接