Section courante

A propos

Section administrative du site

Allocation de mémoire

L'allocation de la mémoire en langage de programmation C se fait essentiellement avec la fonction malloc. Cette fonction permet une allocation de mémoire dynamique en C, et nous aborderons l'allocation de mémoire dynamique en détail. Au fur et à mesure que nous commencerons à allouer de la mémoire dynamique, nous commencerons à voir à quoi les pointeurs peuvent vraiment servir. La plupart des exemples de pointeurs (ceux utilisant des pointeurs pour accéder aux tableaux) n'ont pas fait grand chose pour nous que nous n'aurions pas pu faire en utilisant des tableaux. Cependant, lorsque nous commençons à faire une allocation de mémoire dynamique, les pointeurs sont la seule façon de procéder, car ce que malloc renvoie est un pointeur vers la mémoire qu'il nous donne. (En raison de l'équivalence entre les pointeurs et les tableaux, cependant, nous serons toujours en mesure de penser aux régions d'entreposage allouées dynamiquement comme s'il s'agissait de tableaux, et même d'utiliser une notation d'indice de type tableau sur elles.) Vous devez être prudent avec allocation de mémoire dynamique. La fonction malloc fonctionne à un niveau assez bas; vous devrez souvent faire un certain travail pour gérer la mémoire que cela vous donne. Si vous ne gardez pas une trace précise de la mémoire que malloc vous a donnée, et des pointeurs du vôtre pointant vers lui, il est trop facile d'utiliser accidentellement un pointeur pointant nulle part, avec des résultats généralement désagréables. (Le problème de base est que si vous attribuez une valeur à l'emplacement pointé par un pointeur :

  1. *p = 0;

et si le pointeur p pointe nulle part, et bien en fait il peut être interprété comme pointant quelque part, mais pas là où vous le vouliez, et que le quelque part est l'endroit où le 0 est écrit. Si le quelque part est la mémoire étant utilisée par une autre partie de votre programme, ou pire encore, si le système d'exploitation ne s'est pas protégé contre vous et que le quelque part est en fait utilisé par le système d'exploitation, les choses pourraient devenir laid et plantage pourrait se produire.

Allouer de la mémoire avec malloc

Un problème avec de nombreux programmes simples, est qu'ils ont tendance à utiliser des tableaux de taille fixe pouvant ou non être assez grands. Nous avons un tableau de 100 entiers (int) pour les nombres que l'utilisateur entre et on souhaite trouver la moyenne de celui-ci - et si l'utilisateur entre 101 nombres ? Nous avons un tableau de 100 caractères que nous transmettons à getline pour recevoir l'entrée de l'utilisateur. Que faire si l'utilisateur tape une ligne de 200 caractères ? Si nous avons de la chance, les parties pertinentes du programme vérifient la quantité de tableau qu'elles ont utilisée et affichent un message d'erreur ou abandonnent normalement avant de déborder du tableau. Si nous n'avons pas cette chance, un programme peut quitter la fin d'un tableau, écrasera d'autres données et se comporter assez mal. Dans les deux cas, l'utilisateur ne fait pas son travail. Comment éviter les restrictions des tableaux de taille fixe ? Les réponses impliquent toutes la fonction de bibliothèque standard malloc. Très simplement, malloc renvoie un pointeur vers n octets de mémoire avec lesquels nous pouvons faire tout ce que nous voulons. Si nous ne voulions pas lire une ligne d'entrée dans un tableau de taille fixe, nous pourrions utiliser malloc à la place. Voici la première étape :

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. char * getline(int lenmax) {
  5.     char * line = malloc(lenmax), * linep = line;
  6.     size_t len = lenmax;
  7.     int c;
  8.  
  9.     if(line == NULL) return NULL;
  10.  
  11.     for(;;) {
  12.         c = fgetc(stdin);
  13.         if(c == EOF) break;
  14.         if(--len == 0) {
  15.             len = lenmax;
  16.             char * linen = realloc(linep, lenmax *= 2);
  17.             if(linen == NULL) {
  18.                 free(linep);
  19.                 return NULL;
  20.             }
  21.             line = linen + (line - linep);
  22.             linep = linen;
  23.         }
  24.         if((*line++ = c) == '\n') break;
  25.     }
  26.     *line = '\0';
  27.     return linep;
  28. }
  29.  
  30. int main() {
  31.     char *line;
  32.     int linelen = 100;
  33.     line = getline(linelen);
  34.     printf("Résultat = %s\n",line);
  35.     return 0;
  36. }

La fonction malloc est déclaré dans <stdlib.h>, donc nous utilisons #include avec cet entête dans tout programme appelant malloc. Un octet en C est, par définition, une quantité d'entreposage appropriée pour entreposer un caractère, donc l'invocation ci-dessus de malloc nous donne exactement autant de caractères que nous le demandons. Nous pourrions illustrer le pointeur résultant comme ceci : Les 100 octets de mémoire (n'étant pas tous représentés) pointés par ligne sont ceux alloués par malloc. (Il s'agit d'une toute nouvelle mémoire, conceptuellement un peu différente de la mémoire que le compilateur s'arrange pour avoir allouée automatiquement pour nos variables conventionnelles. Les 100 cases de la figure n'ont pas de nom à côté d'elles, car elles ne sont pas dans l'entreposage pour une variable que nous avons déclarée.)

Comme deuxième exemple, nous pourrions avoir l'occasion d'allouer un morceau de mémoire, et d'y copier une chaîne de caractères avec la fonction strcpy :

  1. #include <stdlib.h>
  2.  
  3. int main() {
  4.     char *p = malloc(15);
  5.     if(p != NULL) strcpy(p, "Bonjour !");
  6.     return 0;
  7. }

Lors de la copie de chaînes de caractères, n'oubliez pas que toutes les chaînes de caractères ont un caractère de fin \0. Si vous utilisez la fonction strlen pour compter les caractères d'une chaîne de caractères à votre place, ce nombre n'inclura pas le \0 de fin, vous devez donc en ajouter un avant d'appeler la fonction malloc :

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. int main() {
  5.     char *somestring, *copy;
  6.      /* ... */
  7.     somestring = "Gladir.com!";
  8.      /* ... */
  9.     copy = malloc(strlen(somestring) + 1); /* +1 pour \0 */
  10.     if(copy != NULL) strcpy(copy, somestring);
  11.     return 0;
  12. }

Et si nous n'allouons pas de caractères, mais des entiers ? Si nous voulons allouer 100 int, combien d'octets est-ce que ça prend ? Si nous connaissons la taille des int sur notre machine (c'est-à-dire selon que nous utilisons une machine 16 ou 32 bits), nous pourrions essayer de le calculer nous-mêmes, mais c'est beaucoup plus sûr et plus portable de laisser C le calculer pour nous. Le langage de programmation C a un opérateur sizeof, calculant la taille, en octets, d'une variable ou d'un type. C'est exactement ce dont nous avons besoin lorsque nous appelons malloc. Pour allouer de l'espace pour 100 int, nous pourrions appeler :

  1. int *ip = malloc(100 * sizeof(int)); 

L'utilisation de l'opérateur sizeof a tendance à ressembler à un appel de fonction, mais c'est vraiment un opérateur et il fait son travail au moment de la compilation. Puisque nous pouvons utiliser la syntaxe d'indexation de tableau sur les pointeurs, nous pouvons traiter une variable de pointeur après un appel à malloc presque exactement comme s'il s'agissait d'un tableau. En particulier, après que l'appel ci-dessus à malloc initialise ip jusqu'au point d'entreposage pour 100 int, nous pouvons accéder à ip[0], ip[1],... jusqu'à ip[99]. De cette façon, nous pouvons obtenir l'effet d'un tableau même si nous ne le savons pas avant l'exécution de la taille du tableau.

Évidemment, il n'y aucun micro-ordinateur réel n'ayant une quantité infinie de mémoire disponible, il n'y a donc aucune garantie que malloc sera en mesure de nous donner autant de mémoire que nous le demandons. Si nous appelons malloc(100000000), ou si nous appelons malloc(10) 10 000 000 de fois, nous allons probablement manquer de mémoire. Lorsque la fonction malloc ne parvient pas à allouer la mémoire demandée, il renvoie un pointeur nul. Un pointeur nul ne pointe définitivement nulle part. C'est un marqueur pas un pointeur; ce n'est pas un pointeur que vous pouvez utiliser. (Un pointeur nul peut être utilisé comme un retour d'échec d'une fonction retournant des pointeurs, et malloc est un exemple parfait.) Par conséquent, chaque fois que vous appelez malloc, il est vital de vérifier le pointeur retourné avant de l'utiliser ! Si vous appelez malloc et qu'il renvoie un pointeur nul et que vous utilisez ce pointeur nul comme s'il pointait quelque part, votre programme risque de planter. Au lieu de cela, un programme devrait immédiatement rechercher un pointeur nul, et s'il en reçoit un, il devrait au moins afficher un message d'erreur et quitter, ou peut-être trouver un moyen de procéder sans la mémoire qu'il a demandée. Mais il ne peut en aucun cas continuer à utiliser le pointeur nul qu'il a récupéré de malloc, car ce pointeur nul ne pointe par définition nulle part. («Il ne peut en aucun cas utiliser un pointeur nul» signifie que le programme ne peut pas utiliser les opérateurs * ou [] sur une telle valeur de pointeur, ni le transmettre à une fonction attendant un pointeur valide.) Un appel à malloc, avec un vérification des erreurs, ressemble généralement à ceci :

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. int main() {
  5.     int *ip = malloc(100 * sizeof(int));
  6.     if(ip == NULL) {
  7.         printf("Manque de mémoire\n");
  8.         exit(EXIT_FAILURE);
  9.     }
  10.     return 0;
  11. }

Après avoir affiché le message d'erreur, ce code doit retourner à son appelant ou quitter complètement le programme; il ne peut pas continuer avec le code ayant utilisé ip. Bien sûr, dans nos exemples jusqu'à présent, nous nous sommes toujours limités aux régions de mémoire de taille fixe, car nous avons appelé malloc avec des paramètres fixes comme 10 ou 100. (Notre appel à getline est toujours limité à 100 lignes de caractères, ou quel que soit le nombre sur lequel nous avons défini la variable linelen; notre variable ip pointe toujours à seulement 100 int) ayant à recompiler le programme (avec un plus grand tableau) pour accueillir des lignes plus longues, et avec un peu plus de travail, nous pourrions faire en sorte que les tableaux deviennent automatiquement aussi grands que nécessaire.

Libérer la mémoire

La mémoire allouée avec malloc dure aussi longtemps que vous le souhaitez. Elle ne disparaît pas automatiquement lors du retour d'une fonction, comme le font les variables de durée automatique, mais elle n'a pas non plus à rester pendant toute la durée de votre programme. Tout comme vous pouvez utiliser malloc pour contrôler exactement quand et combien de mémoire vous allouez, vous pouvez également contrôler exactement quand vous la désallouez. En fait, de nombreux programmes utilisent la mémoire de manière transitoire. Ils allouent de la mémoire, l'utilisent pendant un certain temps, puis atteignent un point où ils n'ont plus besoin de cette pièce particulière. La mémoire n'étant pas inépuisable, il est judicieux de désallouer (c'est-à-dire libérer ou relâcher) la mémoire que vous n'utilisez plus.

La mémoire allouée dynamiquement est désallouée avec la fonction free. Si p contient un pointeur précédemment renvoyé par malloc, vous pouvez appeler :

  1. free(p);

rendant la mémoire au entrepôt de mémoire (parfois appelé «mémoire de tas» ou «bassin») dont les requêtes de malloc sont satisfaites. Appeler free est en quelque sorte le nec plus ultra en matière de recyclage : cela ne vous coûte presque rien et la mémoire que vous rendez est immédiatement utilisable par d'autres parties de votre programme. (Théoriquement, il peut même être utilisable par d'autres programmes.) (Libérer de la mémoire inutilisée est une bonne idée, mais ce n'est pas obligatoire. Lorsque votre programme se ferme, toute mémoire allouée mais non libérée devrait être automatiquement libérée. Si votre micro-ordinateur perdait de la mémoire simplement parce que votre programme a oublié de la libérer, cela indiquerait un problème ou une carence dans votre système d'exploitation.) Naturellement, une fois que vous avez libéré de la mémoire, vous devez vous rappeler de ne plus l'utiliser. Après avoir appelé :

  1. free(p);

