C / CONSOLE INPUT AND OUTPUT
printf format specifiers in depth
Pick the correct conversion letter and length modifier for any C value, and know why a mismatch is undefined behavior, not just wrong text.
What you will learn
- Match each specifier to the promoted type of the argument, not the declared type
- Use l, ll, z and L for long, long long, size_t and long double
- Switch d/i, u, o, x to change how the same integer bits are read and printed
- Write %% for a literal percent and cast pointers to void* for %p
Understanding printf format specifiers in depth
printf is declared as int printf(const char *fmt, ...), so every argument after the format string arrives with no type information attached. The format string is the only description printf has of those bytes: each conversion specification is a promise about which type occupies the next argument slot, and printf extracts the argument using exactly that type. Break the promise and the result is undefined behavior, because printf may read the wrong width, or read from the integer registers when the value was passed as a floating-point one.
Before printf ever sees them, variadic arguments go through the default argument promotions: char, short and _Bool become int, and float becomes double. That is why %d is correct for a char and a short, and why %f is correct for both a float and a double; printf has no float conversion at all. Types that promotion does not touch must be described by a length modifier: l for long, ll for long long, z for size_t, L for long double, and h or hh to convert an int argument back down to short or signed char.
Once the width is right, the conversion letter chooses interpretation and rendering. d and i are the same conversion and read a signed int; u, o, x and X read an unsigned int and render it in base 10, 8 or 16; c takes an int and writes one byte after converting it to unsigned char; s takes a pointer and copies bytes until the terminating NUL; p takes a void *. The floating conversions differ only in presentation: f is fixed point, e is scientific, g picks whichever fits the magnitude and removes trailing zeros, a is hexadecimal. A lone % always starts a specification, so a literal percent sign must be written %%.
<stdio.h>
int main(void)
{
int temp = -42;
unsigned int mask = 3000000000u;
long long big = 9007199254740993LL;
size_t len = sizeof "format";
char grade = 'B';
const char *name = "printf";
double ratio = 0.125;
printf("signed int : %d\n", temp);
printf("unsigned int : %u hex %x HEX %X octal %o\n", mask, mask, mask, mask);
printf("long long : %lld\n", big);
printf("size_t : %zu\n", len);
printf("char as text : %c char as number: %d\n", grade, grade);
printf("string : %s\n", name);
printf("double : %f %e %g\n", ratio, ratio, ratio);
printf("percent sign : 100%%\n");
return 0;
}
The format string is the only type information printf has, so every conversion specification must match the promoted type of its argument exactly.
Worked examples
Promotions decide the specifier
Shows why a char and a short both take %d and why a float takes %f rather than a float-specific conversion.
<stdio.h>
int main(void)
{
char c = 'A';
short s = -1000;
float f = 2.5f;
printf("%d %d %f\n", c, s, f);
printf("%hd %hhd\n", s, c);
return 0;
}
Example explained
Line 1c and s are promoted to int at the call, so %d is the matching conversion for both.
Line 2f is promoted to double, and %f expects a double, which is why C has no separate float conversion in printf.
Line 3%hd and %hhd convert the int argument back down to short and signed char before printing; they do not change how it was passed.
Line 4Sending that same float to %Lf would be undefined behavior, because no promotion ever produces long double.
f, e and g on the same numbers
Demonstrates how %g chooses between fixed and scientific form based on the value's decimal exponent.
<stdio.h>
int main(void)
{
double v[] = { 1234567.0, 100.0, 0.0001, 0.00001 };
int i;
for (i = 0; i < 4; i++)
printf("g=%g\tf=%f\te=%e\n", v[i], v[i], v[i]);
return 0;
}
Example explained
Line 1%g switches to scientific form when the decimal exponent is below -4 or at least the precision, so 1234567 and 0.00001 come out with an exponent.
Line 2%g also drops trailing zeros, which turns 100.0 into 100 and keeps 0.0001 as one significant digit.
Line 3%f always prints six digits after the point regardless of magnitude, so 0.00001 becomes 0.000010.
Line 4%e always shows one digit before the point and pads the exponent to at least two digits.
Same bits, different conversions
Prints one int value through %d, %u and %x to show that the conversion letter chooses the interpretation.
<stdio.h>
<limits.h>
int main(void)
{
int n = -1;
printf("as %%d : %d\n", n);
printf("as %%u : %u\n", (unsigned int)n);
printf("as %%x : %x\n", (unsigned int)n);
printf("int has %zu bytes, %d bits\n",
sizeof(int), (int)(sizeof(int) * CHAR_BIT));
return 0;
}
Example explained
Line 1%%d prints a literal % followed by d, because a single % would start a conversion instead.
Line 2The cast to unsigned int is what makes %u and %x well defined: -1 is not a representable value for an unsigned conversion, and the cast performs the wraparound to 4294967295.
Line 3sizeof yields size_t, so it needs %zu; the bit count is cast to int to match %d.
Line 4The digits shown assume a 32-bit int, the usual case on desktop platforms; a different int width changes both numbers.
Important notes
%zu, %lld and the hh modifier are C99 features; ancient MSVC runtimes expect %Iu and %I64d instead.
Never pass data you did not write as the format string: printf("%s", buf) is safe, printf(buf) lets stray % sequences read arguments that were never passed, and %n can write through a pointer that does not exist.
Common mistakes
Printing a size_t or long long with %d. printf reads only int-sized bytes, so you get a truncated or nonsense number, and on stack-passing ABIs every later specifier in the same call is shifted as well.
Assuming double needs %lf and float needs %f. Both are %f in printf because of promotion; %Lf is only for long double, and feeding it a double prints garbage.
Passing a single char to %s, as in printf("%s", grade). printf treats the character code as an address and dereferences it, which normally crashes the program.
Try it yourself
Change, predict, then run
Declare a size_t, a long long, a char and a double and print each on its own line with the correct specifier. Then print one unsigned int value four times in decimal, lowercase hex, uppercase hex and octal, ending the line with a literal percent sign.
Open the C workspaceCheck your understanding
Passing a float to %f prints correctly, but passing a long long to %d prints nonsense. What explains the difference?
- The default argument promotions widen float to double, which is exactly what %f expects, while nothing narrows long long to int, so %d reads the wrong argument width
- printf inspects a type tag stored with each argument, and floating types carry one while integer types do not
- %d works for any integer whose value fits in an int, so the call would be fine if the long long held a small number
- float and long long occupy the same number of bytes, so only the signedness of the conversion differs
Show answer
float is promoted to double before printf sees it, so %f and the actual argument agree; long long is passed unchanged and %d asks for an int, so the widths disagree. Option 3 is tempting because small values often appear to print correctly on ABIs that pass integers in registers, but correctness depends on the argument's type, not its value, so the call is undefined behavior no matter how small the number is.