保持浮点数精度的Printf宽度标识符

158
有没有一种适用于浮点数的printf宽度说明符,可以自动将输出格式化为必要数量的有效数字,以便在扫描字符串时获得原始浮点值?
例如,假设我将一个float打印到2个小数位的精度:
float foobar = 0.9375;
printf("%.2f", foobar);    // prints out 0.94

当我扫描输出值0.94时,我无法保证符合标准的保证我会得到原始的0.9375浮点数返回(在这个例子中,我可能不会)。
我想要一种方法告诉printf自动打印浮点数值到必要的有效数字位数,以确保可以被扫描回传给printf的原始值。
我可以使用float.h中的一些宏来推导最大宽度传递给printf,但是是否已经有一个说明符自动打印到必要的有效数字位数 - 或者至少到最大宽度?

4
所以你只是建议使用凭空假设而不是采用可移植的方法? - user529758
3
不,我不建议使用“凭空假设”的方式,我建议使用 printf("%f", val) ,这种方法已经是可移植、高效和默认的。 - bobobobo
2
@bobobobo,为了让我能将其添加到答案中,您能否引用C99标准中规定的条款,说明如果未指定精度,则printf语句默认会以最大精度输出浮点类型? - Vilhelm Gray
2
@bobobobo,抱歉,我觉得你可能误解了问题。我关心的是输出的精度(即打印的字符数),而不是数据类型的精度(即float/double如何准确地表示真实值)。 - Vilhelm Gray
2
@Vilhelm Gray C11dr 5.2.4.2.2 "... 十进制数字的数量n,使得任何具有p个基数b数字的浮点数都可以四舍五入为具有n个十进制数字的浮点数,并且再次转换而不改变值,p log10 b b是10的幂⎡1 + p log10 b⎤否则FLT_DECIMAL_DIG 6 DBL_DECIMAL_DIG 10 LDBL_DECIMAL_DIG 10 ..."其中6,10,10是最小值。 - chux - Reinstate Monica
显示剩余6条评论
9个回答

119

我推荐@Jens Gustedt的十六进制解决方案:使用%a。

OP想要“以最大精度打印(或至少打印到最重要的小数位)”。

一个简单的例子是如何打印1/7:

#include <float.h>
int Digs = DECIMAL_DIG;
double OneSeventh = 1.0/7.0;
printf("%.*e\n", Digs, OneSeventh);
// 1.428571428571428492127e-01

但是让我们深入了解一下...

从数学上讲,答案是“0.142857 142857 142857 ...”,但我们使用的是有限精度浮点数。假设我们使用IEEE 754双精度二进制。 因此,OneSeventh = 1.0 / 7.0 的结果如下所示。同时显示了前一个和后一个可表示的double浮点数。

OneSeventh before = 0.1428571428571428 214571170656199683435261249542236328125
OneSeventh        = 0.1428571428571428 49212692681248881854116916656494140625
OneSeventh after  = 0.1428571428571428 769682682968777953647077083587646484375

打印double精确十进制表示法有限的用途。

C有两组宏在<float.h>中可以帮助我们。
第一组是在十进制字符串中打印有效数字的数量,以便在扫描字符串时获取原始浮点数。 它们显示了C规范的最小值示例C11编译器。

FLT_DECIMAL_DIG   6,  9 (float)                           (C11)
DBL_DECIMAL_DIG  10, 17 (double)                          (C11)
LDBL_DECIMAL_DIG 10, 21 (long double)                     (C11)
DECIMAL_DIG      10, 21 (widest supported floating type)  (C99)

第二组是指一个字符串能够被扫描成一个浮点数并打印出来仍然保持相同的字符串呈现方式中的有效数字位数。这些数字与C语言的规范中的最小值以及C11编译器的示例一起显示。我相信在C99之前就已经可用了。

FLT_DIG   6, 6 (float)
DBL_DIG  10, 15 (double)
LDBL_DIG 10, 18 (long double)