il est probablement le cas que p pointe toujours vers la même mémoire. Cependant, comme nous l'avons précédemment expliqué, il est désormais disponible, et un appel ultérieur à malloc pourrait donner cette mémoire à une autre partie de votre programme. Si la variable p est une variable globale ou restera pendant un certain temps, un bon moyen d'enregistrer le fait qu'elle ne doit plus être utilisée serait de la définir sur un pointeur nul :

  1. free(p);
  2. p = NULL;

Maintenant, nous n'avons même plus le pointeur vers la mémoire libérée, et (tant que nous vérifions que p est non-NULL avant de l'utiliser), nous n'utilisons aucune mémoire via le pointeur p. Lorsque vous pensez à malloc, pensez à mémoire libre et allouée dynamiquement en général, rappelez-vous à nouveau la distinction entre un pointeur et ce qu'il pointe. Si vous appelez malloc pour allouer de la mémoire et entreposez le pointeur que malloc vous donne dans une variable de pointeur local, que se passe-t-il lorsque la fonction contenant la variable de pointeur local revient ? Si la variable de pointeur local a une durée automatique (étant la valeur par défaut, sauf si la variable est déclarée statique), elle disparaîtra lorsque la fonction sera renvoyée. Mais la disparition de la variable pointeur ne dit rien sur la mémoire pointée ! Cette mémoire existe toujours et, en ce qui concerne malloc et free, est toujours allouée. La seule chose ayant disparu est la variable de pointeur que vous aviez pointant sur la mémoire allouée. (De plus, s'il contenait la seule copie du pointeur que vous aviez, une fois qu'il disparaîtra, vous n'aurez aucun moyen de libérer la mémoire, ni aucun moyen de l'utiliser non plus. L'utilisation de la mémoire et la libération de mémoire nécessitent toutes deux au moins un pointeur vers la mémoire !)

Réallocation des blocs de mémoire

Parfois, vous n'êtes pas sûr au début de la quantité de mémoire dont vous aurez besoin. Par exemple, si vous avez besoin d'entreposer une série d'éléments que vous avez lus auprès de l'utilisateur, et si le seul moyen de savoir combien il y en a est de les lire jusqu'à ce que l'utilisateur tape un signal de fin, vous n'aurez aucun moyen de savoir, au fur et à mesure que vous commencez à lire et à entreposer les premiers, combien vous en aurez vu au moment où vous voyez ce marqueur de fin. Vous voudrez peut-être allouer de la place pour, par exemple, 100 éléments, et si l'utilisateur entre un 101e élément avant d'entrer le marqueur de fin, vous pourriez souhaiter un moyen de dire «Le malloc se souvient-t-il des 100 éléments que j'ai demandés ? Puis-je changer d'avis et en avoir 200 à la place ?» En fait, vous pouvez faire exactement cela, avec la fonction realloc. Vous remettez un ancien pointeur (comme vous l'avez reçu lors d'un appel initial à malloc) et une nouvelle taille, et la fonction realloc fait ce qu'il peut pour vous donner un morceau de mémoire suffisamment grand pour contenir la nouvelle taille. Par exemple, si nous voulions que la variable ip d'un exemple précédent pointe sur 200 int au lieu de 100, nous pourrions essayer d'appeler :

  1. ip = realloc(ip, 200 * sizeof(int)); 

Puisque vous voulez toujours que chaque bloc de mémoire allouée dynamiquement soit contigu (afin que vous puissiez le traiter comme s'il s'agissait d'un tableau), vous et realloc devez vous soucier du cas où la réallocation ne peut pas agrandir l'ancien bloc de mémoire «en place», mais doit plutôt le déplacer ailleurs afin de trouver suffisamment d'espace contigu pour la nouvelle taille demandée. La fonction realloc le fait en renvoyant un nouveau pointeur. Si realloc a pu agrandir l'ancien bloc de mémoire, il renvoie le même pointeur. Si realloc doit aller ailleurs pour obtenir suffisamment de mémoire contiguë, il renvoie un pointeur vers la nouvelle mémoire, après y avoir copié vos anciennes données. (Dans ce cas, après avoir effectué la copie, il libère l'ancien bloc.) Enfin, si la fonction realloc ne trouve pas assez de mémoire pour satisfaire la nouvelle requête, il renvoie un pointeur nul. Par conséquent, vous ne souhaitez généralement pas remplacer votre ancien pointeur par la valeur de retour de la fonction realloc tant que vous ne l'avez pas testé pour vous assurer qu'il ne s'agit pas d'un pointeur nul. Vous pouvez utiliser un code comme celui-ci :

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3.  
  4. int main() {
  5.     int *ip = malloc(100 * sizeof(int));
  6.     int *newp;
  7.      /* ... */
  8.     newp = realloc(ip, 200 * sizeof(int));
  9.     if(newp != NULL) {
  10.         ip = newp;
  11.     } else {
  12.         printf("Manque de mémoire\n");
  13.         exit(EXIT_FAILURE);
  14.     }
  15.     return 0;
  16. }
  17.  

Si la fonction realloc renvoie autre chose qu'un pointeur nul, il a réussi et nous définissons ip sur ce qu'il a renvoyé. (Nous avons soit défini ip sur ce qu'il était auparavant, soit sur un nouveau pointeur, mais dans les deux cas, il pointe vers où se trouvent nos données.) Si la fonction realloc renvoie un pointeur nul, nous nous accrochons à notre ancien pointeur dans ip pointant toujours vers nos 100 valeurs d'origine.

Sécurité du pointeur

Le plus dur avec les pointeurs n'est pas tant de les manipuler que de s'assurer que la mémoire qu'ils pointent est valide. Lorsqu'un pointeur ne pointe pas où vous pensez qu'il le fait, si vous accédez ou modifiez par inadvertance la mémoire vers laquelle il pointe, vous pouvez endommager d'autres parties de votre programme ou (dans certains cas) d'autres programmes ou le système d'exploitation lui-même ! Lorsque nous utilisons des pointeurs vers des variables simples, il n'y a pas grand chose pouvant mal tourner. Lorsque nous utilisons des pointeurs dans des tableaux, et commençons à déplacer les pointeurs, nous devons être plus prudents, pour nous assurer que les pointeurs itinérants restent toujours dans les limites du ou des tableaux. Lorsque nous commençons à passer des pointeurs vers des fonctions, et surtout lorsque nous commençons à les renvoyer depuis des fonctions nous devons être plus prudents encore, car le code utilisant le pointeur peut être très éloigné du code possédant ou allouant la mémoire. Un problème particulier concerne les fonctions renvoyant des pointeurs. Où est la mémoire vers laquelle pointe le pointeur renvoyé ? Est-il toujours là au moment du retour de la fonction ? La fonction renvoie soit un pointeur nul (ne pointant définitivement nulle part, et que l'appelant vérifie probablement), soit un pointeur pointant dans la chaîne de caractères d'entrée, que l'appelant a fournie, ce qui est assez sûr. Une fonction ne doit pas faire en sorte, cependant, de renvoyer un pointeur vers l'un de ses propres tableaux de durée automatique locaux. N'oubliez pas que les variables de durée automatique (incluant toutes les variables locales non statiques), y compris les tableaux de durée automatique, sont désallouées et disparaissent lorsque la fonction est renvoyée. Si une fonction renvoie un pointeur vers un tableau local, ce pointeur sera invalide au moment où l'appelant essaiera de l'utiliser. Enfin, lorsque nous procédons à une allocation dynamique de mémoire avec malloc, realloc et free, nous devons faire très attention. L'allocation dynamique nous donne beaucoup plus de flexibilité dans la façon dont nos programmes utilisent la mémoire, bien qu'avec cette flexibilité vient la responsabilité de gérer soigneusement la mémoire allouée dynamiquement. Les possibilités de pointeurs mal dirigés et de chaos associé sont les plus grandes dans les programmes utilisant beaucoup l'allocation de mémoire dynamique. Vous pouvez réduire ces possibilités en concevant votre programme de telle manière qu'il soit facile de s'assurer que les pointeurs sont utilisés correctement et que la mémoire est toujours correctement allouée et désallouée. (Si, en revanche, votre programme est conçu de telle manière que le respect de ces garanties est une nuisance fastidieuse, tôt ou tard vous l'oublierez ou le négligerez, et l'entretien sera un cauchemar.)



Dernière mise à jour : Dimanche, le 8 novembre 2020