Every conversion follows one grammar: %[flags][width][.precision][length]conversion. Each part is optional except the conversion letter. printf returns the number of characters written (negative on error); scanf returns the number of successful conversions (or EOF).
| spec | argument type | prints | notes | learn |
|---|---|---|---|---|
%d %i | int | signed decimal: -42 | identical in printf (they differ only in scanf) | → lesson |
%u | unsigned int | unsigned decimal: 3000000000 | never a sign; don't pass negative values (see mismatches) | → lesson |
%o | unsigned int | octal: 644 | # flag forces a leading 0 | |
%x / %X | unsigned int | hex: ff / FF | # adds 0x/0X (nonzero values only) | → lesson |
%f / %F | double | fixed: 3.141593 (6 decimals by default) | float args are promoted to double automatically; F prints INF/NAN uppercase | → lesson |
%e / %E | double | scientific: 3.141593e+00 | precision = digits after the point | |
%g / %G | double | %f-style or %e-style, whichever fits | precision = significant digits; trailing zeros dropped (# keeps them) | |
%a / %A | double | hex float: 0x1.8p+1 (= 3.0) | C99; exact — round-trips a double in text | |
%c | int | one character: A | value is converted to unsigned char; a char arg promotes to int on its own | → lesson |
%s | char * | bytes up to the '\0' | precision caps output: %.3s prints at most 3 chars — substring without a copy | → lesson |
%p | void * | an address: 0x7ffc9a2b4c2c | exact form is ID; cast other pointer types: (void *)p | → lesson |
%n | int * | nothing — stores chars written so far | security footgun; fortified glibc aborts on %n in writable format strings — avoid | |
%% | — | a literal % | consumes no argument | |
%b / %B C23 | unsigned int | binary: 101010 | # adds 0b/0B; %B is optional for implementations | → lesson |
| modifier | with d i | with u o x X (b C23) | with f e g a | with c / s | example |
|---|---|---|---|---|---|
hh (C99) | signed char | unsigned char | — | — | %hhx — one byte in hex |
h | short | unsigned short | — | — | %hd |
| (none) | int | unsigned int | double | int / char * | %d %u %f %c %s |
l | long | unsigned long | no effect (%lf legal since C99) | wint_t / wchar_t * | %ld %lu %ls |
ll (C99) | long long | unsigned long long | — | — | %lld |
j (C99) | intmax_t | uintmax_t | — | — | %jd |
z (C99) | signed sibling of size_t | size_t | — | — | %zu — what sizeof yields |
t (C99) | ptrdiff_t | its unsigned sibling | — | — | %td — pointer difference |
L | — | — | long double | — | %Lf |
wN / wfN C23 | intN_t / int_fastN_t | uintN_t / uint_fastN_t | — | — | %w32d, %w64x |
The same letters size the pointed-to variable in scanf — where l additionally turns %f into double (see below). Fixed-width types like int64_t portably need the PRId64 macros — limits-stdint lesson or the stdint cheatsheet.
| flag | meaning | example |
|---|---|---|
- | left-align within the field width (default is right) | %-8d → 42······ (dots = spaces) |
+ | always print a sign on signed conversions | %+d → +42 |
' ' (space) | print a space where the + would go; ignored if + is given | % d → ·42 |
0 | pad with leading zeros; ignored with -, and for integer conversions when a precision is given | %08d → 00000042 |
# | alternate form: 0x on %x, leading 0 on %o, 0b on %b C23; floats always keep the decimal point; %g keeps trailing zeros | %#x → 0x2a |
| syntax | meaning | example |
|---|---|---|
%8d | minimum field width — pads, never truncates | [ 42] |
%*d | width read from the next int argument; a negative width acts as - flag + positive width | printf("%*d", 6, n) → [ 42] |
%.2f | floats: digits after the point | 3.14 |
%.5d | integers: minimum digits, zero-filled | 00042 |
%.3s | strings: maximum bytes written | hel |
%.4g | %g: significant digits | 3.142 |
%.*f | precision from an int argument; negative = as if omitted | printf("%.*f", 2, pi) |
%07.2f | all combined: width 7, precision 2, zero-pad | 3.5 → 0003.50 |
| rule | details | learn |
|---|---|---|
| check the return value | number of successful conversions, or EOF. if (scanf("%d", &x) != 1) — otherwise you use an uninitialized variable, and the bad input stays stuck in the buffer | → lesson |
%f reads a float | the printf/scanf asymmetry: scanf needs %lf for double, %Lf for long double (%e %g %a behave identically to %f in scanf) | |
%i sniffs the base | like strtol base 0: 0x1f → 31, 010 → 8. %d is always decimal — prefer it | |
%s needs a width — always | scanf("%15s", buf) for char buf[16]: the width counts characters before the terminating '\0'. Bare %s is an unbounded write — see mismatches | → lesson |
%c is literal | reads exactly 1 char (or width chars), does not skip whitespace, appends no '\0'. " %c" (leading space) skips whitespace first | |
%[...] scanset | %31[^\n] = up to 31 chars that aren't newline (then '\0'); like %c it doesn't skip leading whitespace | |
* suppresses | %*d parses and discards an int — the opposite of printf's "width from argument"! | |
| whitespace in the format | matches any run of whitespace, including none. Any other literal character must match the input exactly | |
| pointer sizes must match | %hd → short *, %ld → long *, %zu → size_t * — a wrong size is UB |
The robust input pattern is fgets + sscanf — read a whole bounded line, then parse it. Bad input never wedges the stream. stdio-lib lesson builds it step by step.
| I want to… | write | notes |
|---|---|---|
print a size_t | printf("%zu", sizeof x); | C99+; for ancient compilers cast: (unsigned long)sizeof x with %lu |
| print a pointer | printf("%p", (void *)ptr); | the cast to void * is required by the letter of the standard |
print an int64_t | printf("%" PRId64 "\n", v); | <inttypes.h>; string concatenation glues the pieces — → lesson |
| pad with zeros | %08d, %08.3f, %04X | the sign, 0x, and point all count toward the width |
print a literal % | %% | printf("50%%") → 50% |
| hex-dump a byte | printf("%02X", byte); | with unsigned char byte (it promotes to int, which is fine); pedantic: %02hhX |
| first N chars of a string | %.4s or %.*s with an int length | prints a substring without copying — also safe for non-terminated fixed fields |
| columns / tables | %-10s %8.2f | left-align text, right-align numbers; %*d for runtime widths |
round-trip a double in text | %.17g (or exact hex %a) | 17 significant digits reproduce any double exactly |
print a char as a number | printf("%d", grade); | chars are small integers and promote to int — → lesson |
| read a string safely | scanf("%15s", buf) into char buf[16] | better: fgets + sscanf |
read a double | scanf("%lf", &d) | scanf only — printf takes plain %f for doubles |
| build a string safely | snprintf(dst, sizeof dst, "%s", src) | always terminates; return value ≥ buffer size means truncation — → lesson |
| you wrote | what's wrong | fix |
|---|---|---|
printf("%d", 3.14) | UB — tells printf to read an int where a double's bytes sit; garbage or crash may follow | %f |
printf("%f", 7) | UB — the reverse lie: reads a double where an int was passed | 7.0 or %d |
printf("%d", sizeof x) | UB — size_t is not int (8 vs 4 bytes on 64-bit Linux) | %zu |
printf("%d", n_long) | UB on LP64 — may "work" on one platform and misprint on another | %ld |
printf("%ld", my_int64) | unportable — int64_t is long on Linux/macOS but long long on Windows; the mismatch side is UB | "%" PRId64 |
printf("%u", -1) | signed/unsigned corresponding types are interchangeable only when the value fits both; -1 doesn't — don't rely on seeing 4294967295 | %d, or convert deliberately |
printf("%s", p) with p NULL or unterminated | UB — glibc's (null) is a mercy, not a guarantee | check for NULL; terminate your buffers |
printf(user_input) | format-string vulnerability — attacker's %s/%n read and write your memory | printf("%s", user_input) |
printf("%p", int_ptr) | strictly UB — %p wants exactly void * | (void *)int_ptr |
printf("%d %d", x) | UB — too few arguments. (Extra arguments are the one mercy: evaluated, then ignored — defined) | count your conversions |
scanf("%s", buf) | unbounded write — buffer overflow, UB, and a classic security hole (gets() was removed from C11 for this) | %15s for char[16] |
scanf("%f", &dbl) | UB — writes 4 bytes into an 8-byte double's space, leaves garbage | %lf |
Mismatched specifiers are undefined behavior, not just ugly output. Compile with -Wall (and consider -Wformat=2) and GCC checks every format string against its arguments for free — see the gcc-flags lesson. Why can't printf check at runtime? It's a variadic function: all it ever sees is the format string — variadic-functions lesson.