Russe
Programmation C/Gestion de la mémoire
La gestion dynamique de la mémoire en C se fait à l'aide de principalement deux fonctions de la bibliothèque standard :
Deux autres fonctions permettent de gérer plus finement la mémoire :
Ces deux dernières ne sont pas toujours implémentées, contrairement aux deux premières. Si c'est le cas elles peuvent être remplacées par une séquence de codice_1/codice_8, et codice_1/codice_10/codice_2.
Toutes ces fonctions sont déclarées dans l'en-tête codice_12.
Gestion dynamique de la mémoire.
Les déclarations de variables en C et dans beaucoup d'autres langages ont une limitation très importante : la taille des variables doit être connue à la compilation. Cela pose problème quand on ne sait qu'à l'exécution le nombre de données qu'on doit traiter. Pour résoudre ce problème, et pouvoir décider durant l'exécution d'un programme du nombre de variables à créer, il faudra nécessairement passer par de l'allocation dynamique de mémoire. Avec ce système, le programmeur dispose de fonctions qui permettent de demander au système une zone mémoire d'une certaine taille, qu'il pourra utiliser comme il le souhaite. En C, ces fonctions sont disponibles dans l'en-tête codice_12.
L'un des principaux avantages qu'offre le langage C est sa capacité à fournir au programmeur un contrôle poussé sur la gestion de la mémoire. Cette liberté nécessite néanmoins une grande rigueur, tant les problèmes pouvant survenir sont nombreux et souvent difficiles à diagnostiquer. On peut dire sans prendre beaucoup de risque que la plupart des erreurs de programmation en C, ont pour origine une mauvaise utilisation des fonctions de gestion de la mémoire. Il ne faut pas sous-estimer la difficulté de cette tâche. Autant cela est trivial pour un programme de quelques centaines de lignes, autant cela peut devenir un casse-tête quand ledit programme a subi des changements conséquents, pas toujours faits dans les règles de l'art.
La manière dont la mémoire physique d'un ordinateur est conçue, ainsi que la façon dont le système d'exploitation la manipule, sont très variables. Cependant, un modèle assez classique consiste à découper la mémoire en segments, segments dont on garde les références dans des tables de pages : c'est le modèle de segmentation / pagination. Ce modèle offre beaucoup d'avantages par rapport à un accès purement linéaire. Décrire son fonctionnement en détail est hors de la portée de cet ouvrage, mais on pourra noter tout de même :
Au niveau des inconvénients, on citera essentiellement un problème de performances. Plusieurs niveaux d'indirection impliquent de multiple lectures en mémoire, extrêmement pénalisant en terme de temps d'exécution, au point où des caches sont nécessaires pour garantir des vitesses acceptables. Même si RAM veut dire mémoire à accès aléatoire, il faut bien garder à l'esprit qu'un accès purement séquentiel (adresse mémoire croissante) peut être jusqu'à cent fois plus rapide qu'une méthode d'accès qui met sans cesse à défaut ce système de cache.
Un processus peut donc demander au système de réserver pour son usage exclusif un secteur mémoire de taille déterminée. Il peut également demander au système de modifier la taille d'une zone précédemment réservée ou de la libérer s'il n'en a plus l'utilité.
Fonctions fournies par <stdlib.h>.
codice_1: allocation de mémoire.
La fonction la plus simple d'allocation mémoire est codice_1 :
void * malloc(size_t taille);
Elle prend en argument la "taille" que l'on veut allouer et renvoie un pointeur vers une zone mémoire allouée ou un pointeur nul si la demande échoue (la cause d'erreur la plus courante étant qu'il n'y a plus assez d'espace mémoire disponible). Trois remarques peuvent être faites :
Par exemple, si on souhaite réserver une zone mémoire pour y allouer un entier :
/* Déclaration et initialisation */
int *ptr = NULL;
/* Allocation */
ptr = malloc(sizeof(int));
/* On vérifie que l'allocation a réussi. */
if (ptr != NULL)
 /* Stockage de la valeur "10" dans la zone mémoire pointée par ptr */
 *ptr = 10;
else
 /* décider du traitement en cas d'erreur */
Si on souhaite allouer de l'espace pour une structure de données plus complexe :
typedef struct{
 double b;
 int a;
 MaStructure *suivant;
} MaStructure;
/* Déclaration et initialisation */
MaStructure *ptr = NULL;
/* Allocation */
ptr = (MaStructure *)malloc(sizeof(MaStructure));
if (ptr != NULL)
 /* Initialisation de la structure nouvellement créée */
 ptr->a = 10;
 ptr->b = 3.1415;
 ptr->suivant = NULL;
