Aller au contenu

Programmation C source/gestion de la mémoire

Un livre de Wikilivres.
Programmation C source
Programmation C++
Programmation C++
Sommaire
Modifier ce modèle
#include <stdio.h>

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;
}