Menu

Les fuites de mémoire en C : comment elles surviennent et comment les trouver

Ce qu'est réellement une fuite, les trois façons dont les programmes C en produisent, la discipline de propriété qui les prévient, et comment trouver le reste avec valgrind et -fsanitize=address - le tout se terminant par un programme qui fuit, corrigé étape par étape.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Une fuite de mémoire n'est pas de la mémoire qui s'est évaporée. C'est de la mémoire qui est toujours la vôtre, toujours réservée, et que vous avez perdu la capacité de rendre - parce que le dernier pointeur vers elle a disparu. Rien ne plante. Le programme continue de tourner, un peu plus lourd à chaque fois, jusqu'à ce que quelque chose finisse par échouer ailleurs.

Cette page couvre la naissance des fuites, la discipline qui prévient la plupart d'entre elles, et les deux outils qui trouvent le reste.

À quoi ressemble une fuite

Chaque itération écrase block avec un pointeur neuf. Le bloc précédent est toujours alloué ; aucune variable ne détient son adresse ; il ne pourra jamais être libéré. Trois itérations perdent douze kilooctets. Un serveur faisant cela une fois par requête les perd pour toujours, au rythme d'arrivée des requêtes.

La correction tient en une ligne - free(block); à la fin du corps - mais reconnaître elle va est la vraie compétence.

Comment les fuites surviennent

1. Le pointeur perdu

Toute affectation à un pointeur détenant encore l'unique référence vers un bloc vivant le fait fuir.

char *name = malloc(32);
name = malloc(64);     /* les 32 premiers octets sont maintenant inaccessibles */

La boucle plus haut est le même bug déguisé en boucle. La réaffectation d'un champ de structure aussi, et le raccourci realloc de calloc et realloc également :

p = realloc(p, n);     /* en cas d'echec : p devient NULL, l'ancien bloc est orphelin */

2. Le retour anticipé

Chaque chemin de sortie d'une fonction doit rendre ce que la fonction a déjà pris. Celui qu'on oublie est toujours un chemin d'erreur.

Le chemin heureux est correct et le chemin d'erreur fuit, c'est pourquoi cela survit aux tests : la branche en échec ne s'exécute presque jamais pendant le développement. La solution est une section de nettoyage unique vers laquelle chaque chemin saute :

C'est le seul usage de goto que les programmeurs C expérimentés recommandent activement. Cela fonctionne parce que chaque pointeur démarre à NULL et que free(NULL) ne fait rien, donc un bloc de sortie unique est correct quel que soit le point atteint par la fonction.

3. Une propriété floue

Les fuites les plus subtiles ne sont pas des erreurs de code du tout - ce sont deux fonctions en désaccord sur à qui revenait la tâche.

char *build_message(void);   /* l'appelant doit-il liberer ceci ? */
void  store(char *text);     /* store prend-il la propriete ? */

Si build_message renvoie de la mémoire allouée et que store la copie, l'appelant doit libérer. Si store garde le pointeur, l'appelant ne doit pas. Rien dans le code ne dit lequel, donc l'une des deux hypothèses est faite deux fois - et vous obtenez soit une fuite, soit une double libération.

Le remède est une convention, énoncée dans un commentaire à côté de chaque fonction qui alloue :

/* Renvoie une chaine fraichement allouee ; l'appelant doit la liberer. */
char *build_message(void);

/* Prend la propriete de 'text' ; il sera libere par store_free(). */
void store(char *text);

Écrivez la règle à la fonction, pas dans un document de conception. C'est l'habitude la plus rentable de la gestion mémoire en C.

La discipline de propriété

Quatre règles couvrent presque tout :

  1. Chaque allocation a exactement un propriétaire - un morceau de code responsable de sa libération.
  2. Appariez chaque fonction qui alloue avec une qui libère. vec_init / vec_free, config_load / config_free. La symétrie rend un appel manquant visible.
  3. Libérez dans la même couche que celle qui a alloué, sauf si le commentaire de la fonction transfère explicitement la propriété.
  4. Mettez un pointeur à NULL après l'avoir libéré, pour qu'un usage accidentel ultérieur plante au point fautif au lieu de corrompre le tas en silence.

Trouver les fuites : valgrind

Sous Linux, valgrind ne demande aucune recompilation, même si les symboles de débogage rendent le rapport lisible :

gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program

Pour la boucle qui fuit en haut de cette page, le rapport se termine par quelque chose comme :

==12345== HEAP SUMMARY:
==12345==     in use at exit: 12,000 bytes in 3 blocks
==12345==   total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345==    by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 12,000 bytes in 3 blocks