codice_2: libération de mémoire.
Le C ne possède pas de mécanisme de ramasse-miettes, la mémoire allouée dynamiquement par un programme doit donc être explicitement libérée. La fonction codice_2 permet de faire cette libération.
void free( void * pointeur );
La fonction prend en argument un pointeur vers une zone mémoire précédemment allouée par un appel à malloc, calloc ou realloc et libère la zone mémoire pointée.
Exemple :
/* Déclaration et initialisation */
int *ptr = NULL;
/* Allocation */
ptr = malloc(sizeof(int));
/* On vérifie que l'allocation a réussi. */
if (ptr != NULL)
 /* ... utilisation de la zone allouée ... */
 /* Libérer la mémoire utilisée */
 free(ptr);
 ptr = NULL; /* Pour éviter les erreurs */
else
 /* décider du traitement en cas d'erreur */
L'utilisation du pointeur après libération de la zone allouée (ou la double libération d'une même zone mémoire) est une erreur courante qui provoque des résultats imprévisibles. Il est donc conseillé :
De plus, donner à codice_2 l'adresse d'un objet qui n'a pas été alloué par une des fonctions d'allocation cause un comportement indéfini.
codice_4 : allocation avec initialisation à 0.
La fonction codice_4 permet d'allouer une zone mémoire dont tous les bits seront initialisés à 0. Son prototype est légèrement différent de celui de codice_1, et est pratique pour l'allocation dynamique de tableaux.
Syntaxe :
void * calloc(size_t nb_element, size_t taille);
De manière similaire à codice_1, codice_4 retourne un pointeur de type codice_30 pointant une zone de codice_31 octets allouée en mémoire, dont tous les bits seront initialisés à 0, ou retourne un pointeur nul en cas d'échec.
Exemple :
/* allouer un tableau de 5 entiers */
int* ptr = calloc ( 5, sizeof(int) );
Le pointeur contient l'adresse du premier élément du tableau :
Le pointeur peut être utilisé comme un tableau classique pour accéder aux éléments qu'il contient :
ptr[0] = 3; /* équivaut à *ptr = 3; */
ptr[1] = 1; /* équivaut à *(ptr+1) = 1; */
ptr[2] = 4; /* équivaut à *(ptr+2) = 4; */
Notez que codice_4 place tous les "bits" à zéro, mais que ce n'est pas nécessairement une représentation valide pour un pointeur nul ni pour le nombre zéro en représentation flottante. Ainsi, pour initialiser à zéro un tableau de codice_33 de manière portable, par exemple, il est nécessaire d'assigner la valeur codice_34 à chaque élément du tableau. Étant donné qu'on initialise chaque élément « manuellement », on peut dans ce cas utiliser codice_1 plutôt que codice_4 (la première étant normalement beaucoup plus rapide que la seconde).
codice_6.
Il arrive fréquemment qu'un bloc alloué n'ait pas la taille suffisante pour accueillir de nouvelles données. La fonction codice_6 est utilisée pour changer (agrandir ou réduire) la taille d'une zone allouée par codice_1, codice_4, ou codice_6.
Syntaxe :
void * realloc(void * ancien_bloc, size_t nouvelle_taille);
codice_6 tentera de réajuster la taille du bloc pointé par "ancien_bloc" à la nouvelle taille spécifiée. À noter :
Notez bien que codice_6 ne peut que modifier des espaces mémoires qui ont été alloués par codice_1, codice_4, ou codice_6. En effet, autoriser codice_6 à manipuler des espaces mémoires qui ne sont pas issus des fonctions de la bibliothèque standard pourrait causer des erreurs, ou des incohérences graves de l'état du processus. En particulier, les tableaux, automatiques comme statiques, ne peuvent être passés à codice_6, comme illustré par le code suivant :
void f(void)
 int tab[10];
 int *ptr = realloc(tab, 20 * sizeof(int));