第一组宏似乎符合楼主的要求,能保留有效数字。但是那个并不总是可用。

#ifdef DBL_DECIMAL_DIG
  #define OP_DBL_Digs (DBL_DECIMAL_DIG)
#else  
  #ifdef DECIMAL_DIG
    #define OP_DBL_Digs (DECIMAL_DIG)
  #else  
    #define OP_DBL_Digs (DBL_DIG + 3)
  #endif
#endif

"+ 3"是我之前回答的关键。问题在于,如果已知往返转换字符串-FP-字符串(C89后可用的第2组宏),如何确定FP-字符串-FP的数字?通常,结果是加3。

现在已知要打印多少个有效数字,并由<float.h>控制。

要打印N个有效十进制数字,可以使用各种格式。

使用"%e"精度字段是小数点后面的位数。因此,应该用-1。注意:这个-1不在最初的int Digs = DECIMAL_DIG;中。

printf("%.*e\n", OP_DBL_Digs - 1, OneSeventh);
// 1.4285714285714285e-01

使用"%f"格式说明符时,precision字段表示小数点后的位数。 例如,对于像OneSeventh/1000000.0这样的数字,需要OP_DBL_Digs + 6才能看到所有significant位数。

printf("%.*f\n", OP_DBL_Digs    , OneSeventh);
// 0.14285714285714285
printf("%.*f\n", OP_DBL_Digs + 6, OneSeventh/1000000.0);
// 0.00000014285714285714285

注意:许多人使用"%f"。这将在小数点后显示6位数字;6是显示默认值,而不是数字的精度。


1
@Jingguo Yao 同意参考文献中所说的“精度指定了‘%f’后小数点字符后面有多少位数字”。这里的“精度”并不是以数学意义使用,而只是用来定义小数点后的数字位数。数学上,1234567890.123具有13位精度或有效数字。0.000000000123具有3位数学精度,而不是13位。浮点数是对数分布的。本答案使用了“有效数字”和数学上的“精度”的概念。 - chux - Reinstate Monica
1
@Slipp D. Thompson:“这里展示了C规范的最小值和一个C11编译器的示例。” - chux - Reinstate Monica
1
@chux 啊,我在写作中错过了那个相关性。谢谢。 - Slipp D. Thompson
1
确实,你是对的——我的技巧只适用于值的大小在1.0和1.0eDBL_DIG之间的情况,这可以说是使用 "%f" 打印的唯一适当范围。 如你所示,使用 "%e" 当然是一个更好的方法,并且实际上是一个不错的答案(尽管如果有 "%a" 可用,使用 "%a" 可能会更好,当然,如果 DBL_DECIMAL_DIG 可用,则应该可用)。 我一直希望有一个格式说明符,它总是精确地四舍五入到最大精度(而不是硬编码的6位小数)。 - Greg A. Woods
@pmor应该是max(0, DBL_DECIMAL_DIG - log10(fabs(x))); - undefined
显示剩余13条评论

105

如何无损打印浮点数(使它们可以被准确地读取并返回到相同的数字,除了 NaN 和 Infinity):

  • 如果您的类型是 float:使用 printf("%.9g", number)
  • 如果您的类型是 double:使用 printf("%.17g", number)

请勿使用 %f,因为它仅指定小数点后有多少个有效数字,并将截断小数。 有关参考信息,魔法数字 9 和 17 可在 float.h 中找到,该文件定义了 FLT_DECIMAL_DIGDBL_DECIMAL_DIG


