如果您希望将结果作为字符串,您可能应该以更高的精度打印到字符串,然后自己将其切掉。 (有关 IEEE 64 位 double 需要多少额外精度的详细信息,请参阅 @chux 的答案以避免从 9 的字符串向上取整,因为您想要截断,但所有常用的字符串函数都四舍五入到最近。)
如果你想要double 结果,那么你确定你真的想要这个吗?在计算中间的早期舍入/截断通常只会降低最终结果的准确性。当然,floor/ceil、trunc 和 nearint 在实际算法中也有使用,这只是 trunc 的缩放版本。
如果你只想要一个double,你可以得到相当好的结果,而无需使用字符串。 使用ndigits 和floor(log10(fabs(x))) 计算比例因子,然后将缩放后的值截断为整数,然后再按比例缩小。
经过测试和工作(有和没有-ffast-math)。请参阅Godbolt compiler explorer 上的 asm。这可能会相当有效地运行,尤其是使用-ffast-math -msse4.1(因此 floor 和 trunc 可以内联到 roundsd)。
如果您关心速度,请考虑将pow() 替换为利用指数是一个小整数这一事实的东西。我不确定在这种情况下库 pow() 实现的速度有多快。 GNU C __builtin_powi(x, n) trades accuracy for speed, for integer exponents, doing a multiplication tree, which is less accurate than what pow() does.
#include <float.h>
#include <math.h>
#include <stdio.h>
double truncate_n_digits(double x, int digits)
{
if (x==0 || !isfinite(x))
return x; // good idea stolen from Chux's answer :)
double l10 = log10(fabs(x));
double scale = pow(10., floor(l10) + (1 - digits)); // floor rounds towards -Inf
double scaled = x / scale;
double scaletrunc = trunc(scaled); // trunc rounds towards zero
double truncated = scaletrunc * scale;
#if 1 // debugging code
printf("%2d %24.14g =>\t%24.14g\t scale=%g, scaled=%.30g\n", digits, x, truncated, scale, scaled);
// print with more accuracy to reveal the real behaviour
printf(" %24.20g =>\t%24.20g\n", x, truncated);
#endif
return truncated;
}
测试用例:
int main() {
truncate_n_digits(0.014568, 1);
truncate_n_digits(0.246456, 1);
truncate_n_digits(0.014568, 2);
truncate_n_digits(-0.246456, 2);
truncate_n_digits(1234567, 2);
truncate_n_digits(99999999999, 6);
truncate_n_digits(-99999999999, 6);
truncate_n_digits(99999, 10);
truncate_n_digits(-0.0000000001234567, 3);
truncate_n_digits(1000, 6);
truncate_n_digits(0.001, 6);
truncate_n_digits(1e-312, 2); // denormal, and not exactly representable: 9.999...e-313
truncate_n_digits(nextafter(1e-312, INFINITY), 2); // denormal, just above 1.00000e-312
return 0;
}
每个结果显示两次:首先只有%.14g,所以四舍五入给出了我们想要的字符串,然后再次使用%.20g 显示足够多的地方来揭示浮点数学的现实。大多数数字都不能精确表示,因此即使完美舍入也不可能返回 double 完全 表示截断的十进制字符串。 (大约尾数大小的整数可以精确表示,分母是 2 的幂的分数也是如此。)
1 0.014568 => 0.01 scale=0.01, scaled=1.45679999999999987281285029894
0.014567999999999999353 => 0.010000000000000000208
1 0.246456 => 0.2 scale=0.1, scaled=2.46456000000000008398615136684
0.2464560000000000084 => 0.2000000000000000111
2 0.014568 => 0.014 scale=0.001, scaled=14.5679999999999996163069226895
0.014567999999999999353 => 0.014000000000000000291
2 -0.246456 => -0.24 scale=0.01, scaled=-24.6456000000000017280399333686
-0.2464560000000000084 => -0.23999999999999999112
3 1234.56789 => 1230 scale=10, scaled=123.456789000000000555701262783
1234.567890000000034 => 1230
6 1234.56789 => 1234.56 scale=0.01, scaled=123456.789000000004307366907597
1234.567890000000034 => 1234.5599999999999454
6 99999999999 => 99999900000 scale=100000, scaled=999999.999990000040270388126373
99999999999 => 99999900000
6 -99999999999 => -99999900000 scale=100000, scaled=-999999.999990000040270388126373
-99999999999 => -99999900000
10 99999 => 99999 scale=1e-05, scaled=9999900000
99999 => 99999.000000000014552
3 -1.234567e-10 => -1.23e-10 scale=1e-12, scaled=-123.456699999999983674570103176
-1.234566999999999879e-10 => -1.2299999999999998884e-10
6 1000 => 1000 scale=0.01, scaled=100000
1000 => 1000
6 0.001 => 0.001 scale=1e-08, scaled=100000
0.0010000000000000000208 => 0.0010000000000000000208
2 9.9999999999847e-313 => 9.9999999996388e-313 scale=1e-314, scaled=100.000000003458453079474566039
9.9999999999846534143e-313 => 9.9999999996388074622e-313
2 1.0000000000034e-312 => 9.0000000001196e-313 scale=1e-313, scaled=9.9999999999011865980946822674
1.0000000000034059979e-312 => 9.0000000001195857973e-31
由于您想要的结果通常无法精确表示,(并且由于其他舍入误差)生成的 double 有时会低于您想要的结果,因此以全精度打印它可能会得到 1.19999999 而不是 1.20000011。您可能希望使用 nextafter(result, copysign(INFINITY, original)) 来获得比您想要的更可能具有更高量级的结果。
当然,在某些情况下,这只会让事情变得更糟。但由于我们向零截断,大多数情况下我们得到的结果刚好低于(在数量上)无法表示的精确值。