Lorsque codice_6 reçoit la valeur de codice_54, qui est un pointeur sur le premier élément (i.e. codice_55), il ne peut la traiter, et le comportement est indéfini. Sur cet exemple, il est facile de voir l'erreur, mais dans l'exemple suivant, la situation est plus délicate :
/* 'double' essaye de doubler l'espace mémoire pointé par ptr.
 * En cas de succès, la valeur renvoyée est un pointeur vers le nouvel espace mémoire, et l'ancienne
 * valeur de ptr est invalide.
 * En cas d'échec, l'espace pointé par ptr n'est pas modifié, et la valeur NULL est renvoyée.
void *double(void *ptr, size_t n)
 void *tmp = NULL;
 if ((ptr != NULL) && (n != 0) && (n <= SIZE_MAX / 2))
 tmp = realloc(ptr, 2 * n);
 return tmp;
La fonction codice_33 en elle-même ne comporte pas d'erreur, mais elle peut causer des plantages suivant la valeur de codice_57 qui lui est passée. Pour éviter des erreurs, il faudrait que la documentation de la fonction précise les contraintes sur la valeur de codice_57... et que les programmeurs qui l'utilisent y fassent attention.
On peut aussi noter que, quand codice_6 réussit, le pointeur renvoyé peut très bien être égal au pointeur initial, ou lui être différent.
En particulier, il n'y a aucune garantie que, quand on diminue la taille de la zone mémoire, il le fasse « sur place ». C'est très probable, car c'est ce qui est le plus facile et rapide à faire du point de vue de l'implémentation, mais rien ne l'empêche par exemple de chercher un autre espace mémoire disponible qui aurait exactement la taille voulue, au lieu de garder la zone mémoire initiale.
On peut noter le test codice_60. Il permet d'éviter un "débordement entier" : si n était supérieur à cette valeur, le produit codice_61 devrait avoir une valeur supérieure à codice_62, qui est la plus grande valeur représentable par le type codice_18. Lorsque cette valeur est passée à codice_6, la conversion en codice_18, qui est un type entier non signé, se fera modulo codice_66, et donc la fonction recevra une valeur différente de celle voulue. Si le test n'était pas fait, codice_33 pourrait ainsi retourner à l'appelant une zone mémoire de taille inférieure à celle demandée, ce qui causerait des bogues. Ce genre de bogue (non spécifique à codice_6) est très difficile à détecter, car n'apparaît que lorsque l'on atteint des valeurs limites, ce qui est assez rare, et le problème peut n'être visible que bien longtemps après que l'erreur de calcul soit faite.
Gestion d'erreur.
Pour gérer correctement le cas d'échec, on ne peut faire ainsi :
int *ptr = malloc(10 * sizeof(int));
if (ptr != NULL)
 /* ... */
 /* On se rend compte qu'on a besoin d'un peu plus de place. */
 ptr = realloc(ptr, 20 * sizeof(int));
En effet, si codice_6 échoue, la valeur de codice_57 est alors nulle, et on aura perdu la référence vers l'espace de taille codice_71 qu'on a déjà alloué. Ce type d'erreur s'appelle une "fuite mémoire". Il faut donc faire ainsi :
int *ptr = malloc(10 * sizeof(int));
if (ptr != NULL)
 /* ... */
 /* On se rend compte qu'on a besoin d'un peu plus de place. */
 int *tmp = realloc(ptr, 20 * sizeof(int));
 if (tmp == NULL)
 /* Exemple de traitement d'erreur minimaliste */
 free(ptr);
 return EXIT_FAILURE;
 else
 ptr = tmp;
 /* On continue */
Exemple.
codice_6 peut être utilisé quand on souhaite boucler sur une entrée dont la longueur peut être indéfinie, et qu'on veut gérer la mémoire assez finement. Dans l'exemple suivant, on suppose définie une fonction codice_73 qui :
Cette fonction est utilisée pour construire un tableau d'entiers issus de cette entrée. Comme on ne sait à l'avance combien d'éléments on va recevoir, on augmente la taille d'un tableau au fur et à mesure avec codice_6.
/* Fonction qui utilise 'lire_entree' pour lire un nombre indéterminé
 * d'entiers.
 * Renvoie l'adresse d'un tableau d'entiers (NULL si aucun entier n'est lu).
 * 'taille' est placé au nombre d'éléments lus (éventuellement 0).
 * 'erreur' est placée à une valeur non nulle en cas d'erreur, à une valeur nulle sinon.
int *traiter_entree(size_t *taille, int *erreur)
 size_t max = 0; /* Nombre d'éléments utilisables */
 size_t i = 0; /* Nombre d'éléments utilisés */
 int *ptr = NULL; /* Pointeur vers le tableau dynamique */
 int valeur; /* La valeur lue depuis l'entrée */
 *erreur = 0;
 while (lire_entree(&valeur) != 0)
 if (i >= max)
 /* Il n'y a plus de place pour stocker 'valeur' dans 'ptr[i]' */
 max = max + 10; 
 int *tmp = realloc(ptr, max * sizeof(int));
 if (tmp == NULL)
 /* realloc a échoué : on sort de la boucle */ 
 *erreur = 1;
 break;
 else
 ptr = tmp;
 ptr[i] = valeur;
 i++;
 *taille = i;
 return ptr;
