Description

To create our own printf(), we must first understand variadic functions. These functions can accept a variable number of arguments, unlike standard functions which have a fixed set of parameters. printf() relies on this mechanism to process format specifiers (like %s, %d) and their corresponding values.

In C, variadic functions are defined with at least one fixed argument followed by an ellipsis (...).

return_type func_name(fixed_param, ...);

The <stdarg.h> Header

To access these variable arguments, we use the <stdarg.h> library, which provides the necessary types and macros.

#include <stdarg.h>

We will use four key components:

1. va_list

This data type acts as a pointer to the variable arguments in the stack.

Usage:

va_list args;

2. va_start

Initializes the va_list variable to point to the first variable argument. It requires the va_list instance and the name of the last fixed argument in your function definition.

Signature:

void va_start(va_list ap, last);

Usage:

va_start(args, str);

3. va_arg

Retrieves the next argument from the list. It takes the va_list and the type of the argument you expect to retrieve. Each call advances the internal pointer to the next argument.

Signature:

type va_arg(va_list ap, type);

Usage:

int num = va_arg(args, int);

Important:

  • Types Matter: You must provide the correct type. If you ask for an int but the argument is a double, the result is undefined.
  • Promotions: Default argument promotions apply (e.g., char becomes int). This is why %c retrieves an int.

4. va_end

Cleans up the memory associated with the va_list. This must be called before the function returns.

Usage:

va_end(args);

Designing printf

With these concepts in mind, we can start creating printf().

Let's start with the file printf.c:

#include <libarg.h>
 
int printf(const char *str, ...)
{
	int i;
	int total_len;
	va_list args;
 
	i = 0;
	total_len = 0;
	va_start(args, str);
 
	// process va_arg();
 
	va_end(args)
 
	return total_len;
}

We want printf() to behave similarly to the standard printf(), returning the total number of characters printed or a negative value if an error occurred.

We need to track two things: the loop index i for parsing the format string, and total_len for the output count. Assuming the number of arguments matches the number of % specifiers, we can loop through the string.

#include <libarg.h>
#include <unistd.h>
 
int printf(const char *str, ...)
{
	int i;
	int total_len;
	va_list args;
 
	i = 0;
	total_len = 0;
	va_start(args, str);
 
	while(str[i])
	{
		if (str[i] == '%')
		{
			// process va_arg();
		}
		else
			total_len += putchar_len(str[i]);
		i++;
	}
	va_end(args)
	return total_len;
}

Here, we loop through the fixed argument str. If we find a %, we need to handle a conversion. Otherwise, we verify print the character and increment the total length.

The function putchar_len(char c) uses write() to print a character and returns the number of characters written (typically 1).

int putchar_len(char c)
{
	return (write(1, &c, 1));
}

To process the variable arguments, we'll create a helper function that inspects the character after % and calls va_arg() appropriately.

#include <libarg.h>
#include <unistd.h>
 
int printf(const char *str, ...)
{
	int i;
	int total_len;
	va_list args;
 
	i = 0;
	total_len = 0;
	va_start(args, str);
 
	while(str[i])
	{
		if (str[i] == '%')
		{
			total_len += formats(args, str[i + 1]);
			i++; // Skip the format specifier
		}
		else
			total_len += putchar_len(str[i]);
		i++;
	}
	va_end(args)
	return total_len;
}

Handling Formats

The formats function will parse the following types:

  • %c: Prints a single character.
  • %s: Prints a string.
  • %p: Prints a void * pointer in hexadecimal.
  • %d: Prints a decimal (base 10) number.
  • %i: Prints an integer in base 10.
  • %u: Prints an unsigned decimal (base 10) number.
  • %x: Prints a number in hexadecimal (base 16) lowercase.
  • %X: Prints a number in hexadecimal (base 16) uppercase.
  • %%: Prints a percent sign.

It returns the number of characters printed, which we add to total_len.

int formats(va_list args, const char format)
{
	int len;
 
	len = 0;
	if (format == 'c')
		len += putchar_len(va_arg(args, int));
	else if (format == 's')
		len += putstr_len(va_arg(args, char *));
	else if (format == 'p')
		len += putptr_len(va_arg(args, void *));
	else if (format == 'd' || format == 'i')
		len += putnbr_len(va_arg(args, int));
	else if (format == 'u')
		len += putnbr_base(va_arg(args, unsigned int), "0123456789");
	else if (format == 'x')
		len += putnbr_base(va_arg(args, unsigned int), "0123456789abcdef");
	else if (format == 'X')
		len += putnbr_base(va_arg(args, unsigned int), "0123456789ABCDEF");
	else if (format == '%')
		len += putchar_len('%');
	return (len);
}

Note that for %c, va_arg expects int because of default argument promotions in C.

Helper Functions

We need a set of flexible helper functions to handle the printing logic.

/**
 * Description:
 * Write a single character to stdout and return number of bytes written.
 * c    : The character to write
 *
 * Return:
 * Number of bytes written (1 on success, -1 on error)
 */
int putchar_len(char c)
{
	return (write(1, &c, 1));
}
 
/**
 * Description:
 * Recursive function to print ANY number in ANY base
 * n    : The number to print (unsigned long to handle %p and %u)
 * base : The symbols (e.g., "0123456789" or "0123456789abcdef")
 *
 * Return:
 * Number of characters printted
 */
int putnbr_base(unsigned long long n, char *base)
{
	int count;
	unsigned long long base_len;
 
	count = 0;
	base_len = 0;
	while (base[base_len])
		base_len++;
	if (n >= base_len)
		count += putnbr_base(n / base_len, base);
	count += putchar_len(base[n % base_len]);
	return (count);
}
 
/**
 * Description:
 * Write a null-terminated string to stdout and return the number of characters printed.
 * s    : Pointer to the string to print (may be NULL)
 *
 * Return:
 * Number of characters printed (or number of bytes written for "(null)" when s is NULL)
 */
int putstr_len(char *s)
{
	int i;
 
	i = 0;
	if (!s)
		return (write(1, "(null)", 6));
	while (s[i])
		write(1, &s[i++], 1);
	return (i);
}
 
/**
 * Description:
 * Print a signed integer in base 10 and return the number of characters printed.
 * n    : The integer to print
 *
 * Return:
 * Number of characters printed
 */
int putnbr_len(int n)
{
	int count;
	long nb;
 
	count = 0;
	nb = n;
	if (nb < 0)
	{
		count += write(1, "-", 1);
		nb = -nb;
	}
	count += putnbr_base(nb, "0123456789");
	return (count);
}
 
/**
 * Description:
 * Print a pointer value in hexadecimal with '0x' prefix and return the number of characters printed.
 * ptr  : The pointer to print (may be NULL)
 *
 * Return:
 * Number of characters printed
 */
int putptr_len(void *ptr)
{
	int count;
 
	count = 0;
	if (!ptr)
		return (write(1, "(nil)", 5));
	count += write(1, "0x", 2);
	count += putnbr_base((unsigned long long)ptr, "0123456789abcdef");
	return (count);
}

This approach provides a bare-bones implementation of printf() without memory allocation, focusing on modularity and understanding variadic arguments.


You can find the full code at: ft_printf (42 School Project). It is also available as a module of my libcore project at: core_printf.