Lisez-le en partant du bas. « definitely lost » signifie qu'aucun pointeur vers le bloc n'existait à la sortie - une vraie fuite. La trace d'appels nomme la ligne du malloc qui l'a créé, pas la ligne où il a été perdu, ce qui suffit généralement à trouver le free manquant.

Deux autres catégories apparaissent :

  • indirectly lost - des blocs accessibles uniquement via un bloc lui-même perdu, comme les éléments d'une liste chaînée abandonnée. Corrigez le « definitely lost » et ceux-ci disparaissent.
  • still reachable - alloués à la sortie mais avec un pointeur vivant, typiquement un cache global. Pas une fuite au sens dangereux, mais à libérer quand même pour que le rapport reste vide.

Valgrind attrape aussi les lectures de mémoire non initialisée et les écritures au-delà de la fin d'un bloc, ce qui est souvent la façon dont vous découvrez le bug derrière la fuite.

Trouver les fuites : AddressSanitizer

AddressSanitizer est intégré à GCC et Clang, tourne bien plus vite que valgrind, et fonctionne là où valgrind ne fonctionne pas (y compris sur macOS actuel) :

gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program

Le rapport de fuites s'affiche automatiquement à la sortie :

=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 12000 byte(s) in 3 object(s) allocated from:
    #0 0x7f... in malloc
    #1 0x1086... in main program.c:6

SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).

ASan transforme aussi les utilisations après libération et les débordements de tampon sur le tas en abandons immédiats et clairement étiquetés, plutôt qu'en corruptions mystérieuses plus tard. Compilez vos tests avec lui activé ; compilez les versions de production sans, car il coûte de la mémoire et de la vitesse.

Si la détection de fuites ne se déclenche pas sur votre plateforme, définissez ASAN_OPTIONS=detect_leaks=1 dans l'environnement avant l'exécution.

Corriger un programme qui fuit, étape par étape

Voici un petit programme avec trois fuites distinctes :

Valgrind signale trois enregistrements « definitely lost » avec trois numéros de ligne différents. Corrigés un par un :

La correction 1 est le commentaire de propriété rendu réel : shout alloue, main libère. La correction 2 supprime entièrement l'allocation doublée plutôt que de libérer la première - le code plus simple est aussi le code correct. La correction 3 ajoute le free manquant sur le retour anticipé ; avec plus d'allocations en jeu, l'étiquette cleanup: unique montrée plus haut passe mieux à l'échelle que la répétition des libérations.

Les habitudes qui préviennent les fuites

  • Écrivez le free immédiatement après avoir écrit le malloc, puis remplissez le code entre les deux.
  • Donnez à chaque fonction qui alloue une fonction de libération correspondante.
  • Énoncez la propriété dans un commentaire sur toute fonction qui renvoie ou prend un pointeur qu'elle a alloué.
  • Utilisez un unique bloc de sortie cleanup: dans les fonctions détenant plusieurs allocations.
  • Lancez vos tests sous -fsanitize=address par principe, pas seulement quand quelque chose semble anormal.
  • Traitez « definitely lost: 0 bytes » comme faisant partie d'un test réussi.

Questions fréquentes

Qu'est-ce qu'une fuite de mémoire en C ?

De la mémoire allouée avec malloc que vous ne pouvez plus libérer, parce que plus rien dans le programme ne pointe dessus. Le bloc reste réservé pour toute la vie du processus. Ce n'est pas un plantage - le programme continue de fonctionner, il utilise seulement plus de mémoire à chaque passage jusqu'à en manquer.

Comment trouver les fuites de mémoire en C ?

Exécutez le programme sous valgrind : valgrind --leak-check=full ./program. Il signale chaque bloc encore alloué à la sortie avec la trace d'appels du malloc qui l'a créé. Sous macOS ou là où valgrind n'existe pas, compilez avec -fsanitize=address et le même rapport sort à la fin.

Qu'est-ce qui cause les fuites de mémoire en C ?

Trois motifs couvrent presque tout : écraser le seul pointeur vers un bloc (y compris p = realloc(p, n) en cas d'échec), retourner tôt d'une fonction qui a déjà alloué, et une propriété floue - deux fonctions supposant chacune que l'autre libère, donc ni l'une ni l'autre ne le fait.

Les fuites de mémoire comptent-elles si le programme se termine de toute façon ?

Pour un programme qui s'exécute une fois puis sort, le système d'exploitation récupère tout, donc l'impact pratique est nul. Cela compte pour tout ce qui tourne longtemps - un serveur, une boucle de jeu, un démon - où une fuite par requête croît sans limite. Libérez de façon cohérente quand même : un rapport de fuite est du bruit qui cache celles qui comptent.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER