Gestion de la mémoire C

J’ai toujours entendu dire qu’en C, il fallait vraiment regarder comment gérer la mémoire. Et je commence toujours à apprendre le C, mais jusqu’à présent, je n’ai pas eu à faire de la gestion de la mémoire du tout. J’ai toujours imaginé devoir publier des variables et faire toutes sortes de choses laides. Mais cela ne semble pas être le cas.

Est-ce que quelqu’un peut me montrer (avec des exemples de code) un exemple de quand il faudrait faire de la “gestion de la mémoire”?

Il y a deux endroits où les variables peuvent être mises en mémoire. Lorsque vous créez une variable comme celle-ci:

int a; char c; char d[16]; 

Les variables sont créées dans la ” stack “. Les variables de stack sont automatiquement libérées lorsqu’elles sont hors de scope (lorsque le code ne peut plus les atteindre). Vous pourriez les entendre appeler des variables “automatiques”, mais cela est devenu démodé.

De nombreux exemples de débutants utiliseront uniquement des variables de stack.

La stack est intéressante car elle est automatique, mais elle présente également deux inconvénients: (1) Le compilateur doit connaître à l’avance la taille des variables et (b) l’espace de la stack est quelque peu limité. Par exemple: sous Windows, sous les parameters par défaut de l’éditeur de liens Microsoft, la stack est définie sur 1 Mo et la totalité n’est pas disponible pour vos variables.

Si vous ne savez pas au moment de la compilation quelle est la taille de votre tableau, ou si vous avez besoin d’un grand tableau ou d’une structure, vous avez besoin du “plan B”.

Le plan B est appelé le ” tas “. Vous pouvez généralement créer des variables aussi grandes que le système d’exploitation vous le permet, mais vous devez le faire vous-même. Les publications antérieures vous ont montré une façon de le faire, même s’il existe d’autres moyens:

 int size; // ... // Set size to some value, based on information available at run-time. Then: // ... char *p = (char *)malloc(size); 

(Notez que les variables dans le tas ne sont pas manipulées directement, mais via des pointeurs)

Une fois que vous avez créé une variable de segment, le problème est que le compilateur ne peut pas savoir quand vous en avez fini, vous perdez donc la libération automatique. C’est là qu’intervient la “libération manuelle”. Votre code est maintenant responsable de décider à quel moment la variable n’est plus nécessaire et de la libérer pour que la mémoire puisse être utilisée à d’autres fins. Pour le cas ci-dessus, avec:

 free(p); 

Ce qui rend cette seconde option “entreprise désagréable”, c’est qu’il n’est pas toujours facile de savoir quand la variable n’est plus nécessaire. En oubliant de libérer une variable lorsque vous n’en avez pas besoin, votre programme consumra davantage de mémoire. Cette situation s’appelle une “fuite”. La mémoire “fuite” ne peut être utilisée pour rien jusqu’à ce que votre programme se termine et que le système d’exploitation récupère toutes ses ressources. Même des problèmes plus dangereux sont possibles si vous relâchez une variable de tas par erreur avant que vous en ayez terminé.

En C et C ++, vous devez nettoyer vos variables de segment de mémoire, comme indiqué ci-dessus. Cependant, il existe des langages et des environnements tels que les langages Java et .NET tels que C # qui utilisent une approche différente, où le tas est nettoyé seul. Cette seconde méthode, appelée “garbage collection”, est beaucoup plus facile pour le développeur, mais vous payez une pénalité en frais généraux et en performances. C’est un équilibre

(J’ai passé sous silence de nombreux détails pour donner une réponse plus simple, mais avec un peu plus de précision)

Voici un exemple. Supposons que vous ayez une fonction strdup () qui duplique une chaîne:

 char *strdup(char *src) { char * dest; dest = malloc(strlen(src) + 1); if (dest == NULL) abort(); strcpy(dest, src); return dest; } 

Et vous l’appelez comme ceci:

 main() { char *s; s = strdup("hello"); printf("%s\n", s); s = strdup("world"); printf("%s\n", s); } 

Vous pouvez voir que le programme fonctionne, mais vous avez alloué de la mémoire (via malloc) sans le libérer. Vous avez perdu votre pointeur sur le premier bloc de mémoire lorsque vous avez appelé strdup la deuxième fois.

