9

Let's consider this C code:

#include <stdio.h>

main()
{
  int x=5;
  printf("x is ");
  printf("%d",5);
}

In this, when we wrote int x=5; we told the computer that x is an integer. The computer must remember that x is an integer. But when we output the value of x in printf() we have to again tell the computer that x is an integer. Why is that?

Why does the computer forget that x was an integer?

12
  • 5
    The compiler knows that x is an integer, but printf does not.
    – luiscubal
    Nov 29, 2014 at 15:39
  • it's just how the calling convention works, there is only 1 implementation of printf(char*, ...) and it just gets (what amounts to) a pointer to a collection of data Nov 29, 2014 at 15:42
  • @ratchetfreak Why is it so? Why printf doesn't get the pointer of data type automatically as it is in C++ where "cout" function automatically knows the data type?
    – user106313
    Nov 29, 2014 at 16:03
  • 5
    Have you considered printf("x is %x in hex, and %d in decimal and %o as octal",x,x,x);?
    – user40980
    Nov 29, 2014 at 18:30
  • 1
    Try it with the number 163. Values less than 8 are rather uninteresting in this situation. Or better yet, loop from 0 to 255 and see what the numbers are. The point is that there is more in printf than just they type.
    – user40980
    Nov 30, 2014 at 8:16

5 Answers 5

19

There are two issues at play here:

Issue #1: C is a statically typed language; all type information is determined at compile time. No type information is stored with any object in memory such that its type and size can be determined at run time1. If you examine the memory at any particular address while the program is running, all you'll see is a sludge of bytes; there's nothing to tell you whether that particular address actually contains an object, what the type or size of that object is, or how to interpret those bytes (as an integer, or floating point type, or sequence of characters in a string, etc.). All that information is baked into the machine code when the code is compiled, based on type information specified in the source code; for example, the function definition

void foo( int x, double y, char *z )
{
  ...
}

tells the compiler to generate the appropriate machine code to handle x as an integer, y as a floating-point value, and z as a pointer to char. Note that any mismatches in the number or type of arguments between a function call and a function definition are only detected when the code is being compiled2; it's only during the compilation phase that any type information is associated with an object.

Issue #2: printf is a variadic function; it takes one fixed parameter of type const char * restrict (the format string), along with zero or more additional parameters, the number and type of which are not known at compile time:

int printf( const char * restrict fmt, ... );

The printf function has no way of knowing what the number and types of additional arguments are from the passed arguments themselves; it has to rely on the format string to tell it how to interpret the sludge of bytes on the stack (or in the registers). Even better, because it's a variadic function, arguments with certain types are promoted to a limited set of default types (e.g., short is promoted to int, float is promoted to double, etc.).

Again, there's no information associated with the additional arguments themselves to give printf any clues on how to interpret or format them. Hence the need for the conversion specifiers in the format string.

Note that in addition to telling printf the number and type of additional arguments, conversion specifiers also tell printf how to format the output (field widths, precision, padding, justification, base (decimal, octal, or hex for integer types), etc.).

Edit

