What are the basic concepts of how printf() works? List and describe some of the special format characters? Show some simple C coding examples.
printf() is a variadic function declared in <stdio.h>. Its only fixed parameter is the format string; everything after it is passed through the C variable-argument mechanism in <stdarg.h> (va_list, va_start, va_arg, va_end). Because the callee cannot know the number or types of the extra arguments at compile time, it discovers them by parsing the format string at runtime.
Core algorithm:
- Walk the format string character by character.
- Copy ordinary characters directly to the output (stdout for
printf, aFILE*forfprintf, a buffer forsprintf/snprintf). - When a
%is encountered, parse the conversion specification:%[flags][width][.precision][length]specifier. - Pull the next argument with
va_arg(ap, <type>), where<type>is determined by the specifier and length modifier. - Convert that binary value into its textual representation (e.g., integer → decimal/hex digits, double → decimal string) and emit it, honoring width/precision/flags.
- Repeat until the format string ends.
Output is ultimately delivered through the C library's buffered I/O, which calls the OS write primitive. In embedded systems there is usually no OS and no console, so printf is retargeted: the toolchain provides a hook (e.g., newlib's _write/_sbrk, or fputc/putchar in some libraries, or a Keil/IAR-specific __write) that you implement to push each character out a UART, ITM/SWO trace port, RTT, or a ring buffer. A common minimal example:
/* newlib retarget: route stdout to UART */int _write(int fd, const char *buf, int len) {for (int i = 0; i < len; i++)uart_putchar(buf[i]); /* blocking write of one byte */return len;}
Common conversion specifiers:
| Specifier | Meaning |
|---|---|
%d, %i | signed decimal int |
%u | unsigned decimal int |
%x, %X | unsigned hex (lower / upper case) |
%o | unsigned octal |
%c | single character |
%s | NUL-terminated string |
%f | double in fixed-point notation |
%e, %E | double in scientific notation |
%g, %G | double, shortest of %f/%e |
%p | pointer value |
%% | a literal percent sign |
Length modifiers adjust the argument type: h (short), hh (char), l (long), ll (long long), z (size_t), j (intmax_t), t (ptrdiff_t), L (long double). E.g., %lld prints a long long, %zu prints a size_t.
Flags / width / precision control formatting: - (left-justify), + (always show sign), 0 (zero-pad), space (leading space for positives), # (alternate form, e.g., 0x prefix). Width is a minimum field size; precision (after .) sets decimal digits for floats or max characters for strings.
printf("dec=%d hex=%08X\n", 255, 255); /* dec=255 hex=000000FF */printf("float=%-8.2f|\n", 3.14159); /* float=3.14 | (left-justified, 2 dp) */printf("str=%.3s\n", "embedded"); /* str=emb (precision limits string) */printf("char=%c ptr=%p pct=%%\n", 'A', (void*)&main);printf("u64=%llu size=%zu\n", 10000000000ULL, sizeof(int));
A practical embedded note: full printf with %f pulls in floating-point formatting (and often the soft-float library), which is large. Many projects link a reduced variant (e.g., nano.specs in newlib-nano) or a third-party tiny printf to save flash, sometimes at the cost of dropping float support.
