int main(void)
 printf("----------------------------------------\n");
 printf(" Gestion de la mémoire\n");
 printf("----------------------------------------\n");
 Pour gérer la mémoire, nous avons les fonctions suivantes,
 déclarées dans l'en-tête stdlib.h :
 malloc, pour l'allocation dynamique de mémoire.
 free, pour la libération de mémoire préalablement allouée avec malloc.
 calloc, pour allouer dynamiquement de la mémoire, comme malloc,
 la zone mémoire est remplie avec des 0.
 realloc, pour modifier la taille d'une zone mémoire déjà allouée.
 à savoir :
 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é
 de ce cours, mais on pourra noter tout de même :
 * Indépendance totale de l'espace d'adressage entre les processus :
 un processus ne peut pas accèder à la mémoire d'un autre processus. C'est
 pourquoi transmettre la valeur d'un pointeur à un autre processus ne servira en
 général à rien, car le second processus ne pourra jamais accéder à l'emplacement
 pointé.
 * Gestion fine de la mémoire :
 les segments sont accèdés via plusieurs niveaux d'indirection dans les tables de
 pages. Cela permet de mettre virtuellement les segments n'importe où dans la
 mémoire ou même sur un système de fichier. Dans la pratique, éparpiller trop les
 segments (fragmenter) réduit significativement les performances.
 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 sytème de réserver pour son usage exclusif un
 secteur mémoire de taille déterminée. Il peut également demander au sytè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é.
 printf("----------------------------------------\n");
 printf(" 1. malloc: allocation de mémoire\n");
 printf("----------------------------------------\n");
 #if 0
 #include <stdlib.h>
 void * malloc(size_t taille);
 #endif
 remarques :
 malloc renvoie une valeur de type void *, il n'est pas nécessaire de faire une
 conversion explicite (cela est nécessaire en C++).
 l'argument taille est de type size_t, l'appel à la fonction malloc devrait
 toujours se faire conjointement à un appel à l'opérateur sizeof.
 La zone mémoire allouée, si l'appel réussit, n'est pas initialisée. C'est une
 erreur de conception grave que d'accéder au bloc mémoire en s'attendant à trouver
 une certaine valeur (0 par exemple).
 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)
 /* Stockage de la valeur "10" dans la zone mémoire pointée par ptr */
 *ptr = 10;
 /* Libérer la mémoire utilisée */
 free(ptr);
 /* Pour éviter les erreurs */
 ptr = NULL;
 else
 /* décider du traitement en cas d'erreur */
 résumé : pour allouer on utilise
 void * malloc(size_t taille); avec sizeof
 tester si malloc à réussi
 if (ptr != NULL)
 */
 printf("\n----------------------------------------\n");
 printf(" 2. free: libération de mémoire\n");
 printf("----------------------------------------\n");
 #if 0
 void free( void * pointeur );
 #endif
 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.
 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é :
 * d'attribuer la valeur nulle (NULL) au pointeur juste après la libération de la
 zone pointée, et à toute autre pointeur faisant référence à la même adresse,
 * de tester la valeur nulle avant toute utilisation d'un pointeur.
 De plus, donner à free l'adresse d'un objet qui n'a pas été alloué par une des
 fonctions d'allocation cause un comportement indéfini.
 résumé : on libère avec void free( void * pointeur );
 et pour éviter les erreurs, on attribue NULL
 pointeur = NULL;
 printf("\n----------------------------------------\n");
 printf(" 3. calloc : allocation\n");
 printf(" avec initialisation à 0\n");
 printf("----------------------------------------\n");
 #if 0
 void * calloc(size_t nb_element, size_t taille);
 #endif
 De manière similaire à malloc, calloc retourne un pointeur de type void* pointant
 une zone de nb_element*taille octets allouée en mémoire, dont tous les bits
 seront initialisés à 0, ou retourne un pointeur nul en cas d'échec.
 /* allouer un tableau de 5 entiers */
 ptr = calloc ( 5, sizeof(int) );
 /* Le pointeur contient l'adresse du premier élément du tableau : */
 *ptr = 3; /* premier entier : 3 */
 /* 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; */
 free(ptr);
 ptr = NULL;
 Notez que calloc 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 double de
 manière portable, par exemple, il est nécessaire d'assigner la valeur 0.0 à
 chaque élément du tableau. Étant donné qu'on initialise chaque élément
 « manuellement », on peut dans ce cas utiliser malloc plutôt que calloc (la
 première étant normalement beaucoup plus rapide que la seconde).
 résumé : on alloue avec
 void * calloc(size_t nb_element, size_t taille);
 on utilise sizeof pour taille
 tous les bits sont mis à 0.
 on utilise [] pour accéder aux éléments
 printf("\n----------------------------------------\n");
 printf(" 4. realloc\n");
 printf("----------------------------------------\n");
 Il arrive fréquemment qu'un bloc alloué n'ait pas la taille suffisante pour
 accueillir de nouvelles données. La fonction realloc est utilisée pour changer
 (agrandir ou réduire) la taille d'une zone allouée par malloc, calloc, ou realloc.
 #if 0
 void * realloc(void * ancien_bloc, size_t nouvelle_taille);
 #endif
 realloc tentera de réajuster la taille du bloc pointé par ancien_bloc à la
 nouvelle taille spécifiée. À noter :
 * si nouvelle_taille vaut zéro, l'appel est équivalent à free(ancien_bloc).
 * si ancien_bloc est nul, l'appel est équivalent à malloc(nouvelle_taille).
 * En cas de succès, realloc alloue un espace mémoire de taille nouvelle_taille,
 copie le contenu pointé par le paramètre pointeur(ancien_bloc) dans ce nouvel
 espace (en tronquant éventuellement si la nouvelle taille est inférieure à la
 précédente), puis libère l'espace pointé et retourne un pointeur vers la
 nouvelle zone mémoire.
 * En cas d'échec, cette fonction ne libère pas l'espace mémoire actuel, et
 retourne une adresse nulle.
 Notez bien que realloc ne peut que modifier des espaces mémoires qui ont été
 alloués par malloc, calloc, ou realloc. En effet, autoriser realloc à 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 à realloc, comme illustré par le code suivant :
 Avertissement Ce code contient une erreur volontaire !
 #if 0
 void f(void)
 int tab[10];
 int *ptr = realloc(tab, 20 * sizeof(int));
 #endif
 Lorsque realloc reçoit la valeur de tab, qui est un pointeur sur le premier
 élément (i.e. &tab[0]), 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 :
 #if 0
 #include <stdint.h> /* pour SIZE_MAX */
 #include <stdlib.h>
 '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;
 #endif
 La fonction double en elle-même ne comporte pas d'erreur, mais elle peut causer
 des plantages suivant la valeur de ptr 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 ptr... et que les programmeurs qui l'utilisent y fassent
 attention.
 On peut aussi noter que, quand realloc 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 (n <= SIZE_MAX / 2). Il permet d'éviter un débordement
 entier : si n était supérieur à cette valeur, le produit n * 2 devrait avoir une
 valeur supérieure à SIZE_MAX, qui est la plus grande valeur représentable par le
 type size_t. Lorsque cette valeur est passée à realloc, la conversion en size_t,
 qui est un type entier non signé, se fera modulo SIZE_MAX + 1, et donc la
 fonction recevra une valeur différente de celle voulue. Si le test n'était pas
 fait, double 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 à realloc) 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:
 Avertissement Ce code contient une erreur volontaire !
 #if 0
 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));
 #endif
 En effet, si realloc échoue, la valeur de ptr est alors nulle, et on aura perdu
 la référence vers l'espace de taille 10 * sizeof(int) qu'on a déjà alloué. Ce
 type d'erreur s'appelle une fuite mémoire. Il faut donc faire ainsi:
 #if 0
 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 */
 #endif
 Exemple
 realloc 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 lire_entree qui :
 * lit sur une entrée quelconque (par exemple stdin) un entier, et renvoie 1 si la
 lecture s'est bien passée ;
 * renvoie 0 si aucune valeur n'est présente sur l'entrée, ou en cas d'erreur.
 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 realloc.
 #if 0
 /* 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;
 #endif
 Ici, on utilise max pour se souvenir du nombre d'éléments que contient la zone de
 mémoire allouée, et i pour le nombre d'éléments effectivement utilisés. Quand on
 a pu lire un entier depuis l'entrée, et que i vaut max, 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 max 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é à realloc est nul, la fonction se comporte
 comme malloc.
 Le choix de la formule de calcul de max à utiliser chaque fois que le tableau est
 rempli résulte d'un compromis :
 * augmenter max peu à peu permet de ne pas gaspiller trop de mémoire, mais on
 appellera realloc très souvent.
 * augmenter très vite max génère relativement peu d'appels à realloc, mais une
 grande partie de la zone mémoire peut être perdue.
 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.
 résumé : pour agrandir ou réduire
 une zone allouée par malloc, calloc, ou realloc
 on utilise
 void * realloc(void * ancien_bloc, size_t nouvelle_taille);
 en cas d'échec
 cette fonction ne libère pas l'espace mémoire actuel
 retourne NULL
 on ne fait pas
 ancien_pointeur = realloc(...);
 mais
 tmp = realloc(...);
 if (tmp == NULL)
 free(ancien_pointeur);
 return EXIT_FAILURE;
 else
 ancien_pointeur = tmp;
 */
 printf("\n----------------------------------------\n");
 printf(" 5. Problèmes et erreurs classiques\n");
 printf("----------------------------------------\n");
 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 malloc.
 Exemple:
 Avertissement Ce code contient une erreur volontaire !
 #if 0
 /* Déclaration sans initialisation */
 int *ptr;
 int var;
 /* On stocke dans var la valeur de la zone mémoire pointée par ptr
 ptr n'a pas été initialisé, ptr a une valeur aléatoire ! */
 var = *ptr;
 #endif
 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 :
 Avertissement Ce code contient une erreur volontaire !
 #if 0
 int* ptr = malloc(sizeof(int)); // allouer une zone mémoire pour un entier
 int* ptr2 = ptr; // ptr2 pointe la même zone mémoire
 *ptr = 5;
 printf("%d\n", *ptr2 ); // affiche 5
 /* libération */
 free( ptr );
 ptr = NULL;
 *ptr2 = 10; /* <- résultat imprévisible (plantage de l'application, ...) */
 #endif
 En règle générale, il faut éviter que plusieurs variables pointent la même zone
 mémoire allouée.
 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:
 Avertissement Ce code contient une erreur volontaire !
 #if 0
 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 ...*/
 #endif
 À la sortie de cette boucle on a alloué un millier d'entiers soit environ 1000 * 4
 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.
 résumé :
 un pointeur doit être initialisé à NULL
 éviter les références multiples à une même zone mémoire
 toutes les références multiples doivent être mises à NULL après free()
 attention au fuite mémoire !
 on ne change pas un pointeur avant un appel à free()
 return 0;