The C Path — learn C, visuallyprintable cheatsheet

printf & scanf format strings

Facts per ISO C17 · C23 additions marked · UB = undefined behavior, ID = implementation-defined

Styled for paper — hit Ctrl+P and pin it above your desk.

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).

printf conversion specifiers

specargument typeprintsnoteslearn
%d %iintsigned decimal: -42identical in printf (they differ only in scanf)→ lesson
%uunsigned intunsigned decimal: 3000000000never a sign; don't pass negative values (see mismatches)→ lesson
%ounsigned intoctal: 644# flag forces a leading 0
%x / %Xunsigned inthex: ff / FF# adds 0x/0X (nonzero values only)→ lesson
%f / %Fdoublefixed: 3.141593 (6 decimals by default)float args are promoted to double automatically; F prints INF/NAN uppercase→ lesson
%e / %Edoublescientific: 3.141593e+00precision = digits after the point
%g / %Gdouble%f-style or %e-style, whichever fitsprecision = significant digits; trailing zeros dropped (# keeps them)
%a / %Adoublehex float: 0x1.8p+1 (= 3.0)C99; exact — round-trips a double in text
%cintone character: Avalue is converted to unsigned char; a char arg promotes to int on its own→ lesson
%schar *bytes up to the '\0'precision caps output: %.3s prints at most 3 chars — substring without a copy→ lesson
%pvoid *an address: 0x7ffc9a2b4c2cexact form is ID; cast other pointer types: (void *)p→ lesson
%nint *nothingstores chars written so farsecurity footgun; fortified glibc aborts on %n in writable format strings — avoid
%%a literal %consumes no argument
%b / %B C23unsigned intbinary: 101010# adds 0b/0B; %B is optional for implementations→ lesson

Length modifiers (what size is the argument?)

modifierwith d iwith u o x X (b C23)with f e g awith c / sexample
hh (C99)signed charunsigned char%hhx — one byte in hex
hshortunsigned short%hd
(none)intunsigned intdoubleint / char *%d %u %f %c %s
llongunsigned longno effect (%lf legal since C99)wint_t / wchar_t *%ld %lu %ls
ll (C99)long longunsigned long long%lld
j (C99)intmax_tuintmax_t%jd
z (C99)signed sibling of size_tsize_t%zu — what sizeof yields
t (C99)ptrdiff_tits unsigned sibling%td — pointer difference
Llong double%Lf
wN / wfN C23intN_t / int_fastN_tuintN_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.

Flags

flagmeaningexample
-left-align within the field width (default is right)%-8d42······ (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
0pad with leading zeros; ignored with -, and for integer conversions when a precision is given%08d00000042
#alternate form: 0x on %x, leading 0 on %o, 0b on %b C23; floats always keep the decimal point; %g keeps trailing zeros%#x0x2a

Width & precision

syntaxmeaningexample
%8dminimum field width — pads, never truncates[ 42]
%*dwidth read from the next int argument; a negative width acts as - flag + positive widthprintf("%*d", 6, n)[ 42]
%.2ffloats: digits after the point3.14
%.5dintegers: minimum digits, zero-filled00042
%.3sstrings: maximum bytes writtenhel
%.4g%g: significant digits3.142
%.*fprecision from an int argument; negative = as if omittedprintf("%.*f", 2, pi)
%07.2fall combined: width 7, precision 2, zero-pad3.50003.50

scanf: same grammar, different rules

ruledetailslearn
check the return valuenumber 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 floatthe printf/scanf asymmetry: scanf needs %lf for double, %Lf for long double (%e %g %a behave identically to %f in scanf)
%i sniffs the baselike strtol base 0: 0x1f → 31, 0108. %d is always decimal — prefer it
%s needs a width — alwaysscanf("%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 literalreads 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 formatmatches any run of whitespace, including none. Any other literal character must match the input exactly
pointer sizes must match%hdshort *, %ldlong *, %zusize_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.

Recipes

I want to…writenotes
print a size_tprintf("%zu", sizeof x);C99+; for ancient compilers cast: (unsigned long)sizeof x with %lu
print a pointerprintf("%p", (void *)ptr);the cast to void * is required by the letter of the standard
print an int64_tprintf("%" PRId64 "\n", v);<inttypes.h>; string concatenation glues the pieces — → lesson
pad with zeros%08d, %08.3f, %04Xthe sign, 0x, and point all count toward the width
print a literal %%%printf("50%%")50%
hex-dump a byteprintf("%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 lengthprints a substring without copying — also safe for non-terminated fixed fields
columns / tables%-10s %8.2fleft-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 numberprintf("%d", grade);chars are small integers and promote to int→ lesson
read a string safelyscanf("%15s", buf) into char buf[16]better: fgets + sscanf
read a doublescanf("%lf", &d)scanf only — printf takes plain %f for doubles
build a string safelysnprintf(dst, sizeof dst, "%s", src)always terminates; return value ≥ buffer size means truncation — → lesson

Mismatches — most are undefined behavior

you wrotewhat's wrongfix
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 passed7.0 or %d
printf("%d", sizeof x)UBsize_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 unterminatedUB — glibc's (null) is a mercy, not a guaranteecheck for NULL; terminate your buffers
printf(user_input)format-string vulnerability — attacker's %s/%n read and write your memoryprintf("%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.