Gestion de la mémoire

Ce paragraphe traite des pointeurs, des problèmes liés à l'allocation dynamique de mémoire, et des moyens qui existent de résoudre ces problèmes... plutôt d'éviter leur apparition, car en ce domaine le préventif est bien plus aisé que le curatif...

Qu'est-ce que l'allocation dynamique de mémoire ?

Nous avons vu précédemment que la durée de vie d'une variable s'étendait durant toute la portée avant de son nom. La mémoire est allouée, et le constructeur de l'objet est appelé en début de portée; le destructeur est appelé et la mémoire est rendue au système à la fin de portée. Les objets utilisés ainsi utilisent une partie de la mémoire vive appelée la pile. La structure de pile est en effet parfaitement adaptée à la gestion des règles de portée. Or, il peut être intéressant de stocker des données à des endroits de la mémoire qui ne seront pas sujets soumis aux règles de portée; cela peut se faire grâce à:

L'allocation-libération de mémoire étant à la charge du programmeur, elle peut se faire dans n'importe quel ordre. La structure de pile n'est alors plus adaptée, et de fait la mémoire est allouée dans une autre zône de la mémoire, appelée le tas (heap).

ATTENTION Le programmeur doit effectivement gérer la libération de mémoire... s'il ne le fait pas, ou s'il le fait mal, les pires conséquences (à savoir un plantage du programme) peuvent arriver.

Pointeurs empilés, objets entassés

La seule zône de mémoire que le programme peut adresser directement est la pile. Les pointeurs se trouveront donc quelque part dans la pile, au même titre que n'importe quelle variable. Les objets pointés, par contre, se trouveront dans le tas. Il doit y avoir en permanence un lien entre ces deux zônes de mémoire. Garder ce lien intact est la première préoccupation d'une bonne gestion de la mémoire.

Objets perdus et fuites de mémoire

Lorsqu'un objet est alloué dynamiquement, au moins un pointeur doit pointer sur lui: sinon, le lien évoqué ci-dessus est brisé, et l'objet est inutilisable. On peut dire qu'il est perdu, mais surtout la mémoire correspondante est perdue. Avant de briser le lien, il aurait fallu rendre la mémoire au système. Suivant les cas de figure, cela peut être grave ou pas. Par exemple, si l'allocation de mémoire a lieu dans une boucle, à chaque itération de la boucle on perd un peu de mémoire... d'oû l'expressoin fuite de mémoire. Si le nombre d'itérations est important, il y a un moment oû le système refusera de donner de la mémoire supplémentaire au programme, et celui-ci sera interrompu brutalement.

Les pointeurs qui pendouillent

Il est parfaitement possible de faire pointer plusieurs pointeurs vers le même objet. Mais dans ce cas si l'objet est détruit (car le programmeur a consciencieusement rendu la mémoire au système) les autres pointeurs pointeront sur une zône de mémoire qui ne contient plus de données valides... soit elle contient n'importe quoi ("du jargon" (garbage)), soit elle contient de nouvelles données, mais qui ne sont peut-être pas structurées de la même manière que les précédentes. Le pointeur va donc "pendouiller", et si on cherche à l'utiliser, il peut se passer n'importe quoi, mais le pire est à craindre.

Propriétaires et référents

Compte tenu de ce qui précède, on voit donc qu'on peut définir deux sortes de pointeurs:

Bien sûr, la propriété d'un objet peut passer d'un pointeur à l'autre. D'autre part, il faut bien avoir présent à l'esprit que ces notions, importantes lors de la phase de conception, ne sont pas présentes dans le langage lui-même: le C++ ne comprend en effet aucune gestion de la mémoire, celle-ci restant à la charge du programmeur. Cependant, des objets (dont l'un d'entre eux se trouve déclaré dans la bibliothèque standard) vont pouvoir nous aider.

top


Opérateurs et fonctions

Les opérateurs new et delete

L'opérateur new est utilisé pour allouer de la mémoire pour un objet, delete est utilisé pour redonner la mémoire au système. Le (ou les) paramètres passés à new seront passés au constructeur de l'objet:

main() {
   const complexe J(0,1);
   complexe* C = new complexe(5,5);
   *C = J;
   delete C;
};

Les opérateurs new[] et delete[]

Ils servent à allouer de la mémoire pour un tableau d'objets. Ils ne peuvent etre utilisés qu'à la condition qu'existe pour notre objet un constructeur par défaut, à qui on puisse ne pas passer de paramètres. C'est le cas pour notre objet complexe, nous pouvons donc écrire:

main() {
   const complexe J(0,1);
   int taille=100;
   complexe* C = new complexe[taille];
   for (int i=0; i<100; ++i) {
       C[i] = J;
   }
   delete[] C;
};

Un tableau de 100 complexes est dynamiquement alloué. Les complexes sont tous initialisés à 0 (constructeur par défaut), puis affectés à la valeur J. Enfin, le tableau est détruit et la mémoire est rendue au système.

ATTENTION On ne peut allouer un tableau d'objets de cette manière que si les objets en question possèdent un constructeur par défaut. Il n'est pas possible de passer des paramètres aux constructeurs des objets créés.

ATTENTIONLa taille peut parfaitement être une variable, comme on le voit dans cet exemple.

Fonctions malloc, free, realloc

Nous avons en C des fonctions d'allocation dynamique de mémoire: malloc et free pour allouer de la mémoire et la rendre au système, realloc pour refaire une allocation mémoire lorsque le bloc précédemment alloué est trop juste. Tout cela est réutilisable, à condition de prendre quelques précautions:

