在 C 语言中,如何将浮点数/双精度打印为字符串并将其作为相同的浮点数读回?

In C, how do you print a float/double as a string and read it back as the same float?

提问人:Ernaldo 提问时间:5/17/2023 更新时间:6/3/2023 访问量:322

问:

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

C 字符串 浮点

评论

1赞 Lutz Lehmann 5/17/2023
具有 52 位十进制数字的科学记数法/格式将起作用。仅用于存储,您也可以使用十六进制编码。
1赞 Aconcagua 5/17/2023
如果没有科学记数法,请考虑此处提供的极值 - 不算在内,但至少对于双精度,您可能需要相当大的数组......
0赞 Aconcagua 5/17/2023
“最便携的方式”——如果可能的话,完全避免浮点运算,而是回退到定点算术(例如,使用毫米而不是米,微秒而不是秒,美分而不是欧元/美元或十分之一 - 取决于你的精度要求......这样,您就可以使用简单的整数类型,并可以避免浮点的所有麻烦。
2赞 nielsen 5/17/2023
为此,您需要在不损失任何精度的情况下打印浮点数/双精度。在这里可以找到一种方法:codereview.stackexchange.com/questions/212490/...
0赞 chux - Reinstate Monica 5/17/2023
请参阅 Printf 宽度说明符以保持浮点值的精度

答:

10赞 Steve Summit 5/17/2023 #1

有两个问题:

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

你说如果可能的话,你想避免使用科学记数法,这很好,但是打印像 0.0000000000000000000000123 或 123000000000000000000000 这样的数字有点不合理,所以你可能想为非常大或非常小的数字切换到科学记数法。

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

然后是位数的问题。您需要足够的数字来保持 or 值的内部精度。长话短说,您想要的位数是预定义的常量或 .floatdoubleFLT_DECIMAL_DIGDBL_DECIMAL_DIG

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

sprintf(str, "%.*g", FLT_DECIMAL_DIG, f);

该技术与a完全类似:double

sprintf(str, "%.*g", DBL_DECIMAL_DIG, d);

在这两种情况下,我们都使用间接技术来判断我们想要多少个有效数字。我们本来可以用来让它选择,或者我们可以使用类似的东西来请求 10 个有效数字,但这里我们使用 ,其中 says 使用传入参数来指定有效数字的数量。这让我们可以插入确切的值或 from .%g%g%.10g%.*g*FLT_DECIMAL_DIGDBL_DECIMAL_DIG<float.h>

(还有一个问题,你可能需要多大的绳子。更多内容见下文。

然后你可以用 、 或 从字符串转换回字符串 , 或 :floatdoubleatofstrtodsscanf

f = atof(str);
d = strtod(str, &str2);
sscanf(str, "%g", &f);
sscanf(str, "%lg", &d);

(顺便说一句,朋友们并不太关心格式——你可以使用 、 或 ,它们的工作方式完全相同。scanf%e%f%g

下面是一个将所有这些联系在一起的演示程序:

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

int main()
{
    double d1, d2;
    char str[DBL_DECIMAL_DIG + 10];

    while(1) {
        printf("Enter a floating-point number: ");
        fflush(stdout);

        if(scanf("%lf", &d1) != 1) {
            printf("okay, we're done\n");
            break;
        }

        printf("you entered: %g\n", d1);

        snprintf(str, sizeof(str), "%.*g", DBL_DECIMAL_DIG, d1);

        printf("converted to string: %s\n", str);

        d2 = strtod(str, NULL);

        printf("converted back to double: %g\n", d2);

        if(d1 != d2)
            printf("whoops, they don't match!\n");

        printf("\n");
    }
}

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

  1. 该代码为转换后的字符串选取大小。对于数字、符号、指数和终止“\0”来说,这应该总是足够的。char str[DBL_DECIMAL_DIG + 10]
  2. 该代码使用(强烈推荐的)替代函数而不是 ,以便可以传入目标缓冲区大小,以确保它不会溢出,毕竟如果碰巧它不够大。snprintfsprintf
  3. 你会听到有人说,你永远不应该比较浮点数来获得精确相等,但这是我们想要的情况!如果在谷仓周围转了一圈后,不完全等于,则出了问题。d1d2
  4. 尽管此代码会进行检查以确保 ,但它悄悄地掩盖了可能与您输入的数字不完全相等的事实!大多数实数(以及大多数十进制分数)不能精确地表示为有限精度或值。如果您输入一个看似“简单”的分数,例如 0.1 或 123.456,则不会完全具有该值。 将是一个非常接近的近似值——然后,假设其他一切都正常工作,最终将包含完全相同的非常接近的近似值。要了解这里到底发生了什么,您可以提高“您输入”和“转换回双倍”行打印的精度。参见 浮点数学坏了吗?d1 == d2d1floatdoubled1d1d2
  5. 我们在这里关心的有效数字的数量 - 当我们说或时我们给出的精度 - 是有效数字的数量。它不仅仅是小数点后位数的计数。例如,数字 1234000、12.34、0.1234 和 0.00001234 都有四位有效数字。%g%.10g%.*g

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

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


补遗: 必须注意的是,以这种方式将浮点值准确地传输为十进制字符串确实需要:

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

IEEE-754 标准确实需要属性 (1) 和 (3)。但是不符合 IEEE-754 的实现可能做得不太好。(事实证明,特别是属性(1)非常难以实现,尽管现在已经很好地理解了这样做的技术。


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

if(d1 != d2)
    printf("whoops, they don't match!\n");

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

(也就是说,当数字不匹配时,只有当其中一个不是 NaN 时才出现错误。

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

评论

1赞 Aconcagua 5/17/2023
阵列也需要足够大才能容纳结果 - 那会是多少?我假设常数加 1 表示周期或 e/E 加 3/4 表示指数(分别为 float/double)加 2 表示两个符号(值 + 指数)加上 1 表示空终止符?虽然 32 对于任何一个都应该没问题(根据 cppreference 和提供的IEEE754计算 25 为双倍,但 32 是 2 的幂......
2赞 Andrew Henle 5/17/2023
@AndreasWenzel 或者,只需使用足够大的缓冲区再次进行调用。如果失败,则返回缓冲区需要多大。snprintf()
0赞 Steve Summit 5/17/2023
@Aconcagua 是的,缓冲区大小是一个重要的考虑因素。现在覆盖了。
0赞 Ian Abbott 5/17/2023
DBL_DECIMAL_DIG + 10应该就足够了。10 - 1 (空终止符) -1 (减号) - 1 (小数点) - 1 () - 1 (指数符号) 为指数数字留下 5 个字符,而 IEEE754 64 位双精度只需要 3 个字符作为指数数字。通常,打印 a 所需的最大指数位数为(允许非正常值)。由于 C 标准的最低要求,这将至少为 2,这意味着当选择格式时也足够了。edoubleceil(log10(DBL_DECIMAL_DIG - DBL_MIN_10_EXP))%g%f
0赞 chux - Reinstate Monica 5/17/2023
可以添加到“您可以使用 %e、%f 或 %g,它们的工作方式完全相同”列表中。"%a"
3赞 DevSolar 5/17/2023 #2

每个没有完全过时(因此有错误)的 / (/ ) 实现都应该能够在不损失精度的情况下进行往返。但是,请务必比较往返前后的浮点表示形式,而不是打印为字符串的内容。完全允许实现打印二进制值的近似值,只要它明确标识该二进制值即可。(请注意,可能的十进制表示形式比二进制表示形式多得多。printf()scanf()strtod()

如果您对如何完成此操作的细节感兴趣,该算法称为 Dragon 4。关于这个主题的一个很好的介绍可以在这里找到。

如果您不太关心字符串的可读性,请使用转换说明符。这将打印/读取浮点数的尾数为十六进制(带有十进制指数)。这完全避免了二进制/十进制转换。您也无需担心指定应打印多少位数的精度,因为默认设置是打印精确值。%a

评论

0赞 chux - Reinstate Monica 5/17/2023
二元表示比较也具有挑战性/功能。1) 当 FP 编号有多个编码时 2) FP 有填充(例如一些),不需要比较相同。3) 带有有效载荷的往返 NAN 是它自己的困境。long double
0赞 DevSolar 5/17/2023
@chux-恢复莫妮卡 对于“二元表示”比较,我的意思是比较往返前/与往返后,不是一点一点,而是按.这应该忽略填充。试图往返NaN(甚至不等于它们自己)是毫无意义的。而且我不知道有任何 FP 会具有多个有效(规范化)编码?doublelong double==
0赞 Simon Goater 5/17/2023 #3

将浮点数转换为十进制不是一个精确的过程(除非您使用很长的字符串 - 请参阅注释),反之亦然。如果读回的浮点数完全相同(逐位)很重要,那么您需要保留二进制表示形式,可能为十六进制字符串,如下所示。这将保留非数字值,如 NAN 和 +-INF。十六进制字符串可以安全地写入内存或文件。

如果你需要它是人类可读的,那么你可以发明你自己的字符串格式,它同时使用两者,例如在十进制字符串前面加上十六进制表示。然后,当数字转换回浮点数时,它将使用十六进制值,而不是十进制值,因此将具有与原始值完全相同的值。十六进制字符串只需要固定的 8 个字符,因此价格并不高。正如其他人所指出的,预测打印浮点数或双精度值所需的缓冲区大小可能并不明显,尤其是在您不想损失精度的情况下。请参阅其他人的评论和答案,了解如何打印人类可读表示的选项和问题。

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

/********************************************************************************************
// Floating Points As Hex Strings
// ==============================
// Author: Simon Goater May 2023.
//
// The binary representation of floats must be same for source and destination floats.
// If the endianess of source and destination differ, the hex characters must be 
// permuted accordingly.
*/
typedef union {
  float f;
  double d;
  long double ld;
  unsigned char c[16];
} fpchar_t;

const unsigned char hexchar[16] = {0x30, 0x31, 0x32, 0x33, 
    0x34, 0x35, 0x36, 0x37, 
    0x38, 0x39, 0x41, 0x42, 
    0x43, 0x44, 0x45, 0x46};
const unsigned char binchar[23] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 
  0, 0, 0, 0, 0, 10, 11, 12, 13, 14, 15};
    
void fptostring(void* f, unsigned char* string, uint8_t sizeoffp) {
  fpchar_t floatstring;  
  memcpy(&floatstring.c, f, sizeoffp);
  int i, stringix;
  stringix = 0;
  unsigned char thischar;
  for (i=0; i<sizeoffp; i++) {
    thischar = floatstring.c[i];
    string[stringix] = hexchar[thischar >> 4];
    stringix++;
    string[stringix] = hexchar[thischar & 0xf];
    stringix++;
  }
}

void stringtofp(void* f, unsigned char* string, uint8_t sizeoffp) {
  fpchar_t floatstring;
  int i, stringix;
  stringix = 0;
  for (i=0; i<sizeoffp; i++) {
    floatstring.c[i] = binchar[(string[stringix] - 0x30) % 23] << 4;
    stringix++;
    floatstring.c[i] += binchar[(string[stringix] - 0x30) % 23];
    stringix++;
  }
  memcpy(f, &floatstring.c, sizeoffp);
}

_Bool isfpstring(void* f, unsigned char* string, uint8_t sizeoffp) {
  // Validates the floatstring and if ok, copies value to f.
  int i;
  for (i=0; i<2*sizeoffp; i++) {
    if (string[i] < 0x30) return false;
    if (string[i] > 0x46) return false;
    if ((string[i] > 0x39) && (string[i] < 0x41)) return false;
  }
  stringtofp(f, string, sizeoffp);
  return true;
}

/********************************************************************************************
// Floating Points As Hex Strings - END
// ====================================
*/

int main(int argc, char **argv)
{
  //float f = 1.23f;
  //double f = 1.23;
  long double f = 1.23;
  if (argc > 1) f = atof(argv[1]);
  unsigned char floatstring[33] = {0};
  //printf("fpval = %.32f\n", f);
  printf("fpval = %.32Lf\n", f);
  fptostring((void*)&f, (unsigned char*)floatstring, sizeof(f));
  printf("floathex = %s\n", floatstring);
  f = 1.23f;
  //floatstring[0] = 'a';
  if (isfpstring((void*)&f, (unsigned char*)floatstring, sizeof(f))) {
    //printf("fpval = %.32f\n", f);
    printf("fpval = %.32Lf\n", f);
  } else {
    printf("Error converting floating point from hex.\n");
  }
  exit(0);
}

评论

0赞 chux - Reinstate Monica 5/17/2023
“将浮点数转换为十进制不是一个精确的过程......”--> 对于所有有限 FP,都可以准确地转换为十进制文本。使用高精度或您自己的代码实现良好:打印双精度的函数 - 确切地说printf()
0赞 Simon Goater 5/17/2023
我看到了你的帖子,虽然很有趣,但我没有看到任何让我相信十进制数字表示必须始终终止的东西。证据在哪里?此外,在我看来,使用潜在的数百个字符使它成为一个没有吸引力的选择,但你是对的,我也没有证明我的陈述,而且它可能完全是错误的。
2赞 chux - Reinstate Monica 5/17/2023
“证据在哪里?”每个 2 的幂都精确到十进制:......4, 2, 1, 0.5, 0.25, 0.125 ...所有有限 FP 都是这些精确值的总和。
1赞 Simon Goater 5/18/2023
啊,是的,当然。因此,如果 FP 可以容纳的最小数字是 2^(-n),则最多可以有 n 位小数。您是否有一个可以读回十进制表示的函数?
0赞 chux - Reinstate Monica 5/18/2023
“可以读回您的十进制表示的函数” --> ,就足够了。atof()strtod()
1赞 chux - Reinstate Monica 5/17/2023 #4

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

...适用于任何数量

总的来说,要做好这项工作是具有挑战性的。需要评估的异常注意事项包括:

没有科学记数法

一些质量标准库将执行高精度文本转换,而不会造成微不足道的损失。

double x = -DBL_TRUE_MIN;

#define PRECISION_NEED (DBL_DECIMAL_DIG - DBL_MIN_10_EXP - 1)
//            sign 1   .     fraction       \0
#define BUF_N (1 + 1 + 1 + PRECISION_NEED + 1)
char buf[BUF_N];
sprintf(buf, "%.f", PRECISION_NEED, x);

if (atof(buf) == x) ...

或者你可以自己编码,但这并不简单

最佳实践

按照许多人的建议作为第一步使用。sprintf(large_enough_buffer, "%.g", DBL_DECIMAL_DIG, x)

0赞 Luis Colorado 5/29/2023 #5

一般来说,这是不可能实现的,因为在解码过程中,两种不同的实现可能会导致不同的浮点值。这样做的原因是,将相同的数字表示为十进制 ASCII 数字,并在内部表示为数字的二进制表示形式,这不可能作为二进制应用程序。有时,十进制浮点数(例如 0.1)没有有限表示为二进制数(0.1 十进制转换为 0.00011001100110011001100110011001100100...二进制),并且不能表示为有限位序列(例如,当我们将 1.0 除以 3.0 时,我们得到无限序列 0.333333333333...)

将有限的二进制数转换为十进制总是可能的......每个有限浮点数(没有无限数表示的浮点数)总是产生一个有限(尽管它可能非常大)的字符串。这意味着有限十进制数的十进制字符串表示形式比任何有限二进制表示形式都多。基于此,我们将始终拥有多对一应用程序,该应用程序会导致一些十进制有限表示数字映射到同一个二进制图像。

这可以通过实现来处理,如果我们考虑到从二进制到十进制的对应关系是不确定的,并且总是导致二进制能够被转换,我们可以构建一个逆向,将找到的表示映射到原始表示(我们正在处理有限集合,所以,至少, 我们可以根据具体情况来做)例如,将所有最接近映射数字的数字映射回同一数字的表示形式。但还有另一个缺点,它阻碍了构建映射。任意的、有限的、二进制字符串的映射,总是映射到一个映射、有限长度、十进制字符串......但是,在二进制表示中,存储具有全十进制精度的完整二进制数字所需的位数要求每个二进制数字大约有一个完整的二元有效数字,因此,虽然

0.1(bin) --> 0.5(dec)  (one digit each)

0.0001(bin) --> 0.0625(dec) (four digits after the decimal point)
1.0 * -2^32 -->  0.00000000023283064365386962890625 (23 significative digits after the decimal point)

并且还在增长。保持有界计算(在十进制和二进制数系统中)和四舍五入可以使某些数字四舍五入到最接近的小数点(使用十进制四舍五入),但是当将数字读回计算机时,最接近的数字(这次使用二元舍入或上述最接近的方法)是原始数字的下一个或上一个数字, 并在原始编号和保存后检索到的编号之间进行差值。

但。。。您可以考虑以 ASCII 二进制形式保存一个数字。

这样,您将保证存储的编号将与原始编号完全相同(为什么,因为在这两个过程中,舍入都是在相同的编号基数中进行的,因此对应关系是双向的)。进行这样的转换应该很容易,因此您将获得浮点二进制数的可移植且精确的序列化。这可以以有限和精确的方式完成,因此您永远不会产生舍入错误,并且会保证您的数据已成功保存并在以后恢复。

在当今的架构中,内部二进制浮点表示的标准是 IEEE-754 被广泛使用。因此,一个简单的映射,例如从符号保持字节开始,以十六进制表示字节到有效数的 LSB 位,是一个很好的有效起点。另一个好的转换方法是在大端 IEEE-754 中使用二进制表示的 base64 编码(如上所述),它允许您在独立于架构的架构中将任何数字(包括 NaN 和无穷大)编码为 11 个 ASCII 字符,或 a 编码为 5 个 ASCII 字符。doublefloat

评论

0赞 Steve Summit 5/29/2023
对于高质量的浮点到字符串和字符串到浮点的实现——IMO 今天应该成为常态——这当然可能的。尽管使用十六进制字符串表示形式当然很有吸引力,但这不是必需的。只要您保留精度或数字,十进制表示也应该起作用。(唉,这可能会排除Microsoft。FLT_DECIMAL_DIGDBL_DECIMAL_DIG
0赞 Luis Colorado 5/29/2023
不是史蒂夫,唯一的方法是拥有一张双色地图。如果地图不是双对一的,你就有多对一的地图,这是不能逆转的。您提出的方法需要有界(但不是容易有界)的内存量,因此,结束是无效的。如果你想有效,就不要将二进制转换为十进制数字,而是在存储时将它们存储在二进制中。你可以在 base64、base96、punnycode、QR 码或其他任何语言中执行每比特 char、每半字节 char、每字节 char 操作,但二进制到十进制是一个信息丢失过程,会导致麻烦。
0赞 Steve Summit 5/29/2023
我们将不得不同意不同意。我断言二进制→至少具有 xxx_DECIMAL_DIG 位数字的十进制不会丢失信息,因此可以完全反转。
0赞 Luis Colorado 5/29/2023
在这种情况下,最糟糕的是,在有界精度系统上,舍入误差取决于您在所考虑的舍入位置进行舍入的数字,这可能会使一些转换顺利进行,而使其他转换失败。这与无信息丢失存储系统不兼容。
0赞 Luis Colorado 5/29/2023
也许我们可以不同意......但是我已经在数学上证明了所表示的有理数的非二元应用。只是不同意或表明我在我的演示中失败了。