Table of Contents
Gestion de Mémoire Dynamique en C : Guide Complet
Introduction
La gestion de mémoire dynamique est l’une des compétences les plus importantes et les plus délicates à maîtriser en programmation C. Contrairement aux langages modernes avec garbage collector (Java, Python, Go), le C vous donne un contrôle total sur l’allocation et la libération de la mémoire, mais cette puissance s’accompagne d’une grande responsabilité.
Stack vs Heap : Comprendre les Deux Types de Mémoire
En C, la mémoire d’un programme est divisée en plusieurs segments. Les deux plus importants pour nous sont la pile (stack) et le tas (heap).
La Pile (Stack) :
- Allocation automatique et rapide
- Taille limitée (quelques Mo en général)
- Variables locales et paramètres de fonction
- Libération automatique à la fin du scope
- Accès très rapide (LIFO)
void exemple_stack() {
int x = 10; // Alloué sur la stack
char buffer[100]; // Aussi sur la stack
// Libéré automatiquement à la fin de la fonction
}
Le Tas (Heap) :
- Allocation manuelle et plus lente
- Taille beaucoup plus grande (limité par la RAM)
- Persistance au-delà du scope de la fonction
- Doit être libéré manuellement
- Risque de fragmentation
void exemple_heap() {
int *p = malloc(sizeof(int)); // Alloué sur le heap
*p = 10;
// Reste en mémoire après la fonction si on ne libère pas !
free(p); // Libération manuelle obligatoire
}
Quand Utiliser l’Allocation Dynamique ?
L’allocation dynamique est nécessaire dans plusieurs situations :
- Taille inconnue à la compilation : Quand vous ne connaissez pas la taille des données avant l’exécution
- Données volumineuses : Pour éviter un dépassement de pile
- Durée de vie étendue : Quand les données doivent survivre à la fonction qui les crée
- Structures de données dynamiques : Listes chaînées, arbres, graphes
- Tableaux redimensionnables : Quand la taille peut changer pendant l’exécution
malloc() : L’Allocation de Base
Syntaxe et Utilisation
La fonction malloc() (memory allocation) est la fonction d’allocation de base en C. Elle est déclarée dans <stdlib.h>.
#include <stdlib.h>
void *malloc(size_t size);
Elle alloue un bloc de mémoire de size octets et retourne un pointeur vers le début de ce bloc. La mémoire n’est pas initialisée - elle contient des valeurs indéterminées (garbage).
#include <stdio.h>
#include <stdlib.h>
int main() {
// Allocation d'un entier
int *p = malloc(sizeof(int));
// Allocation d'un tableau de 100 entiers
int *tableau = malloc(100 * sizeof(int));
// Allocation d'une structure
struct Personne {
char nom[50];
int age;
};
struct Personne *pers = malloc(sizeof(struct Personne));
// N'oubliez pas de libérer !
free(p);
free(tableau);
free(pers);
return 0;
}
Gestion des Erreurs : Le NULL Check Obligatoire
malloc() peut échouer si la mémoire est insuffisante. Dans ce cas, elle retourne NULL. Vous devez toujours vérifier le retour de malloc !
int *p = malloc(10 * sizeof(int));
if (p == NULL) {
// Gestion de l'erreur
fprintf(stderr, "Erreur: allocation mémoire échouée\n");
perror("malloc");
exit(EXIT_FAILURE); // ou return -1;
}
// Maintenant on peut utiliser p en toute sécurité
for (int i = 0; i < 10; i++) {
p[i] = i * 2;
}
Pattern recommandé avec vérification immédiate :
int *p;
if ((p = malloc(10 * sizeof(int))) == NULL) {
perror("malloc");
return -1;
}
Calcul de Taille avec sizeof
Règle d’or : Toujours utiliser sizeof pour calculer la taille à allouer.
// MAUVAIS - Taille codée en dur, non portable
int *p = malloc(40); // Suppose sizeof(int) == 4
// BON - Utilise sizeof avec le type
int *p = malloc(10 * sizeof(int));
// ENCORE MIEUX - Utilise sizeof avec le pointeur lui-même
int *p = malloc(10 * sizeof *p);
La dernière forme sizeof *p est préférée car si vous changez le type du pointeur, vous n’avez pas besoin de modifier l’allocation :
// Si on change int* en long*
long *p = malloc(10 * sizeof *p); // Fonctionne toujours correctement !
calloc() : Allocation avec Initialisation à Zéro
Différence avec malloc()
La fonction calloc() (contiguous allocation) est similaire à malloc(), mais avec deux différences importantes :
- Deux paramètres : nombre d’éléments et taille de chaque élément
- Initialisation à zéro : Tous les bits sont mis à 0
void *calloc(size_t nmemb, size_t size);
// Avec malloc (mémoire non initialisée)
int *p1 = malloc(10 * sizeof(int));
// p1[0] à p1[9] contiennent des valeurs aléatoires !
// Avec calloc (mémoire initialisée à 0)
int *p2 = calloc(10, sizeof(int));
// p2[0] à p2[9] valent tous 0
Cas d’Usage Idéaux pour calloc()
1. Tableaux numériques initialisés à zéro :
// Création d'un histogramme
int *histogramme = calloc(256, sizeof(int));
if (histogramme == NULL) {
perror("calloc");
return -1;
}
// Tous les compteurs sont déjà à 0, prêt à l'emploi !
2. Structures avec pointeurs :
struct Noeud {
int valeur;
struct Noeud *gauche;
struct Noeud *droite;
};
// calloc initialise les pointeurs à NULL
struct Noeud *noeud = calloc(1, sizeof(struct Noeud));
// noeud->gauche == NULL et noeud->droite == NULL
3. Buffers de chaînes :
char *buffer = calloc(1024, sizeof(char));
// Le buffer est déjà terminé par '\0'
Performance : calloc() vs malloc() + memset()
Contrairement à une idée reçue, calloc() n’est pas toujours équivalent à malloc() + memset(). Sur de nombreux systèmes, calloc() peut être plus efficace car :
- Le système peut utiliser des pages mémoire pré-zeroed
- Le kernel peut utiliser le copy-on-write pour les grandes allocations
// Ces deux approches ne sont PAS équivalentes en performance
int *p1 = calloc(1000000, sizeof(int)); // Peut être optimisé par l'OS
int *p2 = malloc(1000000 * sizeof(int));
memset(p2, 0, 1000000 * sizeof(int)); // Toujours touche la mémoire
realloc() : Redimensionner la Mémoire
Agrandissement et Réduction
La fonction realloc() permet de redimensionner un bloc de mémoire précédemment alloué.
void *realloc(void *ptr, size_t new_size);
Comportement :
- Si
ptrestNULL: équivalent àmalloc(new_size) - Si
new_sizeest 0 : comportement défini par l’implémentation (à éviter) - Sinon : redimensionne le bloc
#include <stdio.h>
#include <stdlib.h>
int main() {
// Allocation initiale de 5 entiers
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) return -1;
for (int i = 0; i < 5; i++) {
arr[i] = i + 1; // 1, 2, 3, 4, 5
}
// Agrandissement à 10 entiers
int *new_arr = realloc(arr, 10 * sizeof(int));
if (new_arr == NULL) {
free(arr); // Important : arr est toujours valide !
return -1;
}
arr = new_arr;
// Les 5 premiers éléments sont préservés
// arr[5] à arr[9] contiennent des valeurs indéterminées
for (int i = 5; i < 10; i++) {
arr[i] = i + 1; // 6, 7, 8, 9, 10
}
free(arr);
return 0;
}
Pattern Sûr : Éviter les Fuites de Mémoire
DANGER : Le pattern suivant peut causer une fuite mémoire !
// MAUVAIS - Fuite mémoire si realloc échoue !
int *p = malloc(10 * sizeof(int));
p = realloc(p, 20 * sizeof(int)); // Si NULL, on perd le pointeur original !
Pattern sûr avec pointeur temporaire :
int *p = malloc(10 * sizeof(int));
if (p == NULL) return -1;
// Utiliser un pointeur temporaire
int *temp = realloc(p, 20 * sizeof(int));
if (temp == NULL) {
// realloc a échoué, mais p est toujours valide
fprintf(stderr, "realloc failed, keeping original allocation\n");
// On peut continuer avec p ou libérer et quitter
free(p);
return -1;
}
p = temp; // Succès, on met à jour p
Implémentation d’un Tableau Dynamique
Voici un exemple complet d’un tableau dynamique qui grandit automatiquement :
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data;
size_t size; // Nombre d'éléments utilisés
size_t capacity; // Capacité totale allouée
} DynamicArray;
DynamicArray* da_create(size_t initial_capacity) {
DynamicArray *da = malloc(sizeof(DynamicArray));
if (da == NULL) return NULL;
da->data = malloc(initial_capacity * sizeof(int));
if (da->data == NULL) {
free(da);
return NULL;
}
da->size = 0;
da->capacity = initial_capacity;
return da;
}
int da_push(DynamicArray *da, int value) {
// Besoin d'agrandir ?
if (da->size >= da->capacity) {
size_t new_capacity = da->capacity * 2; // Doublement
int *temp = realloc(da->data, new_capacity * sizeof(int));
if (temp == NULL) return -1;
da->data = temp;
da->capacity = new_capacity;
}
da->data[da->size++] = value;
return 0;
}
void da_destroy(DynamicArray *da) {
if (da != NULL) {
free(da->data);
free(da);
}
}
int main() {
DynamicArray *arr = da_create(4);
if (arr == NULL) return -1;
// Ajout de 100 éléments - grandit automatiquement
for (int i = 0; i < 100; i++) {
if (da_push(arr, i) != 0) {
da_destroy(arr);
return -1;
}
}
printf("Size: %zu, Capacity: %zu\n", arr->size, arr->capacity);
da_destroy(arr);
return 0;
}
aligned_alloc() : Allocation Alignée (C11)
Qu’est-ce que l’Alignement Mémoire ?
L’alignement mémoire fait référence à la façon dont les données sont positionnées en mémoire. Une adresse est “alignée sur N octets” si elle est divisible par N.
Adresse 0x1000 : alignée sur 16, 8, 4, 2, 1
Adresse 0x1004 : alignée sur 4, 2, 1 (pas sur 8 ou 16)
Adresse 0x1001 : alignée sur 1 seulement
Pourquoi l’alignement est important :
- Performance : Les accès mémoire alignés sont plus rapides
- Exigences matérielles : Certains processeurs exigent un alignement correct
- Instructions SIMD : SSE/AVX nécessitent des alignements de 16/32 octets
Syntaxe de aligned_alloc()
void *aligned_alloc(size_t alignment, size_t size);
Contraintes :
alignmentdoit être une puissance de 2sizedoit être un multiple dealignment- Disponible depuis C11
#include <stdlib.h>
#include <stdio.h>
int main() {
// Allocation alignée sur 64 octets (ligne de cache typique)
size_t alignment = 64;
size_t size = 1024; // Doit être multiple de 64
void *ptr = aligned_alloc(alignment, size);
if (ptr == NULL) {
perror("aligned_alloc");
return -1;
}
// Vérification de l'alignement
printf("Adresse: %p\n", ptr);
printf("Alignée sur %zu: %s\n", alignment,
((uintptr_t)ptr % alignment == 0) ? "oui" : "non");
free(ptr); // free() fonctionne normalement
return 0;
}
Cas d’Usage : SIMD et Optimisation Cache
1. Instructions SIMD (SSE, AVX) :
#include <stdlib.h>
#include <immintrin.h> // Pour SSE/AVX
void addition_simd(float *a, float *b, float *result, size_t n) {
// SSE nécessite un alignement de 16 octets
// AVX nécessite un alignement de 32 octets
for (size_t i = 0; i < n; i += 8) {
__m256 va = _mm256_load_ps(&a[i]); // Requiert alignement 32
__m256 vb = _mm256_load_ps(&b[i]);
__m256 vr = _mm256_add_ps(va, vb);
_mm256_store_ps(&result[i], vr);
}
}
int main() {
size_t n = 1024;
// Allocation alignée pour AVX
float *a = aligned_alloc(32, n * sizeof(float));
float *b = aligned_alloc(32, n * sizeof(float));
float *result = aligned_alloc(32, n * sizeof(float));
// ... utilisation ...
free(a);
free(b);
free(result);
return 0;
}
2. Optimisation de cache :
// Structure alignée sur une ligne de cache pour éviter le false sharing
#define CACHE_LINE_SIZE 64
typedef struct {
int counter;
char padding[CACHE_LINE_SIZE - sizeof(int)];
} AlignedCounter;
// Allocation de compteurs par thread
AlignedCounter *counters = aligned_alloc(
CACHE_LINE_SIZE,
num_threads * sizeof(AlignedCounter)
);
Alternatives pour les Anciens Standards
Pour le code pré-C11, vous pouvez utiliser :
// POSIX (Linux, macOS)
#include <stdlib.h>
int posix_memalign(void **memptr, size_t alignment, size_t size);
// Windows
#include <malloc.h>
void *_aligned_malloc(size_t size, size_t alignment);
void _aligned_free(void *ptr); // Attention : free() ne fonctionne pas !
free() : Libération de la Mémoire
Libération Correcte
La fonction free() libère un bloc de mémoire précédemment alloué par malloc(), calloc(), realloc() ou aligned_alloc().
void free(void *ptr);
Règles fondamentales :
int *p = malloc(10 * sizeof(int));
// ... utilisation de p ...
free(p); // Libère la mémoire
p = NULL; // Bonne pratique : mettre à NULL après free
free(NULL) est sûr :
int *p = NULL;
free(p); // Pas d'erreur, ne fait rien
Le Danger du Double Free
Libérer deux fois la même mémoire est un comportement indéfini qui peut causer des crashes ou des failles de sécurité.
int *p = malloc(sizeof(int));
free(p);
free(p); // DANGER ! Double free - comportement indéfini
Solution : mettre le pointeur à NULL :
int *p = malloc(sizeof(int));
free(p);
p = NULL; // Maintenant p est NULL
free(p); // Sûr car free(NULL) ne fait rien
Macro utile :
#define SAFE_FREE(ptr) do { free(ptr); ptr = NULL; } while(0)
int *p = malloc(sizeof(int));
SAFE_FREE(p); // Libère et met à NULL
SAFE_FREE(p); // Sûr
Use After Free : L’Erreur Fatale
Accéder à la mémoire après l’avoir libérée est une erreur grave.
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p); // DANGER ! Use after free
*p = 10; // DANGER ! Écriture après free
Pourquoi c’est dangereux :
- La mémoire peut être réallouée à un autre usage
- Corruption de données silencieuse
- Failles de sécurité exploitables
- Crashes aléatoires difficiles à débugger
Détection avec Valgrind :
gcc -g program.c -o program
valgrind --leak-check=full ./program
Patterns d’Allocation Courants
1. Tableaux Dynamiques Bidimensionnels
// Allocation d'une matrice n x m
int **creer_matrice(size_t n, size_t m) {
// Allocation du tableau de pointeurs
int **matrice = malloc(n * sizeof(int*));
if (matrice == NULL) return NULL;
// Allocation de chaque ligne
for (size_t i = 0; i < n; i++) {
matrice[i] = malloc(m * sizeof(int));
if (matrice[i] == NULL) {
// Libérer ce qui a été alloué en cas d'erreur
for (size_t j = 0; j < i; j++) {
free(matrice[j]);
}
free(matrice);
return NULL;
}
}
return matrice;
}
void liberer_matrice(int **matrice, size_t n) {
if (matrice == NULL) return;
for (size_t i = 0; i < n; i++) {
free(matrice[i]);
}
free(matrice);
}
// Allocation contiguë (meilleure pour le cache)
int **creer_matrice_contigue(size_t n, size_t m) {
int **matrice = malloc(n * sizeof(int*));
if (matrice == NULL) return NULL;
// Un seul bloc pour toutes les données
int *data = malloc(n * m * sizeof(int));
if (data == NULL) {
free(matrice);
return NULL;
}
// Configuration des pointeurs de lignes
for (size_t i = 0; i < n; i++) {
matrice[i] = data + i * m;
}
return matrice;
}
void liberer_matrice_contigue(int **matrice) {
if (matrice == NULL) return;
free(matrice[0]); // Libère le bloc de données
free(matrice); // Libère le tableau de pointeurs
}
2. Structures Chaînées
typedef struct Noeud {
int valeur;
struct Noeud *suivant;
} Noeud;
typedef struct {
Noeud *tete;
size_t taille;
} ListeChainee;
ListeChainee* liste_creer() {
ListeChainee *liste = calloc(1, sizeof(ListeChainee));
// calloc initialise tete à NULL et taille à 0
return liste;
}
int liste_ajouter_debut(ListeChainee *liste, int valeur) {
Noeud *nouveau = malloc(sizeof(Noeud));
if (nouveau == NULL) return -1;
nouveau->valeur = valeur;
nouveau->suivant = liste->tete;
liste->tete = nouveau;
liste->taille++;
return 0;
}
void liste_detruire(ListeChainee *liste) {
if (liste == NULL) return;
Noeud *courant = liste->tete;
while (courant != NULL) {
Noeud *suivant = courant->suivant;
free(courant);
courant = suivant;
}
free(liste);
}
3. Allocation par Blocs (Pool Allocator)
Pour des allocations fréquentes de même taille, un pool d’allocation est plus efficace :
#define BLOCK_SIZE 64
#define POOL_SIZE 1024
typedef struct {
char data[BLOCK_SIZE * POOL_SIZE];
int libre[POOL_SIZE];
size_t premier_libre;
} MemoryPool;
MemoryPool* pool_creer() {
MemoryPool *pool = malloc(sizeof(MemoryPool));
if (pool == NULL) return NULL;
// Initialiser la liste des blocs libres
for (size_t i = 0; i < POOL_SIZE - 1; i++) {
pool->libre[i] = i + 1;
}
pool->libre[POOL_SIZE - 1] = -1; // Fin de liste
pool->premier_libre = 0;
return pool;
}
void* pool_alloc(MemoryPool *pool) {
if (pool->premier_libre == (size_t)-1) {
return NULL; // Pool plein
}
size_t index = pool->premier_libre;
pool->premier_libre = pool->libre[index];
return &pool->data[index * BLOCK_SIZE];
}
void pool_free(MemoryPool *pool, void *ptr) {
size_t index = ((char*)ptr - pool->data) / BLOCK_SIZE;
pool->libre[index] = pool->premier_libre;
pool->premier_libre = index;
}
void pool_detruire(MemoryPool *pool) {
free(pool);
}
Bonnes Pratiques
1. Toujours Vérifier les Retours d’Allocation
// Pattern complet avec gestion d'erreur
void* safe_malloc(size_t size) {
void *ptr = malloc(size);
if (ptr == NULL && size != 0) {
fprintf(stderr, "Erreur fatale: allocation de %zu octets échouée\n", size);
exit(EXIT_FAILURE);
}
return ptr;
}
2. Utiliser sizeof Correctement
// Préférer sizeof *pointeur à sizeof(Type)
int *p = malloc(n * sizeof *p); // S'adapte si on change le type
// Pour les structures
struct Data *d = malloc(sizeof *d);
3. Libérer dans l’Ordre Inverse d’Allocation
// Allocation
char *str = malloc(100);
int *arr = malloc(50 * sizeof(int));
struct Data *data = malloc(sizeof(struct Data));
// Libération dans l'ordre inverse
free(data);
free(arr);
free(str);
4. Mettre les Pointeurs à NULL après free()
free(ptr);
ptr = NULL; // Évite les use-after-free et double-free
5. Documenter la Propriété de la Mémoire
/**
* Crée une copie de la chaîne.
* @return Nouvelle chaîne allouée (à libérer par l'appelant)
*/
char* string_duplicate(const char *src);
/**
* Prend possession du buffer passé en paramètre.
* @param buffer Buffer à prendre (sera libéré par cette structure)
*/
void container_set_buffer(Container *c, char *buffer);
6. Utiliser des Outils de Vérification
# Valgrind pour détecter les fuites et erreurs
valgrind --leak-check=full --show-leak-kinds=all ./programme
# AddressSanitizer avec GCC/Clang
gcc -fsanitize=address -g programme.c -o programme
Pièges Courants à Éviter
1. Oublier de Vérifier NULL
// MAUVAIS
int *p = malloc(sizeof(int));
*p = 42; // Crash si malloc a échoué !
// BON
int *p = malloc(sizeof(int));
if (p == NULL) {
// Gestion d'erreur
}
*p = 42;
2. Fuites Mémoire dans les Conditions d’Erreur
// MAUVAIS - Fuite si la deuxième allocation échoue
int *a = malloc(100);
int *b = malloc(100); // Si échec, 'a' fuit
if (b == NULL) return -1;
// BON
int *a = malloc(100);
if (a == NULL) return -1;
int *b = malloc(100);
if (b == NULL) {
free(a); // Ne pas oublier !
return -1;
}
3. Mauvais Calcul de Taille
// MAUVAIS - Confusion pointeur/élément
int *arr = malloc(10); // Alloue 10 octets, pas 10 entiers !
// BON
int *arr = malloc(10 * sizeof(int)); // 10 entiers
4. Utilisation de realloc() Incorrecte
// MAUVAIS - Fuite si realloc échoue
p = realloc(p, new_size);
// BON
int *temp = realloc(p, new_size);
if (temp == NULL) {
// p est toujours valide, on peut continuer ou libérer
free(p);
return -1;
}
p = temp;
5. Mélanger les Allocateurs
// DANGER - Incompatibilité entre allocateurs
// Windows : _aligned_malloc / _aligned_free
// Certaines bibliothèques ont leurs propres allocateurs
void *p = library_alloc(100);
free(p); // MAUVAIS si library_alloc n'utilise pas malloc
void *p = library_alloc(100);
library_free(p); // BON
6. Libérer de la Mémoire Non Allouée
int x = 42;
int *p = &x;
free(p); // DANGER ! x n'est pas sur le heap
char buffer[100];
free(buffer); // DANGER ! buffer est sur la stack
Conclusion
La gestion de mémoire dynamique en C est un sujet fondamental qui requiert rigueur et attention. En maîtrisant les fonctions malloc(), calloc(), realloc(), aligned_alloc() et free(), vous disposez de tous les outils pour gérer efficacement la mémoire de vos applications.
Tableau Récapitulatif des Fonctions
| Fonction | Description | Initialise | Cas d’usage |
|---|---|---|---|
malloc(size) | Alloue size octets | Non | Allocation générale |
calloc(n, size) | Alloue n * size octets | Oui (à 0) | Tableaux, structures avec pointeurs |
realloc(ptr, size) | Redimensionne | Préserve | Tableaux dynamiques |
aligned_alloc(align, size) | Alloue avec alignement | Non | SIMD, optimisation cache |
free(ptr) | Libère la mémoire | - | Toujours après allocation |
Points Clés à Retenir
- Toujours vérifier le retour de
malloc(),calloc(),realloc()etaligned_alloc() - Utiliser sizeof correctement :
sizeof *ptrest préférable - Libérer toute mémoire allouée avec
free() - Mettre les pointeurs à NULL après
free()pour éviter les erreurs - Utiliser des outils comme Valgrind pour détecter les fuites
- Documenter qui est responsable de la libération de la mémoire
En suivant ces bonnes pratiques et en évitant les pièges courants, vous pouvez écrire du code C robuste et sans fuites mémoire. La gestion manuelle de la mémoire peut sembler complexe au début, mais elle offre un contrôle total et des performances optimales quand elle est maîtrisée.
In-Article Ad
Dev Mode
Mahmoud DEVO
Senior Full-Stack Developer
I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.
Enjoyed this article?
Subscribe to get more tech content delivered to your inbox.
Related Articles
Pointeurs en C : Guide Complet de la Manipulation Memoire
Maitrisez les pointeurs en C : declaration, initialisation, arithmetique, pointeurs de pointeurs, void* et bonnes pratiques de gestion memoire.
Tableaux 2D et Matrices en C : Allocation, Manipulation et Bonnes Pratiques
Maitrisez les tableaux bidimensionnels en C : allocation statique et dynamique, passage aux fonctions, row-major order et optimisation memoire.
Bit-fields et tableaux en C : guide pratique pour optimiser la memoire
Maitrisez les bit-fields et tableaux en C. Apprenez a creer des structures compactes, acceder aux elements et iterer efficacement sur vos donnees.