Gestion de la mémoire C

90

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

Quelqu'un peut-il me montrer (avec des exemples de code) un exemple de quand vous auriez à faire de la "gestion de la mémoire"?

Le.Anti.9
la source
Bon endroit pour apprendre G4G
EsmaeelE

Réponses:

230

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 " pile ". Les variables de pile sont automatiquement libérées lorsqu'elles sont hors de portée (c'est-à-dire lorsque le code ne peut plus les atteindre). Vous pourriez les entendre appelées variables "automatiques", mais cela n'est plus à la mode.

De nombreux exemples pour débutants n'utiliseront que des variables de pile.

La pile est agréable parce qu'elle est automatique, mais elle a aussi deux inconvénients: (1) Le compilateur doit savoir à l'avance la taille des variables, et (b) l'espace de pile est quelque peu limité. Par exemple: dans Windows, sous les paramètres par défaut de l'éditeur de liens Microsoft, la pile est définie sur 1 Mo et elle n'est pas entièrement disponible pour vos variables.

Si vous ne savez pas au moment de la compilation la taille de votre tableau, ou si vous avez besoin d'un grand tableau ou 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 précédentes vous ont montré une façon de le faire, bien qu'il existe d'autres façons:

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 dire quand vous en avez terminé, vous perdez donc la libération automatique. C'est là qu'intervient la "libération manuelle" dont vous parliez. Votre code est maintenant responsable de décider quand 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 deuxième option "mauvaise affaire", c'est qu'il n'est pas toujours facile de savoir quand la variable n'est plus nécessaire. Si vous oubliez de publier une variable lorsque vous n'en avez pas besoin, votre programme consommera plus de mémoire dont il a besoin. Cette situation s'appelle une «fuite». La mémoire «perdue» ne peut pas être utilisée pour quoi que ce soit 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 désagréables sont possibles si vous libérez une variable de tas par erreur avant d'en avoir fini avec elle.

En C et C ++, vous êtes responsable de nettoyer vos variables de tas comme indiqué ci-dessus. Cependant, il existe des langages et des environnements tels que Java et .NET comme C # qui utilisent une approche différente, où le tas est nettoyé tout seul. Cette deuxième 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 j'espère plus nivelée)

Euro Micelli
la source
3
Si vous voulez mettre quelque chose sur la pile mais que vous ne savez pas quelle est sa taille au moment de la compilation, alloca () peut agrandir le cadre de la pile pour faire de la place. Il n'y a pas de freea (), le cadre entier de la pile est sauté lorsque la fonction retourne. Utiliser alloca () pour de grosses allocations est périlleux.
DGentry
1
Peut-être pourriez-vous ajouter une ou deux phrases sur l'emplacement mémoire des variables globales
Michael Käfer
Dans C ne jamais lancer le retour de malloc(), sa cause UB, (char *)malloc(size);voir stackoverflow.com/questions/605845/…
EsmaeelE
17

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 la libérer. Vous avez perdu votre pointeur vers 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, selon votre gestionnaire de mémoire) et si vous n'avez pas planté, votre processus s'exécute probablement assez lentement.

Pour résoudre le problème, vous devez appeler free () pour tout ce qui est obtenu avec malloc () après avoir fini de l'utiliser:

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

J'espère que cet exemple vous aidera!

Mark Harrison
la source
J'aime mieux cette réponse. Mais j'ai une petite question secondaire. Je m'attendrais à ce que quelque chose comme ça soit résolu avec des bibliothèques, n'y a-t-il pas une bibliothèque qui imiterait étroitement les types de données de base et leur ajouterait des fonctionnalités de libération de mémoire afin que lorsque les variables soient utilisées, elles soient également libérées automatiquement?
Lorenzo
Aucun qui fait partie de la norme. Si vous passez en C ++, vous obtenez des chaînes et des conteneurs qui gèrent automatiquement la mémoire.
Mark Harrison
Je vois, donc il y a des bibliothèques tierces? Pouvez-vous les nommer?
Lorenzo
9

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 tant que le programme ne sera pas exécuté. 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
Jeremy Ruten
la source
5

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

