Search topics...

What are the basic concepts of how printf() works? List and describe some of the special format characters? Show some simple C coding examples.

0 upvotes
Practice with AISoon

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:

  1. Walk the format string character by character.
  2. Copy ordinary characters directly to the output (stdout for printf, a FILE* for fprintf, a buffer for sprintf/snprintf).
  3. When a % is encountered, parse the conversion specification: %[flags][width][.precision][length]specifier.
  4. Pull the next argument with va_arg(ap, <type>), where <type> is determined by the specifier and length modifier.
  5. Convert that binary value into its textual representation (e.g., integer → decimal/hex digits, double → decimal string) and emit it, honoring width/precision/flags.
  6. 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:

c
/* 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:

SpecifierMeaning
%d, %isigned decimal int
%uunsigned decimal int
%x, %Xunsigned hex (lower / upper case)
%ounsigned octal
%csingle character
%sNUL-terminated string
%fdouble in fixed-point notation
%e, %Edouble in scientific notation
%g, %Gdouble, shortest of %f/%e
%ppointer 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.

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