提问人:Vilhelm Gray 提问时间:5/30/2013 最后编辑:CommunityVilhelm Gray 更新时间:8/21/2023 访问量:377832
Printf 宽度说明符以保持浮点值的精度
Printf width specifier to maintain precision of floating-point value
问:
是否有宽度说明符可以应用于浮点说明符,该浮点说明符会自动将输出格式化为必要数量的有效数字,以便在重新扫描字符串时获取原始浮点值?printf
例如,假设我将 a 打印为小数点后一位的精度:float
2
float foobar = 0.9375;
printf("%.2f", foobar); // prints out 0.94
当我扫描输出时,我无法保证我会获得原始浮点值(在本例中,我可能不会)。0.94
0.9375
我想要一种方法,告诉自动将浮点值打印到必要的有效位数,以确保它可以扫描回传递给 的原始值。printf
printf
我可以使用一些宏来派生要传递到的最大宽度,但是是否已经有一个说明符可以自动打印到必要数量的有效数字 - 或者至少打印到最大宽度?float.h
printf
答:
只需使用 from 的宏和可变宽度转换说明符 ():<float.h>
".*"
float f = 3.14159265358979323846;
printf("%.*f\n", FLT_DIG, f);
评论
printf("%." FLT_DIG "f\n", f);
%e
%f
1.0
%e
打印非常小的数字的有效数字,并且不打印。 打印(进动完全丧失)。 指纹。%f
x = 1e-100
%.5f
0.00000
%.5e
1.00000e-100
FLT_DIG
float
%.7f
%.6f
FLT_DIG
如果您只对位(resp 十六进制模式)感兴趣,则可以使用该格式。这保证了您:%a
这 如果基数 2 中存在精确表示形式,并且其他表示形式足够大以区分 double 类型的值,则默认精度足以精确表示值。
我必须补充一点,这仅适用于 C99。
我推荐@Jens Gustedt 十六进制解决方案:使用 %a。
OP 希望“以最大精度(或至少到最高有效十进制)打印”。
一个简单的例子是打印七分之一,如下所示:
#include <float.h>
int Digs = DECIMAL_DIG;
double OneSeventh = 1.0/7.0;
printf("%.*e\n", Digs, OneSeventh);
// 1.428571428571428492127e-01
但是,让我们更深入地挖掘......
从数学上讲,答案是“0.142857 142857 142857......”,但我们使用的是有限精度的浮点数。
假设 IEEE 754 双精度二进制文件。
因此,结果如下。还显示了前面和后面的可表示浮点数。OneSeventh = 1.0/7.0
double
OneSeventh before = 0.1428571428571428 214571170656199683435261249542236328125
OneSeventh = 0.1428571428571428 49212692681248881854116916656494140625
OneSeventh after = 0.1428571428571428 769682682968777953647077083587646484375
打印 a 的精确十进制表示形式用途有限。double
C 有 2 个宏系列来帮助我们。
第一组是要以十进制形式打印的字符串的有效位数,因此当向后扫描字符串时,
我们得到原始浮点。显示了 C 规范的最小值和示例 C11 编译器。<float.h>
FLT_DECIMAL_DIG 6, 9 (float) (C11)
DBL_DECIMAL_DIG 10, 17 (double) (C11)
LDBL_DECIMAL_DIG 10, 21 (long double) (C11)
DECIMAL_DIG 10, 21 (widest supported floating type) (C99)
第二组是字符串可以扫描成浮点数,然后打印 FP,仍然保留相同的字符串表示。显示了 C 规范的最小值和示例 C11 编译器。我相信 C99 之前可用。
FLT_DIG 6, 6 (float)
DBL_DIG 10, 15 (double)
LDBL_DIG 10, 18 (long double)
第一组宏似乎满足了 OP 的有效数字目标。但该宏并不总是可用的。
#ifdef DBL_DECIMAL_DIG
#define OP_DBL_Digs (DBL_DECIMAL_DIG)
#else
#ifdef DECIMAL_DIG
#define OP_DBL_Digs (DECIMAL_DIG)
#else
#define OP_DBL_Digs (DBL_DIG + 3)
#endif
#endif
“+3”是我之前回答的关键。 它的核心是,如果知道往返转换字符串-FP-字符串(设置 #2 宏可用 C89),如何确定 FP-string-FP(设置 #1 宏在 C89 后可用)的数字?通常,加 3 是结果。
现在,要打印多少个有效数字是已知的,并通过 驱动。<float.h>
要打印 N 个有效十进制数字,可以使用各种格式。
对于,精度字段是前导数字和小数点之后的位数。
所以是有序的。注意:这不是在初始"%e"
- 1
-1
int Digs = DECIMAL_DIG;
printf("%.*e\n", OP_DBL_Digs - 1, OneSeventh);
// 1.4285714285714285e-01
对于,精度字段是小数点后的位数。
对于像这样的数字,需要看到所有有效数字。"%f"
OneSeventh/1000000.0
OP_DBL_Digs + 6
printf("%.*f\n", OP_DBL_Digs , OneSeventh);
// 0.14285714285714285
printf("%.*f\n", OP_DBL_Digs + 6, OneSeventh/1000000.0);
// 0.00000014285714285714285
注意:许多人习惯于.小数点后显示 6 位数字;6 是默认显示,而不是数字的精度。"%f"
评论
"%f"
"%e"
"%a"
"%a"
不,没有这样的 printf 宽度说明符可以以最大精度打印浮点。让我解释一下原因。
and 的最大精度是可变的,并且取决于 or 的实际值。float
double
float
double
召回并以 sign.exponent.mantissa 格式存储。这意味着用于小数的小数分量比用于大数的位数多得多。float
double
例如,可以很容易地区分 0.0 和 0.1。float
float r = 0;
printf( "%.6f\n", r ) ; // 0.000000
r+=0.1 ;
printf( "%.6f\n", r ) ; // 0.100000
但不知道 和 之间的区别。float
1e27
1e27 + 0.1
r = 1e27;
printf( "%.6f\n", r ) ; // 999999988484154753734934528.000000
r+=0.1 ;
printf( "%.6f\n", r ) ; // still 999999988484154753734934528.000000
这是因为所有精度(受尾数位数的限制)都用完了大部分数字,即小数点后剩下的大部分。
修饰符只是表示要从浮点数打印多少个十进制值,就格式而言。可用的精度取决于数字的大小,这一事实取决于您作为程序员来处理。 不能/不能为您处理。%.f
printf
评论
float
FLT_DIG
无损打印浮点数的简短答案(以便可以读取它们 回到完全相同的数字,除了 NaN 和 Infinity):
- 如果类型为 float:请使用 .
printf("%.9g", number)
- 如果类型为 double:请使用 .
printf("%.17g", number)
不要使用 ,因为这仅指定小数点后有多少位有效数字,并且会截断小数字。作为参考,可以在其中找到幻数 9 和 17 定义和 。%f
float.h
FLT_DECIMAL_DIG
DBL_DECIMAL_DIG
评论
%g
在我对一个答案的评论中,我感叹说,我一直想要某种方法以十进制形式以浮点值打印所有有效数字,就像问题问的那样。好吧,我终于坐下来写了它。它不是很完美,这是打印附加信息的演示代码,但它主要适用于我的测试。如果您(即任何人)想要驱动它进行测试的整个包装程序的副本,请告诉我。
static unsigned int
ilog10(uintmax_t v);
/*
* Note: As presented this demo code prints a whole line including information
* about how the form was arrived with, as well as in certain cases a couple of
* interesting details about the number, such as the number of decimal places,
* and possibley the magnitude of the value and the number of significant
* digits.
*/
void
print_decimal(double d)
{
size_t sigdig;
int dplaces;
double flintmax;
/*
* If we really want to see a plain decimal presentation with all of
* the possible significant digits of precision for a floating point
* number, then we must calculate the correct number of decimal places
* to show with "%.*f" as follows.
*
* This is in lieu of always using either full on scientific notation
* with "%e" (where the presentation is always in decimal format so we
* can directly print the maximum number of significant digits
* supported by the representation, taking into acount the one digit
* represented by by the leading digit)
*
* printf("%1.*e", DBL_DECIMAL_DIG - 1, d)
*
* or using the built-in human-friendly formatting with "%g" (where a
* '*' parameter is used as the number of significant digits to print
* and so we can just print exactly the maximum number supported by the
* representation)
*
* printf("%.*g", DBL_DECIMAL_DIG, d)
*
*
* N.B.: If we want the printed result to again survive a round-trip
* conversion to binary and back, and to be rounded to a human-friendly
* number, then we can only print DBL_DIG significant digits (instead
* of the larger DBL_DECIMAL_DIG digits).
*
* Note: "flintmax" here refers to the largest consecutive integer
* that can be safely stored in a floating point variable without
* losing precision.
*/
#ifdef PRINT_ROUND_TRIP_SAFE
# ifdef DBL_DIG
sigdig = DBL_DIG;
# else
sigdig = ilog10(uipow(FLT_RADIX, DBL_MANT_DIG - 1));
# endif
#else
# ifdef DBL_DECIMAL_DIG
sigdig = DBL_DECIMAL_DIG;
# else
sigdig = (size_t) lrint(ceil(DBL_MANT_DIG * log10((double) FLT_RADIX))) + 1;
# endif
#endif
flintmax = pow((double) FLT_RADIX, (double) DBL_MANT_DIG); /* xxx use uipow() */
if (d == 0.0) {
printf("z = %.*s\n", (int) sigdig + 1, "0.000000000000000000000"); /* 21 */
} else if (fabs(d) >= 0.1 &&
fabs(d) <= flintmax) {
dplaces = (int) (sigdig - (size_t) lrint(ceil(log10(ceil(fabs(d))))));
if (dplaces < 0) {
/* XXX this is likely never less than -1 */
/*
* XXX the last digit is not significant!!! XXX
*
* This should also be printed with sprintf() and edited...
*/
printf("R = %.0f [%d too many significant digits!!!, zero decimal places]\n", d, abs(dplaces));
} else if (dplaces == 0) {
/*
* The decimal fraction here is not significant and
* should always be zero (XXX I've never seen this)
*/
printf("R = %.0f [zero decimal places]\n", d);
} else {
if (fabs(d) == 1.0) {
/*
* This is a special case where the calculation
* is off by one because log10(1.0) is 0, but
* we still have the leading '1' whole digit to
* count as a significant digit.
*/
#if 0
printf("ceil(1.0) = %f, log10(ceil(1.0)) = %f, ceil(log10(ceil(1.0))) = %f\n",
ceil(fabs(d)), log10(ceil(fabs(d))), ceil(log10(ceil(fabs(d)))));
#endif
dplaces--;
}
/* this is really the "useful" range of %f */
printf("r = %.*f [%d decimal places]\n", dplaces, d, dplaces);
}
} else {
if (fabs(d) < 1.0) {
int lz;
lz = abs((int) lrint(floor(log10(fabs(d)))));
/* i.e. add # of leading zeros to the precision */
dplaces = (int) sigdig - 1 + lz;
printf("f = %.*f [%d decimal places]\n", dplaces, d, dplaces);
} else { /* d > flintmax */
size_t n;
size_t i;
char *df;
/*
* hmmmm... the easy way to suppress the "invalid",
* i.e. non-significant digits is to do a string
* replacement of all dgits after the first
* DBL_DECIMAL_DIG to convert them to zeros, and to
* round the least significant digit.
*/
df = malloc((size_t) 1);
n = (size_t) snprintf(df, (size_t) 1, "%.1f", d);
n++; /* for the NUL */
df = realloc(df, n);
(void) snprintf(df, n, "%.1f", d);
if ((n - 2) > sigdig) {
/*
* XXX rounding the integer part here is "hard"
* -- we would have to convert the digits up to
* this point back into a binary format and
* round that value appropriately in order to
* do it correctly.
*/
if (df[sigdig] >= '5' && df[sigdig] <= '9') {
if (df[sigdig - 1] == '9') {
/*
* xxx fixing this is left as
* an exercise to the reader!
*/
printf("F = *** failed to round integer part at the least significant digit!!! ***\n");
free(df);
return;
} else {
df[sigdig - 1]++;
}
}
for (i = sigdig; df[i] != '.'; i++) {
df[i] = '0';
}
} else {
i = n - 1; /* less the NUL */
if (isnan(d) || isinf(d)) {
sigdig = 0; /* "nan" or "inf" */
}
}
printf("F = %.*s. [0 decimal places, %lu digits, %lu digits significant]\n",
(int) i, df, (unsigned long int) i, (unsigned long int) sigdig);
free(df);
}
}
return;
}
static unsigned int
msb(uintmax_t v)
{
unsigned int mb = 0;
while (v >>= 1) { /* unroll for more speed... (see ilog2()) */
mb++;
}
return mb;
}
static unsigned int
ilog10(uintmax_t v)
{
unsigned int r;
static unsigned long long int const PowersOf10[] =
{ 1LLU, 10LLU, 100LLU, 1000LLU, 10000LLU, 100000LLU, 1000000LLU,
10000000LLU, 100000000LLU, 1000000000LLU, 10000000000LLU,
100000000000LLU, 1000000000000LLU, 10000000000000LLU,
100000000000000LLU, 1000000000000000LLU, 10000000000000000LLU,
100000000000000000LLU, 1000000000000000000LLU,
10000000000000000000LLU };
if (!v) {
return ~0U;
}
/*
* By the relationship "log10(v) = log2(v) / log2(10)", we need to
* multiply "log2(v)" by "1 / log2(10)", which is approximately
* 1233/4096, or (1233, followed by a right shift of 12).
*
* Finally, since the result is only an approximation that may be off
* by one, the exact value is found by subtracting "v < PowersOf10[r]"
* from the result.
*/
r = ((msb(v) * 1233) >> 12) + 1;
return r - (v < PowersOf10[r]);
}
评论
snprintf(df, n, "% .1f", d);
我运行了一个小实验来验证打印确实完全保留了数字的二进制表示。事实证明,对于我尝试过的编译器和 C 库来说,确实是所需的位数,即使少一位数字进行打印也会造成一个重大问题。DBL_DECIMAL_DIG
DBL_DECIMAL_DIG
#include <float.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
union {
short s[4];
double d;
} u;
void
test(int digits)
{
int i, j;
char buff[40];
double d2;
int n, num_equal, bin_equal;
srand(17);
n = num_equal = bin_equal = 0;
for (i = 0; i < 1000000; i++) {
for (j = 0; j < 4; j++)
u.s[j] = (rand() << 8) ^ rand();
if (isnan(u.d))
continue;
n++;
sprintf(buff, "%.*g", digits, u.d);
sscanf(buff, "%lg", &d2);
if (u.d == d2)
num_equal++;
if (memcmp(&u.d, &d2, sizeof(double)) == 0)
bin_equal++;
}
printf("Tested %d values with %d digits: %d found numericaly equal, %d found binary equal\n", n, digits, num_equal, bin_equal);
}
int
main()
{
test(DBL_DECIMAL_DIG);
test(DBL_DECIMAL_DIG - 1);
return 0;
}
我使用 Microsoft 的 C 编译器 19.00.24215.1 和 gcc 版本 7.4.0 20170516 (Debian 6.3.0-18+deb9u1) 运行它。少使用一位十进制数字会使比较完全相等的数字数量减半。(我还验证了使用时确实会产生大约一百万个不同的数字。以下是详细结果。rand()
Microsoft C
Tested 999507 values with 17 digits: 999507 found numericaly equal, 999507 found binary equal Tested 999507 values with 16 digits: 545389 found numericaly equal, 545389 found binary equal
海湾合作委员会
Tested 999485 values with 17 digits: 999485 found numericaly equal, 999485 found binary equal Tested 999485 values with 16 digits: 545402 found numericaly equal, 545402 found binary equal
评论
RAND_MAX == 32767
u.s[j] = (rand() << 8) ^ rand();
据我所知,有一种扩散良好的算法允许输出到必要数量的有效数字,这样当扫描回字符串时,原始浮点值是由 David Gay 编写的,可以在 Netlib 上找到(另见相关论文)。例如,在 Python、MySQL、Scilab 等中使用此代码。dtoa.c
评论
为了补充 Stéphane 的答案,最先进的算法被称为“Dragon4”和“Grisu”。
在自己研究这个问题时,我发现这篇文章包含背景信息、相关论文的链接和实现:
https://www.ryanjuckett.com/printing-floating-point-numbers/
您需要额外的信息,即值的不确定性。 0.9375 +/- 0.005
float v=0.9275;
float dv=0.005;
int base=10;
int sigfigs=ceil(-log10(dv)/log10(base))-ceil(-log10(v)/log10(base));
评论
sscanf
评论
printf( "%f", val );