在C语言中将浮点数四舍五入为最接近的整数

4

我可能会有一个新手问题,但是在搜索网站上没有找到任何东西。我正在学习使用C语言编程,并尝试从头开始构建一个将浮点数四舍五入为最接近的整数的函数,而不使用math.h库。这是我的代码:

void main()
{
    float b;
    for(b = 0; b <= 2; b = b + 0.1)
    {
        printf("%f    ", b);
        printf("%i    ", (int)b);
        printf("%f    ", b - (int)b);
        printf("Nearest: ");
        if((b - (int)b)<0.5)
            printf("%i    ", (int)b);
        else
            printf("%i    ", (int)b + 1);
        printf("Function: %i    ", round_near(b));
        printf("\n");
    }
    getchar();
}

int round_near(float b)
{
    if((b - (int)b)<0.5)
        return(int)b;
    else
        return (int)b + 1;
}

我的结果看起来像这样:

输入图像描述

代码有些多余,只是为了查看函数的一些单独步骤。 出了什么问题?是否有我不知道的浮点类型变量的诡计?

1
工作:http://ideone.com/TLKAiL - user4651282
2
不要忘记负数... - Eugene Sh.
@EugeneSh。在什么情况下(int)b会大于b - scohe001
2
void main() --> int main(void) - Sourav Ghosh
1
我觉得这是一个非常棒的问题。如果我要教别人C语言中这个不太常见的部分,我找不到比使用这个问题作为例子更好的方式了。 - QuestionC
显示剩余6条评论
5个回答

7

你的代码中没有 int round_near(float b) 的原型,因此你依靠隐式声明。

尝试将以下内容添加到你的代码中。

int round_near (float b); // Prototype

int main(void) // Nitpick: main returns an int!

使用隐式声明来调用 round_near(b) 函数时,b 会被提升为 double 类型。但是函数定义假设它是 float 类型,而这两种类型的二进制布局不同,因此会得到疯狂的随机结果。
为避免这种情况发生,你应该确保你的代码在编译时没有任何警告。虽然编程语言中存在隐式声明仅是为了保持向后兼容性,但是过去10年或20年的每个编译器都会在编译时给出警告,表示这种方式是不好的。

它代表着:仍然是唯一完整的答案。 - alk
“尴尬”,我没想到要检查是否包含了原型。经典的新手错误。不过,很有趣,我不知道浮点数和双精度浮点数不能轻松互换。我以为其中一个只是另一个的缩写,就像 int 和 long int 一样。另外提个问题,为什么 int main(void)void main() 好? - Vlad Danila
1
90%的时间它们是可以互换的。但它们确实有不同的内存布局(浮点数是32位,双精度浮点数是64位),而且你发现了一种情况,即你最终将一个双精度浮点数塞入了一个浮点数的内存布局中,从而导致了错误。 - QuestionC
1
关于 main 函数返回 int 类型:请注意,操作系统会看到 main 函数的返回值。按照惯例,return 0; 表示程序成功执行,而非零则表示某种失败情况。 - QuestionC
如果我的回答解决了你的问题,请接受它作为答案。 - QuestionC

2

当我尝试在gcc下编译这个程序时,出现了以下错误:

/tmp/x1.c:23: error: conflicting types for ‘round_near’
/tmp/x1.c:23: note: an argument type that has a default promotion can’t match an empty parameter name list declaration
/tmp/x1.c:16: error: previous implicit declaration of ‘round_near’ was here

你得到的有趣结果是因为编译器在第一次遇到round_near时不知道它的定义,并假定它是int round_near()。这导致了未定义的行为。
如果你将round_near移到main函数上面或者在main函数上面放置一个声明,你应该会得到预期的结果。

为什么会产生未定义行为? - alk

2
@QuestionC很好地回答了OP的问题:隐含函数签名int round_near(...)int round_near(float b)不兼容,调用round_near(b)将b作为double传递。简单解决方案:原型函数。

关于round_near()的一些问题:

  1. 强制转换为int会严重缩小合法范围。最好使用long long

  2. 负数的通用错误功能。@Eugene Sh。代码应该测试符号。

下面是一个解决方案,利用long long的范围,因为它通常比float可以准确表示的整数范围更大。或者,OP可以用round_near()替换my_roundf()并使用此代码进行测试。 round_near()失败了约40%的时间。

#include <limits.h>
#include <stdio.h>

float my_roundf(float x) {
  // Large `float`s typically have no fractional portion to round
  if (x > LLONG_MAX / 2) return x;
  if (x < LLONG_MIN / 2) return x;
  return x > 0 ? (long long) (x + 0.5f) : (long long) (x - 0.5f);
}

float rand_float(void) {
  union {
    unsigned char uc[sizeof(float)];
    float f;
  } u;
  do {
    unsigned i;
    for (i = 0; i < sizeof(float); i++) {
      u.uc[i] = rand();
    }
  } while (u.f != u.f);  // re-do if NaN encountered
  return u.f;
}

void my_roundf_test(void) {
  unsigned n = 100000;
  while (n-- > 0) {
    float x = rand_float();
    float ymath = roundf(x);
    // float ymy = round_near(x);
    float ymy = my_roundf(x);
    // Exact half-way cases may fail
    if (ymath != ymy) {
      printf("x:% .9e math:% .9e my:% .9e\n", x, ymath, ymy);
    }
  }
}

注意:有关各种浮点舍入模式、负零等确切的中间情况需要考虑,以获得完整的答案。但这些内容留待另一天再讨论。

1
谢谢您的建议。我没有考虑过以0.5递增并向下取整与四舍五入到最近值相同。这很聪明。我甚至没有考虑过使用long long而不是int,因为我大多数情况下使用float来表示小有理数而不是大数。负数是一个完全疏忽的问题。 - Vlad Danila

0
一个简单的例子(float 无法适应 int 类型,因此使用 long long)。
long long round(float a) {
    long long b = a;
    if (a >= 0)
        return a - b < 0.5 ? b : b + 1;
    else
        return b - a < 0.5 ? b : b - 1;
}

-2

输出像-241...而不是通常表示未初始化整数的1或2... 然而,在Linux上,您的代码使用GNU C编译器(gcc)编译得很好,只需将round_near函数移动到int main()之前或者在int main()之前插入一个空白定义该函数(如int round_near(float b);)--即“原型”。

否则,您的函数将被视为int round_near()(请注意缺少参数定义),因此程序打印出未初始化的整数。

另一方面,这样的做法不会产生可移植的代码,因此如果没有以下修改,您的(实际上是C)代码可能会在Visual Studio中编译...但在其他编译器中不行。

还有一个离题的话题:不要在for循环中使用浮点数。浮点数很讨厌!


没有在<code>int main()</code>之前对函数进行适当定义原型,你的函数将被称为<code>int round_near()</code>而不是<code>int round_near(1.2)</code>。看到区别了吗?b被丢弃了,所以没有给参数b提供值=>没有值可用于(int)b以及后续操作... - dam
1
这个“你的函数将被称为 int round_near() 而不是 int round_near(1.2)”是不正确的。C语言是不同的。问题不在于缺少原型,而是float类型会升级为double类型(参见下面QuestionC的回答)。如果OP编写了round_near(double f)的代码,则即使没有原型,代码也能正常工作。 - alk

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