【问题标题】:Printf width specifier to maintain precision of floating-point valuePrintf 宽度说明符以保持浮点值的精度
【发布时间】:2013-05-26 05:37:44
【问题描述】:

是否有一个printf 宽度说明符可以应用于浮点说明符,该说明符会自动将输出格式化为必要数量的有效数字,这样在重新扫描字符串时,获取原始浮点值?

例如,假设我将float 打印到2 小数位的精度:

float foobar = 0.9375;
printf("%.2f", foobar);    // prints out 0.94

当我扫描输出 0.94 时,我无法保证我会得到原始的 0.9375 浮点值(在本例中,我可能不会)。

我想告诉printf 自动将浮点值打印到所需的有效数字个数,以确保可以将其扫描回传递给@987654329 的原始值@。

我可以使用float.hderive the maximum width 中的一些宏来传递给printf,但是已经有一个说明符可以自动打印到所需的有效数字--或至少到最大宽度?

【问题讨论】:

  • @bobobobo 所以您只是建议人们凭空做出假设而不是采用可移植的方法?
  • @H2CO3 不,我不建议使用“空中假设”,我建议使用 printf( "%f", val );,它已经是可移植的、高效的和默认设置。
  • @bobobobo 所以我可以将它添加到答案中,您能否引用 C99 标准中的条款,该条款规定 printf 语句将以 最大精度输出浮点类型 i> 如果没有指定精度,默认情况下?
  • @VilhelmGray 正如@chux 所说,对于您的特定double 的实际精度,有一些相当复杂的数学运算。由于您的 double 变得非常大(与 1.0 相差甚远),它实际上在小数部分(值部分小于 1.0)不太准确。所以你不能在这里得到一个满意的答案,因为你的问题有一个错误的假设(即所有floats/doubles 都是平等的)
  • @Vilhelm Gray C11dr 5.2.4.2.2 "... 小数位数,n,这样任何具有 p 基数 b 位的浮点数都可以四舍五入为浮点数n 个十进制数字并再次返回而不更改值,p log10 bb 是 10 的幂 ⎡1 + p log10 b⎤ 否则 FLT_DECIMAL_DIG 6 DBL_DECIMAL_DIG 10 LDBL_DECIMAL_DIG 10 ..." 6,10,10 是 最小值 值。

标签: c floating-point printf c99 floating-point-precision


【解决方案1】:

只需使用来自 <float.h> 的宏和可变宽度转换说明符 (".*"):

float f = 3.14159265358979323846;
printf("%.*f\n", FLT_DIG, f);

【讨论】:

  • @OliCharlesworth 你的意思是这样吗:printf("%." FLT_DIG "f\n", f);
  • +1 但这对%e 最有效,但对%f 不太好:只有知道要打印的值接近1.0
  • %e 打印非常小的数字的有效数字,%f 不打印。例如x = 1e-100%.5f 打印出0.00000(完全丧失进动)。 %.5e 打印 1.00000e-100.
  • @bobobobo 另外,你错了,因为它“产生了更准确的理由”。 FLT_DIG 被定义为它被定义为的值出于某种原因。如果它是 6,那是因为 float 不能保持超过 6 位的精度。如果您使用%.7f 打印它,最后一个数字将没有意义。 在投反对票之前三思。
  • @bobobobo 不,%.6f 不等价,因为FLT_DIG 并不总是 6。谁在乎效率? I/O 已经非常昂贵了,一个数字或多或少的精度不会成为瓶颈。
【解决方案2】:

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

如果存在以 2 为底的精确表示,则默认精度足以准确表示该值,否则足够大以区分 double 类型的值。

我必须补充一点,这仅在 C99 之后才可用。

