11

Consider this program

int main()
{
        float f = 11.22;
        double d = 44.55;
        int i,j;

        i = f;         //cast float to int
        j = d;         //cast double to int

        printf("i = %d, j = %d, f = %d, d = %d", i,j,f,d);
        //This prints the following:
        // i = 11, j = 44, f = -536870912, d = 1076261027

        return 0;
}

Can someone explain why the casting from double/float to int works correctly in the first case, and does not work when done in printf?
This program was compiled on gcc-4.1.2 on 32-bit linux machine.


EDIT: Zach's answer seems logical, i.e. use of format specifiers to figure out what to pop off the stack. However then consider this follow up question:

int main()
{

    char c = 'd';    // sizeof c is 1, however sizeof character literal
                     // 'd' is equal to sizeof(int) in ANSI C

    printf("lit = %c, lit = %d , c = %c, c = %d", 'd', 'd', c, c);
    //this prints: lit = d, lit = 100 , c = d, c = 100
    //how does printf here pop off the right number of bytes even when
    //the size represented by format specifiers doesn't actually match 
    //the size of the passed arguments(char(1 byte) & char_literal(4 bytes))    

 return 0;
}

How does this work?

Community
  • 1
  • 1
Sandip
  • 715
  • 3
  • 7
  • 13
  • I had a similar doubt. See this thread: http://stackoverflow.com/questions/2377733/how-does-this-program-work – Lazer Mar 08 '10 at 03:16
  • 1
    char is a single character - it's simply an 8-bit integer. When you do any sort of operation on integral type that are smaller than int, they get promoted to integer. This includes when calling a function. So actually it isn't random chance causing your printf call to work, this behavior is defined. Practically speaking, in most C ABIs you always allocate at least one machine word per variable passed on the stack. – Roee Shenberg Jan 22 '16 at 16:00
  • @SurajJain Yes, `` parameter passing only considers types promoted according to default argument promotions (C11 §6.5.2.2/6, §7.16.1.1/2), which does guarantee that `char` and `int` are made compatible. However, this is not quite the same as the promotions used for arithmetic. Also, it's dangerous to reason about the language in terms of the ABI. Confirming that it's OK really requires checking the rules. – Potatoswatter Aug 23 '16 at 05:04
  • @Potatoswatter How did my comment got deleted? – Suraj Jain Aug 23 '16 at 08:14
  • @SurajJain Very strange. This site has a lot of moderators and sometimes things do randomly disappear. – Potatoswatter Sep 07 '16 at 13:49

7 Answers7

20

The printf function uses the format specifiers to figure out what to pop off the stack. So when it sees %d, it pops off 4 bytes and interprets them as an int, which is wrong (the binary representation of (float)3.0 is not the same as (int)3).

You'll need to either use the %f format specifiers or cast the arguments to int. If you're using a new enough version of gcc, then turning on stronger warnings catches this sort of error:

$ gcc -Wall -Werror test.c
cc1: warnings being treated as errors
test.c: In function ‘main’:
test.c:10: error: implicit declaration of function ‘printf’
test.c:10: error: incompatible implicit declaration of built-in function ‘printf’
test.c:10: error: format ‘%d’ expects type ‘int’, but argument 4 has type ‘double’
test.c:10: error: format ‘%d’ expects type ‘int’, but argument 5 has type ‘double’

Response to the edited part of the question:

C's integer promotion rules say that all types smaller than int get promoted to int when passed as a vararg. So in your case, the 'd' is getting promoted to an int, then printf is popping off an int and casting to a char. The best reference I could find for this behavior was this blog entry.

Zach Hirsch
  • 24,631
  • 8
  • 32
  • 29
  • I have a follow up question, please see the edited part of my question. Please comment if the question isn't clear...thanks – Sandip Mar 08 '10 at 02:45
  • 1
    It's true that `char` arguments get promoted to `int` when passed in the variable part of the argument list (this is what's happening when `c` is passed in the example), but character literal constants (like `'d'`) are already of type `int`. (Note also that in theory, under some compilers, `char` arguments could be promoted to `unsigned int` instead). – caf Mar 08 '10 at 04:21
  • 2
    good answer. This behavior is not particular to printf but hold for all variadic functions. How it works is well documented va_arg manpage, including promotion of chars to int. For an exemple of use you can see my answer in http://stackoverflow.com/questions/1688942 – kriss Mar 08 '10 at 08:24
  • The first sentence is implementation-specific (and not very common for the first few arguments now), some arguments are passed by registers – M.M Apr 08 '20 at 03:36
9

There's no such thing as "casting to int in printf". printf does not do and cannot do any casting. Inconsistent format specifier leads to undefined behavior.

In practice printf simply receives the raw data and reinterprets it as the type implied by the format specifier. If you pass it a double value and specify an int format specifier (like %d), printf will take that double value and blindly reinterpret it an an int. The results will be completely unpredictable (which is why doing this formally causes undefined behavior in C).

AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
  • Of course it could cast -- why not? All the information is there. The compiler knows the argument types even better than we do (did anybody mention the default promotions?), and the conversion specifiers are part of the standard. The reason it doesn't properly convert is partly historical (the original C compilers were too simplistic for such analysis), and a change now would possibly even break code which relies on the reinterpretation. – Peter - Reinstate Monica Feb 23 '16 at 17:57
  • 1
    @Peter A. Schneider: Really? What about situations where the format string is a run-time value? How do you expect the compiler to "cast" anything even if "conversion specifiers are part of the standard"? More generally though, this is a fundamental principle of C language design: its standard library does not rely on "magical" features of the compiler (with few exceptions, maybe). Virtually every library feature can be implemented by user in C language itself. As long as C language follows that fundamental principle, `printf` will not be "augmented" with any compiler magic. – AnT stands with Russia Feb 23 '16 at 18:18
  • Admittedly I didn't think of run-time format strings (because in all reality: When is the last time you used one?). But the standard could distinguish the cases -- look what they have done to `sizeof`. As to the cast: It is fairly obvious that the compiler knows how to cast types in general, so I cannot detect any additional magic here; and I also think that the cast probably could be implemented in C anyway. Like a big if/else if switch over the conversion operators, and then the appropriate casts. The breaking change would rather be the changing semantics of the ellipsis prototype. – Peter - Reinstate Monica Feb 24 '16 at 08:30
  • As a suggestion for an implementation I would envision that in the case where the parameter types after the last declared parameter are known/can be inferred at compile time (as in the static fomat string printf), the compiler creates an unnamed function protoype with the given number of parameters with the given types and treats the arguments like with any other ordinary function; e.g. it refuses to compile or link when the arguments are incompatible with the inferred parameter type. This would probably be similar to some instances of type inference in modern C++, i.e. the mechanisms exist. – Peter - Reinstate Monica Feb 24 '16 at 08:38
4

Jack's answer explains how to fix your problem. I'm going to explain why you're getting your unexpected results. Your code is equivalent to:

float f = 11.22;
double d = 44.55;
int i,j,k,l;

i = (int) f;
j = (int) d;
k = *(int *) &f;         //cast float to int
l = *(int *) &d;         //cast double to int

printf("i = %d, j = %d, f = %d, d = %d", i,j,k,l);

The reason is that f and d are passed to printf as values, and then these values are interpreted as ints. This doesn't change the binary value, so the number displayed is the binary representation of a float or a double. The actual cast from float to int is much more complex in the generated assembly.

Community
  • 1
  • 1
Chris Lutz
  • 73,191
  • 16
  • 130
  • 183
  • 2
    Might help him understand his output, but the code is not equivalent. You can't write equivalent code for the misinterpretation printf, or any variadic function, does while still calling the variadic function. –  Mar 08 '10 at 01:59
  • My code does (for me, and probably for him) what his code is doing (for him). Both are undefined behavior, so neither one is guaranteed to do anything in particular. – Chris Lutz Mar 08 '10 at 02:27
2

Because you are not using the float format specifier, try with:

printf("i = %d, j = %d, f = %f, d = %f", i,j,f,d);

Otherwise, if you want 4 ints you have to cast them before passing the argument to printf:

printf("i = %d, j = %d, f = %d, d = %d", i,j,(int)f,(int)d);
Jack
  • 131,802
  • 30
  • 241
  • 343
1

The reason your follow-up code works is because the character constant is promoted to an int before it is pushed onto the stack. So printf pops off 4 bytes for %c and for %d. In fact, character constants are of type int, not type char. C is strange that way.

MtnViewJohn
  • 683
  • 4
  • 9
0

printf uses variable length argument lists, which means you need to provide the type information. You're providing the wrong information, so it gets confused. Jack provides the practical solution.

Matthew Flaschen
  • 278,309
  • 50
  • 514
  • 539
0

It's worth noting that printf, being a function with a variable-length argument list, never receives a float; float arguments are "old school" promoted to doubles.

A recent standard draft introduces the "old school" default promotions first (n1570, 6.5.2.2/6):

If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions.

Then it discusses variable argument lists (6.5.2.2/7):

The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter. The default argument promotions are performed on trailing arguments.

The consequence for printf is that it is impossible to "print" a genuine float. A float expression is always promoted to double, which is an 8 byte value for IEEE 754 implementations. This promotion occurs on the calling side; printf will already have an 8 byte argument on the stack when its execution starts.

If we assign 11.22to a double and inspect its contents, with my x86_64-pc-cygwin gcc I see the byte sequence 000000e0a3702640.

That explains the int value printed by printf: Ints on this target still have 4 bytes, so that only the first four bytes 000000e0 are evaluated, and again in little endian, i.e. as 0xe0000000. This is -536870912 in decimal.

If we reverse all of the 8 bytes, because the Intel processor stores doubles in little endian, too, we get 402670a3e0000000. We can check the value this byte sequence represents in IEEE format on this web site; it's close to 1.122E1, i.e. 11.22, the expected result.

Peter - Reinstate Monica
  • 15,048
  • 4
  • 37
  • 62