如何在C语言中将浮点数值限制为小数点后两位?

268

我该如何在C语言中将浮点数(例如37.777779)四舍五入至小数点后两位(37.78)?


15
由于 float(和 double)不是十进制浮点数,而是二进制浮点数,因此无法正确对数字本身四舍五入到小数位, 因此将它们四舍五入到小数位置没有意义。 但可以对输出进行四舍五入。 - Pavel Minaev
76
不是毫无意义,而是不太准确。两者有很大的区别。 - Brooks Moses
2
你期望哪种舍入方式?四舍五入还是最近偶数舍入? - Truthseeker Rangwan
17个回答

475

如果您只是想为输出目的四舍五入数字,那么"%.2f"格式字符串确实是正确的答案。然而,如果您实际上想要将浮点值四舍五入以进行进一步计算,则可以使用以下代码:

#include <math.h>

float val = 37.777779;

float rounded_down = floorf(val * 100) / 100;   /* Result: 37.77 */
float nearest = roundf(val * 100) / 100;  /* Result: 37.78 */
float rounded_up = ceilf(val * 100) / 100;      /* Result: 37.78 */

请注意,您可能想要选择三种不同的舍入规则:向下取整(即在两个小数位后截断),四舍五入和向上取整。通常,您会选择四舍五入。

正如其他一些人指出的那样,由于浮点表示的怪癖,这些舍入值可能不是“明显”的十进制值,但它们非常接近。

有关舍入的更多(更多!)信息,特别是关于四舍五入到最近的打结规则,请参阅维基百科关于舍入的文章


5
可以进行修改以支持任意精度的四舍五入吗? - user472308
1
@slater 当你说“任意精度”时,是指将舍入到三位而不是两位小数,还是使用实现无限精度十进制值的库?如果是前者,请对常量100进行明显调整;否则,使用您正在使用的任何多精度库执行与上面相同的精确计算。 - Dale Hagglund
2
@DaleHagglung 前者,谢谢。将100替换为pow(10,(int)desiredPrecision)的调整是否正确? - user472308
3
是的。要在小数点后舍入k位,使用一个10^k的比例因子。如果您手写一些小数值并尝试使用10的倍数进行计算,这应该很容易理解。假设您正在处理值1.23456789,并想将其舍入到3个小数位。您可以使用“舍入到整数”的操作。那么,如何将前三个小数位移到小数点左侧呢?我希望很清楚,您需要乘以10^3。现在,您就可以将该值四舍五入为整数了。接下来,通过除以10 ^ 3,将三个低位数的数字放回去。 - Dale Hagglund
1
这是一个通用的宏,可以适用于任意小数位数:#define ROUND_TO_DECIMAL_PLACES(x, decimal_places) (roundf(x * 1e##decimal_places) / 1e##decimal_places) - smileyborg
显示剩余12条评论

122

使用printf的%.2f,它只会打印2个小数点。

示例:

printf("%.2f", 37.777779);

输出:

37.77

这种方法更好,因为没有精度损失。 - albert
3
这样做的好处是,避免了val*100可能会溢出而导致浮点数范围减少的问题。 - chux - Reinstate Monica
1
这里讨论了%.2f是依赖于实现的。 - tueda

44

假设你是在谈论打印输出结果时需要四舍五入,那么Andrew ColesonAraK的回答是正确的:

printf("%.2f", 37.777779);

但是请注意,如果您的目标是将数字精确舍入为37.78以进行内部使用(例如与另一个值进行比较),那么这不是一个好主意,因为浮点数的工作方式:通常不建议使用浮点数进行相等性比较,而是使用目标值+/- sigma 值。或者将数字编码为具有已知精度的字符串,并进行比较。

请参见Greg Hewgill在相关问题的回答中提供的链接,其中还涵盖了为什么不应该在财务计算中使用浮点数的原因。


1
点赞,因为解决了可能存在的问题(或者应该在问题背后的问题!)。这是一个非常重要的观点。 - Brooks Moses
实际上,37.78可以通过浮点数精确表示。浮点数具有11到12位数字的精度。这应该足以处理3778、377.8或所有种类的4位小数。 - Anonymous White
@HaryantoCiu 嗯,说得也有道理,我稍微修改了我的答案。 - John Carter
1
动态精度:printf("%.*f", (int)precision, (double)number); - Minhas Kamal
@AnonymousWhite 这不是正确的,使用32位浮点数最接近37.78的值是37.779998779296875。它无法被精确表示。 - orlp
在C23中,使用浮点数进行金融计算已经过时了。现在的做法是使用十进制浮点数,它非常适合金融代码。 - chux - Reinstate Monica