【讨论】:

    【解决方案3】:

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

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

    一个简单的例子是打印七分之一:

    #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 double-precision binary。 所以OneSeventh = 1.0/7.0 会产生以下值。还显示了前面和后面的可表示的double 浮点数。

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

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

    C 在&lt;float.h&gt; 中有两个宏系列可以帮助我们。
    第一组是在十进制字符串中打印的 significant 位数,因此在扫描回字符串时, 我们得到原来的浮点数。显示了 C 规范的 minimum 值和一个 sample 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)
    

    第二组是有效位字符串可以被扫描成浮点数然后打印FP,仍然保持相同的字符串表示。显示了 C 规范的 minimum 值和一个 sample C11 编译器。我相信在 C99 之前可用。

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

    第一组宏似乎符合 OP 的有效位目标。但是那个并不总是可用的。

    #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-string(设置 #2 宏可用 C89),如何确定 FP-string-FP 的数字(设置 #1 宏在 C89 后可用)?一般来说,加 3 就是结果。

    现在要打印的有效位数是已知的,并通过&lt;float.h&gt; 驱动。

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

    对于"%e"precision 字段是前导数字和小数点之后 的位数。 所以- 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 才能看到所有有效 数字。

    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.428571428571428492127e-01而不是1.428571428571428492127e-001,'e'后面的位数应该是3?
    • @Jingguo Yao 同意参考资料中的说法“精度指定'%f' 的小数点字符后面有多少位数字”。此处的“精度”一词不是在数学意义上使用的,而只是用于定义小数点后的位数。 1234567890.123,在数学上具有 13 位精度或有效数字。 0.000000000123 具有 3 位数学精度,而不是 13。浮点数呈对数分布。此答案使用 significant digitsprecision 的数学意义。
    • @Slipp D. Thompson “图中显示了 C 规范的 minimum 值和 sample C11 编译器。”
    • @chux 啊,我错过了文章中的相关性。干杯。
    • 确实你是对的——我的技巧只对大小在 1.0 和 1.0eDBL_DIG 之间的值有效,这可以说是唯一真正适合首先使用"%f" 打印的范围。正如您所展示的那样使用"%e"当然是一种更好的方法,并且实际上是一个体面的答案(尽管它可能不如使用"%a"那么好,如果它可用,当然"%a"应该可用,如果`DBL_DECIMAL_DIG 是)。我一直希望有一个格式说明符,它总是会精确到最大精度(而不是硬编码的 6 位小数)。
    【解决方案4】:

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

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

    召回floatdoublesign.exponent.mantissa 格式存储。这意味着用于小数的小数部分比用于大数的位数多得多。

    例如,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 不能/不能为你处理。

    【讨论】:

    • 这很好地解释了将浮点值精确打印到特定小数位的局限性。但是,我认为我最初选择的词语过于模棱两可,因此我更新了我的问题以避免使用“最大精度”一词,希望它可以消除混淆。
    • 这仍然取决于您要打印的数字的值。
    • 这部分正确,但它没有回答问题,您对 OP 的要求感到困惑。他在问是否可以查询float 提供的有效[十进制] 位数,而您断言没有这样的事情(即没有FLT_DIG),这是错误的。
    • 您是否假设格式字母必须为“f”?我不认为这是必需的。我对这个问题的解读是,OP 正在寻找能够产生无损往返的 some printf 格式说明符,因此@ccxvii 的答案(“%.9g”表示浮点数,“%. 17g" for double) 是一个不错的选择。删除“宽度”一词可能会更好地表达这个问题。
    • 这不是问题要问的。
    【解决方案5】:

    无损打印浮点数的简短答案(以便可以读取 回到完全相同的数字,除了 NaN 和 Infinity):

    • 如果您的类型是浮点型:使用printf("%.9g", number)
    • 如果您的类型是双重的:使用printf("%.17g", number)

    不要使用%f,因为它只指定小数点后的有效数字位数,并且会截断小数字。作为参考,可以在定义 FLT_DECIMAL_DIGDBL_DECIMAL_DIGfloat.h 中找到幻数 9 和 17。

    【讨论】:

    • 您能解释一下%g 说明符吗?
    • %g 以精度所需的位数打印数字,当数字很小或很大时(1e-5 而不是 .00005)优先使用指数语法并跳过任何尾随零(1 而不是1.00000)。
    • @truthseeker 要表示IEEE 754 binary64 代码确实需要打印至少 15 个有效小数位。但是明确性需要 17,因为二进制数(2、4、8 等)和十进制数(10,100,1000 等)的精度变化永远不会是相同的数字(1.0 除外)。示例:0.1 上方的 2 个double 值:1.000_0000_0000_0000_2e-011.000_0000_0000_0000_3e-01 需要 17 位数字来区分。
    • @chux - 你误会了 %.16g 的行为;对于您将 1.000_0000_0000_0000_2e-01 与 1.000_0000_0000_0000_3e-01 区分开来的示例,它不够。需要 %.17g。
    • @Don Hatch 我同意 "%.16g" 不够,"%.17g""%.16e" 就足够了。 %g的详细信息,我记错了。
    【解决方案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]);
    }
    

    【讨论】:

    • 我不在乎它是否回答了这个问题——这确实令人印象深刻。这需要一些思考,应该得到承认和赞扬。如果您以某种方式(无论是在这里还是其他地方)包含完整的测试代码,也许会很好,但即使没有它,这也确实是一项好工作。请为此 +1!
    【解决方案7】:

    我进行了一个小实验来验证使用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;
    }
    

    我使用 Microsoft 的 C 编译器 19.00.24215.1 和 gcc 版本 7.4.0 20170516 (Debian 6.3.0-18+deb9u1) 运行它。使用少一位十进制数字会使比较完全相等的数字数量减半。 (我还验证了使用的rand() 确实产生了大约一百万个不同的数字。)这里是详细的结果。

    微软 C

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

    海合会

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

    【讨论】:

    • "用微软的 C 编译器运行它" --> 那个编译器可能有RAND_MAX == 32767。考虑u.s[j] = (rand() &lt;&lt; 8) ^ rand(); 或类似的东西,以确保所有位都有机会成为 0 或 1。
    • 确实,它的RAND_MAX是32767,所以你的建议是正确的。
    • 我按照@chux-ReinstateMonica 的建议更新了帖子以处理 RAND_MAX。结果与之前得到的结果相似。
    【解决方案8】:

    据我所知,有一种广泛传播的算法允许输出必要数量的有效数字,以便在扫描回字符串时,在dtoa.c 中获取原始浮点值由 David Gay 编写,可在 Netlib 上使用 here(另请参阅相关的 paper)。此代码用于例如在 Python、MySQL、Scilab 和许多其他语言中。

    【讨论】:

    • 恕我直言,这是真正的正确答案。这应该是顶部投票最多的答案。
    • 大卫盖伊,不是丹尼尔盖伊。 (具体来说是 David M. Gay。不确定 M 代表什么。)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-06-04
    • 1970-01-01
    • 2017-01-19
    • 1970-01-01
    • 2021-06-25
    • 2012-06-09
    相关资源
    最近更新 更多