9
你能否解释一下%g格式说明符的含义? - Vilhelm Gray
19
%g用于打印数字时,会将精度设置为需要的位数,并在数字很小或很大(例如1e-5而不是.00005)时更倾向于使用指数语法,并跳过任何尾随的零(例如1而不是1.00000)。 - ccxvii
在我的编译器(C++Builder XE)中,DBL_DIG代替了DBL_DECIMAL_DIG,其值为15而不是17。 - Albert Wiersch
1
双精度浮点数的尾数长度为53位(1位是隐式的)。因此,双精度浮点数的精度为53 / log2(10)= 15.95位小数。因此,如果要以十进制格式明确表示IEEE 754数字,则需要至少使用ceil(53 / log2(10))= 16个小数位。在我的程序中,我使用了17个小数位,只是为了确保。不确定哪个值是正确的,是16还是17,但15位肯定不够用。 - truthseeker
5
你对%.16g的行为存在误解;它不能够区分1.000_0000_0000_0000_2e-01和1.000_0000_0000_0000_3e-01。需要使用%.17g。 - Don Hatch
1
顺便提一下,这里有几个不错的参考资料,可以解释和推导出9和17:http://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2005.pdf - Don Hatch

26

如果你只对二进制位(或十六进制模式)感兴趣,你可以使用%a格式。这个格式保证了:

如果值在基数2下存在精确表示,那么默认的精度就足以进行精确表示;否则,也足够区分double类型的值。

需要说明的是,这个功能只在C99之后才可用。


24

不,没有这样的 printf 宽度说明符可以打印具有最大精度的浮点数。让我解释一下为什么。

floatdouble 的最大精度是可变的,取决于 floatdouble实际值

回想一下,floatdouble符号.指数.尾数 格式存储。这意味着用于小数部分的比特位数比大数多得多

enter image description here

例如,float 可以轻松区分 0.0 和 0.1。

float r = 0;
printf( "%.6f\n", r ) ; // 0.000000
r+=0.1 ;
printf( "%.6f\n", r ) ; // 0.100000

但是float不知道1e271e27 + 0.1之间的差别。

r = 1e27;
printf( "%.6f\n", r ) ; // 999999988484154753734934528.000000
r+=0.1 ;
printf( "%.6f\n", r ) ; // still 999999988484154753734934528.000000

这是因为所有的精度(由尾数位数限制)都用于小数点左边的大部分数字,因此剩下的小数部分就无法再获得足够的精度。

%.f格式化控制符仅指定要从浮点数中打印多少个小数位作为格式化输出。可用的精度取决于数字的大小,如何处理取决于你作为程序员而言。 printf不能/不会替你处理这个问题。


2
这是一个关于准确打印浮点数到特定小数位的限制的优秀解释。然而,我认为我的原始措辞过于模糊,因此我已经更新了我的问题,避免使用“最大精度”一词,希望能够消除混淆。 - Vilhelm Gray
这仍然取决于您要打印的数字的值。 - bobobobo
3
这部分是正确的,但它没有回答问题,而且你对OP的问题产生了困惑。他正在询问是否可以查询一个"float"提供的有效[小数]位数,而你断言这样的东西不存在(即不存在“FLT_DIG”),这是错误的。 - user529758
@H2CO3 也许你应该编辑我的帖子并给我点个踩(开玩笑)。这个答案声称 FLT_DIG 没有任何意义。而这个答案则声称浮点数内部的值决定了可用的小数位数。 - bobobobo
1
你是否假设格式字母必须是“f”?我认为这并非必需。我理解问题的意思是OP正在寻找某些printf格式说明符,以产生非损失性的往返,因此@ccxvii的答案(对于float是“%.9g”,对于double是“%.17g”)是一个好答案。也许从问题中删除“宽度”一词会更好。 - Don Hatch
1
这不是问题所问的。 - Jarrod Smith

12
只需使用<float.h>中的宏和变宽转换说明符(".*")即可:
float f = 3.14159265358979323846;
printf("%.*f\n", FLT_DIG, f);