25

这样怎么样:

float value = 37.777779;
float rounded = ((int)(value * 100 + .5) / 100.0);

6
这不适用于负数(好的,示例是正数,但仍然不适用)。您没有提到在浮点数中无法精确存储小数值。 - John Carter
36
(a) 你说得对。 (b) 这是什么?一份高中考试吗? - Daniil
:D @Danil 很好的回复 :) - Muhammad Adnan
1
为什么你加了0.5? - muhammad tayyab
1
需要遵循舍入规则。 - Daniil
2
在@Daniil的评论中,“舍入规则”是指“四舍五入到最近的数”。 - Shmil The Cat

21
printf("%.2f", 37.777779);

如果你想要写入C字符串:

char number[24]; // dummy size, you should take care of the size!
sprintf(number, "%.2f", 37.777779);

@Sinan:为什么要修改?@AraK:不,应该注意大小:)。使用snprintf()。 - aib
1
@aib:我猜是因为 /**/ 是C风格的注释,而这个问题标记为C。 - Michael Haren
5
C89只允许使用/**/-样式的注释,C99引入了对//-样式的支持。如果使用老旧的编译器(或强制使用C89模式),则无法使用//-样式。话虽如此,现在是2009年,让我们将它们都视为C和C ++风格的注释。 - Andrew Coleson

16

在此情况下,始终使用printf函数族。即使您想要将值作为浮点数获取,最好使用snprintf将四舍五入的值作为字符串获取,然后使用atof解析回来:

#include <math.h>
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>

double dround(double val, int dp) {
    int charsNeeded = 1 + snprintf(NULL, 0, "%.*f", dp, val);
    char *buffer = malloc(charsNeeded);
    snprintf(buffer, charsNeeded, "%.*f", dp, val);
    double result = atof(buffer);
    free(buffer);
    return result;
}

我之所以这么说,是因为当前得票最高的答案和其他几个答案的方法存在两个缺陷:
  • 对于某些值,它会因为乘以100而改变决定四舍五入方向的小数位,导致舍入方向错误,这是由于浮点数的不精确性所致。
  • 对于一些值,乘以100然后再除以100不会进行完全的舍入,即使没有进行舍入,最终结果也将是错误的。

为了说明第一种错误-有时舍入方向错误-尝试运行此程序:

int main(void) {
    // This number is EXACTLY representable as a double
    double x = 0.01499999999999999944488848768742172978818416595458984375;

    printf("x: %.50f\n", x);

    double res1 = dround(x, 2);
    double res2 = round(100 * x) / 100;

    printf("Rounded with snprintf: %.50f\n", res1);
    printf("Rounded with round, then divided: %.50f\n", res2);
}

你将看到以下输出:

x: 0.01499999999999999944488848768742172978818416595459
Rounded with snprintf: 0.01000000000000000020816681711721685132943093776703
Rounded with round, then divided: 0.02000000000000000041633363423443370265886187553406

请注意,我们起始的值小于0.015,所以当我们将其舍入为2位小数时,数学上正确的答案是0.01。当然,0.01无法被精确地表示为双精度浮点数,但我们期望我们的结果是最接近0.01的双精度浮点数。使用snprintf可以给我们这个结果,但使用round(100 * x) / 100会给我们0.02,这是错误的。为什么?因为100 * x的结果恰好是1.5。因此,乘以100会改变正确的舍入方向。
为了说明第二种错误——由于*100/100不是真正的互逆运算而导致的结果有时会出错——我们可以用一个非常大的数字做类似的练习:
int main(void) {
    double x = 8631192423766613.0;

    printf("x: %.1f\n", x);

    double res1 = dround(x, 2);
    double res2 = round(100 * x) / 100;

    printf("Rounded with snprintf: %.1f\n", res1);
    printf("Rounded with round, then divided: %.1f\n", res2);
}

我们现在的数字甚至没有小数部分,它是一个整数值,只是用类型 double 存储。因此,四舍五入后的结果应该与我们开始的数字相同,对吗?
如果您运行上面的程序,您会看到:
x: 8631192423766613.0
Rounded with snprintf: 8631192423766613.0
Rounded with round, then divided: 8631192423766612.0

