我想知道最简单的,最便携的和普遍认为的最佳实践来实现这一点,适用于任何数字。我还希望与数字相关的字符串以十进制表示,如果可能的话,不要使用科学记数法。
eufgjt7s1#
这里有两个问题:1.你需要什么格式,还有1.你需要多少位有效数字?你说如果可能的话,你想避免使用科学记数法,这很好,但是打印0.00000000000000000000123或1230000000000000000这样的数字是不合理的,所以你可能想用科学记数法来处理很大或很小的数字。碰巧的是,有一种printf格式正好可以做到这一点:%g。如果可以,它的行为类似于%f,但如果必须,它会切换到%e。然后是位数的问题。您需要足够的位数来保留float或double值的内部精度。长话短说,您需要的位数是预定义的常量FLT_DECIMAL_DIG或DBL_DECIMAL_DIG。所以,把这些放在一起,你可以把一个float转换成一个字符串,如下所示:
%g
%f
%e
float
double
FLT_DECIMAL_DIG
DBL_DECIMAL_DIG
sprintf(str, "%.*g", FLT_DECIMAL_DIG, f);
double的技术非常类似:
sprintf(str, "%.*g", DBL_DECIMAL_DIG, d);
在这两种情况下,我们都使用间接技术来告诉%g我们需要多少个有效数字。我们可以使用%g来让它选择,或者我们可以使用类似%.10g的东西来请求10个有效数字,但这里我们使用%.*g,其中*表示使用传入的参数来指定有效数字的数量。这使我们可以从<float.h>插入精确值FLT_DECIMAL_DIG或DBL_DECIMAL_DIG。(还有一个问题是你可能需要多大的字符串。更多关于这一点在下面)。然后,您可以使用atof、strtod或sscanf将字符串转换回float或double:
%.10g
%.*g
*
<float.h>
atof
strtod
sscanf
f = atof(str); d = strtod(str, &str2); sscanf(str, "%g", &f); sscanf(str, "%lg", &d);
(By顺便说一句,scanf和朋友们并不真正关心格式这么多-你可以使用%e,%f或%g,他们都将工作完全相同。下面是一个将所有这些结合在一起的演示程序:
scanf
#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可能不完全等于您输入的数字的事实!大多数真实的(和大多数小数)不能精确地表示为有限精度float或double值。如果你输入一个看似“简单”的分数,如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_DIG或DBL_DECIMAL_DIG。”这些常量实际上是获取内部浮点值所需的最小有效位数,将其转换为十进制(字符串)表示,将其转换回内部浮点值,并获得完全相同的值。这显然正是我们想要的。还有另一个看似相似的常量对FLT_DIG和DBL_DIG,它们给予了在从外部十进制(字符串)表示转换为内部浮点值,然后再转换回十进制时保证保留的最小位数。对于典型的IEEE-754实现,FLT_DIG/DBL_DIG是6和15,而FLT_DECIMAL_DIG/DBL_DECIMAL_DIG是9和17。请参阅此回答以了解更多信息。
d1
d2
char str[DBL_DECIMAL_DIG + 10]
snprintf
sprintf
d1 == d2
FLT_DIG
DBL_DIG
FLT_DECIMAL_DIG和DBL_DECIMAL_DIG是保证二进制到十进制到二进制的往返转换所需的最小位数,但它们不一定足以精确显示实际的内部二进制值。对于那些你可能需要的十进制数位有多少二进制位的有效数。例如,如果我们从十进制数123.456开始,并将其转换为float,我们会得到类似于123.45600128...如果我们用FLT_DECIMAL_DIG或9个有效数字打印它,我们得到123.456001,然后转换回123.45600128...,所以我们成功了。但 * 实际 * 内部值是以16为基数的7b.74bc8,或以二进制表示的1111011.01110100101111001,具有24个有效位。这些数字的实际全精度十进制转换为123.45600128173828125。
7b.74bc8
1111011.01110100101111001
wvyml7n52#
每一个没有完全过时(因此也没有被窃听)的printf()/scanf()(/strtod())实现都应该能够在不损失精度的情况下进行往返。不过,重要的是,您要比较往返前后的浮点表示,而不是打印为字符串的内容。一个实现完全可以 * 允许 * 打印二进制值的 * 近似值 *,只要它 * 明确地 * 标识该二进制值。(注意,十进制表示比二进制表示有更多的可能性。如果你对如何做到这一点的细节感兴趣,该算法被称为Dragon 4。关于这个主题的一个很好的介绍是here。如果您不太关心字符串的可读性,可以使用%a转换说明符。这将浮点数的尾数 * 打印/读取为十六进制 *(带有十进制指数)。这完全避免了二进制/十进制转换。您也不需要担心应该打印多少位精度,因为默认情况下是打印 precise 值。
printf()
scanf()
strtod()
%a
uqxowvwt3#
...适用于任何号码
这是一个挑战,要做好一般。需要评估的异常考虑因素包括:
long 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) ...
或者你可以将其编码为yourself,但这并不简单。
最佳实践
使用sprintf(large_enough_buffer, "%.g", DBL_DECIMAL_DIG, x)作为第一步。
sprintf(large_enough_buffer, "%.g", DBL_DECIMAL_DIG, x)
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); }
4条答案
按热度按时间eufgjt7s1#
这里有两个问题:
1.你需要什么格式,还有
1.你需要多少位有效数字?
你说如果可能的话,你想避免使用科学记数法,这很好,但是打印0.00000000000000000000123或1230000000000000000这样的数字是不合理的,所以你可能想用科学记数法来处理很大或很小的数字。
碰巧的是,有一种printf格式正好可以做到这一点:
%g
。如果可以,它的行为类似于%f
,但如果必须,它会切换到%e
。然后是位数的问题。您需要足够的位数来保留
float
或double
值的内部精度。长话短说,您需要的位数是预定义的常量FLT_DECIMAL_DIG
或DBL_DECIMAL_DIG
。所以,把这些放在一起,你可以把一个
float
转换成一个字符串,如下所示:double
的技术非常类似:在这两种情况下,我们都使用间接技术来告诉
%g
我们需要多少个有效数字。我们可以使用%g
来让它选择,或者我们可以使用类似%.10g
的东西来请求10个有效数字,但这里我们使用%.*g
,其中*
表示使用传入的参数来指定有效数字的数量。这使我们可以从<float.h>
插入精确值FLT_DECIMAL_DIG
或DBL_DECIMAL_DIG
。(还有一个问题是你可能需要多大的字符串。更多关于这一点在下面)。
然后,您可以使用
atof
、strtod
或sscanf
将字符串转换回float
或double
:(By顺便说一句,
scanf
和朋友们并不真正关心格式这么多-你可以使用%e
,%f
或%g
,他们都将工作完全相同。下面是一个将所有这些结合在一起的演示程序:
此程序提示输入双精度值
d1
,将其转换为字符串,再将其转换回双精度值d2
,并检查以确保值匹配。有几点需要注意:1.代码为转换后的字符串选择大小
char str[DBL_DECIMAL_DIG + 10]
。这对于数字、符号、指数和终止'\0'来说应该总是足够的。1.代码使用(强烈推荐的)替代函数
snprintf
而不是sprintf
,以便可以传入目的地缓冲区大小,以确保它不会溢出,如果不幸它毕竟不够大的话。1.你会听到有人说,你永远不应该比较浮点数的精确相等,但这是一个我们想要的情况!如果绕着谷仓转了一圈后,
d1
并不 * 完全 * 等于d2
,那就出了问题。1.虽然这段代码检查以确保
d1 == d2
,但它悄悄地掩盖了d1
可能不完全等于您输入的数字的事实!大多数真实的(和大多数小数)不能精确地表示为有限精度float
或double
值。如果你输入一个看似“简单”的分数,如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_DIG
或DBL_DECIMAL_DIG
。”这些常量实际上是获取内部浮点值所需的最小有效位数,将其转换为十进制(字符串)表示,将其转换回内部浮点值,并获得完全相同的值。这显然正是我们想要的。还有另一个看似相似的常量对FLT_DIG
和DBL_DIG
,它们给予了在从外部十进制(字符串)表示转换为内部浮点值,然后再转换回十进制时保证保留的最小位数。对于典型的IEEE-754实现,FLT_DIG
/DBL_DIG
是6和15,而FLT_DECIMAL_DIG
/DBL_DECIMAL_DIG
是9和17。请参阅此回答以了解更多信息。FLT_DECIMAL_DIG
和DBL_DECIMAL_DIG
是保证二进制到十进制到二进制的往返转换所需的最小位数,但它们不一定足以精确显示实际的内部二进制值。对于那些你可能需要的十进制数位有多少二进制位的有效数。例如,如果我们从十进制数123.456开始,并将其转换为float
,我们会得到类似于123.45600128...如果我们用FLT_DECIMAL_DIG
或9个有效数字打印它,我们得到123.456001,然后转换回123.45600128...,所以我们成功了。但 * 实际 * 内部值是以16为基数的7b.74bc8
,或以二进制表示的1111011.01110100101111001
,具有24个有效位。这些数字的实际全精度十进制转换为123.45600128173828125。wvyml7n52#
每一个没有完全过时(因此也没有被窃听)的
printf()
/scanf()
(/strtod()
)实现都应该能够在不损失精度的情况下进行往返。不过,重要的是,您要比较往返前后的浮点表示,而不是打印为字符串的内容。一个实现完全可以 * 允许 * 打印二进制值的 * 近似值 *,只要它 * 明确地 * 标识该二进制值。(注意,十进制表示比二进制表示有更多的可能性。如果你对如何做到这一点的细节感兴趣,该算法被称为Dragon 4。关于这个主题的一个很好的介绍是here。
如果您不太关心字符串的可读性,可以使用
%a
转换说明符。这将浮点数的尾数 * 打印/读取为十六进制 *(带有十进制指数)。这完全避免了二进制/十进制转换。您也不需要担心应该打印多少位精度,因为默认情况下是打印 precise 值。uqxowvwt3#
我想知道最简单的,最便携的和普遍认为的最佳实践来实现这一点,适用于任何数字。我还希望与数字相关的字符串以十进制表示,如果可能的话,不要使用科学记数法。
...适用于任何号码
这是一个挑战,要做好一般。需要评估的异常考虑因素包括:
double
来表示long double
的双重编码。这种格式对于非规范的配对也有额外的问题。不使用科学计数法
一些质量标准库将执行高精度的文本转换,而不会有微不足道的损失。
或者你可以将其编码为yourself,但这并不简单。
最佳实践
使用
sprintf(large_enough_buffer, "%.g", DBL_DECIMAL_DIG, x)
作为第一步。qqrboqgw4#
将浮点数转换为十进制并不是一个精确的过程(除非你使用很长的字符串--见注解),匡威。如果读回的浮点数必须完全相同,那么您需要保留二进制表示,可能是如下所示的十六进制字符串。这将保留非数值,如NAN和+-INF。十六进制字符串可以安全地写入内存或文件。
如果你需要它是人类可读的,那么你可以发明你自己的字符串格式,它同时使用这两种格式,比如在十进制字符串前面加上十六进制表示。然后,当数字转换回浮点数时,它将使用十六进制值,而不是十进制值,因此将与原始值完全相同。十六进制字符串只需要一个固定的8个字符,所以不是那么昂贵。正如其他人所指出的,预测打印float或double所需的缓冲区大小可能并不明显,特别是如果你不想损失精度的话。关于如何打印人类可读的表示,请参阅其他人的评论和答案。