在 C 中,如何将 float/double 作为字符串打印并将其作为相同的 float 读回?

Ern*_*ldo 3 c string floating-point

我想知道实现这一目标的最简单、最便携且普遍认为的最佳实践,适用于任何数字。我还希望与数字关联的字符串采用十进制表示形式,如果可能的话,不使用科学记数法。

Ste*_*mit 10

有两个问题:

\n
    \n
  1. 您需要什么格式,以及
  2. \n
  3. 您需要多少位有效数字?
  4. \n
\n

你说你想尽可能避免使用科学记数法,这很好,但是打印像 0.00000000000000000123 或 12300000000000000000 这样的数字有点不合理,所以你可能想切换到科学记数法来表示非常大或非常小的数字。

\n

碰巧,有一种 printf 格式可以做到这一点:%g. 它的行为就像%f是可以的话,但%e如果必须的话就会切换到。

\n

还有位数的问题。float您需要足够的数字来保留or值的内部精度double。长话短说,您想要的位数是预定义的常数FLT_DECIMAL_DIGDBL_DECIMAL_DIG

\n

因此,将所有这些放在一起,您可以将 a 转换float为字符串,如下所示:

\n
sprintf(str, "%.*g", FLT_DECIMAL_DIG, f);\n
Run Code Online (Sandbox Code Playgroud)\n

a 的技术double完全类似:

\n
sprintf(str, "%.*g", DBL_DECIMAL_DIG, d);\n
Run Code Online (Sandbox Code Playgroud)\n

在这两种情况下,我们都使用间接技术来告诉%g我们需要多少个有效数字。我们本来可以%g让它选择,或者我们可以使用类似的方法%.10g来请求 10 个有效数字,但这里我们使用%.*g,其中*表示使用传入的参数来指定有效数字的数量。这让我们可以插入准确的值FLT_DECIMAL_DIGDBL_DECIMAL_DIG来自 的值<float.h>

\n

(还有一个问题是您可能需要多大的字符串。下面将详细介绍。)

\n

然后你可以使用, , floator从字符串转换回 a或:doubleatofstrtodsscanf

\n
f = atof(str);\nd = strtod(str, &str2);\nsscanf(str, "%g", &f);\nsscanf(str, "%lg", &d);\n
Run Code Online (Sandbox Code Playgroud)\n

(顺便说一句,scanf朋友们并不那么关心 \xe2\x80\x94 的格式,你可以使用%e, %f, 或%g,它们的工作方式完全相同。)

\n

这是一个将所有这些结合在一起的演示程序:

\n
#include <stdio.h>\n#include <stdlib.h>\n#include <float.h>\n\nint main()\n{\n    double d1, d2;\n    char str[DBL_DECIMAL_DIG + 10];\n\n    while(1) {\n        printf("Enter a floating-point number: ");\n        fflush(stdout);\n\n        if(scanf("%lf", &d1) != 1) {\n            printf("okay, we\'re done\\n");\n            break;\n        }\n\n        printf("you entered: %g\\n", d1);\n\n        snprintf(str, sizeof(str), "%.*g", DBL_DECIMAL_DIG, d1);\n\n        printf("converted to string: %s\\n", str);\n\n        d2 = strtod(str, NULL);\n\n        printf("converted back to double: %g\\n", d2);\n\n        if(d1 != d2)\n            printf("whoops, they don\'t match!\\n");\n\n        printf("\\n");\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

该程序提示输入 double value d1,将其转换为字符串,再将其转换回 double d2,并检查以确保值匹配。有几点需要注意:

