Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    #include <stdio.h>
    int printf(const char *format, ...);

Reading it with Chapter 12.1's four questions:

QuestionAnswer
assumesformat is a valid string; every conversion has a matching argument of exactly the right type; there are no extra arguments
returnsthe number of characters written
fails byreturning a negative value — convention A
does not promiseanything 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, and printf works 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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             */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc -std=c17 -c p1.c -o /dev/null
(8 warnings)

-Wformat is part of -Wall and is enabled even without it. You already have this check. Treat a -Wformat warning 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    const char *fmt = "%d\n";       /* not a literal at the call site */
    double d = 3.5;
    printf(fmt, d);                 /* mismatched, and unverifiable  */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

SpecifierArgument typePrints
%d %iintsigned decimal
%uunsigned intunsigned decimal
%x %Xunsigned inthexadecimal, lower/upper case
%ounsigned intoctal
%cint (a character value)one character
%schar *characters up to the terminator
%pvoid *an implementation-defined pointer form
%f %e %gdoublefixed / 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    unsigned int value = 0x1Fu;
    printf("%08x\n", value);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0000001f

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

FlagEffect
-left-justify within the width
0pad numbers with leading zeros
+always show a sign on a signed conversion
spacea 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
float  with %f  -> 1.500000
char   with %d  -> 65
short  with %d  -> -7
char   with %c  -> A

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
size_t    %zu  -> 42
ptrdiff_t %td  -> -3
long long %lld -> 1234567890123
uint32_t  PRIu32 -> 31
pointer   %p   -> 0x16da0270c
TypeSpecifierWhy it is not optional
size_t%zuit is unsigned long here and unsigned int elsewhere; %u is a coincidence
ptrdiff_t%tdsame reasoning, signed
long / long long%ld / %lldlong is 8 bytes here and 4 on many targets
uint32_t and friendsPRIu32 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

FunctionWrites toNote
printfstdout
fprintfa stream you namefprintf(stderr, ...) for diagnostics
snprintfa buffer, with a sizethe one to use for buffers — Chapter 8.4
sprintfa buffer, no sizeno 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

  1. printf returns the number of characters written and negative on failure — a return value almost nobody checks, and Chapter 12.4 is where it matters.
  2. printf is variadic, so the compiler applies no checks to the arguments after the format by any ordinary rule.
  3. printf works out what to expect by reading the format at run time.
  4. A mismatch is undefined behaviour, not a cosmetic error — it reads storage under a type that was never there.
  5. Measured: -Wformat caught 7 of 7 mismatches, naming both types, and fired with no -W flags at all.
  6. Measured: a non-literal format produced no diagnostic at all — the check is a special case performed on literals.
  7. -Wformat-nonliteral reports that no check was possible. Keep formats literal; select between whole calls, not between format strings.
  8. Never pass data as a format string — printf("%s", data), never printf(data).
  9. Default argument promotions: float → double, narrow integers → int. That is why %f takes a float and 'A' prints as 65.
  10. c.ts's own example: %08x of 0x1Fu gives 0000001f — flag, width, conversion.
  11. Width is a minimum; precision truncates only for %s.
  12. Mandatory spellings: %zu, %td, %lld, the <inttypes.h> macros, and %p with an explicit (void *).

10. Exercises

Exercise 1 — give the specifier. For each, write the complete printf call.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
|left      |     right|
0x0000001f
+42 and -42
   3.14
value: 007

Exercise 5 — close the hole. Rewrite this so every format is checked, and say what you gave up.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sprintf(buf, user_supplied, n);

11. Summary

  • printf returns a character count, negative on failure.
  • It is variadic, so the compiler checks nothing by the ordinary rules, and printf reads the format at run time.
  • A mismatch is undefined behaviour — storage read under a type that was never there.
  • Measured: -Wformat caught 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 %f for a float is correct and 'A' prints as 65.
  • %08x of 0x1Fu is 0000001f. Width is a minimum; only %s truncates.
  • Mandatory: %zu, %td, %lld, <inttypes.h> macros, %p with (void *).
  • snprintf for buffers; stderr for 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.