En C, la responsabilité de s'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 les pointeurs, ce qui rend difficile la rédaction de C.

Les réponses publiées à ce jour se concentrent sur les allocations de variables automatiques (pile) et de tas. L'utilisation de l'allocation de pile permet une mémoire gérée automatiquement et pratique, mais dans certaines circonstances (grands tampons, algorithmes récursifs), cela peut conduire au terrible 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 ê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 au langage. Il s'agit essentiellement d'un ensemble d'appels de bibliothèque qui vous accorde la propriété d'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 incalculable du programmeur. Les problèmes sont simples (libérer la même mémoire deux fois, ou pas du tout [fuites de mémoire], ne pas allouer suffisamment de mémoire [buffer overflow], etc.) mais difficiles à éviter et à déboguer. Une approche très disciplinée est absolument obligatoire dans la pratique, mais bien sûr, la langue ne l'exige pas.

Je voudrais mentionner un autre type d'allocation de mémoire qui a été ignoré par d'autres messages. 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 une 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 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 intégrés et de jeux C ont été construits sans aucune allocation de tas.

Bill Forster
la source
4

Il y a quelques bonnes réponses ici 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, vous terminez up with est le cousin de ce site - un débordement de tampon - et vous pouvez être en train 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 un nul - \ 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 de fin serait placé à la fin de ceci - une partie de la mémoire qui n'a pas été allouée pour votre utilisation 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 non répétables).

Chris BC
la source
Ici, vous allouez 5 octets. Détachez-le en attribuant un pointeur. Toute tentative de libérer ce pointeur conduit à un comportement indéfini. Remarque Les chaînes C ne surchargent pas l'opérateur = il n'y a pas de copie.
Martin York
Cependant, cela dépend vraiment du malloc que vous utilisez. De nombreux opérateurs malloc s'alignent sur 8 octets. Donc, si ce malloc utilise un système d'en-tête / pied de page, malloc réserverait 5 + 4 * 2 (4 octets pour l'en-tête et le pied de page). Ce serait 13 octets, et malloc vous donnerait juste 3 octets supplémentaires pour l'alignement. Je ne dis pas que c'est une bonne idée d'utiliser ceci, car cela ne concernera que les systèmes dont le malloc fonctionne comme ça, mais il est au moins important de savoir pourquoi faire quelque chose de mal pourrait fonctionner.
kodai
Loki: J'ai modifié la réponse à utiliser à la strcpy()place de =; Je suppose que c'était l'intention de Chris BC.
echristopherson
Je crois que la protection de la mémoire matérielle des plates-formes modernes empêche les processus de l'espace utilisateur d'écraser les espaces d'adressage d'autres processus; vous auriez plutôt une erreur de segmentation. Mais cela ne fait pas partie de C en soi.
echristopherson
4

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

Hernán
la source
2

Vous pouvez également utiliser l'allocation de mémoire dynamique lorsque vous devez définir un énorme tableau, par exemple int [10000]. Vous ne pouvez pas simplement le mettre dans la pile parce qu'alors, hm ... vous aurez un débordement de pile.

Un autre bon exemple serait une implémentation d'une structure de données, disons 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.

Serge
la source
2

(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 à la fois beaucoup d'espace sur la pile, c'est un bogue.) Typiquement, la première structure de données que vous aurez besoin d'apprendre est une sorte de liste . En voici un seul lié, 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 quelques autres fonctions, mais c'est pour cela que vous avez besoin de la gestion de la mémoire. Je dois souligner qu'il existe un certain nombre de trucs qui sont possibles avec la gestion de la mémoire "manuelle", par exemple,

  • En utilisant le fait que malloc est garanti (par le standard du langage) pour renvoyer un pointeur divisible par 4,
  • allouer de l'espace supplémentaire à vos propres fins sinistres,
  • création de pools de mémoire .

Obtenez un bon débogueur ... Bonne chance!

Anders Eurenius
la source
L'apprentissage des structures de données est la prochaine étape clé dans la compréhension de la gestion de la mémoire. Apprendre les algorithmes pour exécuter correctement ces structures vous montrera les méthodes appropriées pour surmonter ces obstacles. C'est pourquoi vous trouverez des structures de données et des algorithmes enseignés dans les mêmes cours.
aj.toulan
0

À Euro Micelli

Un point négatif à ajouter est que les pointeurs vers la pile ne sont plus valides lorsque la fonction est renvoyée, vous ne pouvez donc pas renvoyer un pointeur vers une variable de pile à partir d'une fonction. C'est une erreur courante et une des principales raisons pour lesquelles vous ne pouvez pas vous en tirer avec juste des variables de pile. Si votre fonction a besoin de renvoyer un pointeur, alors vous devez malloc et vous occuper de la gestion de la mémoire.

Jonathan Branam
la source
0

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

Vous avez raison, bien sûr. Je pense 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 transtypages pour rendre la «magie» plus visible. Parfois, cela aide à la lisibilité, parfois non, et parfois cela provoque un bogue silencieux détecté par le compilateur. Pourtant, je n'ai pas d'opinion tranchée à ce sujet, d'une manière ou d'une autre.

Ceci 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 ++ qu'en C. Merci de l'avoir remarqué.

Euro Micelli
la source
@echristopherson, merci. Vous avez raison, mais veuillez noter que ce Q / R datait d'août 2008, avant même que Stack Overflow ne soit en version bêta publique. À l'époque, nous étions encore en train de déterminer comment le site devrait fonctionner. Le format de cette question / réponse ne doit pas nécessairement être considéré comme un modèle d'utilisation de la SO. Merci!
Euro Micelli
Ah, merci de l'avoir signalé - je ne savais pas que cet aspect du site était encore en évolution.
echristopherson
0

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 au premier aussi longtemps que possible. Cependant, la mémoire gérée automatiquement en C est extrêmement limitée et vous devrez gérer manuellement la mémoire dans de nombreux cas, tels que:

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

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

struct paire * new_pair (int val) {
   paire de structures * np = malloc (sizeof (paire de structures));
   np-> val = val;
   np-> suivant = NULL;
   return np;
}

b. vous voulez avoir de la 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);
pour (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 semble tellement désordonnée):

