ft_printf · 42 // TECHNICAL GUIDE
SYS:ONLINE REC // 02
Projet 42 · Branche Algorithmique

ft_printf

// Une réécriture pédagogique de printf(3) — la machine à formater du système.

Recoder printf, c'est affronter trois monstres d'un coup : les fonctions variadiques du C, un parseur à machine à états sur une grammaire ambiguë, et un dispatcher qui route vers une poignée de handlers spécialisés. Ce guide dissèque l'implémentation fichier par fichier, du va_start jusqu'au buffer batché.

Difficulté
★★★☆☆
Temps estimé
40 – 70 h
Fonctions autorisées
write · malloc · free · va_*
Sortie attendue
libftprintf.a
// Contrainte clé

Le sujet exige une gestion correcte des conversions sSpdDioOuUxXcC, du modificateur %%, et des flags #0-+ et espace. La gestion de la largeur, de la précision et des length modifiers (hh h l ll j z) est bonus mais nécessaire pour un 100%. Aucune fonction de la libc hors write/malloc/free ne peut être appelée.

01

Le projet

printf est l'une des fonctions les plus chargées de la libc : elle doit interpréter une chaîne de format contenant du texte littéral mélangé à des spécifications de conversion introduites par %, récupérer les arguments correspondants depuis une pile variadique, les formater selon des règles précises (flags, largeur, précision, base numérique), et tout écrire sur la sortie standard — tout en renvoyant le nombre de caractères effectivement écrits.