Ce n’est pas grave pour cette petite quantité de mémoire, mais considérez le cas:

 for (i = 0; i < 1000000000; ++i) /* billion times */ s = strdup("hello world"); /* 11 bytes */ 

Vous avez maintenant utilisé 11 Go de mémoire (peut-être plus, en fonction de votre gestionnaire de mémoire) et si vous ne vous êtes pas écrasé, votre processus fonctionnera probablement assez lentement.

Pour corriger, vous devez appeler free () pour tout ce qui est obtenu avec malloc () une fois que vous avez fini de l'utiliser:

 s = strdup("hello"); free(s); /* now not leaking memory! */ s = strdup("world"); ... 

J'espère que cet exemple aide!

Vous devez faire de la “gestion de la mémoire” lorsque vous souhaitez utiliser de la mémoire sur le tas plutôt que sur la stack. Si vous ne savez pas quelle taille faire un tableau avant l’exécution, vous devez utiliser le segment de mémoire. Par exemple, vous voudrez peut-être stocker quelque chose dans une chaîne, mais vous ne saurez pas quelle sera la taille de son contenu tant que le programme ne sera pas exécuté. Dans ce cas, vous écrivez quelque chose comme ceci:

  char *ssortingng = malloc(ssortingnglength); // ssortingnglength is the number of bytes to allocate // Do something with the ssortingng... free(ssortingng); // Free the allocated memory 

Je pense que la manière la plus concise de répondre à la question consiste à considérer le rôle du pointeur en C. Le pointeur est un mécanisme léger mais puissant qui vous offre une immense liberté au prix d’une immense capacité à vous tirer dans le pied.

En C, la responsabilité de vous assurer que vos pointeurs pointent vers la mémoire que vous possédez est à vous et à vous seul. Cela nécessite une approche organisée et disciplinée, à moins que vous n’abandonniez des pointeurs, ce qui rend difficile l’écriture efficace.

Les réponses envoyées à ce jour se concentrent sur les allocations de variables automatiques (stack) et de tas. L’utilisation de l’allocation de stack rend la mémoire gérée et pratique automatiquement, mais dans certaines circonstances (tampons volumineux, algorithmes récursifs), cela peut entraîner un problème épouvantable de débordement de stack. Savoir exactement combien de mémoire vous pouvez allouer sur la stack dépend beaucoup du système. Dans certains scénarios incorporés, quelques dizaines d’octets peuvent être votre limite, dans certains scénarios de bureau, vous pouvez utiliser en toute sécurité des mégaoctets.

L’allocation de tas est moins inhérente à la langue. Il s’agit essentiellement d’un ensemble d’appels de bibliothèque qui vous permet de posséder un bloc de mémoire d’une taille donnée jusqu’à ce que vous soyez prêt à le renvoyer («libre»). Cela semble simple, mais est associé à un chagrin indescriptible de programmeur. Les problèmes sont simples (libérer la même mémoire deux fois, voire pas du tout [memory leaks], ne pas allouer suffisamment de mémoire [buffer overflow], etc.) mais difficile à éviter et à déboguer. Une approche hautement disciplinée est absolument obligatoire dans la pratique mais, bien sûr, la langue ne le prescrit pas.

J’aimerais mentionner un autre type d’allocation de mémoire qui a été ignoré par d’autres articles. Il est possible d’allouer statiquement des variables en les déclarant en dehors de toute fonction. Je pense qu’en général ce type d’allocation a un mauvais rap parce qu’il est utilisé par des variables globales. Cependant, rien ne dit que la seule façon d’utiliser la mémoire ainsi allouée est une variable globale indisciplinée dans un désordre de code spaghetti. La méthode d’allocation statique peut être utilisée simplement pour éviter certains des pièges du tas et des méthodes d’allocation automatique. Certains programmeurs C sont surpris d’apprendre que de grands programmes sophistiqués, embarqués en C et de jeux, ont été construits sans aucune utilisation de l’allocation de tas.

Une chose à retenir est de toujours initialiser vos pointeurs sur NULL, car un pointeur non initialisé peut contenir une adresse mémoire valide de manière pseudo-aléatoire qui peut provoquer des erreurs de pointeur en mode silencieux. En imposant un pointeur à initialiser avec NULL, vous pouvez toujours intercepter si vous utilisez ce pointeur sans l’initialiser. La raison en est que les systèmes d’exploitation “connectent” l’adresse virtuelle 0x00000000 aux exceptions de protection générale pour intercepter l’utilisation du pointeur nul.