3
你的意思是这样吗:printf("%." FLT_DIG "f\n", f); - Vilhelm Gray
4
+1 可以用于 %e,但对于 %f 效果不佳:只有在知道要打印的值接近 1.0 时才可以使用。 - Pascal Cuoq
4
%e会在输出非常小的数字时保留有效数字,而%f则不会。例如,当x = 1e-100时,%.5f将输出0.00000(丢失所有精度),而%.5e将输出1.00000e-100 - chux - Reinstate Monica
1
@bobobobo 此外,你的观点是错误的,因为它“提供更准确的原因”。FLT_DIG被定义为其所定义的值有其原因。如果它是6,那是因为float不能保留超过6位有效数字。如果您使用%.7f打印它,最后一位数字将没有意义。 - user529758
6
@bobobobo 不, %.6f 并不等同,因为 FLT_DIG 并不总是6。而且谁在乎效率呢?I/O 已经很耗费资源了,多一个或少一个小数位的精度不会导致瓶颈。 - user529758
显示剩余14条评论

6

在我的一个回答评论中,我抱怨很长一段时间以来一直想以十进制形式打印浮点值中的所有有效数字,就像问题所问的那样。好吧,最终我坐下来写了它。它不是完美的,这是演示代码,打印额外的信息,但它大多数情况下适用于我的测试。如果你(任何人)想要整个驱动程序的副本进行测试,请告诉我。

static unsigned int
ilog10(uintmax_t v);

/*
 * Note:  As presented this demo code prints a whole line including information
 * about how the form was arrived with, as well as in certain cases a couple of
 * interesting details about the number, such as the number of decimal places,
 * and possibley the magnitude of the value and the number of significant
 * digits.
 */