\n
    \n
  1. char str[DBL_DECIMAL_DIG + 10]该代码为转换后的字符串选择一个大小。对于数字、符号、指数和终止符“\\0”来说,这应该足够了。
  2. \n
  3. 该代码使用(强烈推荐的)替代函数snprintf而不是sprintf,以便可以传入目标缓冲区大小,以确保它不会溢出,如果由于某种意外它不够大的话。
  4. \n
  5. 您会听到有人说您永远不应该比较浮点数是否完全相等,但在这种情况下我们想要这样做!如果绕过谷仓后,d1完全等于d2,则说明出现了问题。
  6. \n
  7. 尽管此代码会进行检查以确保d1 == d2,但它悄悄地掩盖了这样一个事实:d1可能并不完全等于您输入的数字!大多数实数(和大多数小数)不能精确地表示为有限精度floatdouble值。如果您输入看似“简单”的分数(例如 0.1 或 123.456),d1不会得到确切的值。 d1将是一个非常接近的近似值 \xe2\x80\x94 ,然后,假设其他一切正常工作,d2最终将包含完全相同的非常接近的近似值。要了解这里到底发生了什么,您可以提高“您输入”和“转换回双精度”行打印的精度。另请参阅浮点数学是否已损坏?
  8. \n
  9. 我们在这里关心的有效数字位数 \xe2\x80\x94%g当我们说%.10gor \xe2\x80\x94 时我们给出的精度是有效数字%.*g位数。它不仅仅是小数点后位数的计数。例如,数字 1234000、12.34、0.1234 和 0.00001234 都有四位有效数字。
  10. \n
\n

上面我说过,“长话短说,你想要的位数是预定义的常数FLT_DECIMAL_DIGDBL_DECIMAL_DIG。” 这些常量实际上是获取内部浮点值、将其转换为十进制(字符串)表示形式、将其转换回内部浮点值并获取完全相同的值所需的最小有效位数。显然这正是我们想要的。还有另一对看似相似的常量,FLT_DIGDBL_DIG给出了在从外部十进制(字符串)表示形式转换为内部浮点值时保证保留的最小位数,并且然后回到十进制。对于典型的 IEEE-754 实现,FLT_DIG/DBL_DIG是 6 和 15,而FLT_DECIMAL_DIG/DBL_DECIMAL_DIG是 9 和 17。有关更多信息,请参阅此 SO 答案。

\n

FLT_DECIMAL_DIGDBL_DECIMAL_DIG是保证往返二进制到十进制到二进制转换所需的最小位数,但它们不一定足以精确显示实际的内部二进制值是什么。对于那些,您可能需要与有效数中的二进制位一样多的十进制数字。例如,如果我们从十进制数 123.456 开始,并将其转换为float,我们会得到类似 123.45600128... 的结果。如果我们打印FLT_DECIMAL_DIG9 位有效数字,我们会得到 123.456001,然后转换回 123.45600128...,所以我们成功了。但实际的内部值以7b.74bc816 为基数,即1111011.01110100101111001二进制,有 24 个有效位。这些数字的实际全精度十进制转换为 123.45600128173828125。

\n
\n

附录:\n必须注意的是,以这种方式将浮点值准确地传输为十进制字符串绝对需要:

\n
    \n
  1. 一个结构良好的浮点到十进制字符串转换器(即sprintf %g)。将 N 位转换为 M 位时,它们必须始终是 M 个适当舍入的数字。
  2. \n
  3. 足够的数字(FLT_DECIMAL_DIGDBL_DECIMAL_DIG,如上所述)。
  4. \n
  5. 一个结构良好的十进制字符串到浮点转换器(例如strtod())。将 N 位数字转换为 M 位时,它们必须始终是 M 个适当舍入的位。
  6. \n
\n

IEEE-754 标准确实需要属性 (1) 和 (3)。但不符合 IEEE-754 的实现可能效果不佳。(事实证明,属性(1)尤其难以实现,尽管现在已经很好地理解了实现这一点的技术。)

\n
\n

附录 2:\n我使用上述程序的修改进行了实证测试,循环遍历许多值,而不仅仅是从用户扫描的单个值。\n在这个“回归测试”版本中,我已经替换了测试

\n
if(d1 != d2)\n    printf("whoops, they don\'t match!\\n");\n
Run Code Online (Sandbox Code Playgroud)\n

\n
if(d1 != d2 && (!isnan(d1) || !(isnan(d1) && isnan(d2))))\n    printf("whoops, they don\'t match!\\n");\n
Run Code Online (Sandbox Code Playgroud)\n

(也就是说,当数字不匹配时,仅当其中一个不是NaN时才会出错。)

\n

无论如何,我已经测试了 4,294,967,296 个类型的值float。\n我已经测试了 100,000,000,000 个随机选择的类型值double(公平地说,只是其中的一小部分)。\n不是一次(除了故意引起的错误之外,要测试测试)我看到它打印“哎呀,它们不匹配!”。

\n

  • @AndreasWenzel 或者只是使用足够大的缓冲区再次进行调用。如果“snprintf()”失败,它会返回缓冲区需要有多大。 (2认同)