C Part I · Module 12 · Standard Library, Files and Program Interfaces
printf and Formatted Output
This is the function you have used more than any other, and reading it with the previous chapter's method turns out to be more interesting than it sounds. printf takes a variable number of arguments of types it cannot know, so nothing in C's ordinary rules lets the compiler compare them against the format string — and the standard's position on a mismatch is not that you get odd-looking output. It is that the behaviour is undefined, in the full Chapter 9.12 sense, which makes the question not what gets printed but whether the program means anything at all. There is a safety net, it is better than you might expect, and it has a precise edge: it is a special case the compiler performs on string literals, and it vanishes the moment the format comes from a variable.
Foundation19 min readCPart IFundamentalsStandard Library
Part I · Module 12 · Chapter 12.2 · printf and Formatted Output
1. The Contract
#include <stdio.h>
int printf(const char *format, ...);Reading it with Chapter 12.1's four questions:
| Question | Answer |
|---|---|
| assumes | format is a valid string; every conversion has a matching argument of exactly the right type; there are no extra arguments |
| returns | the number of characters written |
| fails by | returning a negative value — convention A |
| does not promise | anything if a conversion and its argument disagree |
Two of those rows deserve more than a table cell.
The return value is a count, and almost nobody checks it. For output to a terminal that is usually fine; for output to a file or a pipe it is how you learn that a write failed, and Chapter 12.4 is where that matters.
The precondition in row one is unusually strong. Not "the arguments should be sensible" — exactly the right type, for every conversion, with no extras.
2. Why a Mismatch Is Undefined
printf is variadic: the ... means it accepts any number of further arguments, of any types. That has a consequence that is easy to miss.
A variadic function has no declared parameter types after the
..., so the compiler applies no conversions and performs no checks on those arguments. The values are passed as they are, andprintfworks out what to expect by reading the format string at run time.
So when printf sees %d it fetches an int-sized argument from wherever the calling convention puts them. If you passed a double, it reads the wrong number of bytes, from the wrong place, and interprets them as an int. Nothing about that operation is defined — it is not "printing the wrong number", it is reading storage under a type that was never there, which is exactly Chapter 9.12's territory.
The standard says so directly: if a conversion specification does not match the corresponding argument, the behaviour is undefined.
That is why this chapter runs no mismatched call and publishes no output from one. Chapter 9.12 §3 measured what a compiler is entitled to do with undefined behaviour — it replaced a whole function with a trap instruction — and a mismatched printf is in the same category. The useful question is not "what does it print?" but "how do I ensure it cannot happen?"
3. What the Compiler Does Catch
Fortunately, rather a lot. Seven deliberate mismatches:
long l = 1; size_t z = 2; double d = 3.5; char *s = "x"; int i = 4;
printf("%d\n", l); /* int specifier, long argument */
printf("%d\n", z); /* int specifier, size_t argument */
printf("%d\n", d); /* int specifier, double argument */
printf("%s\n", i); /* string specifier, int argument */
printf("%d\n", s); /* int specifier, pointer */
printf("%d %d\n", i); /* too few arguments */
printf("%d\n", i, i); /* too many arguments */warning: format specifies type 'int' but the argument has type 'long' [-Wformat]
warning: format specifies type 'int' but the argument has type 'size_t' (aka 'unsigned long') [-Wformat]
warning: format specifies type 'int' but the argument has type 'double' [-Wformat]
warning: format specifies type 'char *' but the argument has type 'int' [-Wformat]
warning: format specifies type 'int' but the argument has type 'char *' [-Wformat]
warning: more '%' conversions than data arguments [-Wformat-insufficient-args]
warning: data argument not used by format string [-Wformat-extra-args]Seven of seven, each naming the specifier's type and the argument's. The compiler is reading the format string at compile time and checking it against the arguments — which is not something C's type system requires, but is something every serious compiler implements.
And it is on by default. With no -W flags at all, the same file produced the same diagnostics:
$ cc -std=c17 -c p1.c -o /dev/null
(8 warnings)
-Wformatis part of-Walland is enabled even without it. You already have this check. Treat a-Wformatwarning as an error — it is reporting undefined behaviour, not a style preference.
4. The Edge of the Net
The check is a special case, and the special case has a boundary:
const char *fmt = "%d\n"; /* not a literal at the call site */
double d = 3.5;
printf(fmt, d); /* mismatched, and unverifiable */$ cc -std=c17 -Wall -Wextra -Wpedantic -c p2.c -o /dev/null
$Nothing. Not one diagnostic, at the curriculum's full warning level, for a call that is just as undefined as any in §3.
The reason is immediate once stated: the compiler checks a format string by reading it, and it can only read one that is present at the call site. A variable's value is a run-time property. The moment the format is anything but a literal, the entire check is gone.
There is a flag that reports the situation:
$ cc -std=c17 -Wall -Wextra -Wpedantic -Wformat-nonliteral -c p2.c -o /dev/null
p2.c:6:12: warning: format string is not a string literal [-Wformat-nonliteral]5. The Conversions
The common ones, with the type each requires:
| Specifier | Argument type | Prints |
|---|---|---|
%d %i | int | signed decimal |
%u | unsigned int | unsigned decimal |
%x %X | unsigned int | hexadecimal, lower/upper case |
%o | unsigned int | octal |
%c | int (a character value) | one character |
%s | char * | characters up to the terminator |
%p | void * | an implementation-defined pointer form |
%f %e %g | double | fixed / exponent / shorter of the two |
%% | — | a literal % |
Between the % and the specifier you may write, in order: flags, a width, a precision, and a length modifier.
unsigned int value = 0x1Fu;
printf("%08x\n", value);0000001fThat is 0 (pad with zeros), 8 (minimum width), x (hexadecimal) — and it is the idiom you will use constantly the moment a value's bit pattern matters.
| Flag | Effect |
|---|---|
- | left-justify within the width |
0 | pad numbers with leading zeros |
+ | always show a sign on a signed conversion |
| space | a space where a + would go |
# | alternate form — 0x prefix for %x, a forced point for %f |
Width is a minimum, never a maximum: %5d pads to five columns and does not truncate a longer number. Precision means different things by conversion — digits after the point for %f, significant digits for %g, and a maximum number of characters for %s, which is the one case where something is cut short.
printf("[%-10s][%5.2f][%+d]\n", "left", 3.14159, 42);Both width and precision may be given as *, taking the value from an int argument that comes first — useful, and one more thing the format check has to reconcile.
6. Why %f Works for a float
A rule that prevents a whole family of false alarms. When an argument is passed to the variadic part of a call, C applies the default argument promotions: float becomes double, and any integer type narrower than int becomes int.
float f = 1.5f; char c = 'A'; short s = -7;
printf("float with %%f -> %f\n", f);
printf("char with %%d -> %d\n", c);
printf("short with %%d -> %d\n", s);
printf("char with %%c -> %c\n", c);float with %f -> 1.500000
char with %d -> 65
short with %d -> -7
char with %c -> ANone of those is a mismatch. printf never receives a float, a char or a short — by the time it looks, they are a double and two ints. This is why there is no %f-for-float problem to solve, why %c takes an int, and why 'A' prints as 65 under %d, which is Chapter 2.6's point that a char is a small integer type.
The same promotions are why %hd and %hhd exist at all: they do not change what is passed — that is still an int — they tell printf to convert the value back to short or signed char before printing.
7. The Specifiers You Must Not Guess
Several types have a mandatory spelling, and getting it wrong is undefined behaviour that happens to work on one machine.
size_t z = 42;
ptrdiff_t d = -3;
long long L = 1234567890123LL;
uint32_t u = 0x1Fu;
int *p = ...;
printf("size_t %%zu -> %zu\n", z);
printf("ptrdiff_t %%td -> %td\n", d);
printf("long long %%lld -> %lld\n", L);
printf("uint32_t PRIu32 -> %" PRIu32 "\n", u); /* <inttypes.h> */
printf("pointer %%p -> %p\n", (void *)p);size_t %zu -> 42
ptrdiff_t %td -> -3
long long %lld -> 1234567890123
uint32_t PRIu32 -> 31
pointer %p -> 0x16da0270c| Type | Specifier | Why it is not optional |
|---|---|---|
size_t | %zu | it is unsigned long here and unsigned int elsewhere; %u is a coincidence |
ptrdiff_t | %td | same reasoning, signed |
long / long long | %ld / %lld | long is 8 bytes here and 4 on many targets |
uint32_t and friends | PRIu32 etc. | the underlying type varies; <inttypes.h> supplies the right string |
| any pointer | %p with (void *) | %p requires void *, and no implicit conversion happens in a variadic call |
The %p row is the one that is genuinely easy to get wrong, because int * converts to void * silently everywhere except here. Chapter 7.12 established the conversion; the variadic part of a call is the one place it does not happen, so the cast is required.
And PRIu32 works by string-literal concatenation — "%" PRIu32 "\n" becomes one literal before compilation, which is Chapter 11.2's preprocessing and the reason the format is still checkable.
8. The Family
| Function | Writes to | Note |
|---|---|---|
printf | stdout | |
fprintf | a stream you name | fprintf(stderr, ...) for diagnostics |
snprintf | a buffer, with a size | the one to use for buffers — Chapter 8.4 |
sprintf | a buffer, no size | no way to prevent overflow; do not use it |
vprintf family | — | takes an argument list; for writing your own variadic wrappers |
Two points carried forward rather than re-taught.
snprintf returns the length the complete output would have needed, so a return value greater than or equal to the buffer size means truncation. Chapter 8.4 §4 established this and Chapter 8.5 made it the output-buffer rule.
Diagnostics belong on stderr. It is a separate stream from stdout, so a user can redirect the program's output without losing its errors. Chapter 12.4 covers streams properly; the habit is worth starting now.
9. What You Should Be Able to Say Now
printfreturns the number of characters written and negative on failure — a return value almost nobody checks, and Chapter 12.4 is where it matters.printfis variadic, so the compiler applies no checks to the arguments after the format by any ordinary rule.printfworks out what to expect by reading the format at run time.- A mismatch is undefined behaviour, not a cosmetic error — it reads storage under a type that was never there.
- Measured:
-Wformatcaught 7 of 7 mismatches, naming both types, and fired with no-Wflags at all. - Measured: a non-literal format produced no diagnostic at all — the check is a special case performed on literals.
-Wformat-nonliteralreports that no check was possible. Keep formats literal; select between whole calls, not between format strings.- Never pass data as a format string —
printf("%s", data), neverprintf(data). - Default argument promotions:
float→double, narrow integers →int. That is why%ftakes afloatand'A'prints as 65. - c.ts's own example:
%08xof0x1Fugives0000001f— flag, width, conversion. - Width is a minimum; precision truncates only for
%s. - Mandatory spellings:
%zu,%td,%lld, the<inttypes.h>macros, and%pwith an explicit(void *).
10. Exercises
Exercise 1 — give the specifier. For each, write the complete printf call.
size_t count; long total; double ratio; unsigned char flags; char *name; int *addr; ptrdiff_t gap;Exercise 2 — predict the diagnostic. For each, say whether -Wall reports it and what it would say.
printf("%d\n", 3.0);
printf("%s\n", 'x');
printf("%u\n", -1);
printf("%d %s\n", 1);
printf("%p\n", &n); /* n is an int */
printf(msg); /* msg is a const char * */Exercise 3 — explain the undefined. In four sentences, explain why printf("%d\n", some_double) is undefined rather than merely wrong, using §2's description of how a variadic call works.
Exercise 4 — format the output. Write the calls that produce each line exactly.
|left | right|
0x0000001f
+42 and -42
3.14
value: 007Exercise 5 — close the hole. Rewrite this so every format is checked, and say what you gave up.
const char *fmt = compact ? "%s,%d\n" : "%-20s %6d\n";
printf(fmt, name, value);Exercise 6 — the promotions. Explain why none of these is a mismatch, and what printf actually receives in each case.
float f = 2.5f; printf("%f\n", f);
char c = 'z'; printf("%d\n", c);
short s = 1000; printf("%d\n", s);Exercise 7 — the pointer cast. Explain why printf("%p\n", p) needs (void *) when void *q = p; does not, naming the chapter that established the implicit conversion and the reason it does not apply here.
Exercise 8 — audit a line. This line has three defects. Find them all.
sprintf(buf, user_supplied, n);11. Summary
printfreturns a character count, negative on failure.- It is variadic, so the compiler checks nothing by the ordinary rules, and
printfreads the format at run time. - A mismatch is undefined behaviour — storage read under a type that was never there.
- Measured:
-Wformatcaught 7 of 7, and is on without-Wall. Treat its warnings as errors. - Measured: a non-literal format is checked by nothing. Add
-Wformat-nonliteral; keep formats literal. - Never pass data as a format string.
- Promotions:
float→double, narrow integers →int— so%ffor afloatis correct and'A'prints as 65. %08xof0x1Fuis0000001f. Width is a minimum; only%struncates.- Mandatory:
%zu,%td,%lld,<inttypes.h>macros,%pwith(void *). snprintffor buffers;stderrfor diagnostics.
Output has an advantage that is easy to overlook: you already know what you are writing. Input is the opposite — the program does not know how much is coming, what shape it has, or whether it is well-formed, and every one of those unknowns is a place for something to go wrong. The obvious reading functions handle them badly. One of them has no way at all to limit how much it writes and was removed from the language for that reason; another reports partial success and failure with the same value, so a program cannot tell a malformed line from an empty one. The next chapter is about reading input in a way that survives being given something unexpected, which is the normal case rather than the exceptional one.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