void
print_decimal(double d)
{
        size_t sigdig;
        int dplaces;
        double flintmax;

        /*
         * If we really want to see a plain decimal presentation with all of
         * the possible significant digits of precision for a floating point
         * number, then we must calculate the correct number of decimal places
         * to show with "%.*f" as follows.
         *
         * This is in lieu of always using either full on scientific notation
         * with "%e" (where the presentation is always in decimal format so we
         * can directly print the maximum number of significant digits
         * supported by the representation, taking into acount the one digit
         * represented by by the leading digit)
         *
         *        printf("%1.*e", DBL_DECIMAL_DIG - 1, d)
         *
         * or using the built-in human-friendly formatting with "%g" (where a
         * '*' parameter is used as the number of significant digits to print
         * and so we can just print exactly the maximum number supported by the
         * representation)
         *
         *         printf("%.*g", DBL_DECIMAL_DIG, d)
         *
         *
         * N.B.:  If we want the printed result to again survive a round-trip
         * conversion to binary and back, and to be rounded to a human-friendly
         * number, then we can only print DBL_DIG significant digits (instead
         * of the larger DBL_DECIMAL_DIG digits).
         *
         * Note:  "flintmax" here refers to the largest consecutive integer
         * that can be safely stored in a floating point variable without
         * losing precision.
         */
#ifdef PRINT_ROUND_TRIP_SAFE
# ifdef DBL_DIG
        sigdig = DBL_DIG;
# else
        sigdig = ilog10(uipow(FLT_RADIX, DBL_MANT_DIG - 1));
# endif
#else
# ifdef DBL_DECIMAL_DIG
        sigdig = DBL_DECIMAL_DIG;
# else
        sigdig = (size_t) lrint(ceil(DBL_MANT_DIG * log10((double) FLT_RADIX))) + 1;
# endif
#endif
        flintmax = pow((double) FLT_RADIX, (double) DBL_MANT_DIG); /* xxx use uipow() */
        if (d == 0.0) {
                printf("z = %.*s\n", (int) sigdig + 1, "0.000000000000000000000"); /* 21 */
        } else if (fabs(d) >= 0.1 &&
                   fabs(d) <= flintmax) {
                dplaces = (int) (sigdig - (size_t) lrint(ceil(log10(ceil(fabs(d))))));
                if (dplaces < 0) {
                        /* XXX this is likely never less than -1 */
                        /*
                         * XXX the last digit is not significant!!! XXX
                         *
                         * This should also be printed with sprintf() and edited...
                         */
                        printf("R = %.0f [%d too many significant digits!!!, zero decimal places]\n", d, abs(dplaces));
                } else if (dplaces == 0) {
                        /*
                         * The decimal fraction here is not significant and
                         * should always be zero  (XXX I've never seen this)
                         */
                        printf("R = %.0f [zero decimal places]\n", d);
                } else {
                        if (fabs(d) == 1.0) {
                                /*
                                 * This is a special case where the calculation
                                 * is off by one because log10(1.0) is 0, but
                                 * we still have the leading '1' whole digit to
                                 * count as a significant digit.
                                 */
#if 0
                                printf("ceil(1.0) = %f, log10(ceil(1.0)) = %f, ceil(log10(ceil(1.0))) = %f\n",
                                       ceil(fabs(d)), log10(ceil(fabs(d))), ceil(log10(ceil(fabs(d)))));
#endif
                                dplaces--;
                        }
                        /* this is really the "useful" range of %f */
                        printf("r = %.*f [%d decimal places]\n", dplaces, d, dplaces);
                }
        } else {
                if (fabs(d) < 1.0) {
                        int lz;

                        lz = abs((int) lrint(floor(log10(fabs(d)))));
                        /* i.e. add # of leading zeros to the precision */
                        dplaces = (int) sigdig - 1 + lz;
                        printf("f = %.*f [%d decimal places]\n", dplaces, d, dplaces);
                } else {                /* d > flintmax */
                        size_t n;
                        size_t i;
                        char *df;

                        /*
                         * hmmmm...  the easy way to suppress the "invalid",
                         * i.e. non-significant digits is to do a string
                         * replacement of all dgits after the first
                         * DBL_DECIMAL_DIG to convert them to zeros, and to
                         * round the least significant digit.
                         */
                        df = malloc((size_t) 1);
                        n = (size_t) snprintf(df, (size_t) 1, "%.1f", d);
                        n++;                /* for the NUL */
                        df = realloc(df, n);
                        (void) snprintf(df, n, "%.1f", d);
                        if ((n - 2) > sigdig) {
                                /*
                                 * XXX rounding the integer part here is "hard"
                                 * -- we would have to convert the digits up to
                                 * this point back into a binary format and
                                 * round that value appropriately in order to
                                 * do it correctly.
                                 */
                                if (df[sigdig] >= '5' && df[sigdig] <= '9') {
                                        if (df[sigdig - 1] == '9') {
                                                /*
                                                 * xxx fixing this is left as
                                                 * an exercise to the reader!
                                                 */
                                                printf("F = *** failed to round integer part at the least significant digit!!! ***\n");
                                                free(df);
                                                return;
                                        } else {
                                                df[sigdig - 1]++;
                                        }
                                }
                                for (i = sigdig; df[i] != '.'; i++) {
                                        df[i] = '0';
                                }
                        } else {
                                i = n - 1; /* less the NUL */
                                if (isnan(d) || isinf(d)) {
                                        sigdig = 0; /* "nan" or "inf" */
                                }
                        }
                        printf("F = %.*s. [0 decimal places, %lu digits, %lu digits significant]\n",
                               (int) i, df, (unsigned long int) i, (unsigned long int) sigdig);
                        free(df);
                }
        }

        return;
}


static unsigned int
msb(uintmax_t v)
{
        unsigned int mb = 0;

        while (v >>= 1) { /* unroll for more speed...  (see ilog2()) */
                mb++;
        }

        return mb;
}

static unsigned int
ilog10(uintmax_t v)
{
        unsigned int r;
        static unsigned long long int const PowersOf10[] =
                { 1LLU, 10LLU, 100LLU, 1000LLU, 10000LLU, 100000LLU, 1000000LLU,
                  10000000LLU, 100000000LLU, 1000000000LLU, 10000000000LLU,
                  100000000000LLU, 1000000000000LLU, 10000000000000LLU,
                  100000000000000LLU, 1000000000000000LLU, 10000000000000000LLU,
                  100000000000000000LLU, 1000000000000000000LLU,
                  10000000000000000000LLU };

        if (!v) {
                return ~0U;
        }
        /*
         * By the relationship "log10(v) = log2(v) / log2(10)", we need to
         * multiply "log2(v)" by "1 / log2(10)", which is approximately
         * 1233/4096, or (1233, followed by a right shift of 12).
         *
         * Finally, since the result is only an approximation that may be off
         * by one, the exact value is found by subtracting "v < PowersOf10[r]"
         * from the result.
         */
        r = ((msb(v) * 1233) >> 12) + 1;

        return r - (v < PowersOf10[r]);
}