糟糕。我们的snprintf方法再次返回了正确的结果,但是乘以-然后四舍五入-然后除以的方法失败了。这是因为数学上正确的8631192423766613.0 * 100的值863119242376661300.0不能精确地表示为双精度浮点数; 最接近的值是863119242376661248.0。当您将其除以100时,您得到的是8631192423766612.0 - 与您开始的数字不同。

希望这足以证明使用roundf进行四舍五入到小数位数是错误的,并且应该改用snprintf。如果您觉得这是一个可怕的hack,也许您会放心知道这就是CPython基本上所做的


4
请问您需要翻译成中文吗?如果是的话,以下是我的翻译:我希望你能提供一个具体的例子,说明我的答案及类似答案的问题在IEEE浮点数的怪异性方面出了什么问题,并提供一个简单明了的替代方案。我曾经有些涉猎,很久以前,关于将打印和传输中的浮点数值变得安全以便回转的工作。我猜想那时所做的工作现在可能会显现出来。 - Dale Hagglund
嗯,抱歉最后一句话有点混乱,现在太晚了无法编辑。我想表达的是,“......花了很多心思使printf和其他相关函数更加安全......” - Dale Hagglund
1
不幸的是,snprintf 的结果可能取决于实现;dround(0.125, 2) 可能会得到 0.120.13。https://godbolt.org/z/xz4K1P13z - tueda
@tueda 真糟糕!所以,如果你想要一个既适用于非平局又在各个平台上保持一致的四舍五入方法,在C标准库中就没有方便的方法可以使用,只能自己动手实现? - Mark Amery
1
我猜这就是为什么人们编写自己的 dtoa,例如在 CPython 中的 _Py_dg_dtoa - tueda

12

此外,如果您正在使用C ++,您可以创建一个这样的函数:

string prd(const double x, const int decDigits) {
    stringstream ss;
    ss << fixed;
    ss.precision(decDigits); // set # places after decimal
    ss << x;
    return ss.str();
}

你可以使用以下代码将任何双精度myDouble输出为小数点后n位:

std::cout << prd(myDouble,n);

12

由于浮点数的限制,没有一种方法可以将一个float舍入到另一个float,因为舍入后的float可能无法被表示。例如,假设您将37.777779舍入为37.78,但最接近的可表示数字是37.781。

然而,您可以通过使用格式化字符串函数来“舍入”float


3
这段话的意思是:“无法用浮点数除以另一个浮点数并得到一个浮点数,因为除法结果可能无法表示。”这个说法或许是正确的,但却不太相关。浮点数始终是不精确的,即使是基本的加法也是如此;我们总是假设实际得到的是“最接近精确舍入答案的浮点数”。 - Brooks Moses
1
我的意思是你不能将float四舍五入到n位小数,然后期望结果总是有n位小数。你仍然会得到一个float,只不过不是你期望的那个。 - Andrew Keeton
你的第一句话听起来可能是正确的,但许多编程语言确实允许你将一个浮点数四舍五入为另一个。例如考虑Python的round()函数:https://www.pythontutorial.net/advanced-python/python-rounding/ 令人惊讶的是,像这样基本的功能在C++中被省略了。 - Raleigh L.

10

您仍然可以使用以下方法:

float ceilf(float x); // don't forget #include <math.h> and link with -lm.

示例:

float valueToRound = 37.777779;
float roundedValue = ceilf(valueToRound * 100) / 100;

这将在小数点处截断(即将产生37),他需要在小数点后四舍五入到两位。 - Pavel Minaev
将小数点后的值四舍五入到两位是一个微不足道的变化,但仍应在答案中提到;ZeroCool,想要添加编辑吗?float roundedValue = ceilf(valueToRound * 100.0) / 100.0; - Brooks Moses
这个解决方案为什么不太受欢迎呢?它用最少的代码实现了应有的功能。难道有什么要注意的吗? - Andy

10
在C++中(或在C语言中使用C风格的转换),您可以创建以下函数:
/* Function to control # of decimal places to be output for x */
double showDecimals(const double& x, const int& numDecimals) {
    int y=x;
    double z=x-y;
    double m=pow(10,numDecimals);
    double q=z*m;
    double r=round(q);

    return static_cast<double>(y)+(1.0/m)*r;
}

那么 std::cout << showDecimals(37.777779,2); 将会输出:37.78。

显然,您不需要在该函数中创建所有 5 个变量,但我将它们保留在那里以便您可以看到逻辑。可能有更简单的解决方案,但对我来说这很有效--尤其是因为它允许我根据需要调整小数点后的位数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接