Ici, on utilise codice_76 pour se souvenir du nombre d'éléments que contient la zone de mémoire allouée, et codice_77 pour le nombre d'éléments effectivement utilisés. Quand on a pu lire un entier depuis l'entrée, et que codice_77 vaut codice_76, on sait qu'il n'y a plus de place disponible et qu'il faut augmenter la taille de la zone de mémoire. Ici, on incrémente la taille codice_76 de 10 à chaque fois, mais il est aussi possible de la multiplier par 2, ou d'utiliser toute autre formule. On utilise par ailleurs le fait que, quand le pointeur envoyé à codice_6 est nul, la fonction se comporte comme codice_1.
Le choix de la formule de calcul de codice_76 à utiliser chaque fois que le tableau est rempli résulte d'un compromis :
Une allocation mémoire est une opération qui peut être coûteuse en terme de temps, et un grand nombre d'allocations mémoire peut fractionner l'espace mémoire disponible, ce qui alourdit la tâche de l'allocateur de mémoire, et au final peut causer des pertes de performance de l'application. Aucune formule n'est universelle, chaque situation doit être étudiée en fonction de différents paramètres (système d'exploitation, capacité mémoire, vitesse du matériel, taille habituelle/maximale de l'entrée...).
Toutefois, l'essentiel est bien souvent d'avoir un algorithme qui marche, l'optimisation étant une question secondaire. Dans une telle situation, utilisez d'abord une méthode simple (incrémentation ou multiplication par une constante), et n'en changez que si le comportement du programme devient gênant.
Problèmes et erreurs classiques.
Défaut d'initialisation d'un pointeur.
Pour éviter des erreurs, un pointeur devrait toujours être initialisé lors de sa déclaration ; soit à NULL, soit avec l'adresse d'un objet, soit avec la valeur de retour d'une fonction « sûre » comme codice_1. Méditons sur l'exemple suivant :
/* Déclaration sans initialisation */
int *ptr;
int var;
/* On stocke dans var la valeur de la zone mémoire pointée par ptr*/
var = *ptr;
Ce code va compiler! Si on est chanceux l'exécution de ce code provoquera une erreur à la troisième étape lorsqu'on déréférence codice_57. Si on l'est moins, ce code s'exécute sans souci et stocke dans codice_90 une valeur aléatoire, ce qui pourrait provoquer une erreur lors d'une utilisation ultérieure de la variable codice_90 alors que l'erreur réelle se situe bien plus en amont dans le code! Une variable (automatique) de type pointeur est en effet comme toutes les autres variables : si elle n'est pas initialisée explicitement, son contenu est indéfini ; son déréférencement peut donc causer n'importe quel comportement.
Lorsqu'on déclare une variable ou qu'on alloue une zone mémoire, on ne dispose d'aucune garantie sur le contenu de cette zone ou de cette variable. Initialiser systématiquement les variables dès leur déclaration, en particulier lorsqu'il s'agit de pointeurs, fait partie des bons réflexes à prendre et pour la plupart des applications ne dégrade pas le temps d'exécution de manière significative. Ce type d'erreur peut être extrèmement difficile à localiser à l'intérieur d'un programme plus complexe.
Référence multiple à une même zone mémoire.
La recopie d'un pointeur dans un autre n'alloue pas une nouvelle zone. La zone est alors référencée par deux pointeurs. Un problème peut survenir si on libère la zone allouée sans réinitialiser tous les pointeurs correspondants :
int* ptr = malloc(sizeof(int)); // allouer une zone mémoire pour un entier
int* ptr2 = ptr; // ptr2 pointe la même zone mémoire
printf("%d\n", *ptr2 ); // affiche 5
/* libération */
free( ptr );
ptr = NULL;
En règle générale, il faut éviter que plusieurs variables pointent la même zone mémoire allouée.
L'utilisation d'un outil de vérification statique permet de détecter ce genre d'erreur.
Fuite mémoire.
La perte du pointeur associé à un secteur mémoire rend impossible la libération du secteur à l'aide de free. On qualifie cette situation de "fuite mémoire", car des secteurs demeurent réservés sans avoir été désalloués. Cette situation perdure jusqu'à la fin du programme principal. Voyons l'exemple suivant :
int i = 0; // un compteur
int* ptr = NULL; // un pointeur
while (i < 1001) {
 ptr = malloc(sizeof(int)); // on écrase la valeur précédente de ptr par une nouvelle
 if (ptr != NULL) 
 /* traitement ...*/
À la sortie de cette boucle on a alloué un millier d'entiers soit environ 2000 octets, que l'on ne peut pas libérer car le pointeur ptr est écrasé à chaque itération (sauf la dernière). La mémoire disponible est donc peu à peu grignotée jusqu'au dépassement de sa capacité. Ce genre d'erreurs est donc à proscrire pour des processus tournant en boucle pendant des heures, voire indéfiniment. Si son caractère cumulatif la rend difficile à détecter, il existe de nombreux utilitaires destinés à traquer la moindre fuite.