Que doit faire ft_printf ?

  • Être déclarée int ft_printf(const char *format, ...);
  • Gérer les conversions c s p d i u x X % au minimum
  • Gérer les combinaisons de flags #, 0, -, +, espace
  • Gérer la largeur minimale du champ et la précision
  • Bonus : length modifiers, o, O, D, U, C, S
  • Renvoyer le nombre total de caractères écrits (ou -1 en cas d'erreur)

Le piège conceptuel

Contrairement à la plupart des projets 42, ici le défi n'est pas algorithmique mais structurel. Un printf naïf (un seul fichier monolithique, des if en cascade) devient vite illisible. Le sujet suggère explicitement l'usage d'un tableau de pointeurs sur fonctions pour dispatcher vers les handlers. C'est l'occasion d'apprendre un pattern d'architecture réutilisable partout.

// Conseil

Avant d'écrire une seule ligne, lisez man 3 printf en entier, puis testez les cas tordus avec le vrai printf. Votre seule source de vérité, c'est le comportement observé du vrai. Le man est parfois ambigu ; le vrai ne l'est jamais.

Anatomie d'une spécification de conversion

Tout commence après un %. La grammaire est :

grammarBNF simplifié
% [flags] [width] [.precision] [length] conversion
     │           │           │             │            │
     │           │           │             │            └─ sSpdDioOuUxXcC %
     │           │           │             └─ hh h l ll j z
     │           │           └─ . puis chiffres ou *
     │           └─ chiffres ou *  (largeur min du champ)
     └─ # 0 - + espace  (dans n'importe quel ordre)

Chaque tranche est optionnelle sauf la conversion finale. Le parser doit les reconnaître dans cet ordre strict, sinon le comportement est indéfini. La subtilité : % peut être suivi de n'importe quoi, et tout ce qui n'est pas reconnu comme une spec valide doit être imprimé tel quel (le % + le caractère inconnu).

02

Les fonctions variadiques

Une fonction variadique accepte un nombre variable d'arguments. printf en est l'exemple canonique : printf("%d %s", 42, "x") ou printf("%d %d %d %d", 1,2,3,4) — le compilateur n'impose aucune borne supérieure. Le C fournit quatre macros dans <stdarg.h> pour parcourir ces arguments : va_start, va_arg, va_end, va_copy.

Comment ça marche : la pile d'appel

Sur x86-64 (System V ABI), les premiers arguments entiers/pointeurs passent par des registres (rdi, rsi, rdx, rcx, r8, r9), les suivants vont sur la pile. Mais peu importe l'ABI exact : du point de vue du programmeur C, on peut imaginer tous les arguments empilés de droite à gauche. La va_list est essentiellement un curseur opaque qui avance dans cette zone.

// Diagramme — pile d'appel pour ft_printf("%d %s %c", 42, "hi", 'A')
Adresse haute (bottom of stack)
arg 4 'A'  (int)
arg 3 "hi"  (char*)
arg 2 42   (int)
arg 1 format → "%d %s %c"
← va_list pointe ici après va_start(ap, format)
return address / frame précédente
Adresse basse (top of stack)
va_start(ap, format) positionne le curseur juste après format (le dernier paramètre nommé). Chaque va_arg(ap, T) lit T octets à la position courante puis avance. Le type T est crucial : il détermine combien d'octets lire.

Les quatre macros

va_start(ap, last)
Initialise la va_list ap pour commencer à lire après le paramètre nommé last. À appeler une fois avant tout va_arg.
va_arg(ap, type)
Renvoie l'argument suivant et avance le curseur. Vous devez connaître le type — il n'y a aucune vérification. Mauvais type = UB.
va_end(ap)
Libère les ressources associées à ap (sur certaines plateformes c'est un no-op, sur d'autres il faut nettoyer). À appeler avant de retourner.
va_copy(dest, src)
Clone l'état courant d'une va_list. Indispensable pour parcourir deux fois les mêmes args (par ex. une passe de mesure, une passe d'écriture).
// Piège classique

Le C ne stocke aucune information de type ni de nombre d'arguments au runtime. C'est la chaîne de format qui dicte quoi lire. Si elle dit %s mais que l'argument est un int, vous lisez une adresse absurde et crash garanti. C'est pour ça que les compilateurs modernes émettent -Wformat : le contrôle se fait à la compilation, jamais à l'exécution.

Signature et squelette minimal

ft_printf.c// squelette variadique
int   ft_printf(const char *format, ...)
{
    va_list ap;
    t_buf   buf;
    int     ret;

    buf.len   = 0;
    buf.total = 0;
    va_start(ap, format);          /* 1. initialiser le curseur après 'format' */
    process_format(format, &ap, &buf); /* 2. parser + dispatcher + buffer */
    va_end(ap);                    /* 3. nettoyer */
    buf_flush(&buf);                /* 4. écrire le reste du buffer */
    return (buf.total);
}

Passer une va_list entre fonctions

Une subtilité essentielle : on ne passe jamais une va_list par valeur si on veut que l'appelé puisse consommer des arguments. On passe un pointeur sur la va_list. Sinon, selon l'ABI, la modification n'est pas visible côté appelant (la va_list peut être un tableau sur certaines plateformes).

conv_int.c// pattern correct
/* BIEN : pointeur sur va_list, l'avance est visible partout */
void  conv_int(t_conv *c, va_list *ap, t_buf *buf)
{
    long long n;

    n = va_arg(*ap, int);   /* consomme un int, visible par l'appelant */
    ...
}

/* MAL : copie locale, l'appelant ne voit pas l'avance sur certaines plateformes */
void  conv_int_broken(t_conv *c, va_list ap, t_buf *buf)
{
    long long n = va_arg(ap, int); /* l'avance reste locale */
    ...
}
// À retenir

Le type passé à va_arg subit les promotions par défaut : char/shortint, floatdouble. On lit donc toujours un int pour %c, jamais un char. C'est pourquoi le code cast : (char)va_arg(*ap, int).

03

Architecture

L'implémentation se découpe en quatre couches distinctes, chacune dans son fichier. Cette séparation n'est pas cosmétique : elle rend chaque brique testable et remplaçable indépendamment.

FichierRôleFonctions clés
ft_printf.cEntry point + dispatcher + boucleft_printf, dispatch, process_format
parse.cMachine à états sur la specparse_conv, parse_flags...
conv_int.cConversions d/u (entiers)conv_int, conv_uint
conv_hex.cConversions o/x/Xconv_oct, conv_hex
conv_text.cConversions c/s/p/%conv_char, conv_str...
buffer.cBatching des writesbuf_char, buf_flush...
utils.cOutils + paddingft_strlen, ull_to_base, apply_padding

Flux général d'exécution

À chaque caractère de la chaîne de format, le flux est déterministe :

// Pipeline — un caractère de format traverse ces étapes
char 'c'
c == '%' ?
parse_conv()
dispatch()
conv_*()
buf_*()
write() (× 1 / 4096)
Si c != '%' : on court-circuite, on écrit directement dans le buffer. Sinon on entre dans le parseur, puis le dispatcher choisit le handler, qui formatte et pousse dans le buffer. Le write réel n'a lieu qu'à la fin (ou quand le buffer déborde).

La boucle principale

ft_printf.c// process_format
static void  process_format(const char *fmt, va_list *ap, t_buf *buf)
{
    t_conv  conv;

    while (*fmt)
    {
        if (*fmt != '%')
        {
            buf_char(buf, *fmt);   /* texte littéral → buffer direct */
            fmt++;
            continue;
        }
        fmt++;                            /* skip '%' */
        conv.flags     = 0;
        conv.width     = 0;
        conv.precision = -1;          /* -1 = non spécifiée */
        conv.length    = LEN_NONE;
        conv.conv      = 0;
        fmt = parse_conv(fmt, &conv, ap);  /* remplit conv */
        if (conv.conv)
            dispatch(&conv, ap, buf);        /* route vers le handler */
        else if (*fmt)                      /* spec invalide → imprime % + char */
        {
            buf_char(buf, '%');
            buf_char(buf, *fmt);
            fmt++;
        }
        else
            buf_char(buf, '%');   /* '%' final solitaire */
    }
}

Notez la valeur sentinelle -1 pour precision : elle permet de distinguer « précision non demandée » de « précision demandée mais valant 0 » (%.0d). Cette distinction est cruciale pour %d avec la valeur 0.

Le dispatcher

Le sujet suggère un tableau de pointeurs sur fonctions. Deux approches coexistent : un switch lisible, ou un tableau indexé par le caractère de conversion. Les deux sont valables ; le tableau est plus « idiomatique 42 » et évite une chaîne de if/else.

ft_printf.c// dispatch — version switch
int   dispatch(t_conv *c, va_list *ap, t_buf *buf)
{
    char  conv = c->conv;

    if (conv == 'd' || conv == 'i' || conv == 'D')
        conv_int(c, ap, buf);
    else if (conv == 'u' || conv == 'U')
        conv_uint(c, ap, buf);
    else if (conv == 'o' || conv == 'O')
        conv_oct(c, ap, buf);
    else if (conv == 'x' || conv == 'X')
        conv_hex(c, ap, buf);
    else if (conv == 'c' || conv == 'C')
        conv_char(c, ap, buf);
    else if (conv == 's' || conv == 'S')
        conv_str(c, ap, buf);
    else if (conv == 'p')
        conv_ptr(c, ap, buf);
    else if (conv == '%')
        conv_percent(c, ap, buf);
    return (0);
}

Version alternative avec tableau — plus extensible, plus « 42 » :

ft_printf.c// dispatch — version tableau
typedef void (*t_handler)(t_conv*, va_list*, t_buf*);

/* Table indexée par code ASCII ; NULL pour les chars sans handler. */
static t_handler g_table[128] = {
    ['d'] = conv_int,  ['i'] = conv_int,  ['D'] = conv_int,
    ['u'] = conv_uint, ['U'] = conv_uint,
    ['o'] = conv_oct,  ['O'] = conv_oct,
    ['x'] = conv_hex,  ['X'] = conv_hex,
    ['c'] = conv_char, ['C'] = conv_char,
    ['s'] = conv_str,  ['S'] = conv_str,
    ['p'] = conv_ptr,
    ['%'] = conv_percent,
};

int   dispatch(t_conv *c, va_list *ap, t_buf *buf)
{
    unsigned char  conv = (unsigned char)c->conv;

    if (conv < 128 && g_table[conv])
        g_table[conv](c, ap, buf);
    return (0);
}
// Table de routage du dispatcher
d i D
conv_int
u U
conv_uint
o O
conv_oct
x X
conv_hex
c C
conv_char
s S
conv_str
p
conv_ptr
%
conv_percent
// Pourquoi un tableau ?

Avec un if/else en cascade, le temps de dispatch croît linéairement avec le nombre de cas. Avec un tableau indexé, c'est un accès direct O(1). Pour 8 handlers la différence est négligeable, mais le pattern est réutilisable (moteur de commandes, opcodes de VM, événements clavier…). Sur un projet 42, un correcteur qui voit un tableau de pointeurs sur fonctions sait immédiatement que vous avez compris l'enjeu d'architecture.

Les structures de données

Deux structures portent tout le projet :

t_conv — contexte d'une conversion
typedef struct s_conv {
    int    flags;     /* bitmask FLAG_* */
    int    width;     /* largeur min */
    int    precision; /* -1 si absent */
    t_len  length;    /* hh h l ll j z */
    char   conv;      /* 'd','x',... */
}              t_conv;
t_buf — buffer d'écriture
typedef struct s_buf {
    char data[BUFF_SIZE]; /* 4096 */
    int  len;              /* rempli */
    int  total;            /* return value */
}         t_buf;

Les flags sont stockés dans un bitmask plutôt que dans 5 booléens séparés. Avantages : passage par copie trivial, combinaisons testables en une expression (c->flags & (FLAG_ZERO | FLAG_MINUS)), et économie de mémoire. C'est le pattern standard pour tout ensemble d'options binaires en C.

ft_printf.h// définitions des flags
#define FLAG_HASH   (1 << 0)   /* #  */
#define FLAG_ZERO   (1 << 1)   /* 0  */
#define FLAG_MINUS  (1 << 2)   /* -  */
#define FLAG_PLUS   (1 << 3)   /* +  */
#define FLAG_SPACE  (1 << 4)   /* ' '*/

typedef enum e_len {
    LEN_NONE = 0, LEN_HH, LEN_H, LEN_L, LEN_LL, LEN_J, LEN_Z
}              t_len;
04

Le parsing

Le parser est une machine à états linéaire : il consomme la chaîne de format de gauche à droite, dans l'ordre strict flags → width → precision → length → conversion. Chaque étape avance un pointeur const char ** ; si l'étape ne reconnaît rien, elle ne fait simplement rien et passe à la suivante.

parse.c// orchestrateur
const char *parse_conv(const char *fmt, t_conv *c, va_list *ap)
{
    parse_flags(&fmt, c);
    parse_width(&fmt, c, ap);
    parse_precision(&fmt, c, ap);
    parse_length(&fmt, c);
    if (*fmt)
    {
        c->conv = *fmt;
        fmt++;
    }
    return (fmt);
}

Étape 1 — Les flags

Les flags # 0 - + espace peuvent apparaître dans n'importe quel ordre et être répétés. On boucle tant qu'on en reconnaît un, et on positionne le bit correspondant dans le bitmask.

parse.c// parse_flags
static void  parse_flags(const char **fmt, t_conv *c)
{
    while (**fmt == '#' || **fmt == '0' || **fmt == '-' ||
           **fmt == '+' || **fmt == ' ')
    {
        if (**fmt == '#')     c->flags |= FLAG_HASH;
        else if (**fmt == '0')  c->flags |= FLAG_ZERO;
        else if (**fmt == '-')  c->flags |= FLAG_MINUS;
        else if (**fmt == '+')  c->flags |= FLAG_PLUS;
        else if (**fmt == ' ')  c->flags |= FLAG_SPACE;
        (*fmt)++;
    }
}
// Subtilité

%-0d et %0-d sont équivalents : les deux flags sont posés, mais à l'affichage c'est - qui gagne (le 0 est ignoré en présence de -). Le parser ne fait pas ce choix — il stocke tout. C'est le handler d'affichage qui applique la règle de priorité. Séparation parsing / affichage = code plus clair.

Étape 2 — La largeur

La largeur minimale du champ est soit un nombre décimal, soit * qui signifie « lire la largeur depuis la va_list ». La particularité : si la largeur lue via * est négative, le comportement est équivalent à un flag - suivi de la valeur absolue.

parse.c// parse_width
static void  parse_width(const char **fmt, t_conv *c, va_list *ap)
{
    if (**fmt == '*')
    {
        c->width = va_arg(*ap, int);
        if (c->width < 0)           /* width négative = flag '-' implicite */
        {
            c->flags |= FLAG_MINUS;
            c->width = -c->width;
        }
        (*fmt)++;
    }
    else if (**fmt >= '0' && **fmt <= '9')
        c->width = ft_atoi(fmt);  /* avance fmt */
}

Étape 3 — La précision

Introduite par un point .. Sans chiffre derrière, elle vaut 0 (%.d%.0d). Avec *, elle vient de la va_list ; si elle est négative, elle est ignorée (comme si elle n'avait pas été spécifiée).

parse.c// parse_precision
static void  parse_precision(const char **fmt, t_conv *c, va_list *ap)
{
    if (**fmt == '.')
    {
        (*fmt)++;
        c->precision = 0;             /* "." seul = précision 0 */
        if (**fmt == '*')
        {
            c->precision = va_arg(*ap, int);
            if (c->precision < 0)
                c->precision = -1;     /* précision négative = ignorée */
            (*fmt)++;
        }
        else
            c->precision = ft_atoi(fmt);
    }
}
// Différence fondamentale

La précision a deux sémantiques selon la conversion :

  • Pour les nombres (diouxX) : nombre minimum de chiffres. Ajoute des zéros non significatifs.
  • Pour les chaînes (s) : nombre maximum de caractères affichés. Tronque.

Étape 4 — Les length modifiers

hh h l ll j z indiquent la taille attendue de l'argument. Ils déterminent comment on doit le lire dans la va_list et éventuellement le caster. Attention : hh et ll sont à deux caractères, il faut les tester avant h et l seuls.

parse.c// parse_length
static void  parse_length(const char **fmt, t_conv *c)
{
    if ((*fmt)[0] == 'h' && (*fmt)[1] == 'h')  { c->length = LEN_HH; *fmt += 2; }
    else if ((*fmt)[0] == 'h')                    { c->length = LEN_H;  (*fmt)++; }
    else if ((*fmt)[0] == 'l' && (*fmt)[1] == 'l')  { c->length = LEN_LL; *fmt += 2; }
    else if ((*fmt)[0] == 'l')                    { c->length = LEN_L;  (*fmt)++; }
    else if ((*fmt)[0] == 'j')                    { c->length = LEN_J;  (*fmt)++; }
    else if ((*fmt)[0] == 'z')                    { c->length = LEN_Z;  (*fmt)++; }
}
ModifierType attenduEffet sur la lecture
hhsigned/unsigned charCast depuis int(signed char)
hsigned/unsigned shortCast depuis int(short)
llong / unsigned longva_arg(ap, long)
lllong long / unsigned long longva_arg(ap, long long)
jintmax_t / uintmax_tLu comme long long (suffisant en 64-bit)
zsize_t / ssize_tLu comme long long
// Astuce

Une fois l'argument lu et promu en long long / unsigned long long, tout le reste du traitement (conversion en base, padding) est identique. Les length modifiers ne changent que la première ligne de chaque handler. C'est ce qui permet de factoriser ull_to_base au lieu d'écrire 6 variantes.

05

Les conversions

Chaque handler a la même signature void conv_x(t_conv *c, va_list *ap, t_buf *buf) et suit le même plan :

  1. Lire l'argument selon le length modifier (toujours promu vers le type large).
  2. Convertir en chaîne de chiffres via ull_to_base (ou ft_strlen pour les strings).
  3. Appliquer la précision (zérosLeading pour les nombres, troncature pour les strings).
  4. Désactiver le flag 0 si une précision est donnée (règle du man).
  5. Calculer signe + préfixe (+, -, 0x, 0…).
  6. Appliquer le padding gauche/droite/zero via apply_padding.

Entier signé — %d %i

Le cas d'école. On lit un int (ou long/long long selon le modifier), on extrait le signe, on convertit la valeur absolue en base 10, puis on gère précision et padding. Le piège INT_MIN est géré en travaillant en unsigned long long après avoir extrait le signe : -n sur INT_MIN déborderait en int signé, mais pas en ull.

conv_int.c// conv_int (extrait commenté)
void  conv_int(t_conv *c, va_list *ap, t_buf *buf)
{
    long long  n;
    char       num[32];
    int        num_len;
    char       sign;

    /* 1. LIRE — promotion selon length modifier */
    if      (c->length == LEN_HH) n = (signed char)va_arg(*ap, int);
    else if (c->length == LEN_H)  n = (short)va_arg(*ap, int);
    else if (c->length == LEN_L)  n = va_arg(*ap, long);
    else if (c->length == LEN_LL) n = va_arg(*ap, long long);
    else                       n = va_arg(*ap, int);

    /* 2. SIGNE — mémorisé à part, n devient valeur absolue (en ull) */
    sign = 0;
    if (n < 0)                  { sign = '-'; n = -n; }
    else if (c->flags & FLAG_PLUS)  sign = '+';
    else if (c->flags & FLAG_SPACE) sign = ' ';

    /* 3. CONVERTIR en base 10 (digits écrits à l'envers) puis reverse */
    ull_to_base((unsigned long long)n, 10, 0, num, &num_len);
    reverse(num, num_len);   /* helper — voir utils */

    /* 4. PRÉCISION — précision 0 + valeur 0 → affiche rien */
    if (c->precision == 0 && n == 0)
        num_len = 0;
    else if (c->precision > num_len)
        num_len = prepend_zeros(num, num_len, c->precision);

    /* 5. DÉSACTIVER '0' si précision donnée (règle du man) */
    if (c->precision != -1)
        c->flags &= ~FLAG_ZERO;

    /* 6. PADDING selon flags '-' / '0' / rien */
    apply_padding(c, buf, num, num_len, sign, "", 0);
}
// Le piège INT_MIN

INT_MIN = -2147483648. Sur un int signé, -INT_MIN est un débordement (UB) car +2147483648 n'est pas représentable. La solution : caster en long long avant la négation, ou — comme ici — travailler en unsigned long long dès qu'on a extrait le signe. ull_to_base reçoit une valeur non signée, donc plus aucun souci.

Non signé — %u et octal %o

%u est la version non signée de %d : pas de signe, mais même logique de précision et de padding. %o convertit en base 8 ; avec le flag #, un 0 est ajouté en préfixe (pour garantir que le résultat commence par un 0, convention octale).

conv_hex.c// conv_oct (extrait — préfixe #)
void  conv_oct(t_conv *c, va_list *ap, t_buf *buf)
{
    unsigned long long  n;
    char                num[32];
    char               *prefix = "";
    int                 prefix_len = 0;

    n = read_unsigned(c, ap);                 /* selon length */
    ull_to_base(n, 8, 0, num, &num_len);
    reverse(num, num_len);

    if (c->flags & FLAG_HASH)               /* '#' → préfixe "0" */
    {
        prefix = "0";
        prefix_len = 1;
    }
    /* Note : pour octal, '#' interagit avec la précision :
       si precision <= 1 et n==0, le '0' du préfixe compte dans la précision. */
    if (c->precision == 0 && n == 0 && !(c->flags & FLAG_HASH))
        num_len = 0;
    /* ... précision (prepend zeros), disable '0' flag, apply_padding ... */
    apply_padding(c, buf, num, num_len, 0, prefix, prefix_len);
}

Hexadécimal — %x %X

%x affiche en hexa minuscule, %X en majuscule. Le flag # ajoute le préfixe 0x ou 0X — mais seulement si la valeur est non nulle. %#x de 0 affiche 0, pas 0x0.

conv_hex.c// conv_hex (extrait — préfixe conditionnel)
void  conv_hex(t_conv *c, va_list *ap, t_buf *buf)
{
    unsigned long long  n;
    char                num[32];
    int                 upper  = (c->conv == 'X');
    char               *prefix = "";
    int                 prefix_len = 0;

    n = read_unsigned(c, ap);
    ull_to_base(n, 16, upper, num, &num_len);  /* digits ABCDEF si upper */
    reverse(num, num_len);

    /* '#' ajoute 0x/0X UNIQUEMENT si n != 0 */
    if (c->flags & FLAG_HASH && n != 0)
    {
        prefix     = upper ? "0X" : "0x";
        prefix_len = 2;
    }
    if (c->precision == 0 && n == 0)
        num_len = 0;
    /* ... précision, disable '0', apply_padding ... */
    apply_padding(c, buf, num, num_len, 0, prefix, prefix_len);
}

Chaîne — %s

Cas particulier : la précision devient un maximum de caractères affichés, pas un minimum. Et si l'argument est NULL, le vrai printf affiche la chaîne littérale (null) (comportement gnu, pas POSIX strict, mais c'est ce que testent les corrections 42).

conv_text.c// conv_str — complet
void  conv_str(t_conv *c, va_list *ap, t_buf *buf)
{
    char *s;
    int  len;

    s = va_arg(*ap, char *);
    if (s == NULL)                       /* gère NULL → "(null)" */
        s = "(null)";
    len = ft_strlen(s);
    if (c->precision != -1 && c->precision < len)
        len = c->precision;            /* précision = max chars (troncature) */

    if (c->flags & FLAG_MINUS)           /* aligné à gauche */
    {
        buf_str(buf, s, len);
        buf_pad(buf, ' ', c->width - len > 0 ? c->width - len : 0);
    }
    else                                /* aligné à droite (défaut) */
    {
        buf_pad(buf, ' ', c->width - len > 0 ? c->width - len : 0);
        buf_str(buf, s, len);
    }
}
// Edge case — NULL

Avec une précision < 6, printf("%.3s", (char*)NULL) affiche (nu (troncature de (null)). Testez ce comportement avec le vrai printf avant de le reproduire — certaines versions affichent une chaîne vide.

Caractère — %c et littéral %%

%c lit un int (rappel : promotion par défaut) et l'affiche comme un char. Seuls les flags - et la largeur sont pertinents. %% est un cas dégénéré : il affiche un % littéral et ne consomme aucun argument de la va_list.

conv_char
void  conv_char(t_conv *c, va_list *ap, t_buf *buf)
{
    char  ch = (char)va_arg(*ap, int);

    if (c->flags & FLAG_MINUS)
    {
        buf_char(buf, ch);
        buf_pad(buf, ' ', c->width - 1 > 0 ? c->width - 1 : 0);
    }
    else
    {
        buf_pad(buf, ' ', c->width - 1 > 0 ? c->width - 1 : 0);
        buf_char(buf, ch);
    }
}
conv_percent
void  conv_percent(t_conv *c, va_list *ap, t_buf *buf)
{
    (void)ap;   /* ne consomme RIEN */
    if (c->flags & FLAG_MINUS)
    {
        buf_char(buf, '%');
        buf_pad(buf, ' ', c->width - 1 > 0 ? c->width - 1 : 0);
    }
    else if (c->flags & FLAG_ZERO)
    {                                  /* '%050%' → "0000%" */
        buf_pad(buf, '0', c->width - 1 > 0 ? c->width - 1 : 0);
        buf_char(buf, '%');
    }
    else
    {
        buf_pad(buf, ' ', c->width - 1 > 0 ? c->width - 1 : 0);
        buf_char(buf, '%');
    }
}

Pointeur — %p

Un pointeur, c'est une adresse — un nombre non signé qu'on affiche en hexa avec un préfixe 0x systématique (contrairement à %#x où le préfixe dépend de la valeur). Sur 64-bit, un pointeur tient dans un unsigned long long.

conv_text.c// conv_ptr
void  conv_ptr(t_conv *c, va_list *ap, t_buf *buf)
{
    unsigned long long  addr;
    char                num[32];
    int                 num_len;

    addr = (unsigned long long)va_arg(*ap, void *);
    ull_to_base(addr, 16, 0, num, &num_len);
    reverse(num, num_len);

    /* "0x" TOUJOURS, même pour NULL → "0x0" */
    apply_padding(c, buf, num, num_len, 0, "0x", 2);
}
// Note de portabilité

Sur certaines plateformes rares, void* n'a pas la même taille que unsigned long long. Pour être strictement correct, il faudrait utiliser uintptr_t de <stdint.h>. Mais ce header n'est pas autorisé en C 42 norme stricte ; le cast via unsigned long long est l'approche pragmatique universellement acceptée en correction.

Le helper apply_padding

Toute la logique d'alignement (gauche / droite / zero-padding) est factorisée dans une seule fonction réutilisée par tous les handlers numériques. Elle prend le contenu déjà formaté, le signe éventuel, et le préfixe éventuel, puis écrit dans le bon ordre.

utils.c// apply_padding — le cœur du padding
static void  apply_padding(t_conv *c, t_buf *buf,
                              char *content, int content_len,
                              char sign, char *prefix, int prefix_len)
{
    int   total_len = content_len + (sign ? 1 : 0) + prefix_len;
    int   pad      = c->width - total_len;
    char  pad_char = (c->flags & FLAG_ZERO && !(c->flags & FLAG_MINUS)
                          && c->precision == -1) ? '0' : ' ';

    if (c->flags & FLAG_MINUS)              /* '-' : contenu puis espaces */
    {
        if (sign)  buf_char(buf, sign);
        buf_str(buf, prefix, prefix_len);
        buf_str(buf, content, content_len);
        buf_pad(buf, ' ', pad > 0 ? pad : 0);
    }
    else if (pad_char == '0')            /* '0' : signe+prefix+zéros+chiffres */
    {
        if (sign)  buf_char(buf, sign);
        buf_str(buf, prefix, prefix_len);
        buf_pad(buf, '0', pad > 0 ? pad : 0);
        buf_str(buf, content, content_len);
    }
    else                                /* défaut : espaces puis contenu */
    {
        buf_pad(buf, ' ', pad > 0 ? pad : 0);
        if (sign)  buf_char(buf, sign);
        buf_str(buf, prefix, prefix_len);
        buf_str(buf, content, content_len);
    }
}
// Ordre du zero-padding

Quand on pad avec des 0, l'ordre est : signe → préfixe (0x) → zéros → chiffres. C'est crucial : %+08.0x de 255 donne +00000ff, pas 0000+ff. Le signe et le préfixe restent collés aux chiffres, jamais noyés dans le padding.

06

Les flags

Cinq flags, chacun avec sa sémantique fine et ses interactions. Voici la référence complète, avec exemples sur la valeur 42 (ou -42 pour les flags de signe).

FlagNomEffetExempleSortie
#hashPréfixe 0x/0X pour hex (si n≠0), 0 pour octal%#x 2550xff
0zeroPad avec des 0 au lieu d'espaces (ignoré si - ou précision)%05d 4200042
-minusAligne à gauche (pad à droite). Prioritaire sur 0%-5d| 4242 |
+plusAffiche toujours le signe (+ ou -). Prioritaire sur espace%+d 42+42
spacePréfixe un espace aux positifs (alignement des négatifs)% d 42 42

Le flag # — préfixe de base

Ajoute un préfixe qui indique explicitement la base : 0x/0X pour l'hexa, 0 pour l'octal. Comportement subtil pour l'octal : %#o de 0 affiche 0 (un seul zéro, pas 00) car le préfixe est déjà le chiffre zéro. Pour l'hexa, %#x de 0 affiche 0 sans 0x.

Le flag 0 — zero-padding

Remplace le padding espace par du padding zéro, mais uniquement à gauche (n'a pas de sens avec -). Et surtout, il est ignoré si une précision est spécifiée pour les conversions numériques. C'est une règle du man qui surprend beaucoup de monde.

conv_int.c// règle d'invalidation du '0'
/* '0' est invalide si :                                   */
/*   1. '-' est présent   (le '-' aligne à gauche)          */
/*   2. une précision est donnée pour un nombre            */
/*      (la précision gère déjà les zéros significatifs)   */

if (c->precision != -1)
    c->flags &= ~FLAG_ZERO;     /* disable '0' */

/* Le '-' est géré directement dans apply_padding : le test */
/* (c->flags & FLAG_ZERO && !(c->flags & FLAG_MINUS))        */
/* garantit que '-' gagne toujours.                         */
// Vérifiez vous-même

printf("%08.3d", 42) 042 (5 espaces + 042). Le 0 est ignoré à cause de la précision. Sans la règle, on aurait eu 00000042. Toujours comparer avec le vrai printf.

Le flag - — alignement gauche

Par défaut, tout est aligné à droite dans la largeur. - inverse : le contenu d'abord, puis le padding d'espaces à droite. Toujours gagne contre 0 : %-05d de 42 donne 42 (espaces), pas 42000.

Les flags + et espace — signe explicite

+ force l'affichage d'un + devant les positifs. espace met un espace à la place. + est prioritaire. Utile pour aligner visuellement des nombres positifs et négatifs dans un tableau. N'a d'effet que sur les conversions signées (d i), pas sur u o x.

SpecValeurSortieCommentaire
%d4242défaut
%+d42+42signe + explicite
% d42 42espace pour positif
%d-42-42négatif : toujours un -
%+d-42-42+ n'affecte pas les négatifs
%+05d42+0042signe collé aux zéros
% 5d42 42padding normal
07

Précision & largeur

C'est la source numéro un des bugs. La largeur (width) et la précision (precision) sont deux concepts orthogonaux mais qui interagissent.

Largeur — width
Largeur minimale du champ total (signe + préfixe + chiffres inclus).

Si le contenu est plus court, on pad (espaces ou 0).
Si le contenu est plus long, on ne tronque jamais.
Sémantique identique pour toutes les conversions.
Précision — precision
Dépend de la conversion :
• Nombres : nombre minimum de chiffres → ajoute des zéros à gauche.
• String : nombre maximum de caractères → tronque.
Par défaut -1 (non spécifiée).

L'interaction, en image

Soit printf("%8.3d", 42). Décomposons :

// Décomposition de "%8.3d" appliqué à 42
42
precision=3 → 042 (3 digits)
signe absent, préfixe absent
total=3, width=8 → pad=5 espaces
     042

La précision agit en premier (sur les chiffres seuls), puis la largeur englobe le tout. C'est pourquoi 0 est désactivé par la précision : il y aurait conflit entre deux sources de zéros.

Cas tordus à maîtriser

SpecValeurSortieExplication
%.0d0(vide)précision 0 + valeur 0 → rien
%.0d4242précision 0 n'affecte que le zéro
%5.0d0(5 espaces)rien + largeur 5 → 5 espaces
%-5.0d|0(5 espaces)|aligné gauche
%05.0d0(5 espaces)0 désactivé par précision
%.3s"hello"heltroncature à 3 chars
%.10s"hi"hipas d'extension pour les strings
%10.3s"hello" heltroncature puis largeur
// La règle d'or

Précision = contenu. Largeur = contenant. La précision modifie ce qui est imprimé (nombre de digits, nombre de chars), la largeur ne fait qu'ajouter du vide autour. Cette image mentale résout 90% des cas tordus.

Largeur et précision dynamiques — *

Le * permet de lire la largeur ou la précision depuis la va_list au runtime. L'ordre de consommation est strictement gauche-droite dans la spec : d'abord la largeur, puis la précision.

exemple// ordre des arguments avec *
/* %*.*d : arg1=largeur, arg2=précision, arg3=valeur */
ft_printf("%*.*d", 8, 3, 42);
/*             ^   ^   ^                                  */
/*             |   |   └─ valeur à afficher             */
/*             |   └───── précision (3)                  */
/*             └───────── largeur (8)                    */
/* → "     042"  (5 espaces + "042")                     */

Largeur négative via * → équivaut à - + valeur absolue. Précision négative via * → ignorée (comme si pas de précision).

08

Le buffer

Appeler write(1, ...) pour chaque caractère serait suicidaire en termes de performance. write est un appel système : chaque appel provoque un context switch user→kernel, copie les données en espace noyau, puis vers le driver terminal. Sur un printf qui écrit 10000 caractères, c'est 10000 syscalls au lieu d'un seul.

Pourquoi un buffer ?

Syscalls = cher
Chaque write coûte ~1-10 µs en contexte user/kernel. Sur 10k caractères, on passe de 10 ms à 10 µs.
Atomicité
Un seul write par batch → la sortie n'est pas entrelacée avec d'autres processus écrivant sur le même terminal.
Return value fiable
Le compteur buf.total compte les caractères logiques, indépendamment des write physiques.

La structure et les opérations

ft_printf.h// le buffer
#define BUFF_SIZE 4096    /* taille d'une page mémoire typique */

typedef struct s_buf {
    char data[BUFF_SIZE];   /* stockage local */
    int  len;                /* octets actuellement stockés */
    int  total;              /* compteur logique (return value) */
}         t_buf;
buffer.c// implémentation
/* Flush : vide le buffer sur stdout en un seul write */
void  buf_flush(t_buf *buf)
{
    if (buf->len > 0)
    {
        write(1, buf->data, buf->len);
        buf->len = 0;
    }
}

/* Ajoute un caractère ; flush automatique si plein */
void  buf_char(t_buf *buf, char c)
{
    if (buf->len >= BUFF_SIZE)   /* débordement → on vide d'abord */
        buf_flush(buf);
    buf->data[buf->len] = c;
    buf->len++;
    buf->total++;              /* compte logique toujours incrémenté */
}

/* Ajoute une chaîne de longueur connue */
void  buf_str(t_buf *buf, const char *s, int len)
{
    int  i = 0;
    while (i < len)
        buf_char(buf, s[i++]);   /* gère le flush auto */
}

/* Ajoute n fois le caractère c (pour le padding) */
void  buf_pad(t_buf *buf, char c, int n)
{
    int  i = 0;
    while (i < n)
        buf_char(buf, c), i++;
}
// Cycle de vie du buffer
buf_char × N
data[0..len-1]
len < 4096 ?
len == 4096 → buf_flush()
write(1, data, 4096)
len = 0, continue
À la fin de ft_printf, un dernier buf_flush garantit que tout est écrit. Le return value (buf.total) reflète le nombre logique de caractères, pas le nombre de write.
// Optimisation subtile

buf_str et buf_pad appellent buf_char en boucle. C'est correct mais légèrement sous-optimal : on vérifie len >= BUFF_SIZE à chaque caractère. Une version rapide copierait par memcpy des blocs entiers quand c'est possible. Pour ft_printf c'est négligeable, mais c'est l'optimisation naturelle si on voulait pousser plus loin.

// Piège — return value

Le vrai printf renvoie le nombre de caractères qui auraient été écrits, même si write échoue partiellement. En suivant buf.total plutôt que la valeur de retour de write, on reproduit ce comportement. Mais attention : si write échoue (erreur disque, pipe fermé), le vrai printf renvoie -1. Pour un 100%, il faudrait propager l'erreur. Beaucoup de projets 42 l'omettent et passent quand même — mais c'est un point à connaître.

Taille du buffer : pourquoi 4096 ?

4096 est la taille d'une page mémoire standard sur x86/ARM. C'est aussi la taille de buffer typique du kernel pour les écritures sur pipe/terminal. En dessous (256, 1024), on augmente le nombre de syscalls sans gain. Au-dessus (64k), on gaspille de la stack (la t_buf est locale à ft_printf) sans gagner en débit. 4096 est le sweet spot.

// Détail structurel

La t_buf fait 4096 + 8 octets. Si on la déclare locale à ft_printf, elle vit sur la stack. C'est OK pour 4 Ko, mais pour des buffers plus gros il faudrait malloc. La stack Linux par défaut est 8 Mo, donc on a de la marge — mais un projet qui declare un buffer de 1 Mo en local fera planter certaines fonctions récursives.

09

Pièges & edge cases

Ces subtilités font la différence entre un ft_printf qui passe la moulinette et un qui plante. Chaque cas ci-dessous a été rencontré sur au moins un des 9 testeurs de la communauté.

1. Passer va_list en pointeur, pas en valeur

Sur certaines architectures (notamment x86-64 Linux), va_list est un type tableau. Quand vous passez un tableau par valeur à une fonction, C dégrade silencieusement en pointeur — donc la fonction reçoit un pointeur sur le premier élément, et va_arg modifie l'original. Mais sur d'autres ABI, va_list est un struct copié par valeur, et l'avance n'est pas visible par l'appelant. La solution portable : toujours passer &ap (pointeur sur va_list) et déréférencer avec *ap dans va_arg.

conv_int.c// pattern correct
/* CORRECT : pointeur sur va_list */
void  conv_int(t_conv *c, va_list *ap, t_buf *buf)
{
    int n = va_arg(*ap, int);
    /* ... traite n ... */
}

/* CASSÉ : copie locale — l'appelant ne voit pas l'avance sur certains ABI */
void  conv_int_broken(t_conv *c, va_list ap, t_buf *buf)
{
    int n = va_arg(ap, int);
}

2. Caster selon le length modifier

Quand vous appelez va_arg(*ap, TYPE), le type doit correspondre au length modifier de la spec. Sinon vous lisez la mauvaise quantité de mémoire. Attention : les types plus petits que int (char, short) sont promus en int via les default argument promotions — vous devez donc toujours faire va_arg(ap, int) puis caster le résultat.

Specva_arg typeCast après va_arg
%d %iint
%hdint(short)
%hhdint(signed char)
%ldlong
%lldlong long
%jdlong long
%zdlong long
%uunsigned int
%luunsigned long
%lluunsigned long long

3. Le flag 0 est invalidé par la précision

glibc dit : si une précision est spécifiée pour une conversion entière (%d, %i, %u, %o, %x, %X), le flag 0 est ignoré. La précision contrôle déjà le nombre de digits, donc le zero-padding n'a pas de sens. Cette règle ne s'applique qu'aux entiers.

conv_int.c// règle d'invalidation
/* %08.3d avec 42 → "     042" (espaces, pas zeros !) */
if (c->precision != -1)
    c->flags &= ~FLAG_ZERO;  /* désactive le zero-padding */

4. %p avec pointeur NULL → (nil)

glibc affiche (nil) pour un pointeur NULL, pas 0x0. macOS affiche 0x0 — c'est une différence de plateforme.

conv_text.c// conv_ptr
void  conv_ptr(t_conv *c, va_list *ap, t_buf *buf)
{
    unsigned long long addr;
    addr = (unsigned long long)va_arg(*ap, void *);
    if (addr == 0)
    {
        apply_padding(c, buf, "(nil)", 5, 0, "", 0);
        return ;
    }
    /* ... conversion hex avec préfixe "0x" ... */
}

5. %s avec NULL + précision

glibc affiche (null) pour une string NULL. Mais si la précision est inférieure à 6 (longueur de "(null)"), rien n'est affiché.

conv_text.c// conv_str
if (s == NULL)
{
    if (c->precision != -1 && c->precision < 6)
        len = 0;
    else
    {
        s = "(null)";
        len = 6;
    }
}

6. INT_MIN et le débordement de signe

INT_MIN = -2147483648. L'expression -INT_MIN déborde car INT_MAX = 2147483647. Pour éviter ça, on convertit en unsigned long long avant de négationner. Notre implémentation utilise long long pour n, ce qui suffit pour int et long, mais pour LLONG_MIN il faut caster en unsigned avant la négation.

conv_int.c// gestion INT_MIN
long long n = va_arg(*ap, int);
char sign = 0;

if (n < 0)
{
    sign = '-';
    n = -n;  /* OK car n est long long, pas int */
}
ull_to_base((unsigned long long)n, 10, 0, num, &num_len);

7. La valeur de retour

ft_printf doit retourner le nombre exact de caractères écrits. En cas d'erreur (format NULL), retourner -1. Le comptage se fait dans buf.total, incrémenté à chaque buf_char.

ft_printf.c
int  ft_printf(const char *format, ...)
{
    va_list ap;
    t_buf   buf;

    if (!format)
        return (-1);
    buf.len = 0;
    buf.total = 0;        /* compteur global */
    va_start(ap, format);
    process_format(format, &ap, &buf);
    va_end(ap);
    buf_flush(&buf);
    return (buf.total);     /* nombre de chars écrits */
}

/* Chaque buf_char incrémente buf.total */
void  buf_char(t_buf *buf, char c)
{
    buf->data[buf->len++] = c;
    buf->total++;          /* <-- comptage */
}

8. Pas de crash — gérer les entrées invalides

Le sujet dit : "In no way can your program quit in an unexpected manner." Votre ft_printf doit gérer gracieusement : ft_printf(NULL) → -1, ft_printf("%s", NULL)(null), ft_printf("%")% sans crash, conversions inconnues affichées littéralement.

9. Respecter la Norme 42

RègleImpact sur ft_printf
Max 5 fonctions par .cSplitter les conversions en conv_int.c, conv_hex.c, conv_text.c
Max 25 lignes par fonctionFactoriser apply_padding, ull_to_base en helpers
Max 80 caractères par ligneSignatures multi-lignes
Pas de for, do, switchUtiliser while et if/else if
Pas de déclaration au milieu d'un blocToutes les variables en haut de la fonction
Fichier authorxlogin\n à la racine

10. Pas de fuites mémoire

Le sujet dit "Your project cannot leaks". Notre implémentation n'utilise aucun malloc — le buffer est sur la pile (char data[4096]), les numéros dans un tableau local (char num[512]). Zéro fuite possible, zéro appel à free.

11. La partie bonus (optionnelle)

Le bonus n'est évalué que si le mandatory est 100% parfait. Le sujet liste : %e %E (scientifique), %f %F (flottant), %g %G, %a %A, %n (compteur), flags $ L ', conversions custom %b %r %k, couleurs.

12. Stratégie de test : comparer avec le vrai printf

Pour chaque cas de test, comparer la sortie et la valeur de retour de ft_printf avec le vrai printf. Capturer stdout via pipe, comparer avec strcmp. Les testeurs de la communauté (Tripouille, paulo-santana, Sfabi28, etc.) utilisent cette technique avec des centaines de cas.

13. Taille du buffer numérique

Un test comme %.200d produit 200 digits. Si votre buffer fait 32 ou 64 bytes, vous aurez un stack overflow. Utilisez char num[512] minimum.

// Récapitulatif des 13 pièges
  1. va_list en pointeur &ap
  2. Caster selon le length modifier (promotion en int)
  3. Flag 0 invalidé par précision (entiers seulement)
  4. %p NULL → (nil) (Linux/glibc)
  5. %s NULL + précision < 6 → vide
  6. INT_MIN : caster en unsigned avant négation
  7. Valeur de retour = nombre exact de chars écrits
  8. Pas de crash : gérer NULL, format invalide
  9. Norme 42 : max 5 fonctions, 25 lignes, 80 chars
  10. Pas de fuites : éviter malloc entièrement
  11. Bonus seulement si mandatory = 100%
  12. Tester en comparant avec le vrai printf
  13. Buffer numérique ≥ 512 bytes
10

Compilation & tests

Le Makefile

Comme tout projet 42, on produit une bibliothèque statique libftprintf.a. Les cibles standard all / clean / fclean / re sont obligatoires.

Makefile// extrait
NAME        = libftprintf.a
CC          = gcc
CFLAGS      = -Wall -Wextra -Werror

SRC_DIR     = src
OBJ_DIR     = obj
INC_DIR     = includes

SRCS        = $(SRC_DIR)/ft_printf.c \
              $(SRC_DIR)/buffer.c \
              $(SRC_DIR)/utils.c \
              $(SRC_DIR)/parse.c \
              $(SRC_DIR)/conv_int.c \
  $(SRC_DIR)/conv_hex.c \
  $(SRC_DIR)/conv_text.c

OBJS        = $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)

all:        $(NAME)

$(NAME):    $(OBJS)
            ar rcs $(NAME) $(OBJS)

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
            $(CC) $(CFLAGS) -I$(INC_DIR) -c $< -o $@

$(OBJ_DIR):
            mkdir -p $(OBJ_DIR)

clean:
            rm -rf $(OBJ_DIR)

fclean:     clean
            rm -f $(NAME)

re:         fclean all

.PHONY:     all clean fclean re

Compiler et lier

shell// build
# 1. Construire la bibliothèque
$ make

# 2. Compiler un programme de test qui l'utilise
$ gcc -Wall -Wextra -Iincludes -o test test.c -L. -lftprintf

# 3. Lancer
$ ./test

Stratégie de test

La seule vérité, c'est le vrai printf. La méthode : rediriger stdout vers un pipe, capturer la sortie des deux fonctions, et strcmp + comparer les valeurs de retour. Le script tests/run_tests.sh fait exactement ça.

tests/run_tests.sh// macro de test
#define TEST(name, fmt, ...) \
    do { \
        char ft_buf[1024], std_buf[1024]; \
        int  ft_ret, std_ret; \
        /* rediriger stdout vers un pipe, capturer ft_printf */ \
        int saved = dup(1); \
        int pipefd[2]; pipe(pipefd); \
        dup2(pipefd[1], 1); \
        ft_ret = ft_printf(fmt, ##__VA_ARGS__); \
        fflush(NULL); \
        close(pipefd[1]); \
        dup2(saved, 1); \
        int ft_len = read(pipefd[0], ft_buf, sizeof ft_buf - 1); \
        ft_buf[ft_len] = '\0'; \
        /* idem avec le vrai printf */ \
        ... \
        if (strcmp(ft_buf, std_buf) == 0 && ft_ret == std_ret) \
            pass_count++; \
        else \
            printf("FAIL: %s\n  ft:  \"%s\" (%d)\n  std: \"%s\" (%d)\n", \
                   name, ft_buf, ft_ret, std_buf, std_ret); \
    } while(0)

Catalogue de cas à tester

Conversions de base
%d 42 · %d -42 · %d 0 · %u 4294967295 · %o 8 · %x 255 · %X 255 · %c 'A' · %s "hi" · %%
Flags
%#o 8 · %#x 255 · %#x 0 · %05d 42 · %-5d| 42 · %+d 42 · %+d -42 · % d 42
Largeur & précision
%5d 42 · %*d 5,42 · %.3d 42 · %.0d 0 · %.0d 42 · %8.3d 42 · %08.3d 42 · %-8.3d| 42
Edge cases
%s NULL · %d INT_MIN · %d INT_MAX · %x -1 · %u -1 · %lld LLONG_MAX · %hhd 255 · format vide · % final solitaire

Erreurs courantes

SymptômeCauseFix
Crash sur %sArgument NULL non géréAfficher (null)
Affichage étrange sur INT_MIN-n sur int signéTravailler en ull
%08.3d affiche 0000042Flag 0 non désactivé par précisionif (prec != -1) flags &= ~FLAG_ZERO
Comportement erratiqueva_list passée par valeurPasser va_list*
Sortie tronquée à 4096Flush final oubliébuf_flush avant return
%c affiche n'importe quoiva_arg(ap, char)(char)va_arg(ap, int)
%#x de 0 affiche 0x0Préfixe ajouté sans testif (n != 0)
// Workflow recommandé

1. Implémentez %s %c %d %x sans flags ni largeur. Faites passer les tests de base. 2. Ajoutez la largeur. 3. Ajoutez la précision (testez %.0d 0 dès maintenant). 4. Ajoutez les flags un par un, dans l'ordre -, 0, +, espace, #. 5. Bonus : length modifiers. Testez après chaque étape, jamais tout à la fin.

// Le test ultime

Si votre ft_printf passe le script de test fourni et le moulinette-style ft_printf de la correction 42, vous avez gagné. La moulinette teste des centaines de cas générés aléatoirement — il n'y a pas de raccourci, il faut que chaque règle du man soit implémentée à la lettre. Bon courage, androïde.