Aller au contenu principal

Programmation C · L2 · Section 4/12

Gestion mémoire & pointeurs

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#Gestion de la mémoire et pointeurs

Ce chapitre installe une discipline : en C, allouer ne suffit pas, il faut savoir qui possède le bloc, jusqu’à quand, et qui a le droit de le libérer. Nous couvrons les quatre primitives du tas, le suivi d’un bug réel de redistribution de pointeurs, la lecture des signatures de pointeurs, puis l’outillage de détection (sanitizers, Valgrind). Deux ateliers consolident l’ensemble : une arène à allocation par avance, le précurseur direct de l’allocateur de Vaλisp.

#1. Le contrat du tas

cc

1void *malloc(size_t n);                        /* bloc NON initialisé, taille non spécifiée */2void *calloc(size_t n, size_t taille);         /* n éléments, TOUT à zéro ; vérifie le produit */3void *realloc(void *p, size_t n);              /* agrandit/déplace ; peut renvoyer un NOUVEAU pointeur */4void free(void *p);                            /* p peut être NULL ; libère exactement une fois */

Règles d’or, chacune tirée d’un bug réel :

  • Toute allocation peut échouer : malloc renvoie NULL. Vérifiez systématiquement, au moins dans les modules de bas niveau.
  • free(NULL) est défini et sans effet ; en revanche libérer deux fois, ou libérer un pointeur non issu de la famille *alloc, est un comportement indéfini.
  • Après free(p), la valeur de p est invalide : même la lire pour la comparer est interdit. Mettez p = NULL après libération si le nom survit.
  • realloc(ptr, 0) a un statut devenu obsolète en C23 : ne l’utilisez jamais ; pour vider, free puis remettez à NULL.

L’accident le plus formateur du chapitre est celui-ci :

cc

1int *t = malloc(10 * sizeof *t);2int *copie = t;                /* partage le MÊME bloc, pas une copie */3t = realloc(t, 20 * sizeof *t); /* peut déplacer le bloc : copie pointe alors dans le vide */4copie[0] = 1;                  /* use-after-free : comportement indéfini */

Règle qui en découle : un pointeur n’est pas un duplicata de la ressource. Partager un pointeur donne un accès, pas un bail ; après realloc seul le pointeur retourné est valide.

#2. Documenter la propriété par le code

La convention « celui qui alloue libère » doit se lire dans l’interface, pas dans un commentaire perdu. Le module opaque est le motif standard :

cc

1/* tampon.h — l'appelant ne voit jamais la structure */2typedef struct Tampon Tampon;3Tampon *tampon_init(size_t capacite);      /* renvoie NULL si échec ; l'appelant devient propriétaire */4void tampon_ajoute(Tampon *t, int v);      /* emprunte t ; ne le libère jamais */5void tampon_destroy(Tampon *t);            /* libère tout ; accepte NULL */

Trois niveaux de lecture des paramètres : propriétaire transféré (retour de tampon_init), emprunt (tampon_ajoute : la fonction utilise sans libérer), propriété reprise (tampon_destroy). En C ces niveaux ne sont pas typés : le nommage par verbe (init, destroy) et le fichier d’en-tête portent le contrat.

Le fichier .c garde la structure privée :

cc

1/* tampon.c */2#include <stdlib.h>3#include "tampon.h"4 5struct Tampon { size_t n, cap; int *data; };6 7Tampon *tampon_init(size_t capacite) {8    Tampon *t = malloc(sizeof *t);9    if (!t) return NULL;10    t->data = malloc(capacite * sizeof *t->data);11    if (!t->data) { free(t); return NULL; }12    t->n = 0; t->cap = capacite;13    return t;14}

#3. Lire et écrire les signatures

C déclare les types « de l’identifiant vers l’extérieur ». Le réflexe : trouver l’identifiant, puis lire autour.

  • int (*cmp)(const void *, const void *) : cmp est un pointeur vers une fonction qui prend deux const void * et renvoie un int. C’est le contrat de qsort :
cc

