Programmation C · L2 · Section 4/12
Gestion mémoire & pointeurs
Progression
#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
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 :
mallocrenvoieNULL. 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 depest invalide : même la lire pour la comparer est interdit. Mettezp = NULLaprès libération si le nom survit. realloc(ptr, 0)a un statut devenu obsolète en C23 : ne l’utilisez jamais ; pour vider,freepuis remettez àNULL.
L’accident le plus formateur du chapitre est celui-ci :
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 :
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 :
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 *):cmpest un pointeur vers une fonction qui prend deuxconst void *et renvoie unint. C’est le contrat deqsort:
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:argvest un pointeur vers des pointeurs verschar, c’est-à-dire le tableau de chaînes demain.argv[i]est unchar *.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 (commeqsortavec 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 :
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 :
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 :
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 :
1./test_vector2# attendu : 5 entiers stockes, dernier = 423# attendu : 3 points (struct {double x,y;}) stockes, y[2] = 6.0Le 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).