4

I might have a noob question here, but searching the site hasn't yelded anything. I'm learning to program in C and I'm trying to build a function from scratch that rounds floats to the nearest integer, without using math.h. Here's my code:

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;
}

My results looks like this:

enter image description here

Some of the code is superfluous and was just meant to see some of the individual steps of my function. What gives? Are there some shenanigans with float type variables I'm not aware of?

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
Vlad Danila
  • 49
  • 1
  • 3

5 Answers5

7

You don't have a prototype for int round_near(float b), so you're relying on implicit declarations.

Try adding this to your code.

int round_near (float b); // Prototype

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

Using implicit declarations for round_near(b), b is being promoted to a double. But the definition assumes it's a float, which has a different binary layout, so you get crazy random results.

You should make sure your code compiles without any warnings to avoid this sort of stuff. The only reason implicit declaration is in the language is for backwards compatibility, but every compiler for the last decade or two warns you that it's bad on compile.

QuestionC
  • 10,006
  • 4
  • 26
  • 44
  • It stands: Still the only complete answer. – alk Jul 08 '15 at 17:26
  • _cringe_ I hadn't thought to check if I included my prototype. Classic rookie mistake. Still, it's intersting, I wasn't aware that floats and doubles aren't easily interchangeable. I thought one was just a shorter version of the other, like int and long int. Side question, why is `int main(void)` better than `void main()`? – Vlad Danila Jul 09 '15 at 18:44
  • 1
    90% of the time they are interchangeable. But they do have different memory layouts (floats are 32 bits, doubles are 64 bits) and you've found a case where you end up shoving a double into a float's memory layout, so bugs. – QuestionC Jul 10 '15 at 01:43
  • 1
    As for main returning an int: http://stackoverflow.com/a/207992/3294441 Note that the return of your main function is seen by the operating system and by convention, `return 0;` indicates program success while non-zero indicates some sort of failure. – QuestionC Jul 10 '15 at 01:48
  • If my question answered your question, please accept it as the answer. – QuestionC Jul 10 '15 at 01:49
2

When I tried to compile this under gcc I got the following error:

/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

The funny results you're getting is because your compiler didn't know the definition of round_near at the time it was first encountered and assumed it was int round_near(). So this resulted in undefined behavior.

If you either move round_near above main or put in a declaration above main you should get the expected results.

dbush
  • 205,898
  • 23
  • 218
  • 273
2

@QuestionC well answered OP's immediate problem: implied function signature of int round_near(...) is incompatible with int round_near(float b) and call of round_near(b) which passes b as a double.

Simple solution: prototype the function.

Some issues about the round_near()

  1. Casting to int severely narrows the legitimate range. Better to use long long.

  2. General incorrect functionality with negative numbers. @Eugene Sh. Code should test for sign.

Below is a solution that takes advantage of the range of long long as it is usually greater than the continuous range of integers a float can represent exactly. Alternatively OP can replace my_roundf() with round_near() and use this code for testing. round_near() fails about 40% of the time.

#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);
    }
  }
}

Note: There are exact half-way cases per various floating point rounding modes, negative zero, etc. to consider for a complete answer. But leave that for another day.

Community
  • 1
  • 1
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • 1
    Thanks for the input. I hadn't considered that incrementing by 0.5 and rounding down would be the same as rounding to nearest. That's clever. I also hadn't even considered using long long instead of int since I mostly use float to express small rational numbers rather than large numbers. The negative numbers was a total oversight. – Vlad Danila Jul 09 '15 at 19:45
0

A simple one (float won't fit in an int type, so 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;
}
Shreevardhan
  • 12,233
  • 3
  • 36
  • 50
-2

Output like -241... instead of 1 or 2 usually denote uninitialized integers... However, your code compiles just fine with GNU C compiler (gcc) on Linux, only after either moving the round_near function BEFORE the int main() or simply inserting a blank definition of that function (as int round_near(float b);) before the int main() -- that is "prototyping".

Otherwise, your function will be "seen" as int round_near() (see the lack of argument definition) and hence the uninitialized integers printed out by the program.

On the other, such a practice won't produce portable code, so without the modifications bellow your (actually C) code may compile in Visual Studio ... but not with other compilers.

Just another off-topic: don't use floats in for loops. Floats are nasty!

dam
  • 21
  • 1
  • Without prototyping of proper definition before int main() your function will be called as int round_near() instead of int round_near(1.2). See the difference? b is discarded, so no value for argument b => no value for (int)b and so on... – dam Jul 08 '15 at 17:22
  • 1
    This "*your function will be called as `int round_near()` instead of `int round_near(1.2)`*" is not correct. C is different. The issue is not the missing prototype, but the `float` getting promoted to `double` (see *QuestionC*'s answer below). If the OP would have coded `round_near(double f)` the code would work fine, even without the prototype. – alk Jul 08 '15 at 17:28