1int cmp_int(const void *a, const void *b) {   /* ordre croissant */2    int x = *(const int *)a, y = *(const int *)b;3    return (x > y) - (x < y);                 /* pas de x - y : débordement signé possible */4}5 6qsort(t, n, sizeof t[0], cmp_int);
  • char **argv : argv est un pointeur vers des pointeurs vers char, c’est-à-dire le tableau de chaînes de main. argv[i] est un char *.
  • void * : un accordéon d’adresse. Convertir vers un type concret déréférencé est de votre responsabilité ; l’API qui le reçoit documente le type attendu (comme qsort avec la taille d’élément).

#4. Détecter plutôt que deviner

  • -fsanitize=address (avec -g) : arrêt à la première erreur, avec pile d’appels et numéro de ligne. Rapide, précis, idéal pendant le développement.
  • valgrind --leak-check=full --show-leak-kinds=all ./prog : compte toutes les allocations non libérées à la sortie et retrace la ligne d’allocation initiale. Plus lent, mais complet sur un test de bout en bout.

Un projet sain se juge au zéro défaut sous ASan et zéro octet perdu sous Valgrind, avec un test qui exerce tous les chemins de libération.

#Atelier 1 : une arène à allocation par avance

Implémentez arena.c : un bloc unique alloué au départ, dans lequel on avance d’un cran à chaque demande :

cc

1typedef struct {2    size_t capacite;   /* octets totaux */3    size_t utilise;    /* octets déjà servis */4    unsigned char *base;5} Arena;6 7Arena *arena_init(size_t capacite);8void  *arena_alloc(Arena *a, size_t n);   /* NULL si plein ; aligne sur _Alignof(max_align_t) */9void   arena_destroy(Arena *a);           /* libère TOUT le bloc d'un coup */

Contraintes : aucun free individuel, la durée de vie de tout ce qui sort de arena_alloc est celle de l’arène entière. C’est exactement l’allocateur de Vaλisp 1.

Jalon observable :

shsh

1cc -std=c17 -Wall -Wextra -Wpedantic -g -fsanitize=address,undefined arena.c test_arena.c -o test_arena2./test_arena3# attendu : 1024 octets servis, 3 blocs, arena liberee4valgrind -q --leak-check=full ./test_arena5# attendu : aucune ligne (zéro fuite)
Points de vigilance

L’alignement : arrondissez n et l’offset courant au multiple de _Alignof(max_align_t) avant de servir le bloc, sinon un double stocké non aligné plantera sur ARM. Le débordement arithmétique : vérifiez utilise + n <= capacite avant d’écrire. La propriété : arena_alloc emprunte a, seul arena_destroy touche à free.

#Atelier 2 : un vecteur générique par void *

Un Vector qui stocke des octets bruts et connaît la taille d’un élément :

cc

1typedef struct {2    size_t n, cap, elem_size;3    unsigned char *data;4} Vector;5 6int  vector_init(Vector *v, size_t elem_size);      /* 0 si succès, -1 sinon */7int  vector_push(Vector *v, const void *elem);      /* copie elem_size octets */8void vector_get(const Vector *v, size_t i, void *out); /* copie vers out */9void vector_destroy(Vector *v);

elem_size capture la taille au moment de l’init : le même code stocke des int, des double, des structures. Jalon observable :

shsh

1./test_vector2# attendu : 5 entiers stockes, dernier = 423# attendu : 3 points (struct {double x,y;}) stockes, y[2] = 6.0
Le realloc piégé, corrigé

Dans vector_push, écrivez unsigned char *nd = realloc(v->data, nouvelle_cap); if (!nd) return -1; v->data = nd;. Jamais realloc(v->data, ...) assigné directement : si l’agrégation échoue, l’ancien pointeur serait écrasé et le bloc perdu. Doubler la capacité (puis rester à au moins elem_size) donne le coût amorti en O(1).

Que se passe-t-il si l'on garde un pointeur sur un bloc passé avec succès à realloc ?
Que se passe-t-il si l'on garde un pointeur sur un bloc passé avec succès à realloc ?