Conclusion: pour du code C++, il n'y a aucune raison de ne pas utiliser les opérateurs du C++, new et delete. Mais il faut savoir que le code C écrit avec malloc et free (même realloc dans le cas de zônes d'entiers ou de caractères, par exemple) reste utilisable.

Allocation mémoire et constructeurs-destructeurs

Le constructeur d'un objet est l'endroit rêvé pour appeler new. De même, le destructeur du même objet est l'endroit rôvé pour appeler delete.

Constructeurs de copie

Attention au constructeur de copie; à chaque copie, il faudra prendre une décision; il peut en effet se présenter plusieurs cas de figure:

  1. L'objet source est propriétaire d'un objet référent, l'objet copié pointe vers le référent sans être propriétaire.
  2. L'objet source est propriétaire d'un référent, à la suite de la copie l'objet copié devient le nouveau propriétaire.
  3. Le référent de l'objet source est copié dans une autre zône mémoire, et l'objet copié est propriétaire de la copie du référent.

ATTENTION Des trois solutions ci-dessus, la première est très dangereuse: en effet, elle risque fort d'aboutir à des objets "irresponsables" vis-à-vis de l'allocation mémoire. Cette solution est toutefois acceptable lorsque les objets référents comptent eux-mêmes les références apres. La seconde solution peut être implémentée par un auto_ptr.

top


Objets utilisés pour la gestion de la mémoire

L'objet auto_ptr

auto_ptr fait partie de la bibliothèque standard du C++ apres . Il sert à définir un pointeur "intelligent"... en tous cas fort sympathique, ayant les caractéristiques suivantes:

Le code suivant, qui utilise des objets de type complexe, illustre l'utilisation d'auto_ptr:

typedef auto_ptr<complexe> complexe_ptr;

void main() {
  complexe_ptr c2(new complexe);            // allocation d'un auto_ptr.
  {
    complexe_ptr c1(new complexe(5,5));	    // allocation d'un auto_ptr
    c2=c1;			            // c2 devient proprietaire du complexe
					    // son ancien referent est detruit
					    // c1 n'est plus le propriétaire, 
  };					    // il peut être détruit.
}

Un objet de type auto_ptr peut donc avantageusement être alloué dans le constructeur à la place d'un objet ordinaire. La destruction du référent sera automatique au moment de la destruction de l'objet, sans même qu'il soit nécessaire de le spécifier dans le destructeur. Dans le code ci-dessus, tous nos ennuis viennent en effet uniquement de c3, déclaré comme complexe*.

ATTENTION En fait, l'important n'est pas là: après tout, vous êtes bien assez malin pour ne pas oublier d'écrire les quelques lignes de code du destructeur correspondant aux instructions delete. L'important, c'est qu'il est fort possible que le constructeur, après avoir alloué un ou plusieurs pointeurs, génère une exception (fonctionnement normal pour un constructeur). La construction de l'objet est alors interrompue, et le destructeur n'est pas appelé. Résultat: une fuite de mémoire. L'auto_ptr est donc la solution élégante, car lorsque l'auto_ptr sera détruit, le référent sera lui aussi détruit.

Objets gestionnaires de ressources

Pour éviter les ennuis évoqués ci-dessus, arrangez-vous pour qu'un objet ne gère qu'une seule ressource. Eventuellement, si vous devez gérer trois ressources, rien ne vous empêche d'utiliser trois objets différents, quitte à les insérer dans un autre objet.

REGLE  D'OR Les trois principes fondamentaux pour gérer les ressources sont:

Propriété
A chaque ressource allouée, correspond un et un seul objet gestionnaire, qui sera propriétaire de cette ressource.
Responsabilité
L'objet gestionnaire est responsable de la ressource, et il est le seul responsable
Simplicité
L'objet gestionnaire ne fait rien d'autre.

auto_ptr est un bon exemple d'objet dont l'unique raison d'être est la gestion d'une ressource (en l'occurrence la mémoire).

Produire du code robuste, malgré les exceptions

On l'a vu, les exceptions peuvent avoir pour conséquence l'apparition de fuites de mémoire ou de pointeurs pendouillants. Voici quelques astuces permettant d'éviter ces désagrément.

auto_ptr

Utiliser auto_ptr le plus souvent possible.

p=NULL

Après avoir exécuté delete p, on se retrouve avec un pointeur pendouillant. Donc, remettez les choses en ordre dès la ligne suivante (à moins, bien sûr, qu'on ne sorte de la portée). Par exemple avec p=NULL; En effet, s'il arrive qu'un second appel delete soit lancé (par exemple à partir du destructeur), il ne se passera rien si p vaut NULL, alors que le résultat sera catastrophique si p pendouille.

ATTENTION Le code suivant est dangereux:NOOON

delete p ;
p = new toto();

REGLE  D'ORSi toto envoie une exception, p continue à pendouiller. Il vaut mieux faire:

delete p ;
p = NULL;
p = new toto();

Les objets à comptage de référence

Il est possible d'implémenter des objets qui comptent le nombre de pointeurs mis sur eux. De cette manière, il est aisé, lorsque ce nombre arrive à 0, de faire en sorte que ces objets s'auto-détruisent. Cela passe, bien sûr, par la surcharge des opérateurs =, *, ->.

top


xhtml Licence Creative Commons Emmanuel Courcelle <emmanuel.courcelle@inp-toulouse.fr>