A from-scratch reimplementation of a subset of the C standard library's printf, built as a static library (libftprintf.a). This is the mandatory-only version — conversions only, no flags/width/precision.
| Conversion | Meaning |
|---|---|
%c |
Single character |
%s |
String (prints (null) if given a NULL pointer, matching glibc's behavior) |
%p |
Pointer address, in 0x... hex form |
%d / %i |
Signed decimal integer |
%u |
Unsigned decimal integer |
%x / %X |
Unsigned hex, lowercase / uppercase |
%% |
A literal % |
No flags, width, or precision modifiers (-, 0, field width, .precision, #, etc.) are implemented — only the bare conversions above.
make # -> libftprintf.a
make clean # remove object files
make fclean # remove object files and libftprintf.a
make re # fclean + all#include "ft_printf.h"
int main(void)
{
ft_printf("Hello, %s! You are %d years old (%p).\n", "world", 42, (void *)&main);
return (0);
}cc -Wall -Wextra -Werror your_file.c -I/path/to/ft_printf -L/path/to/ft_printf -lftprintf -o your_program.
├── Makefile
├── ft_printf.h # Public prototype + internal helper prototypes
├── ft_printf.c # Main loop: walks the format string, dispatches on '%'
├── ft_printffunc.c # Per-conversion printing (string, hex, pointer, unsigned)
└── ft_printf_utils.c # ft_strlen, ft_itoa (handles INT_MIN correctly)
Study notes on variadic functions, written while working through this project.
What's called a variadic function relies on va_start, va_arg, va_end, and va_copy.† These all operate on a variable of type va_list.
† (the original notes had this as "va_cpy" — the actual standard name is va_copy)
va_list is the type used by functions that take an unspecified number of arguments of unspecified type.
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;On x86-64 System V (the common Linux/macOS ABI), va_list is implemented as a struct shaped roughly like the above — the exact layout is architecture-specific.
The register save area is where values temporarily assigned to CPU registers get stored during a function call. Only a limited number of registers are available at once, and how many depends on the architecture.
The overflow argument area holds whatever arguments didn't fit into the register save area.
Both of these areas are normally managed behind the scenes by the C compiler — reading a value that's sitting in a register is faster and simpler than always going through memory, which seems to be the main reason they exist.
Offset — a distance indicator: the integer number of bytes from an object's starting point to some later point.
General-purpose registers (GPRs) are used for general-purpose data types (char, int, short, long, addresses, ...). Each register is 8 bytes.
Floating-point registers (FPRs) are dedicated to floating-point numbers. Each one is 16 bytes.
va_start initializes the va_list right before you start reading the variable arguments. Since the arguments live on the stack, this setup reaches into that stack frame: it computes the offset between the last named argument and the first variadic one, points the va_list at that first argument, and the compiler may perform some additional compiler-specific setup on top of that.
va_arg takes two things — the va_list and a type — and steps past one argument at a time. Since it already holds the current address, it uses the type it's given to work out where that argument ends, and reads it accordingly.
va_end invalidates the va_list (runs it through cleanup) — it would need to be re-initialized with va_start before being used again. Being an internal data structure, cleanup mainly means resetting its internal pointers/flags; the compiler may also do its own compiler-specific teardown on top of that, possibly resetting whatever stack- or register-related state the va_list had been using — though the precise mechanism there is implementation-defined and not something the C standard spells out in detail.