Il y a d’excellentes réponses sur la façon d’allouer et de libérer de la mémoire. À mon avis, le plus difficile en utilisant C est de s’assurer que la seule mémoire que vous utilisez est de la mémoire que vous avez allouée. avec le cousin de ce site – un débordement de tampon – et vous pouvez écraser la mémoire utilisée par une autre application, avec des résultats très imprévisibles.

Un exemple:

 int main() { char* mySsortingng = (char*)malloc(5*sizeof(char)); mySsortingng = "abcd"; } 

À ce stade, vous avez alloué 5 octets à mySsortingng et l’avez rempli avec “abcd \ 0” (les chaînes se terminent par un null – \ 0). Si votre allocation de chaîne était

 mySsortingng = "abcde"; 

Vous assigneriez “abcde” dans les 5 octets que vous avez alloués à votre programme, et le caractère nul final serait placé à la fin – une partie de la mémoire qui n’a pas été allouée à votre usage et pourrait être gratuit, mais pourrait également être utilisé par une autre application – Ceci est la partie critique de la gestion de la mémoire, où une erreur aura des conséquences imprévisibles (et parfois irremplaçables).

Vous pouvez également utiliser l’allocation de mémoire dynamic lorsque vous devez définir un grand tableau, disons int [10000]. Vous ne pouvez pas simplement le mettre en stack, car alors, hm … vous aurez un débordement de stack.

Un autre bon exemple serait une implémentation d’une structure de données, par exemple une liste chaînée ou un arbre binary. Je n’ai pas de code exemple à coller ici, mais vous pouvez le google facilement.

(J’écris parce que j’estime que les réponses ne sont pas encore à la hauteur.)

La raison pour laquelle vous avez besoin de mentionner la gestion de mémoire est lorsque vous avez un problème / une solution qui vous oblige à créer des structures complexes. (Si vos programmes se bloquent si vous allouez beaucoup d’espace sur la stack en même temps, c’est un bogue.) En général, la première structure de données à apprendre est une sorte de liste . Voici un lien unique, du haut de ma tête:

 typedef struct listelem { struct listelem *next; void *data;} listelem; listelem * create(void * data) { listelem *p = calloc(1, sizeof(listelem)); if(p) p->data = data; return p; } listelem * delete(listelem * p) { listelem next = p->next; free(p); return next; } void deleteall(listelem * p) { while(p) p = delete(p); } void foreach(listelem * p, void (*fun)(void *data) ) { for( ; p != NULL; p = p->next) fun(p->data); } listelem * merge(listelem *p, listelem *q) { while(p != NULL && p->next != NULL) p = p->next; if(p) { p->next = q; return p; } else return q; } 

Naturellement, vous aimeriez avoir quelques autres fonctions, mais en gros, c’est ce dont vous avez besoin pour la gestion de la mémoire. Je tiens à souligner qu’il existe des astuces qui sont possibles avec la gestion de mémoire “manuelle”, par exemple:

  • En utilisant le fait que malloc est garanti (par le standard de langage) pour renvoyer un pointeur divisible par 4,
  • allouer un espace supplémentaire pour un but sinistre de votre part,
  • créer un pool de mémoire s ..

Obtenez un bon débogueur … Bonne chance!

@ Euro Micelli

Un point négatif à append est que les pointeurs vers la stack ne sont plus valides lorsque la fonction retourne. Vous ne pouvez donc pas renvoyer un pointeur vers une variable de stack à partir d’une fonction. Ceci est une erreur commune et une raison majeure pour laquelle vous ne pouvez pas vous en sortir avec des variables de stack. Si votre fonction a besoin de renvoyer un pointeur, alors vous devez malloc et gérer la gestion de la mémoire.

@ Ted Percival :
… vous n’avez pas besoin de lancer la valeur de retour de malloc ().

Vous avez raison, bien sûr. Je crois que cela a toujours été vrai, même si je n’ai pas de copie de K & R à vérifier.

