Question

J'ai toujours entendu dire qu'en C, il faut vraiment surveiller la façon dont on gère la mémoire.Et je commence encore à apprendre le C, mais jusqu'à présent, je n'ai pas du tout eu à faire de gestion de mémoire pour des activités connexes.J'ai toujours imaginé devoir libérer des variables et faire toutes sortes de choses laides.Mais cela ne semble pas être le cas.

Quelqu'un peut-il me montrer (avec des exemples de code) un exemple de cas dans lequel vous devrez effectuer une "gestion de la mémoire" ?

Était-ce utile?

La solution

Il existe deux emplacements 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 le "empiler".Les variables de pile sont automatiquement libérées lorsqu'elles sortent de leur portée (c'est-à-dire lorsque le code ne peut plus les atteindre).Vous les entendrez peut-être appelées variables « automatiques », mais cela est passé de mode.

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

La pile est sympa car elle est automatique, mais elle présente aussi deux inconvénients :(1) Le compilateur doit connaître à l'avance la taille des variables et (b) l'espace de la pile est quelque peu limité.Par exemple:sous Windows, selon les paramètres par défaut de l'éditeur de liens Microsoft, la pile 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 grande structure, vous avez besoin du "plan B".

Le plan B s'appelle 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.Des publications précédentes vous ont montré une façon de procéder, bien qu'il existe d'autres méthodes :

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

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

Une fois que vous avez créé une variable de tas, le problème est que le compilateur ne peut pas savoir quand vous en avez terminé, vous perdez donc la libération automatique.C'est là qu'intervient la "libération manuelle" à laquelle vous faisiez référence.Votre code est désormais chargé de décider quand la variable n'est plus nécessaire et de la libérer afin que la mémoire puisse être utilisée à d'autres fins.Pour le cas ci-dessus, avec :

free(p);

Ce qui rend cette deuxième option « une mauvaise affaire », c'est qu'il n'est pas toujours facile de savoir quand la variable n'est plus nécessaire.Oublier de libérer une variable alors que vous n'en avez pas besoin entraînera votre programme à consommer plus de mémoire qu'il n'en a besoin.Cette situation est appelée 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.Des problèmes encore plus graves sont possibles si vous publiez une variable de tas par erreur avant en fait, vous en avez fini avec ça.

En C et C++, vous êtes responsable du nettoyage de vos variables de tas comme indiqué ci-dessus.Cependant, il existe des langages et des environnements tels que Java et les langages .NET comme C# qui utilisent une approche différente, dans laquelle le tas est nettoyé tout seul.Cette deuxième méthode, appelée "garbage collection", est beaucoup plus simple pour le développeur mais vous payez une pénalité en termes de frais généraux et de performances.C'est un équilibre.

