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

rvpgvaaj  于 2023-05-22  发布在  其他
关注(0)|答案(4)|浏览(172)

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

eufgjt7s

eufgjt7s1#

这里有两个问题:
1.你需要什么格式,还有
1.你需要多少位有效数字?
你说如果可能的话,你想避免使用科学记数法,这很好,但是打印0.00000000000000000000123或1230000000000000000这样的数字是不合理的,所以你可能想用科学记数法来处理很大或很小的数字。
碰巧的是,有一种printf格式正好可以做到这一点:%g。如果可以,它的行为类似于%f,但如果必须,它会切换到%e
然后是位数的问题。您需要足够的位数来保留floatdouble值的内部精度。长话短说,您需要的位数是预定义的常量FLT_DECIMAL_DIGDBL_DECIMAL_DIG
所以,把这些放在一起,你可以把一个float转换成一个字符串,如下所示:

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

double的技术非常类似:

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

在这两种情况下,我们都使用间接技术来告诉%g我们需要多少个有效数字。我们可以使用%g来让它选择,或者我们可以使用类似%.10g的东西来请求10个有效数字,但这里我们使用%.*g,其中*表示使用传入的参数来指定有效数字的数量。这使我们可以从<float.h>插入精确值FLT_DECIMAL_DIGDBL_DECIMAL_DIG
(还有一个问题是你可能需要多大的字符串。更多关于这一点在下面)。
然后,您可以使用atofstrtodsscanf将字符串转换回floatdouble

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

(By顺便说一句,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");
    }
}

此程序提示输入双精度值d1,将其转换为字符串,再将其转换回双精度值d2,并检查以确保值匹配。有几点需要注意:
1.代码为转换后的字符串选择大小char str[DBL_DECIMAL_DIG + 10]。这对于数字、符号、指数和终止'\0'来说应该总是足够的。
1.代码使用(强烈推荐的)替代函数snprintf而不是sprintf,以便可以传入目的地缓冲区大小,以确保它不会溢出,如果不幸它毕竟不够大的话。
1.你会听到有人说,你永远不应该比较浮点数的精确相等,但这是一个我们想要的情况!如果绕着谷仓转了一圈后,d1并不 * 完全 * 等于d2,那就出了问题。
1.虽然这段代码检查以确保d1 == d2,但它悄悄地掩盖了d1可能不完全等于您输入的数字的事实!大多数真实的(和大多数小数)不能精确地表示为有限精度floatdouble值。如果你输入一个看似“简单”的分数,如0.1或123.456,d1将 * 不 * 具有确切的值。d1将是一个非常接近的近似值-然后,假设其他一切都正常工作,d2最终将包含完全相同的非常接近的近似值。要了解这里到底发生了什么,可以通过“youentered”和“converted back to double”行来提高打印的精度。参见Is floating-point math broken?
1.这里我们关心的有效位数-当我们说%.10g%.*g时,我们给予%g的精度-是 * 有效位数 * 的数量。它不仅仅是小数点后的位数。例如,数字1234000、12.34、0.1234和0.00001234都有四位有效数字。
上面我说过,“长话短说,你想要的位数是预定义的常量FLT_DECIMAL_DIGDBL_DECIMAL_DIG。”这些常量实际上是获取内部浮点值所需的最小有效位数,将其转换为十进制(字符串)表示,将其转换回内部浮点值,并获得完全相同的值。这显然正是我们想要的。还有另一个看似相似的常量对FLT_DIGDBL_DIG,它们给予了在从外部十进制(字符串)表示转换为内部浮点值,然后再转换回十进制时保证保留的最小位数。对于典型的IEEE-754实现,FLT_DIG/DBL_DIG是6和15,而FLT_DECIMAL_DIG/DBL_DECIMAL_DIG是9和17。请参阅此回答以了解更多信息。

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

wvyml7n5

wvyml7n52#

每一个没有完全过时(因此也没有被窃听)的printf()/scanf()(/strtod())实现都应该能够在不损失精度的情况下进行往返。不过,重要的是,您要比较往返前后的浮点表示,而不是打印为字符串的内容。一个实现完全可以 * 允许 * 打印二进制值的 * 近似值 *,只要它 * 明确地 * 标识该二进制值。(注意,十进制表示比二进制表示有更多的可能性。
如果你对如何做到这一点的细节感兴趣,该算法被称为Dragon 4。关于这个主题的一个很好的介绍是here
如果您不太关心字符串的可读性,可以使用%a转换说明符。这将浮点数的尾数 * 打印/读取为十六进制 *(带有十进制指数)。这完全避免了二进制/十进制转换。您也不需要担心应该打印多少位精度,因为默认情况下是打印 precise 值。

uqxowvwt

uqxowvwt3#

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

...适用于任何号码

这是一个挑战,要做好一般。需要评估的异常考虑因素包括:

  • 一个值有多种编码的浮点数:例如,使用2 × double来表示long double的双重编码。这种格式对于非规范的配对也有额外的问题。
  • 不带payload的数字。
  • Negative zero
  • 带填充的浮点。example
    不使用科学计数法

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

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) ...

或者你可以将其编码为yourself,但这并不简单。

最佳实践

使用sprintf(large_enough_buffer, "%.g", DBL_DECIMAL_DIG, x)作为第一步。

qqrboqgw

qqrboqgw4#

将浮点数转换为十进制并不是一个精确的过程(除非你使用很长的字符串--见注解),匡威。如果读回的浮点数必须完全相同,那么您需要保留二进制表示,可能是如下所示的十六进制字符串。这将保留非数值,如NAN和+-INF。十六进制字符串可以安全地写入内存或文件。
如果你需要它是人类可读的,那么你可以发明你自己的字符串格式,它同时使用这两种格式,比如在十进制字符串前面加上十六进制表示。然后,当数字转换回浮点数时,它将使用十六进制值,而不是十进制值,因此将与原始值完全相同。十六进制字符串只需要一个固定的8个字符,所以不是那么昂贵。正如其他人所指出的,预测打印float或double所需的缓冲区大小可能并不明显,特别是如果你不想损失精度的话。关于如何打印人类可读的表示,请参阅其他人的评论和答案。

#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);
}

相关问题