Je n’aime pas beaucoup les conversions implicites en C, donc j’ai tendance à utiliser des moulages pour rendre la “magie” plus visible. Parfois, cela aide à la lisibilité, parfois pas, et parfois, le compilateur détecte un bogue silencieux. Pourtant, je n’ai pas d’opinion forte à ce sujet, d’une manière ou d’une autre.

Cela est particulièrement probable si votre compilateur comprend les commentaires de style C ++.

Ouais … tu m’as attrapé là-bas. Je passe beaucoup plus de temps en C ++ que C. Merci de l’avoir remarqué.

En C, vous avez en fait deux choix différents. Premièrement, vous pouvez laisser le système gérer la mémoire pour vous. Sinon, vous pouvez le faire vous-même. En règle générale, vous voudrez vous en tenir à la première aussi longtemps que possible. Cependant, la mémoire gérée automatiquement en C est extrêmement limitée et vous devrez gérer la mémoire manuellement dans de nombreux cas, tels que:

une. Vous voulez que la variable dépasse les fonctions et vous ne voulez pas avoir de variable globale. ex:

 struct paire {
    int val;
    struct paire * next;
 }

 struct paire * new_pair (int val) {
    struct paire * np = malloc (sizeof (paire struct));
    np-> val = val;
    np-> next = NULL;
    retourner np;
 }

b. vous voulez avoir de la mémoire allouée dynamicment. L’exemple le plus courant est le tableau sans longueur fixe:

 int * my_special_array;
 my_special_array = malloc (sizeof (int) * number_of_element);
 pour (i = 0; i

c. Vous voulez faire quelque chose de très sale. Par exemple, je voudrais une structure pour représenter plusieurs types de données et je n'aime pas l'union (l'union a l'air tellement désordonnée):

données de structure { int data_type; long data_in_mem; }; struct animal {/ * quelque chose * /}; struct person {/ * autre chose * /}; struct animal * read_animal (); struct person * read_person (); / * En main * / échantillon de données struct; sampe.data_type = input_type; switch (input_type) { case DATA_PERSON: sample.data_in_mem = read_person (); Pause; case DATA_ANIMAL: sample.data_in_mem = read_animal (); défaut: printf ("Oh hoh! Je vous préviens encore une fois et je vais vous reprocher votre système d'exploitation"); }

Voir, une valeur longue est suffisante pour tenir quelque chose. Rappelez-vous juste de le libérer, ou vous regretterez. Ceci est parmi mes astuces préférées pour s’amuser en C: D.

Cependant, en général, vous voudriez restr loin de vos astuces préférées (T___T). Vous allez briser votre système d’exploitation, tôt ou tard, si vous les utilisez trop souvent. Tant que vous n’utilisez pas * alloc et free, vous pouvez toujours dire que vous êtes toujours vierge et que le code est toujours joli.

Sûr. Si vous créez un object qui existe en dehors de la scope, utilisez-le. Voici un exemple artificiel (gardez à l’esprit que ma syntaxe sera désactivée; mon C est rouillé, mais cet exemple illustrera toujours le concept):

 class MyClass { SomeOtherClass *myObject; public MyClass() { //The object is created when the class is constructed myObject = (SomeOtherClass*)malloc(sizeof(myObject)); } public ~MyClass() { //The class is destructed //If you don't free the object here, you leak memory free(myObject); } public void SomeMemberFunction() { //Some use of the object myObject->SomeOperation(); } }; 

Dans cet exemple, j’utilise un object de type SomeOtherClass pendant la durée de vie de MyClass. L’object SomeOtherClass est utilisé dans plusieurs fonctions. J’ai donc alloué dynamicment la mémoire: l’object SomeOtherClass est créé lors de la création de MyClass, utilisé plusieurs fois au cours de la vie de l’object, puis libéré une fois que MyClass est libéré.

Evidemment, si c’était du vrai code, il n’y aurait aucune raison (hormis éventuellement la consommation de mémoire) de créer myObject de cette manière, mais ce type de création / destruction d’object devient utile lorsque vous avez beaucoup d’objects et que vous voulez contrôler quand ils sont créés et détruits (pour que votre application n’absorbe pas 1 Go de RAM pendant toute sa durée de vie, par exemple), et dans un environnement Windowed, c’est plutôt obligatoire, comme les objects que vous créez (disons, disons) , besoin d’exister bien en dehors de la scope d’une fonction particulière (ou même d’une classe).