In printf all expressions must be able to be resolved to a string. Knowing this, when the program reaches an expression that does not resolve to a string, before it is couched as an error, the compiler program looks to see if there is a cast it can call that will result in a string, and if so writes that. Because it is done during parsing it doesn't require overloading.
printf and its friends are not common procedures in C. They allow variable number of arguments, which are implemented through vararg mechanism. The first argument is guaranteed to be a string, and you can figure out the type of the subsequent ones from that string.
printf is a library procedure in C. It takes variable number of arguments, and with the help of formatting notations such as %d, %f etc, it recognizes various arguments as integers, floats etc, and then convert them to strings. Once the entire set of arguments have been combined into a single string based on the template provided with the help of formatting symbol sequences, it can easily send it to the printer driver as a single string.
Maybe you need to go through a brief C tutorial for functions with a variable number of arguments: http://www.cprogramming.com/tutorial/c/lesson17.html
The first answer above is incorrect: the compiler does NOT try to cast things to strings. Here's what actually happens:
In the earliest versions of C (1970's), you didn't have to declare functions before calling them: when the compiler saw a function call, it would assume you were passing the right number and types of arguments, and that the called function would somehow figure out what to do with them. printf is a leftover from that time.
printf sees a region of memory where its arguments have been placed. It assumes the first one is (a pointer to) a string. and walks through that string from left to right. When it sees a "%d", it assumes the next argument exists and is an integer. When it sees a "%f", it assumes the next argument exists and is a float. When it sees a "%s", it assumes the next argument exists and is a pointer to a string. And so on.
If you call printf with "wrong" format specifiers, it will treat the bits you passed as if they were the expected type. For example,
printf ("Pi = %d", 3.14159);
doesn't print anything even remotely resembling 3.14159, but rather the integer with the same binary representation as the float 3.14159 (on my computer it prints 1606415552).
If you call printf with fewer format specifiers than data arguments, it will ignore any extra ones:
printf ("Here's a number: %d", 3, 5, 7, 9, 11);
Even more amusing: if you call printf with MORE format specifiers than data arguments, as in
printf ("Here are some integers: %d %d %d %d", 7);
it will grab the 7, and print it as expected, and it will also grab the next three words of memory after the 7, treat them as integers, and print them. Since the arguments were probably passed on the stack, this means you're printing out the return address of the current function call, and possibly some local variables belonging to the function that called you.
Many modern C compilers recognize printf as a special case, looking at the format string, comparing it with the types and numbers of arguments, and producing a warning message if they don't match. But if you're determined to get around this, you can write
char format[] = "Here are some integers: %d %d %d %d";