我不在乎它是否回答了问题 - 这真的很令人印象深刻。这需要一些思考,应该得到认可和赞扬。也许如果你能在某种程度上包含完整的测试代码(无论是在这里还是其他地方),那就更好了,但即使没有,这也是一个非常出色的工作。为此给你点个赞! - Pryftan
@GregA.Woods 代码在处理负数时存在问题,因为最后一位数字偏移了一个位置。也许可以使用 snprintf(df, n, "% .1f", d);(添加空格)来修复缓冲区长度,无论是正数还是负数。 - chux - Reinstate Monica
啊,是的,负数。谢谢您的评论!我会在原始来源中做个注记,并在有空时尝试改进它。 - Greg A. Woods

6
据我所知,David Gay在dtoa.c中编写了一个广泛使用的算法,允许输出必要数量的有效数字,以便在将字符串扫描回来时获得原始浮点值。该代码可在Netlib上这里找到(请参阅相关论文)。例如,Python、MySQL、Scilab等软件都使用了这个代码。

David Gay,不是Daniel Gay。(具体来说是David M. Gay。不确定M代表什么。) - Mark VY

6

我进行了一个小实验,以验证使用DBL_DECIMAL_DIG打印确实可以完全保留数字的二进制表示。结果表明,对于我尝试过的编译器和C库,DBL_DECIMAL_DIG确实是所需的位数,即使少打印一位数字也会导致重大问题。

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

union {
    short s[4];
    double d;
} u;

void
test(int digits)
{
    int i, j;
    char buff[40];
    double d2;
    int n, num_equal, bin_equal;

    srand(17);
    n = num_equal = bin_equal = 0;
    for (i = 0; i < 1000000; i++) {
        for (j = 0; j < 4; j++)
            u.s[j] = (rand() << 8) ^ rand();
        if (isnan(u.d))
            continue;
        n++;
        sprintf(buff, "%.*g", digits, u.d);
        sscanf(buff, "%lg", &d2);
        if (u.d == d2)
            num_equal++;
        if (memcmp(&u.d, &d2, sizeof(double)) == 0)
            bin_equal++;
    }
    printf("Tested %d values with %d digits: %d found numericaly equal, %d found binary equal\n", n, digits, num_equal, bin_equal);
}

int
main()
{
    test(DBL_DECIMAL_DIG);
    test(DBL_DECIMAL_DIG - 1);
    return 0;
}

我使用微软的C编译器19.00.24215.1和gcc版本7.4.0 20170516(Debian 6.3.0-18+deb9u1)运行此程序。减少一个小数位数会使完全相等的数字数量减半。(我还验证了rand()确实产生了大约一百万个不同的数字。)以下是详细的结果。

Microsoft C

测试了999507个17位数值:999507个数值上相等,999507个二进制相等
测试了999507个16位数值:545389个数值上相等,545389个二进制相等

GCC

测试了999485个17位数值:999485个数值上相等,999485个二进制相等
测试了999485个16位数值:545402个数值上相等,545402个二进制相等

1
请使用微软的C编译器运行此程序。该编译器可能具有RAND_MAX == 32767。考虑使用u.s[j] = (rand() << 8) ^ rand();或类似方法,以确保所有位都有机会成为0或1。 - chux - Reinstate Monica
的确,它的RAND_MAX为32767,所以您的提议是正确的。 - Diomidis Spinellis
1
我根据@chux-ReinstateMonica的建议更新了帖子,以处理RAND_MAX。结果与之前获得的结果类似。 - Diomidis Spinellis

0

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