malloc, calloc, realloc, free : différences en C
malloc, calloc, realloc, free : à quoi sert chacun, la différence entre malloc et calloc, et les pièges du redimensionnement et de la libération en C.
Les quatre fonctions tiennent dans une ligne de #include <stdlib.h>, et pourtant elles concentrent la plupart des bugs mémoire d'un débutant en C. Savoir laquelle appeler, et surtout comment ne pas perdre un bloc en route, change tout.
En bref
malloc(taille) réserve un bloc sur le tas sans l'initialiser. calloc(nombre, taille) fait la même chose mais met tout à zéro et vérifie que nombre * taille ne déborde pas. realloc(p, taille) redimensionne un bloc existant. free(p) le rend. Chaque bloc alloué a exactement un free. Teste toujours le retour contre NULL, et ne fais jamais p = realloc(p, n) directement, car un échec te ferait perdre l'ancien bloc.
Lequel utiliser, en un tableau
Les quatre vivent dans <stdlib.h> et travaillent toutes sur le tas, la zone où un bloc survit à la fonction qui l'a créé (la différence avec la pile est détaillée dans pile et tas en C).
| Fonction | Rôle | Initialise ? | Vérifie le débordement ? |
|---|---|---|---|
malloc(size) | réserve size octets | non, contenu indéterminé | non |
calloc(nmemb, size) | réserve nmemb * size octets | oui, tout à zéro | oui |
realloc(ptr, size) | redimensionne un bloc | garde l'ancien contenu | non |
free(ptr) | rend un bloc | sans objet | sans objet |
La fiche de chaque fonction donne sa signature exacte et sa valeur de retour : malloc(3), calloc(3), realloc(3), free(3).
malloc ou calloc : quelle différence ?
La première différence se voit à l'œil nu. malloc te rend un bloc tel quel, avec ce qui traînait en mémoire ; calloc te le rend à zéro. Ce petit programme affiche les 24 premiers octets de chaque, juste après avoir libéré une chaîne pour salir le tas :
#include <stdio.h>#include <stdlib.h>#include <string.h>
static void dump(const char *label, const unsigned char *p, size_t n){ printf("%-7s", label); for (size_t i = 0; i < n; i++) printf(" %02x", p[i]); printf("\n");}
int main(void){ char *old = malloc(24); strcpy(old, "mot de passe secret"); free(old);
unsigned char *m = malloc(24); dump("malloc", m, 24); unsigned char *c = calloc(24, 1); dump("calloc", c, 24); free(m); free(c); return 0;}$ gcc -Wall -Wextra -g init.c -o init$ ./initmalloc 61 6b 00 00 00 00 00 00 00 00 00 00 00 00 00 00 72 65 74 00 00 00 00 00calloc 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00Le bloc de malloc contient des restes (72 65 74, soit les lettres ret de « secret »), celui de calloc est propre. Si ton code lit une case avant de l'avoir écrite, malloc te donne du hasard et calloc un zéro prévisible.
La deuxième différence est invisible mais plus grave. calloc prend deux dimensions et refuse l'allocation si leur produit déborde size_t, là où malloc(nmemb * size) calcule le produit toi-même. Un produit qui déborde donne un petit nombre :
#include <stdint.h>#include <stdio.h>#include <stdlib.h>
int main(void){ size_t n = SIZE_MAX / sizeof(int) + 2;
int *a = malloc(n * sizeof(int)); int *b = calloc(n, sizeof(int));
printf("n * sizeof(int) = %zu\n", n * sizeof(int)); printf("malloc : %p\n", (void *)a); printf("calloc : %p\n", (void *)b); free(a); free(b); return 0;}$ gcc -Wall -Wextra -g overflow.c -o overflowoverflow.c: In function 'main':overflow.c:10:14: warning: product '4611686018427387905 * 4' of arguments 1 and 2 exceeds 'SIZE_MAX' [-Walloc-size-larger-than=] 10 | int *b = calloc(n, sizeof(int)); | ^~~~~~~~~~~~~~~~~~~~~~...$ ./overflown * sizeof(int) = 4malloc : 0x3c96f2a0calloc : (nil)n * sizeof(int) s'est enroulé jusqu'à 4. malloc a donc réservé 4 octets en croyant répondre à une demande gigantesque, et tout accès au-delà déborde. calloc a vu le débordement et renvoyé NULL. C'est la formulation même de C23, qui dit que calloc renvoie un pointeur « or a null pointer if the space cannot be allocated or if the product nmemb * size would wraparound size_t ». Dès qu'une des dimensions vient de l'extérieur, calloc est le bon réflexe.
realloc : agrandir sans perdre le bloc
realloc sert à changer la taille d'un bloc déjà alloué. Il garde le contenu jusqu'à la plus petite des deux tailles, et peut soit l'agrandir sur place, soit allouer ailleurs, recopier et libérer l'ancien. Dans ce dernier cas, l'adresse change.
Pile (stack)
- int *notes
- vieux
Tas (heap)
Pile (stack)
- int *notes
- neuf
Tas (heap)
Les quatre valeurs d'origine sont bien là, les quatre nouvelles cases ont des valeurs indéterminées. C23 le dit : « Any bytes in the new object beyond the size of the old object have unspecified values. »
Le piège mortel est d'écrire le résultat dans le pointeur qu'on passe. Si realloc échoue, il renvoie NULL sans toucher à l'ancien bloc, « the original block is left untouched; it is not freed or moved ». Mais si tu as fait p = realloc(p, n), tu viens d'écraser le seul pointeur vers ce bloc toujours alloué : il est perdu pour de bon.
p = realloc(p, n);if (p == NULL) return 1; /* l'ancien bloc est introuvable */tmp = realloc(p, n);if (tmp == NULL) { free(p); /* p est toujours valide */ return 1;}p = tmp;On peut le vérifier sous Valgrind. La version qui réaffecte directement perd ses octets quand l'énorme realloc échoue, la version avec temporaire ne perd rien :
$ valgrind -q --leak-check=full ./growplus de mémoire, les 4 notes sont perdues==14== 16 bytes in 1 blocks are definitely lost in loss record 1 of 1==14== at 0x488545C: malloc (vg_replace_malloc.c:446)==14== by 0x400713: main (grow.c:7)$ valgrind -q --leak-check=full ./grow-okplus de mémoire, on garde les 4 notes : 13Deux raccourcis sont codés dans la fonction. realloc(NULL, size) se comporte comme malloc(size). Et realloc(p, 0), qui libérait le bloc jusqu'à C17, est devenu un comportement indéfini en C23 : ne t'en sers plus pour libérer, écris free(p).
free : une fois, et une seule
free rend un bloc au tas. La règle tient en une phrase : chaque adresse renvoyée par malloc, calloc ou realloc est passée à free exactement une fois, telle quelle. Un seul cas sort du lot et il est confortable : free(NULL) ne fait rien, c'est garanti par la norme. Tu peux donc initialiser tes pointeurs à NULL et les libérer sans test.
Tout le reste est un comportement indéfini : libérer deux fois le même bloc, libérer un pointeur qu'on a décalé (free(p + 1)), libérer une variable de pile ou une chaîne littérale. Sur un système glibc, ça se traduit par un message comme free(): double free detected in tcache 2 et un arrêt brutal, décrypté dans double free detected in tcache.
Un réflexe simple évite la plupart de ces cas : après free(p), écris p = NULL;. Un second free ne fera alors rien, et un accès accidentel plantera franchement au lieu de lire une zone recyclée.
calloc
Réécris calloc toi-même : vérifie d'abord le débordement avec nmemb > SIZE_MAX / size, renvoie NULL au lieu de laisser le produit s'enrouler, puis alloue et mets chaque octet à zéro. C'est exactement la garantie qui manque à malloc.
mem_resize
Le vrai realloc à deux arguments ne se réécrit pas en C, faute de pouvoir demander la taille d'un bloc existant. L'exercice te donne cette taille : déplace un bloc vers un buffer d'une nouvelle taille en gardant ses premiers octets, puis libère l'ancien, dans le bon ordre.
Pour enchaîner ces fonctions sur des cas réels, de la copie de chaîne à l'allocateur maison, le module du parcours C les regroupe :
Questions fréquentes
À quoi sert malloc, concrètement ?
À demander de la mémoire dont tu ne connais la taille qu'à l'exécution, ou qui doit survivre à la fonction qui la crée. Une variable locale vit sur la pile et disparaît à la fin de son bloc ; un bloc de malloc vit jusqu'à ton free, peu importe où tu es dans le programme.
malloc ou calloc, lequel choisir ?
calloc dès que tu veux une zone à zéro, ou dès qu'une des deux dimensions (nmemb, size) vient d'une entrée extérieure, parce qu'il refuse un produit qui déborde. malloc quand tu vas écrire chaque octet toi-même juste après et que les tailles sont sûres.
Faut-il tester le retour de malloc contre NULL ?
Oui, à chaque fois. Un malloc qui échoue renvoie NULL, et écrire dans un pointeur NULL provoque un segmentation fault. Le test coûte une ligne.
Pourquoi ne pas écrire p = realloc(p, n) ?
Parce que si realloc échoue, il renvoie NULL sans libérer l'ancien bloc. En réaffectant p, tu perds le seul pointeur vers ce bloc toujours alloué : c'est une fuite. Passe par un temporaire et ne réaffecte p qu'en cas de succès.
free(NULL) est-il dangereux ?
Non, c'est explicitement sans effet. Ça permet de libérer un pointeur sans vérifier qu'il n'est pas nul, et de faire free(p); p = NULL; sans risque sur un second passage.
Sources