struct data { int data_type; long data_in_mem; }; struct animal {/ * quelque chose * /}; struct person {/ * autre chose * /}; struct animal * read_animal (); struct personne * read_person (); / * En main * / échantillon de données struct; sampe.data_type = type_entrée; commutateur (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, ça encore une fois et je vais faire une faute à votre OS"); }

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

Cependant, en général, vous voudrez rester à l'écart de vos astuces préférées (T___T). Vous briserez votre système d'exploitation, tôt ou tard, si vous les utilisez trop souvent. Tant que vous n'utilisez pas * alloc et free, il est prudent de dire que vous êtes encore vierge et que le code est toujours beau.

magice
la source
"Vous voyez, une valeur longue suffit pour contenir TOUT" -: / de quoi parlez-vous, sur la plupart des systèmes, une valeur longue est de 4 octets, exactement la même chose qu'un int. La seule raison pour laquelle il intègre les pointeurs ici est que la taille de long se trouve être la même que la taille du pointeur. Vous devriez vraiment utiliser void *, cependant.
Score_Under du
-2

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 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 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éé lorsque MyClass est créé, 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 code réel, il n'y aurait aucune raison (à part éventuellement de 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 voulez contrôler finement lorsqu'ils sont créés et détruits (pour que votre application n'aspire pas 1 Go de RAM pendant toute sa durée de vie, par exemple), et dans un environnement fenêtré, c'est à peu près obligatoire, en tant qu'objets que vous créez (boutons, par exemple) , doivent exister bien en dehors de la portée de toute fonction particulière (ou même de classe).

Le Schtroumpf
la source
1
Heh, ouais, c'est du C ++, n'est-ce pas? Incroyable qu'il ait fallu cinq mois à quiconque pour m'appeler.
TheSmurf