提问人:Ernaldo 提问时间:5/17/2023 更新时间:6/3/2023 访问量:322
在 C 语言中,如何将浮点数/双精度打印为字符串并将其作为相同的浮点数读回?
In C, how do you print a float/double as a string and read it back as the same float?
答:
有两个问题:
- 您需要什么格式,以及
- 您需要多少位有效数字?
你说如果可能的话,你想避免使用科学记数法,这很好,但是打印像 0.0000000000000000000000123 或 123000000000000000000000 这样的数字有点不合理,所以你可能想为非常大或非常小的数字切换到科学记数法。
碰巧的是,有一种 printf 格式可以做到这一点: .如果可以,它就会像它一样运行,但如果有必要,它会切换到。%g
%f
%e
然后是位数的问题。您需要足够的数字来保持 or 值的内部精度。长话短说,您想要的位数是预定义的常量或 .float
double
FLT_DECIMAL_DIG
DBL_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_DIG
DBL_DECIMAL_DIG
<float.h>
(还有一个问题,你可能需要多大的绳子。更多内容见下文。
然后你可以用 、 或 从字符串转换回字符串 , 或 :float
double
atof
strtod
sscanf
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");
}
}
该程序提示输入双精度值,将其转换为字符串,再将其转换回双精度值,并检查以确保值匹配。有几点需要注意:d1
d2
- 该代码为转换后的字符串选取大小。对于数字、符号、指数和终止“\0”来说,这应该总是足够的。
char str[DBL_DECIMAL_DIG + 10]
- 该代码使用(强烈推荐的)替代函数而不是 ,以便可以传入目标缓冲区大小,以确保它不会溢出,毕竟如果碰巧它不够大。
snprintf
sprintf
- 你会听到有人说,你永远不应该比较浮点数来获得精确相等,但这是我们想要的情况!如果在谷仓周围转了一圈后,不完全等于,则出了问题。
d1
d2
- 尽管此代码会进行检查以确保 ,但它悄悄地掩盖了可能与您输入的数字不完全相等的事实!大多数实数(以及大多数十进制分数)不能精确地表示为有限精度或值。如果您输入一个看似“简单”的分数,例如 0.1 或 123.456,则不会完全具有该值。 将是一个非常接近的近似值——然后,假设其他一切都正常工作,最终将包含完全相同的非常接近的近似值。要了解这里到底发生了什么,您可以提高“您输入”和“转换回双倍”行打印的精度。参见 浮点数学坏了吗?
d1 == d2
d1
float
double
d1
d1
d2
- 我们在这里关心的有效数字的数量 - 当我们说或时我们给出的精度 - 是有效数字的数量。它不仅仅是小数点后位数的计数。例如,数字 1234000、12.34、0.1234 和 0.00001234 都有四位有效数字。
%g
%.10g
%.*g
在上面,我说,“长话短说,你想要的位数是预定义的常数或。从字面上看,这些常量是获取内部浮点值、将其转换为十进制(字符串)表示形式、将其转换回内部浮点值并返回完全相同的值所需的最小有效位数。这显然正是我们在这里想要的。还有一对看似相似的常量,它们给出了从外部十进制(字符串)表示形式转换为内部浮点值,然后再转换回十进制时可以保证保留的最小位数。对于典型的 IEEE-754 实现,/ 是 6 和 15,而 / 是 9 和 17。有关此内容的更多信息,请参阅此 SO 答案。FLT_DECIMAL_DIG
DBL_DECIMAL_DIG
FLT_DIG
DBL_DIG
FLT_DIG
DBL_DIG
FLT_DECIMAL_DIG
DBL_DECIMAL_DIG
FLT_DECIMAL_DIG
并且是保证二进制到十进制到二进制的往返转换所需的最小位数,但它们不一定足以准确显示实际的内部二进制值。对于那些,您可能需要与有效位数中的二进制位一样多的十进制数字。例如,如果我们从十进制数 123.456 开始,并将其转换为 ,我们会得到类似 123.45600128....如果我们用 或 9 位有效数字打印它,我们得到 123.456001,然后转换回 123.45600128...,所以我们成功了。但实际内部值以 16 为基数,或二进制,有 24 个有效位。这些数字的实际全精度十进制转换是 123.45600128173828125。DBL_DECIMAL_DIG
float
FLT_DECIMAL_DIG
7b.74bc8
1111011.01110100101111001
补遗: 必须注意的是,以这种方式将浮点值准确地传输为十进制字符串确实需要:
- 一个构造良好的浮点到十进制字符串转换器(即 ).将 N 位转换为 M 位时,它们必须始终是 M 位正确舍入的数字。
sprintf
%g
- 足够的数字(如上所述,或 )。
FLT_DECIMAL_DIG
DBL_DECIMAL_DIG
- 一个构造良好的十进制字符串到浮点转换器(例如)。将 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 个随机选择的类型值(公平地说,这只是其中的一小部分)。
我没有一次(除了故意引起的错误,以测试测试)看到它打印“哎呀,它们不匹配!float
double
评论
snprintf()
DBL_DECIMAL_DIG + 10
应该就足够了。10 - 1 (空终止符) -1 (减号) - 1 (小数点) - 1 () - 1 (指数符号) 为指数数字留下 5 个字符,而 IEEE754 64 位双精度只需要 3 个字符作为指数数字。通常,打印 a 所需的最大指数位数为(允许非正常值)。由于 C 标准的最低要求,这将至少为 2,这意味着当选择格式时也足够了。e
double
ceil(log10(DBL_DECIMAL_DIG - DBL_MIN_10_EXP))
%g
%f
"%a"
每个没有完全过时(因此有错误)的 / (/ ) 实现都应该能够在不损失精度的情况下进行往返。但是,请务必比较往返前后的浮点表示形式,而不是打印为字符串的内容。完全允许实现打印二进制值的近似值,只要它明确标识该二进制值即可。(请注意,可能的十进制表示形式比二进制表示形式多得多。printf()
scanf()
strtod()
如果您对如何完成此操作的细节感兴趣,该算法称为 Dragon 4。关于这个主题的一个很好的介绍可以在这里找到。
如果您不太关心字符串的可读性,请使用转换说明符。这将打印/读取浮点数的尾数为十六进制(带有十进制指数)。这完全避免了二进制/十进制转换。您也无需担心指定应打印多少位数的精度,因为默认设置是打印精确值。%a
评论
long double
double
long double
==
将浮点数转换为十进制不是一个精确的过程(除非您使用很长的字符串 - 请参阅注释),反之亦然。如果读回的浮点数完全相同(逐位)很重要,那么您需要保留二进制表示形式,可能为十六进制字符串,如下所示。这将保留非数字值,如 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);
}
评论
printf()
atof()
strtod()
我想知道实现这一目标的最简单、最便携和普遍认为的最佳实践,适用于任何数字。我还希望与数字相关的字符串采用十进制表示,如果可能的话,没有科学记数法。
...适用于任何数量
总的来说,要做好这项工作是具有挑战性的。需要评估的异常注意事项包括:
对单个值具有多个编码的浮点:例如,使用 2 double 表示
长
double 的double
编码。对于非规范配对,此格式还存在其他问题。非有效载荷的数字。
负零。
带填充的浮点。示例。
没有科学记数法
一些质量标准库将执行高精度文本转换,而不会造成微不足道的损失。
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)
一般来说,这是不可能实现的,因为在解码过程中,两种不同的实现可能会导致不同的浮点值。这样做的原因是,将相同的数字表示为十进制 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 字符。double
float
评论
FLT_DECIMAL_DIG
DBL_DECIMAL_DIG
评论