← HUB ✦ Holberton School — Memory Debugging ✦

VALGRIND

Zéro leak. Zéro erreur. Zéro compromis.

C'EST QUOI VALGRIND ?
🔬 DÉFINITION — COMPRENDRE L'OUTIL

Valgrind est un framework d'analyse dynamique de programmes. Son outil principal, Memcheck, surveille chaque accès mémoire de ton programme en temps réel et détecte les fuites mémoire, les accès invalides, les utilisations de mémoire non initialisée, etc.

🕹️ ANALOGIE GAMING

Valgrind c'est un anti-triche invisible dans ton jeu. Il enveloppe ton programme entier et surveille chaque octet de mémoire que tu touches. Si tu accèdes à une zone interdite, si tu oublies de libérer une zone, si tu lis une valeur que tu n'as jamais écrite — il te le signale immédiatement avec l'adresse exacte et la pile d'appels.

🧠
MEMCHECK

Outil principal. Détecte leaks et accès invalides

📊
CALLGRIND

Profiling de performance et comptage d'instructions

🏔️
MASSIF

Profiling du heap — suivre l'usage mémoire dans le temps

À Holberton, on utilise presque exclusivement Memcheck (l'outil par défaut). Quand on dit "lancer Valgrind", on parle de Memcheck.
INSTALLATION ET PRÉREQUIS
⚙️ INSTALLER ET PRÉPARER
# Installer Valgrind
sudo apt-get install valgrind

# Vérifier la version
valgrind --version
valgrind-3.18.1

# ⚠️ OBLIGATOIRE — Compiler avec -g pour avoir les numéros de lignes
gcc -Wall -Werror -Wextra -g main.c -o prog

# Sans -g, Valgrind signale les erreurs mais sans numéros de lignes :
==123== at 0x401130: main (in /home/user/prog)   ← adresse seulement
# Avec -g :
==123== at 0x401130: main (main.c:12)            ← ligne précise ✅
Valgrind ralentit l'exécution de 10 à 50x car il surveille chaque accès mémoire. C'est normal — c'est uniquement pour le debug, jamais en production.
LES COMMANDES — DU BASIQUE AU COMPLET
🚀 LANCER VALGRIND
# Commande de base (Memcheck implicite)
valgrind ./prog

# ✅ Commande RECOMMANDÉE Holberton — la plus complète
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose ./prog

# Avec des arguments (argv)
valgrind --leak-check=full ./prog arg1 arg2

# Sauvegarder le rapport dans un fichier
valgrind --leak-check=full --log-file=rapport.txt ./prog

# Spécifier l'outil explicitement
valgrind --tool=memcheck --leak-check=full ./prog
La commande exacte attendue à Holberton : valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose ./prog — à connaître par cœur.
🎛️ LES OPTIONS — TOUTES EXPLIQUÉES
OptionCe qu'elle fait
--leak-check=no Pas de rapport de leaks (rapide, juste les erreurs d'accès)
--leak-check=summary Résumé des leaks seulement (défaut)
--leak-check=full ✅ Rapport complet avec stack trace pour chaque leak
--show-leak-kinds=all ✅ Affiche TOUS les types de leaks (definitely, indirectly, possibly, reachable)
--show-leak-kinds=definite Affiche seulement les leaks certains
--track-origins=yes ✅ Trace l'origine des valeurs non initialisées (plus lent mais précieux)
--verbose ✅ Mode verbeux — plus d'informations dans le rapport
--error-exitcode=N Retourne le code N si des erreurs sont détectées (utile en CI/CD)
--log-file=fichier.txt Sauvegarder le rapport dans un fichier
--suppressions=fichier Ignorer certaines erreurs connues (bibliothèques tierces)
--tool=callgrind Utiliser un autre outil que Memcheck
--num-callers=N Nombre de frames à afficher dans la stack trace (défaut: 12)
LIRE LA SORTIE VALGRIND — DÉCRYPTER CHAQUE LIGNE
📄 ANATOMIE D'UNE SORTIE VALGRIND
==12345== ← PID du processus (change à chaque exécution)
==12345== Memcheck, a memory error detector
==12345== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==12345== Using Valgrind-3.18.1 and LibVEX; ...
==12345==

# ── SI TON PROGRAMME A DES ARGUMENTS ──────────────────
==12345== Command: ./prog arg1 arg2

# ── UNE ERREUR D'ACCÈS INVALIDE ────────────────────────
==12345== Invalid read of size 4               ← type d'erreur + taille
==12345==    at 0x401155: main (main.c:12)      ← où (fichier:ligne)
==12345==  Address 0x5204040 is 0 bytes after a block of size 16 alloc'd
==12345==    at 0x4848899: malloc (in .../vgpreload_memcheck...)
==12345==    by 0x401140: main (main.c:8)        ← où le malloc a été fait

# ── LE HEAP SUMMARY ────────────────────────────────────
==12345== HEAP SUMMARY:
==12345==     in use at exit: 40 bytes in 1 blocks   ← mémoire non libérée
==12345==   total heap usage: 3 allocs, 2 frees, 72 bytes allocated
                              ↑ 3 malloc, 2 free → 1 oublié !

# ── LE LEAK SUMMARY ────────────────────────────────────
==12345== LEAK SUMMARY:
==12345==    definitely lost: 40 bytes in 1 blocks   ← leak certain
==12345==    indirectly lost: 0 bytes in 0 blocks
==12345==      possibly lost: 0 bytes in 0 blocks
==12345==    still reachable: 0 bytes in 0 blocks
==12345==         suppressed: 0 bytes in 0 blocks

# ── L'ERROR SUMMARY ────────────────────────────────────
==12345== ERROR SUMMARY: 2 errors from 2 contexts   ← résumé final

# ── RÉSULTAT PARFAIT ───────────────────────────────────
==12345== All heap blocks were freed -- no leaks are possible
==12345== ERROR SUMMARY: 0 errors from 0 contexts   ← ✅ parfait !
Le PID (==12345==) préfixe chaque ligne. C'est le numéro du processus — il change à chaque lancement. C'est normal, c'est juste Valgrind qui identifie son processus de monitoring.
LES 4 TYPES DE LEAKS — LES COMPRENDRE TOUS
🔴 DEFINITELY LOST VS REACHABLE VS POSSIBLE
💀 DEFINITELY LOST
La mémoire a été allouée mais aucun pointeur ne pointe plus vers elle. Elle est définitivement perdue — impossible à libérer même si tu voulais. C'est le leak le plus grave, le seul inacceptable à Holberton.
void func() {
    int *p = malloc(40);
    /* p sort de scope → adresse perdue → DEFINITELY LOST */
}
definitely lost: 40 bytes in 1 blocks
🟠 INDIRECTLY LOST
Mémoire perdue à cause d'un definitely lost parent. Le bloc est accessible seulement depuis un bloc déjà perdu. Si tu fixes le definitely lost, l'indirectly lost disparaît aussi.
typedef struct { int *data; } Node;
Node *n = malloc(sizeof(Node));
n->data = malloc(10);
/* Si on perd n → n->data est INDIRECTLY LOST */
indirectly lost: 10 bytes in 1 blocks
🟡 POSSIBLY LOST
Valgrind a trouvé un pointeur vers l'intérieur d'un bloc malloc (pas vers son début). Peut être un vrai leak ou un pointeur arithmétique intentionnel. Rare en C simple.
int *arr = malloc(5 * sizeof(int));
arr++;  /* ptr ne pointe plus sur le début ! */
/* free(arr) serait faux → POSSIBLY LOST */
possibly lost: 16 bytes in 1 blocks
🟣 STILL REACHABLE
Mémoire encore accessible via un pointeur au moment de la fin du programme (variable globale, pointeur encore en scope). Pas un vrai leak — Holberton l'accepte généralement. Mais à éviter par bonne pratique.
int *global = malloc(20); /* variable globale */
int main() { return (0); }  /* oubli de free(global) */
still reachable: 20 bytes in 1 blocks
✅ RÉSULTAT PARFAIT (0 bytes)
Tout a été libéré proprement. C'est le seul résultat acceptable pour un projet Holberton.
All heap blocks were freed -- no leaks are possible
ERROR SUMMARY: 0 errors from 0 contexts
Question classique de quiz : Quelle différence entre "definitely lost" et "still reachable" ? → Definitely lost = plus aucun pointeur vers le bloc. Still reachable = un pointeur existe encore mais free() n'a pas été appelé.
LES ERREURS MÉMOIRE DÉTECTÉES PAR VALGRIND
💥 INVALID READ / INVALID WRITE

Accès à une adresse mémoire invalide — hors des limites d'un bloc, après un free, ou via un pointeur nul.

/* Cas 1 : lecture hors limites (buffer overflow) */
int *arr = malloc(5 * sizeof(int));
int x = arr[5];  /* index 5 hors des 5 éléments (0 à 4) ! */
==123== Invalid read of size 4
==123==    at 0x401155: main (main.c:6)
==123==  Address 0x5204054 is 0 bytes after a block of size 20 alloc'd

/* Cas 2 : écriture après free (use-after-free) */
free(arr);
arr[0] = 42;  /* écriture sur mémoire libérée ! */
==123== Invalid write of size 4
==123==    at 0x401160: main (main.c:9)
==123==  Address 0x5204040 is 0 bytes inside a block of size 20 free'd
Lire le "size N" — c'est la taille de l'accès en octets : size 1 = char, size 4 = int, size 8 = long/pointeur. Ça t'aide à deviner le type impliqué.
❓ USE OF UNINITIALISED VALUE

Lecture d'une variable dont la valeur n'a jamais été assignée. Valgrind détecte aussi quand une décision (if/while) dépend d'une valeur non initialisée.

int x;              /* déclarée mais pas initialisée */
if (x > 0)          /* lecture de garbage ! */
    printf("positif\n");

==123== Conditional jump or move depends on uninitialised value(s)
==123==    at 0x401140: main (main.c:4)

/* Avec --track-origins=yes, Valgrind te dit où x a été créé */
==123==  Uninitialised value was created by a stack allocation
==123==    at 0x401130: main (main.c:2)  ← ligne de déclaration
--track-origins=yes est crucial pour ce type d'erreur — sans lui, Valgrind signale où la valeur est utilisée, mais pas où elle a été créée.
♻️ INVALID FREE / DOUBLE FREE
/* Double free — libérer deux fois le même bloc */
int *p = malloc(40);
free(p);
free(p);   /* ❌ */

==123== Invalid free() / delete / delete[] / realloc()
==123==    at 0x484B27F: free (vg_replace_malloc.c:872)
==123==    by 0x401155: main (main.c:5)
==123==  Address 0x5204040 is 0 bytes inside a block of size 40 free'd
==123==    at 0x484B27F: free (...)
==123==    by 0x401150: main (main.c:4)  ← premier free ici

/* Free sur une variable non-malloc (stack) */
int x = 5;
free(&x);   /* ❌ x est sur la stack ! */

==123== Invalid free() / delete / delete[] / realloc()
==123==  Address 0x... is on thread 1's stack
🔗 INVALID READ/WRITE SIZE — MISMATCHED FREE
/* Mauvais appel de free (new/delete mismatch — plus courant en C++) */

/* En C — oubli du \0 dans un malloc de string */
char *s = malloc(5);   /* 5 chars mais "hello" fait 6 avec \0 */
strcpy(s, "hello");    /* écrit hors limites ! */

==123== Invalid write of size 1
==123==    at 0x...: strcpy (in .../mc_replace_strmem.c)
==123==    by 0x401150: main (main.c:4)
==123==  Address 0x5204045 is 0 bytes after a block of size 5 alloc'd
Le piège classique à Holberton : malloc(strlen(str)) au lieu de malloc(strlen(str) + 1). Valgrind le détecte avec "Invalid write of size 1".
LIRE LE HEAP SUMMARY — COMPTER LES ALLOCS/FREES
🧮 COMPRENDRE LE HEAP SUMMARY
HEAP SUMMARY:
    in use at exit: 72 bytes in 3 blocks     ← mémoire non libérée en fin de prog
  total heap usage: 5 allocs, 2 frees, 1,024 bytes allocated
                    ↑ 5 malloc    ↑ 2 free → 3 malloc non libérés → 3 leaks !
❌ MAUVAIS — leaks présents
HEAP SUMMARY:
  in use at exit: 40 bytes in 1 blocks
  total heap usage: 3 allocs, 2 frees
LEAK SUMMARY:
  definitely lost: 40 bytes
✅ PARFAIT — 0 leak
HEAP SUMMARY:
  in use at exit: 0 bytes in 0 blocks
  total heap usage: 3 allocs, 3 frees
All heap blocks were freed --
no leaks are possible
Règle mathématique : nombre de free() doit égaler le nombre de malloc(). Si "5 allocs, 3 frees" → 2 leaks garantis. Le nombre dans "in use at exit" doit être 0.
CALLOC ET REALLOC — AUSSI TRACKÉS PAR VALGRIND
🔄 VALGRIND SURVEILLE TOUTES LES ALLOCATIONS

Valgrind trace toutes les fonctions d'allocation — pas seulement malloc. Chaque appel compte dans le HEAP SUMMARY.

FonctionComptée commeNécessite
malloc(size)1 alloc1 free()
calloc(n, size)1 alloc1 free()
realloc(ptr, size)1 alloc + 1 free implicite1 free() final
strdup(str)1 alloc (malloc interne)1 free()
/* calloc — alloue ET initialise à 0 */
int *arr = calloc(5, sizeof(int));
/* Valgrind : 1 alloc de 20 bytes */
free(arr);   /* ✅ 1 free → 0 leak */

/* realloc — le piège classique */
int *p = malloc(10 * sizeof(int));
p = realloc(p, 20 * sizeof(int));   /* ❌ SI realloc retourne NULL */
/* → p vaut NULL, l'ancien bloc est perdu → DEFINITELY LOST */

/* Fix — toujours utiliser un tmp avec realloc */
int *tmp = realloc(p, 20 * sizeof(int));
if (tmp == NULL) { free(p); return (NULL); }  /* ✅ */
p = tmp;
/* Heap summary : "2 allocs, 2 frees" ← realloc = 1 free + 1 alloc */
Question piège : Si tu fais 1 malloc + 1 realloc + 1 free, combien d'allocs/frees dans le HEAP SUMMARY ? → "2 allocs, 2 frees" — realloc libère l'ancien bloc et en alloue un nouveau.
CAS PRATIQUES — BUGS ET LEURS SORTIES VALGRIND
🐛 CAS 1 — OUBLI DE FREE SUR UNE LINKED LIST
💀 DEFINITELY LOST
/* Code bugué — free_list manquant */
node_t *head = NULL;
add_node(&head, 1);
add_node(&head, 2);
add_node(&head, 3);
return (0);   /* ❌ oubli de free_list(head) */

/* Sortie Valgrind */
HEAP SUMMARY:
    in use at exit: 48 bytes in 3 blocks
  total heap usage: 3 allocs, 0 frees, 48 bytes allocated

LEAK SUMMARY:
   definitely lost: 16 bytes in 1 blocks   ← le head (perdu en 1er)
   indirectly lost: 32 bytes in 2 blocks   ← les 2 nœuds suivants

/* Fix : */
free_list(head);
return (0);   /* ✅ */
🐛 CAS 2 — MALLOC SANS +1 POUR \0
💥 INVALID WRITE
/* Code bugué */
char *s = malloc(strlen("hello"));   /* 5 bytes, mais besoin de 6 */
strcpy(s, "hello");                   /* écrit le \0 hors limites */

==123== Invalid write of size 1
==123==    at 0x...: strcpy (mc_replace_strmem.c)
==123==    by 0x401150: main (main.c:5)
==123==  Address is 0 bytes after a block of size 5 alloc'd

/* Fix : */
char *s = malloc(strlen("hello") + 1);  /* ✅ +1 pour \0 */
🐛 CAS 3 — VARIABLE NON INITIALISÉE
❓ UNINITIALISED VALUE
/* Code bugué */
int result;                  /* non initialisée */
if (argc > 1)
    result = atoi(argv[1]);
printf("%d\n", result);      /* si argc == 1, result = garbage ! */

==123== Use of uninitialised value of size 4
==123==    at 0x...: printf (...)
==123==    by 0x401160: main (main.c:6)
==123==  Uninitialised value was created by a stack allocation
==123==    at 0x401130: main (main.c:2)    ← ligne de déclaration

/* Fix : */
int result = 0;              /* ✅ toujours initialiser */
🐛 CAS 4 — LIBÉRATION PARTIELLE D'UNE STRUCT
💀 DEFINITELY LOST
typedef struct {
    char *name;   /* mallocé séparément */
    int   age;
} user_t;

user_t *u = malloc(sizeof(user_t));
u->name = malloc(20);
strcpy(u->name, "Alice");

free(u);   /* ❌ oubli de free(u->name) avant ! */

==123== definitely lost: 20 bytes in 1 blocks   ← u->name perdu
==123==    at 0x401170: main (main.c:10)

/* Fix — toujours libérer dans l'ordre inverse */
free(u->name);   /* ✅ 1. libérer les champs d'abord */
free(u);         /* ✅ 2. libérer la struct ensuite */
🐛 CAS 5 — REALLOC SANS TMP (le leak invisible)
💀 DEFINITELY LOST
/* Code bugué — realloc direct sans variable temporaire */
int *arr = malloc(5 * sizeof(int));
arr = realloc(arr, 10 * sizeof(int));  /* ❌ si realloc retourne NULL */
/* arr vaut NULL, l'ancien bloc est inaccessible → DEFINITELY LOST */
/* ET on ne peut même plus faire free(arr) car arr == NULL */

HEAP SUMMARY:
    in use at exit: 20 bytes in 1 blocks   ← l'ancien bloc perdu
LEAK SUMMARY:
   definitely lost: 20 bytes in 1 blocks

/* Fix — TOUJOURS utiliser un tmp */
int *arr = malloc(5 * sizeof(int));
int *tmp = realloc(arr, 10 * sizeof(int));
if (tmp == NULL)
{
    free(arr);       /* ✅ arr est encore valide si realloc échoue */
    return (NULL);
}
arr = tmp;           /* ✅ seulement si succès */
free(arr);
Ce bug est particulièrement vicieux car il n'apparaît que si le système manque de mémoire — difficile à reproduire. Prends l'habitude systématique d'utiliser un tmp avec realloc.
BONNES PRATIQUES — PASSER VALGRIND À COUP SÛR
🏆 LES RÈGLES D'OR POUR UN VALGRIND PROPRE
1 malloc = 1 free — sans exception

Chaque bloc alloué doit être libéré exactement une fois. Ni deux fois (double free), ni zéro fois (leak).

Toujours +1 pour les strings

malloc(strlen(str) + 1) — sans le +1, le \0 écrit hors limites et Valgrind le détecte.

Libérer dans l'ordre inverse d'allocation

Pour une struct avec des champs mallocés : libérer les champs d'abord, la struct en dernier. Pour une liste : libérer nœud par nœud, jamais juste le head.

Gérer les chemins d'erreur

Si malloc échoue au milieu d'une fonction, libérer ce qui a déjà été alloué avant de retourner NULL.

node_t *n = malloc(sizeof(node_t));
if (!n) return (NULL);
n->name = malloc(20);
if (!n->name) {
    free(n);         /* ✅ libérer ce qui est déjà alloué */
    return (NULL);
}
Initialiser toutes les variables

Déclarer avec une valeur initiale : int x = 0;, char *p = NULL;. Valgrind détecte les lectures de variables non initialisées.

Ne jamais accéder hors des limites

Un tableau de N éléments : indices valides de 0 à N-1. arr[N] est invalide — Valgrind le voit.

QUIZ — LES QUESTIONS TYPIQUES DU JEUDI
🎯 12 QUESTIONS / RÉPONSES POUR ÊTRE INCOLLABLE
Q1 — Que détecte Valgrind ?

Les fuites mémoire, les accès invalides (invalid read/write), l'utilisation de valeurs non initialisées, les double free et les free invalides.

Q2 — Pourquoi compiler avec -g ?

Sans -g, Valgrind affiche des adresses hexadécimales sans noms de fichiers ni numéros de lignes, rendant le rapport inutilisable pour déboguer.

Q3 — Quelle est la différence entre "definitely lost" et "still reachable" ?

"Definitely lost" : aucun pointeur ne pointe plus vers le bloc — irrécupérable. "Still reachable" : un pointeur existe encore mais free() n'a pas été appelé.

Q4 — Que signifie "3 allocs, 2 frees" dans le HEAP SUMMARY ?

Trois appels à malloc/calloc/realloc ont été faits, mais seulement deux free() ont été appelés. Il reste 1 bloc non libéré. Selon si un pointeur pointe encore vers lui, ce sera "definitely lost" ou "still reachable".

Q5 — Quel résultat est attendu par Holberton ?

All heap blocks were freed -- no leaks are possible ET ERROR SUMMARY: 0 errors from 0 contexts. Attention : certains "still reachable" liés aux buffers internes de stdio (printf, etc.) peuvent apparaître et sont tolérés par Holberton. Le "definitely lost" lui, est toujours inacceptable.

Q6 — À quoi sert --track-origins=yes ?

Il trace l'origine des valeurs non initialisées — Valgrind indique non seulement où la valeur est utilisée, mais aussi où la variable a été créée (déclarée sans initialisation). Sans cette option, tu sais qu'une valeur est non initialisée, mais pas où elle a été déclarée.

Q7 — Qu'est-ce qu'un "Invalid read of size 4" ?

Lecture de 4 octets (un int) à une adresse invalide : hors des limites d'un malloc, après un free (use-after-free), ou via un pointeur NULL/non initialisé. Le "size N" indique la taille de l'accès : 1=char, 4=int, 8=pointeur/long.

Q8 — Valgrind peut-il trouver des bugs dans du code sans malloc ?

Oui — il détecte aussi l'utilisation de variables locales non initialisées (stack), les lectures hors limites de tableaux locaux, et les accès invalides en général. La détection de mémoire ne se limite pas au heap.

Q9 — Qu'est-ce qu'un "indirectly lost" ?

Mémoire accessible seulement depuis un bloc "definitely lost". Par exemple, les nœuds d'une linked list dont le head est perdu. Corriger le "definitely lost" parent élimine automatiquement l'"indirectly lost".

Q10 — Quel est l'outil par défaut de Valgrind ?

Memcheck — c'est l'outil implicite quand tu lances valgrind ./prog. Les autres outils doivent être spécifiés avec --tool=. Les principaux : Callgrind (performance), Massif (usage heap), Helgrind (data races en multi-thread).

Q11 — À quoi sert Helgrind ?

Helgrind détecte les data races dans les programmes multi-threadés — situations où deux threads accèdent à la même donnée sans synchronisation. On l'utilise avec valgrind --tool=helgrind ./prog. Différent de Memcheck qui lui se concentre sur la mémoire.

Q12 — Que fait --error-exitcode=1 ?

Valgrind retourne le code de sortie 1 si des erreurs sont détectées, au lieu du code de sortie du programme. Utilisé dans les scripts de vérification automatique (CI/CD) à Holberton pour qu'un projet "fail" automatiquement si Valgrind trouve des erreurs.

valgrind --leak-check=full --error-exitcode=1 ./prog
echo $?   # → 1 si erreurs, 0 si clean
RÉSUMÉ — TOUT EN UN COUP D'ŒIL
🗺️ LA CARTE MENTALE COMPLÈTE
╔══════════════════════════════════════════════════════════════════╗
║  COMMANDE HOLBERTON STANDARD                                     ║
╚══════════════════════════════════════════════════════════════════╝
gcc -g -Wall -Werror -Wextra main.c -o prog
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose ./prog

╔══════════════════════════════════════════════════════════════════╗
║  LES 4 TYPES DE LEAKS (du plus grave au moins grave)             ║
╚══════════════════════════════════════════════════════════════════╝
definitely lost  → plus aucun pointeur vers le bloc   ← inacceptable !
indirectly lost  → accessible via un "definitely lost" ← corriger le parent
possibly lost    → pointeur vers l'intérieur d'un bloc ← rare en C simple
still reachable  → pointeur existe mais pas de free()  ← acceptable (mais éviter)

╔══════════════════════════════════════════════════════════════════╗
║  LES TYPES D'ERREURS MÉMOIRE                                     ║
╚══════════════════════════════════════════════════════════════════╝
Invalid read/write of size N    → accès hors limites ou après free
Use of uninitialised value      → variable lue sans avoir été initialisée
Invalid free()                  → double free ou free d'une variable stack
Conditional jump on uninit...   → if/while basé sur valeur non initialisée

╔══════════════════════════════════════════════════════════════════╗
║  RÉSULTAT PARFAIT ATTENDU PAR HOLBERTON                          ║
╚══════════════════════════════════════════════════════════════════╝
in use at exit: 0 bytes in 0 blocks
All heap blocks were freed -- no leaks are possible
ERROR SUMMARY: 0 errors from 0 contexts

╔══════════════════════════════════════════════════════════════════╗
║  RÈGLES D'OR                                                     ║
╚══════════════════════════════════════════════════════════════════╝
# ✅ 1 malloc = 1 free — sans exception
# ✅ malloc(strlen(s) + 1) pour les strings — toujours +1
# ✅ Libérer champs AVANT la struct
# ✅ realloc → toujours utiliser un tmp, jamais p = realloc(p, ...)
# ✅ Gérer les chemins d'erreur (free ce qui est déjà alloué)
# ✅ Initialiser toutes les variables à la déclaration
# ✅ Indices de 0 à N-1 — jamais arr[N]
# ✅ calloc/strdup/realloc comptent aussi comme des allocs
# ❌ Double free → crash + invalid free Valgrind
# ❌ Free sur variable locale (stack) → invalid free
# ❌ Lire/écrire après free → invalid read/write
# ❌ "definitely lost" JAMAIS acceptable — "still reachable" parfois toléré