Files
42-ft_printf/README.md
T
da7eandClaude Opus 5.5 c8a9ee10cf Merge mandatory and bonus into a single ft_printf, fix edge cases, add README
- Use the flag-aware implementation for both make and make bonus
  (bonus/ moved to src/, _bonus suffix dropped, bonus rule = all)
- Do not treat zero-length output (%s with "", %.0d with 0) as an error
- Return -1 without printing when a % has no conversion ("abc%", "%5")
- Keep field width with '#', '+' and ' '; ignore '0' with '-'
- Fix '#' prefix on zero, '+'/' ' with zero padding, INT_MIN negation UB
- Add README.md required by the subject

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 22:22:28 +02:00

7.3 KiB

This project has been created as part of the 42 curriculum by dgaillet.

ft_printf

Description

ft_printf is a reimplementation of the libc printf() function, packaged as a static library (libftprintf.a). The goal of the project is to learn how variadic functions work in C (va_start, va_arg, va_end) and to build code that is easy to extend with new conversions and flags.

int	ft_printf(const char *format, ...);

It writes to standard output and returns the number of characters it printed. It returns -1 on a write error or an invalid format.

Supported conversions (mandatory)

Conversion Output
%c a single character
%s a string ((null) for a NULL pointer)
%p a pointer in hexadecimal, with a 0x prefix ((nil) for NULL)
%d / %i a signed decimal integer
%u an unsigned decimal integer
%x / %X an unsigned integer in lowercase / uppercase hexadecimal
%% a literal %

Supported flags (bonus)

The same implementation handles both the mandatory conversions and the bonus flags, so there is only one version of ft_printf.

  • Minimum field width, for example %10d
  • -: left-justify within the field
  • 0: pad numbers with zeros (ignored when - or a precision is given)
  • .: precision (minimum digits for numbers, maximum characters for %s)
  • #: 0x / 0X prefix for non-zero %x / %X
  • +: always print a sign for signed numbers
  • (space): print a space in front of positive signed numbers

Any combination of these flags behaves like glibc's printf.

Instructions

Compilation

make          # builds libftprintf.a (conversions and flags)
make bonus    # same as make: the bonus is built into the library
make clean    # removes object files
make fclean   # removes object files and libraries
make re       # fclean + all

The Makefile first builds libft with its own Makefile, then adds the ft_printf objects to a copy of libft.a. This way, libftprintf.a contains everything it needs.

Usage

#include "ft_printf.h"

int	main(void)
{
	int	len;

	len = ft_printf("Hello %s, you are %d years old (%#x)\n", "Bob", 42, 42);
	ft_printf("%d characters printed\n", len);
	return (0);
}
cc main.c -Iinclude -L. -lftprintf -o demo && ./demo

Algorithm and data structures

Main loop

Before printing anything, ft_printf checks the whole format string with ft_check_format. If a % is not followed by a conversion (for example, "abc%" or "%5"), it returns -1 without writing anything, like glibc. This is not buffering: once the format is valid, output is still written as soon as it is ready, as the subject requires.

ft_printf then reads the format string one character at a time:

  1. An ordinary character is written right away with write.
  2. A % starts a conversion specifier. The specifier is parsed into a t_arg structure, printed by the matching print_* function, and then skipped in the format string.
  3. Every printing function returns how many characters it wrote, and ft_printf adds them up.

Data structure

Each conversion is described by a t_arg structure, which is allocated for that conversion and freed once it has been printed:

typedef struct s_arg
{
	char	arg;      /* conversion character: c s p d i u x X % */
	int		padding;  /* minimum field width */
	int		minus;    /* '-' flag: width for left justification */
	int		zero;     /* '0' flag: width to fill with zeros */
	int		dot;      /* precision */
	int		hash;     /* '#' flag */
	int		space;    /* ' ' flag */
	int		plus;     /* '+' flag */
}			t_arg;

Every field starts at -1, meaning "not set". This keeps "not given" (-1) separate from "given as zero" (0), which matters for things like %.0d. A plain conversion such as %d leaves every flag at -1, so the mandatory conversions follow the same path with no special case.

Processing a conversion

  1. Find the conversion (ft_main_arg): skip flag, width and precision characters (-0# +. and digits). The first other character is the conversion.
  2. Parse (ft_parsing): walk the specifier and fill in the t_arg. Each flag stores the number written right after it, the precision is read after the ., and the width is read backwards from the . or the conversion character. If - is present, 0 is ignored, as the C standard requires.
  3. Print (print_*): ft_print_arg_flags fetches the argument with va_arg using the matching type. Each print_* function works out its own padding, in this order: leading spaces, then the sign or 0x prefix, then zeros (from the 0 flag or the precision), then the digits. A trailing - field is padded with spaces at the end, in ft_print_arg_flags.
  4. Skip (ft_to_skip): move the format index past the whole specifier.

Printing numbers

Numbers are printed by the recursive functions ft_putnbr_base (for unsigned int) and ft_putnbr_base_ll (for 64-bit pointers). They first recurse on nbr / base_size, then write base[nbr % base_size], so the digits come out in the right order without a temporary buffer. The same functions print decimal, lowercase hexadecimal and uppercase hexadecimal, just by changing the base string. A negative int is turned into its absolute value as an unsigned int before printing, so INT_MIN works without overflow.

Error handling

On failure, every printing function returns a negative value (-1 from write, or a large negative sentinel from the helpers). After each step, ft_printf checks whether the running total went down. If it did, ft_printf stops and returns -1. A conversion that prints nothing, such as %s with "" or %.0d with 0, adds 0 and is not an error.

Why this design

This layout has one small parser and one function per conversion. Supporting a new flag means adding a field to t_arg, a case in the parser and the related padding logic, without changing the main loop.

Resources

Use of AI

The implementation was written by hand. An AI assistant (Claude) was used at the end of the project to:

  • Review the code against the subject and the real printf. It wrote a test program that compares the output and return value of ft_printf with glibc's printf on many edge cases (empty strings, NULL, INT_MIN, zero precision, flag combinations, trailing %). It also ran the tests with AddressSanitizer and UBSan.
  • Fix the bugs it found, with minimal changes. These were: empty output being treated as an error, field width being lost when combined with #/+/space, 0 not being ignored with -, the # prefix on zero, and signed overflow on INT_MIN.
  • Merge the mandatory and bonus implementations into a single version, and check the format before printing so an incomplete % returns -1 without any output.
  • Write this README.

Every change was reviewed and understood before it was kept.