To avoid extensive discussion in the comments (and because the chat page is blocked from my work system - yes I'm being a bad boy), I'm going to address the last two questions here.

IF I do this:
float b;          
float c;           
b=3.1;    
c=(5.0/9.0)*(b);
In the last statement how does the compiler know that b is of type float?

During translation, the compiler maintains a table (often called a symbol table) that stores information about an object's name, type, storage duration, scope, etc. You declared b and c as float, so any time the compiler sees an expression with b or c in it, it will generate the machine code to handle a floating-point value.

I took your code above and wrapped a full program around it:

/**
 * c1.c
 */
#include <stdio.h>
int main( void )
{
  float b;
  float c;
  b = 3.1;
  c = (5.0 / 9.0) * b;

  printf( "c = %f\n", c );
  return 0;
}

I used the -g and -Wa,-aldh options with gcc to create a listing of the generated machine code interleaved with the C source code3:

GAS LISTING /tmp/ccmGgGG2.s                     page 1

   1                            .file   "c1.c"
   9                    .Ltext0:
  10                            .section        .rodata
  11                    .LC2:
  12 0000 63203D20              .string "c = %f\n"
  12      25660A00
  13                            .align 8
  14                    .LC1:
  15 0008 721CC771              .long   1908874354
  16 000c 1CC7E13F              .long   1071761180
  17                            .text
  18                    .globl main
  20                    main:
  21                    .LFB2:
  22                            .file 1 "c1.c"
   1:c1.c          **** #include <stdio.h>
   2:c1.c          **** int main( void )
   3:c1.c          **** {
  23                            .loc 1 3 0
  24 0000 55                    pushq   %rbp
  25                    .LCFI0:
  26 0001 4889E5                movq    %rsp, %rbp
  27                    .LCFI1:
  28 0004 4883EC10              subq    $16, %rsp
  29                    .LCFI2:
   4:c1.c          ****   float b;
   5:c1.c          ****   float c;
   6:c1.c          ****   b = 3.1;
  30                            .loc 1 6 0
  31 0008 B8666646              movl    $0x40466666, %eax
  31      40
  32 000d 8945F8                movl    %eax, -8(%rbp)
   7:c1.c          ****   c = (5.0 / 9.0) * b;
  33                            .loc 1 7 0
  34 0010 F30F5A4D              cvtss2sd        -8(%rbp), %xmm1
  34      F8
  35 0015 F20F1005              movsd   .LC1(%rip), %xmm0
  35      00000000
  36 001d F20F59C1              mulsd   %xmm1, %xmm0
  37 0021 F20F5AC0              cvtsd2ss        %xmm0, %xmm0
  38 0025 F30F1145              movss   %xmm0, -4(%rbp)
  38      FC
   8:c1.c          ****
   9:c1.c          ****   printf( "c = %f\n", c );
  39                            .loc 1 9 0
  40 002a F30F5A45              cvtss2sd        -4(%rbp), %xmm0
  40      FC
  41 002f BF000000              movl    $.LC2, %edi
  41      00
  42 0034 B8010000              movl    $1, %eax
  42      00
  43 0039 E8000000              call    printf
  43      00
  10:c1.c          ****   return 0;
  44                            .loc 1 10 0
  45 003e B8000000              movl    $0, %eax

GAS LISTING /tmp/ccmGgGG2.s                     page 2

  11:c1.c          **** }
  46                            .loc 1 11 0
  47 0043 C9                    leave
  48 0044 C3                    ret

Here's how to read the assembly listing:

  40 002a F30F5A45              cvtss2sd        -4(%rbp), %xmm0
  40      FC
  ^  ^    ^                     ^               ^
  |  |    |                     |               |
  |  |    |                     |               +-- Instruction operands
  |  |    |                     +------------------ Instruction mnemonic
  |  |    +---------------------------------------- Actual machine code (instruction and operands)
  |  +--------------------------------------------- Byte offset of instruction from subroutine entry point
  +------------------------------------------------ Line number of assembly listing

One thing to note here. In the generated assembly code, there are no symbols for b or c; they only exist in the source code listing. When main executes at runtime, space for b and c (along with some other stuff) is allocated from the stack by adjusting the stack pointer:

subq    $16, %rsp

The code refers to those objects by their offset from the frame pointer4, with b being -8 bytes from the address stored in the frame pointer and c being -4 bytes from it, as follows:

   7:c1.c          ****   c = (5.0 / 9.0) * b;
  .loc 1 7 0
  cvtss2sd        -8(%rbp), %xmm1  ;; converts contents of b from single- to double-
                                   ;; precision float, stores result to floating-
                                   ;; point register xmm1
  movsd   .LC1(%rip), %xmm0        ;; writes the pre-computed value of 5.0/9.0  
                                   ;; to floating point register xmm0
  mulsd   %xmm1, %xmm0             ;; multiply contents of xmm1 by xmm0, store result
                                   ;; in xmm0
  cvtsd2ss        %xmm0, %xmm0     ;; convert result in xmm0 from double- to single-
                                   ;; precision float
  movss   %xmm0, -4(%rbp)          ;; save result to c

Since you declared b and c as floats, the compiler generated machine code to specifically handle floating-point values; the movsd, mulsd, cvtss2sd instructions are all specific to floating-point operations, and the registers %xmm0 and %xmm1 are used to store double-precision floating point values.

If I change the source code so that b and c are integers instead of floats, the compiler generates different machine code:

/**
 * c2.c
 */
#include <stdio.h>
int main( void )
{
  int b;
  int c;
  b = 3;
  c = (9 / 4) * b; // changed these values since integer 5/9 == 0, making for
                   // some really boring machine code.

  printf( "c = %d\n", c );
  return 0;
}

Compiling with gcc -o c2 -g -std=c99 -pedantic -Wall -Werror -Wa,-aldh=c2.lst c2.c gives:

GAS LISTING /tmp/ccyxHwid.s                     page 1

   1                            .file   "c2.c"
   9                    .Ltext0:
  10                            .section        .rodata
  11                    .LC0:
  12 0000 63203D20              .string "c = %d\n"
  12      25640A00
  13                            .text
  14                    .globl main
  16                    main:
  17                    .LFB2:
  18                            .file 1 "c2.c"
   1:c2.c          **** #include <stdio.h>
   2:c2.c          **** int main( void )
   3:c2.c          **** {
  19                            .loc 1 3 0
  20 0000 55                    pushq   %rbp
  21                    .LCFI0:
  22 0001 4889E5                movq    %rsp, %rbp
  23                    .LCFI1:
  24 0004 4883EC10              subq    $16, %rsp
  25                    .LCFI2:
   4:c2.c          ****   int b;
   5:c2.c          ****   int c;
   6:c2.c          ****   b = 3;
  26                            .loc 1 6 0
  27 0008 C745F803              movl    $3, -8(%rbp)
  27      000000
   7:c2.c          ****   c = (9 / 4) * b;
  28                            .loc 1 7 0
  29 000f 8B45F8                movl    -8(%rbp), %eax
  30 0012 01C0                  addl    %eax, %eax
  31 0014 8945FC                movl    %eax, -4(%rbp)
   8:c2.c          ****
   9:c2.c          ****   printf( "c = %d\n", c );
  32                            .loc 1 9 0
  33 0017 8B75FC                movl    -4(%rbp), %esi
  34 001a BF000000              movl    $.LC0, %edi
  34      00
  35 001f B8000000              movl    $0, %eax
  35      00
  36 0024 E8000000              call    printf
  36      00
  10:c2.c          ****   return 0;
  37                            .loc 1 10 0
  38 0029 B8000000              movl    $0, %eax
  38      00
  11:c2.c          **** }
  39                            .loc 1 11 0
  40 002e C9                    leave
  41 002f C3                    ret

Here's the same operation, but with b and c declared as integers:

   7:c2.c          ****   c = (9 / 4) * b;
  .loc 1 7 0
  movl    -8(%rbp), %eax  ;; copy value of b to register eax
  addl    %eax, %eax      ;; since 9/4 == 2 (integer arithmetic), double the
                          ;; value in eax
  movl    %eax, -4(%rbp)  ;; write result to c

This is what I meant earlier when I said that type information was "baked in" to the machine code. When the program runs, it doesn't examine b or c to determine their type; it already knows what their type should be based on the generated machine code.

If the compiler determines the type and size at run time then why doesn't the following program works:
float b='H';         
printf(" value of b is %c \n",b);

It doesn't work because you're lying to the compiler. You tell it that b is a float, so it will generate machine code to handle floating-point values. When you initialize it, the bit pattern corresponding to the constant 'H' will be interpreted as a floating-point value, not a character value.

You lie to the compiler again when you use the %c conversion specifier, which expects a value of type char, for the argument b. Because of this, printf won't interpret the contents of b correctly, and you'll wind up with garbage output5. Again, printf can't know the number or types of any additional arguments based on the arguments themselves; all it sees is an address on the stack (or a bunch of registers). It needs the format string to tell it what additional arguments have been passed, and what their types are.


1. The one exception being variable-length arrays; since their size isn't established until runtime, there's no way to evaluate sizeof on a VLA at compile time.

2. As of C89, anyway. Prior to that, the compiler could only catch mismatches in the function return type; it couldn't detect mismatches in the function parameter lists.

3. This code is generated on a 64-bit SuSE Linux Enterprise 10 system using gcc 4.1.2. If you're on a different implementation (compiler/OS/chip architecture), then the exact machine instructions will be different, but the general point will still hold; the compiler will generate different instructions to handle floats vs. ints vs. strings, etc.

4. When you call a function in a running program, a stack frame is created to store the function arguments, local variables, and the address of the instruction following the function call. A special register called the frame pointer is used to keep track of the current frame.

5. For example, assume a big-endian system where the high-order byte is the addressed byte. The bit pattern for H will be stored to b as 0x00000048. However, because the %c conversion specifier indicates that the argument should be a char, only the first byte will be read, so printf will try to write the character corresponding to the encoding 0x00.

19
  • If C is a statically typed language, then how does putchar() prints the correct data type without mentioning it the type?
    – user106313
    Dec 4, 2014 at 8:45
  • @user31782: The definition of the putchar function says that it expects 1 argument of type int; when the compiler generates the machine code, that machine code will assume that it always receives that single integer argument. There's no need to specify the type at runtime.
    – John Bode
    Dec 4, 2014 at 16:00
  • I can print alphabets using putchar().
    – user106313
    Dec 4, 2014 at 16:02
  • 2
    @user31782: printf formats all its output as text (ASCII or otherwise); the conversion specifier tells it how to format the output. printf( "%d\n", 65 ); will write the sequence of characters '6' and '5' to standard output, because the %d conversion specifier tells it to format the corresponding argument as a decimal integer. printf( "%c\n", 65 ); will write the character 'A' to standard output, because %c tells printf to format the argument as a character from the execution character set.
    – John Bode
    Dec 4, 2014 at 16:13
  • 1
    @user31782: Not without a change to the language definition. Sure, it's possible (C++ is also statically typed, but is able to infer types for the << and >> I/O operators), but it would add some complexity to the language. Inertia is hard to overcome sometimes.
    – John Bode
    Dec 10, 2014 at 21:22
8

Because at the moment when printf is called and does its job, the compiler is no longer there to tell it what to do.

The function doesn't get any information except what's in its parameters, and the vararg parameters don't have any type, so printf would have no clue how to print any if them if it didn't get explicit instructions via the format string. The compiler could (usually) deduce what type each argument is, but you would still have to write format string to tell it where to print each argument relative to the constant text. Compare "$%d" and "%d$"; they do different things, and the compiler can't guess which you want. Since you have to compose a format string manually anyway to specify argument positions, it's an obvious choice to offload the task of stating the argument types to the user as well.

The alternative would be for the compiler to scan the format string for positions, then deduce the types, rewrite the format string to add the type information, and then compile the changed string into your binary instead. But that would only work for literal format strings; C also allows dynamically assigned format strings, and there would always be cases where the compiler can't accurately reconstruct what the format string is going to be at run-time. (Also, sometimes you want to print something as a different, related type, effectively performing a narrowing cast; that is also something that no compiler can predict.)

7
  • So, the printf() function knows that "x" is a variable but it doesn't know what is its type, yet the compiler knows. Can't we update printf() so that it could know the type of data. Moreover I recall that in C++ "cout" can print a data by automatically knowing its type.
    – user106313
    Nov 29, 2014 at 15:58
  • @user31782 C function calls are extremely simple. All that printf() gets passed is a pointer to the format string and a pointer to the buffer where the arguments can be found. Not even the length of this buffer is passed! This is one of the reasons C can be so much faster than other languages. What you're proposing is orders of magnitude more complex. Nov 29, 2014 at 16:11
  • @grahamparks Is "cout" in C++ slower than "printf". Would making printf() like "cout" make it slow?
    – user106313
    Nov 29, 2014 at 16:14
  • 3
    @user31782: not necessarily slower at runtime (it depends, as usual), but it requires language features that simply aren't there in C. C doesn't have function overloading, let alone the template mechanisms used for cout in C++.
    – Mat
    Nov 29, 2014 at 16:24
  • 5
    Yes, but you should remember that C harks from 1972. These features were only invented much later. Nov 29, 2014 at 16:32
5

printf() is what's known as a variadic function, which is one that accepts a variable number of arguments.

Variadic functions in C use a special prototype to tell the compiler that the list of arguments is of unknown length:

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

Standard C provides a set of functions in stdarg.h that can be used to retrieve the arguments one at a time and cast them to a given type. This means that variadic functions have to decide the type of each argument for themselves. printf() makes this decision based on what's in the format string.

This is a gross oversimplification of how printf() actually works, but the process goes like this:

int printf(const char *format, ...) {

    /* Get ready to process arguments that follow 'format' */
    va_list ap;
    va_start(ap, format);

    /* Deep in the function, something that's dissected the
       format string has decided that the next argument is a
       string.  Grab the next argument, cast it to char * and
       write it to wherever it should go.
     */
    char *string = va_arg(ap, char *);
    write_string_to_output(string);

    /* Conclude processing of arguments */
    va_end(ap);
}

The same process happens for all of the types printf() is capable of converting. You can see an example of this in the source code for OpenBSD's implementation of vfprintf(), which is the function that underpins printf().

Some C compilers are smart enough to spot calls to printf(), evaluate the format string if it's a constant and verify that the types of the rest of the arguments are compatible with the conversions specified. This behavior is not required, which is why the standard still requires providing the type as part of the format string. Before these kinds of checks were made, mismatches between the format string and the argument list simply produced bogus output.

In C++, << is an operator, which makes uses of cout such as cout << foo << bar an infix expression that can be evaluated for correctness at compile time and turned into code that casts the right-hand expressions into something cout can deal with.

3

The designers of C wanted to make the compiler as simple as possible. While it would have been possible to handle I/O in much the fashion of other languages, and require that the compiler automatically supply the I/O routine with information about the types of passed parameters, and while such an approach might in many cases have allowed for more efficient code than is possible with printf(*), defining things that way would have made the compiler more complicated.

In the earliest days of C, code which called a function neither knew nor cared what arguments it was expecting. Each argument would push some number of words on the stack according to its type, and functions would expect to find different parameters in the topmost, second-to-top, etc. stack slot below the return address. If a printf method could figure out where to find its arguments on the stack, there was no way for the compiler to treat it any differently from any other method.

In practice, the pattern of parameter passing envisioned by C is very seldom used anymore except when calling variadic functions like printf, and if printf had been defined as using special parameter-passing conventions [e.g. having the first parameter be a compiler-generated const char* containing auto-generated information about the types to be passed], compilers would have been able to generate better code for it (avoiding the need for integer and floating-point promotions among other things).] Unfortunately, I perceive zero likelihood of any compiler adding features to have compilers report variable types to called code.

I find it curious that null pointers are considered the "billion-dollar mistake", given their utility, and given that they generally only cause severely bad behavior in languages which don't trap null pointer arithmetic and accesses. I'd consider the harm caused by printf and zero-terminated strings to be much worse.

0

Think about it like you are passing variables to another function that you have defined. You normally tell the other function which type of data it should expect/receive. The same way with printf(). It is already defined in the stdio.h library and requires you to tell it which data it is receiving so that it can output it in the right format (as in your case int).

Not the answer you're looking for? Browse other questions tagged or ask your own question.