(J'ai passé sous silence de nombreux détails pour donner une réponse plus simple, mais, espérons-le, plus nivelée)

Autres conseils

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 tu l'appelles ainsi :

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 la libérer.Vous avez perdu votre pointeur vers le premier bloc mémoire lorsque vous avez appelé strdup pour la deuxième fois.

Ce n'est pas grave pour cette petite quantité de mémoire, mais considérons 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, selon votre gestionnaire de mémoire) et si vous n'avez pas planté, votre processus s'exécute probablement assez lentement.

Pour résoudre ce problème, 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 vous aidera !

Vous devez effectuer une "gestion de la mémoire" lorsque vous souhaitez utiliser la mémoire sur le tas plutôt que sur la pile.Si vous ne savez pas quelle taille créer un tableau avant l’exécution, vous devez utiliser le tas.Par exemple, vous souhaiterez peut-être stocker quelque chose dans une chaîne, mais vous ne savez pas quelle sera la taille de son contenu jusqu'à l'exécution du programme.Dans ce cas, vous écririez quelque chose comme ceci :

 char *string = malloc(stringlength); // stringlength is the number of bytes to allocate

 // Do something with the string...

 free(string); // Free the allocated memory

Je pense que la manière la plus concise de répondre à la question est de 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 une balle dans le pied.

En C, la responsabilité de garantir que vos pointeurs pointent vers la mémoire que vous possédez vous appartient et n'appartient qu'à vous.Cela nécessite une approche organisée et disciplinée, à moins que vous n'abandonniez les pointeurs, ce qui rend difficile l'écriture d'un langage C efficace.

Les réponses publiées à ce jour se concentrent sur les allocations automatiques de variables (pile) et de tas.L'utilisation de l'allocation de pile permet une gestion automatique et pratique de la mémoire, mais dans certaines circonstances (tampons volumineux, algorithmes récursifs), cela peut conduire à l'horrible problème de débordement de pile.Savoir exactement combien de mémoire vous pouvez allouer sur la pile dépend beaucoup du système.Dans certains scénarios intégrés, quelques dizaines d'octets peuvent constituer votre limite. Dans certains scénarios de bureau, vous pouvez utiliser des mégaoctets en toute sécurité.

L'allocation de tas est moins inhérente au langage.Il s'agit essentiellement d'un ensemble d'appels à la bibliothèque qui vous accordent la propriété d'un bloc de mémoire d'une taille donnée jusqu'à ce que vous soyez prêt à le restituer (« gratuitement »).Cela semble simple, mais est associé à un chagrin indescriptible pour les programmeurs.Les problèmes sont simples (libérer deux fois la même mémoire, ou pas du tout [fuites de mémoire], ne pas allouer suffisamment de mémoire [débordement de tampon], etc.) mais difficiles à éviter et à déboguer.Une approche très disciplinée est absolument obligatoire dans la pratique, mais bien sûr, le langage ne l'exige pas réellement.

Je voudrais 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 mauvaise réputation car il est utilisé par des variables globales.Cependant, rien n'indique que la seule façon d'utiliser la mémoire allouée de cette manière est de le faire en tant que variable globale indisciplinée dans un fouillis de code spaghetti.La méthode d'allocation statique peut être utilisée simplement pour éviter certains des pièges des méthodes d'allocation de tas et d'allocation automatique.Certains programmeurs C sont surpris d'apprendre que de vastes et sophistiqués programmes de jeux et embarqués en C ont été construits sans aucune utilisation d'allocation de tas.

Il y a ici d'excellentes réponses sur la façon d'allouer et de libérer de la mémoire, et à mon avis, le côté le plus difficile de l'utilisation de C est de s'assurer que la seule mémoire que vous utilisez est la mémoire que vous avez allouée - si cela n'est pas fait correctement, ce que vous finissez up with est le cousin de ce site - un débordement de tampon - et vous risquez d'écraser la mémoire utilisée par une autre application, avec des résultats très imprévisibles.

Un exemple:

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

À ce stade, vous avez alloué 5 octets pour myString et l'avez rempli avec "abcd\0" (les chaînes se terminent par null - \0).Si votre allocation de chaîne était

myString = "abcde";

Vous attribueriez "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 - C'est la partie critique de la gestion de la mémoire, où une erreur aura des conséquences imprévisibles (et parfois irremplaçables).

Une chose à retenir est de toujours initialisez vos pointeurs sur NULL, car un pointeur non initialisé peut contenir une adresse mémoire valide pseudo-aléatoire qui peut provoquer des erreurs de pointeur silencieusement.En imposant l'initialisation d'un pointeur avec NULL, vous pouvez toujours détecter si vous utilisez ce pointeur sans l'initialiser.La raison en est que les systèmes d'exploitation « câblent » l'adresse virtuelle 0x00000000 aux exceptions de protection générale pour piéger l'utilisation du pointeur nul.

Vous souhaiterez peut-être également utiliser l'allocation dynamique de mémoire lorsque vous devez définir un énorme tableau, par exemple int[10000].Vous ne pouvez pas simplement le mettre en pile parce qu'alors, hm...vous obtiendrez un débordement de pile.

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

(J'écris parce que je pense que les réponses jusqu'à présent ne sont pas tout à fait exactes.)

La raison pour laquelle vous devez mentionner la gestion de la mémoire est lorsque vous avez un problème/une solution qui vous oblige à créer des structures complexes.(Si vos programmes plantent si vous allouez trop d'espace sur la pile à la fois, c'est un bug.) Généralement, la première structure de données que vous devrez apprendre est une sorte de liste.En voici un seul lié, qui me vient à l'esprit :

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 quelques autres fonctions, mais en gros, c'est pour cela que vous avez besoin de la gestion de la mémoire.Je dois souligner qu'il existe un certain nombre d'astuces possibles avec la gestion "manuelle" de la mémoire, par exemple :

  • En utilisant le fait que malloc est garanti (par le standard du langage) de renvoyer un pointeur divisible par 4,
  • allouer un espace supplémentaire à votre propre objectif sinistre,
  • créer pool de mémoires..

Procurez-vous un bon débogueur... Bonne chance!

@Euro Micelli

Un inconvénient à ajouter est que les pointeurs vers la pile ne sont plus valides au retour de la fonction, vous ne pouvez donc pas renvoyer un pointeur vers une variable de pile à partir d'une fonction.Il s'agit d'une erreur courante et d'une raison majeure pour laquelle vous ne pouvez pas vous en sortir avec uniquement des variables de pile.Si votre fonction doit renvoyer un pointeur, vous devez alors effectuer un malloc et gérer la gestion de la mémoire.

@Ted Percival:
... vous n'avez pas besoin de convertir 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, j'ai donc tendance à utiliser des conversions pour rendre la « magie » plus visible.Parfois, cela améliore la lisibilité, parfois non, et parfois cela provoque la détection d'un bogue silencieux par le compilateur.Pourtant, je n’ai pas d’opinion bien arrêtée à 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à.Je passe beaucoup plus de temps en C++ qu'en C.Merci d'avoir remarqué cela.

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.Alternativement, vous pouvez le faire vous-même.En général, vous voudriez vous en tenir au premier aussi longtemps que possible.Cependant, la mémoire auto-gérée en C est extrêmement limitée et vous devrez gérer manuellement la mémoire dans de nombreux cas, tels que :

un.Vous voulez que la variable survive aux fonctions et vous ne voulez pas avoir de variable globale.ex:

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

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

b.vous souhaitez disposer d'une mémoire allouée dynamiquement.L'exemple le plus courant est un tableau sans longueur fixe :

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

c.Vous voulez faire quelque chose de VRAIMENT sale.Par exemple, je voudrais qu'une structure représente de nombreux types de données et je n'aime pas l'union (l'union a l'air tellement compliquée) :

struct data{ int data_type; long data_in_mem; }; struct animal{/*something*/}; struct person{/*some other thing*/}; struct animal* read_animal(); struct person* read_person(); /*In main*/ struct data sample; sampe.data_type = input_type; switch(input_type){ case DATA_PERSON: sample.data_in_mem = read_person(); break; case DATA_ANIMAL: sample.data_in_mem = read_animal(); default: printf("Oh hoh! I warn you, that again and I will seg fault your OS"); }

Vous voyez, une valeur longue est suffisante pour contenir TOUT.N'oubliez pas de le libérer, sinon vous le regretterez.Cela fait partie de mes astuces préférées pour m'amuser en C :D.

Cependant, en général, vous voudriez rester à l’écart de vos tricks préférés (T___T).Vous briserez votre système d’exploitation, tôt ou tard, si vous l’utilisez trop souvent.Tant que vous n'utilisez pas *alloc et free, on peut dire que vous êtes toujours vierge et que le code est toujours joli.

Bien sûr.Si vous créez un objet qui existe en dehors de la portée dans laquelle vous l'utilisez.Voici un exemple artificiel (gardez à l'esprit que ma syntaxe sera erronée ;mon C est rouillé, mais cet exemple illustrera quand même 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 objet de type SomeOtherClass pendant la durée de vie de MyClass.L'objet SomeOtherClass est utilisé dans plusieurs fonctions, j'ai donc alloué dynamiquement la mémoire :l'objet SomeOtherClass est créé lors de la création de MyClass, utilisé plusieurs fois au cours de la vie de l'objet, puis libéré une fois MyClass libéré.

Évidemment, s'il s'agissait de vrai code, il n'y aurait aucune raison (à part éventuellement la consommation de mémoire de la pile) de créer myObject de cette manière, mais ce type de création/destruction d'objet devient utile lorsque vous avez beaucoup d'objets et que vous souhaitez contrôler finement lorsqu'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 fenêtré, cela est quasiment obligatoire, car les objets que vous créez (des boutons, par exemple) , doivent exister bien en dehors de la portée d'une fonction particulière (ou même d'une classe).

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top