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
intbut the argument is adouble, the result is undefined.- Promotions: Default argument promotions apply (e.g.,
charbecomesint). This is why%cretrieves anint.
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 avoid *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.