What happens to a float variable when %d is used in a printf?

I'm trying to learn C by reading C Programming Language, 2nd Edition . I have some programming experience but not with C.

I'm currently in Chapter 1. I have the following code:


  float f;
  for (f = 0.0; f <= 3; f += 1.1)
      printf("A: %3f B: %6.2f\n", f, f + 0.15);

It prints the output:

A: 0.000000 B:   0.15
A: 1.100000 B:   1.25
A: 2.200000 B:   2.35

Looks fine.


Now I change the printf as follows:

printf("A: %3d B: %6.2f\n", f, f + 0.15);

The new output is

A:   0 B:   0.00
A: -1610612736 B:   0.00
A: -1610612736 B: -625777476808257557292155887552002761191109083510753486844893290688350183831589633800863219712.00

What's going on here? I would expect the float to be converted to int because I used %d but that's not what happened. Also, why did the the value B go wrong as well? What happened to f here?


Solution 1:

When you called:

printf("A: %3d B: %6.2f\n", f, f + 0.15);

C automatically converts the float values to double (it is a standard conversion made when you call a function that takes variable arguments, such as int printf(const char *fmt, ...);). For sake of argument, we will assume that sizeof(int) is 4 and sizeof(double) is 8 (there are exceptions, but they are few and far between).

The call, therefore, has pushed a pointer onto the stack, plus an 8-byte double for f, and another 8-byte double for f + 0.15. When it is processing the format string, the %d tells printf() that you pushed a 4-byte int onto the stack after the format string. Since that is not what you did, you have invoked undefined behaviour; whatever happens next is OK according to the C standard.

However, the most likely implementation blithely reads 4 bytes and prints them as if they were an int (it trusts you to tell it the truth). Then it comes across the %6.2f format; it will read 8-bytes off the stack as a double. There's an outside chance that this would cause a memory fault for misaligned access (it would require a 64-bit machine with a requirement that double be aligned on an 8-byte boundary, such as a SPARC), or it will read 4 bytes from f and 4 bytes from f + 0.15, putting them together to create some rather unexpected double value -- as your example shows.

Solution 2:

Printf will treat the memory you point as however you tell it to. There is no conversion going on. It is treating the memory that represents the float as an int. Because the two are stored differently, you get what is essentially a random number.

If you want to output your float as an integer, you should cast it first:

printf("A: %3d B: %6.2f\n", (int)f, f + 0.15); 

Solution 3:

It is possible to print any integer value one wants, regardless of the floating point parameter:

  printf("A: %d B: %6.2f\n", f, f + 0.15);

Here is how you can print arbitrary integers on Intel architecture:

  int print_it(int, int /* nameless but printed */, float f)
  {
      printf("A: %d B: %6.2f\n", f, f + 0.15);
  }
  int main()
  {
      print_it(0, 12 /* will be printed */, 0.0);
      print_it(0, 123 /* printed */, 1.1);
      print_it(0, 1234 /* printed */ , 2.2);
  }

This output:

A: 12 B:   0.00
A: 123 B:   1.10
A: 1234 B:   2.20

Explanation: Obviously, mismatched format string and parameters lead to undefined behavior. Nevertheless, sometimes this can be predicted. On Intel architecture, the first few parameters are passed by registers. Floating point values are passed on different registers.

Despite having the same printf instruction as in the question, the output is different. What happens is that 12, 123, 1234 are passed through the general purpose register responsible for the second non-floating point parameter. Since printf has only one non-floating point parameter, the register of the second non-fp parameter is unchanged. This register retains the value it got from the second parameter of print_it(0, int_value, fp_value).

But the original gives garbage:

  for (f = 0.0; f <= 3; f += 1.1)
      printf("A: %3f B: %6.2f\n", f, f + 0.15);

It gives different garbage because printf calls other functions internally. These functions trash the general purpose register that printf("... %d ...", ...) reads.

Obviously, this behavior happens only on systems that pass floating point parameters in a separate set of registers. Obviously, this happens only if the compiler optimization does not modify the code in some way, because it is allowed to do wild things when undefined behavior is at play.

Solution 4:

For most functions, if you pass a float but the function expects an int, the compiler knows to automatically convert the float to int. But printf is special (very special). %d expects an int, and it's your job to pass it an int. In printf's case, no automatic mechanism in the compiler is able to perform the conversion for you.

With that said, though, good compilers detect and warn about this problem. If yours didn't, you need a better one.

The longer explanation is that for most functions, the function prototype gives the number and type of all the arguments, and that's the mechanism that lets the compiler learn that it might need to introduce conversions. But the prototype for printf is

extern int printf(const char *, ...);

where those three dots ... literally mean, "there will be a variable number of arguments here, don't know how many or what types". By the time printf is actually running, and it finds the %d in the format string telling it to expect an int that was passed, its too late to do any conversions if what you passed was something other than an int.