version on-line : les programmes se trouvent à la fin de l'article.

Le C perd la mémoire

La maladie d'Alzimer selon Kernigan et Richie



La mémoire des ordinateurs a toujours été assez complexe. Toujours les concepteurs nous trouvent des excuses pour ne pas avoir un système linéaire. Que ce soit les banks sur Amstrad, ou les pages sur PC. Seul l'Amiga avait et a toujours un système
que l'on qualifiera de simple.
Mais dans notre cas, nous sommes sur PC : il va falloir faire avec. Et vous avez de la chance car le PC a une mémoire particulièrement bordélique. Aujourd'hui nous allons commencer par programmer la mémoire conventionnelle en C.

Allouons enfants de la patrie...

Comme vous êtes des gens biens et que vous ne voulez pas avoir un message plantant le système à la sortie de votre programme, vous allez allouer de la mémoire. Car avoir un tableau d'entiers de 65000 éléments est un peu lourd. Vous risquez un débordement lors de la compilation (et ca c'est le pavé dans la mare qui fait déborder la vase). De plus votre tableau va prendre de la mémoire même si vous ne vous en servez pas : avouez que c'est plutôt gênant.
Donc, vous allez quémander de la mémoire au DOS, en passant par l'instruction FARMALLOC(long Nb). Notre premier programme fait ‡a bien, en désallouant la mémoire par la suite (Exemple1).

C'est NULL

J'ai oublié ma mémoire


Tout le monde me dira que ce programme marche. Mais imaginons que votre programme est hypercomplexe, et biensûr vous ne vous rappelez plus si vous avez déjà libéré la mémoire utilisée par votre tableau. Or, si vous lancez le programme, vous vous apercevrez qu'une fois la mémoire libérée, le pointeur n'a pas bougé : il pointe vers une zone mémoire qui ne nous appartient plus. Rajoutons donc une instruction pour la réinitialiser.

Bon, cette fois, tout est dans l'ordre. Tout ? Hmmm... Oui et non, n'oublions pas le titre de cet article : "le C perd la mémoire".
Alors ? Eh bien imaginons que vous voulez reprogrammer un Win'95 en lui otant tous ses bugs. Certes y'a du boulot, mais vous aurez sûrement besoin d'allouer de la mémoire, et notamment plusieurs blocs, dynamiquement. Comme vous être plutot désorganisé, vous ne savez plus dans quel ordre vous avez alloué vos blocs. Vous obtenez donc le programme suivant.

"Ca ne fait rien" entends-je au fond de l'assemblée aujourd'hui réunie ! Tout à fait vrai : le C s'en fiche dans quel ordre vous libérez la mémoire. Il est aussi vrai qu'avec la plupart des compilateurs, vous pouvez "oublier" de libérer la
mémoire ou de fermer des fichiers : le C le fera pour vous en quittant votre programme.
Mais vous vous exposez à de gros déboirs. Si vous libérez une zone qui n'est pas la dernière allouée elle ne sera pas vraiment libérée car elle n'est pas accessible. Le C note dans un coin qu'elle est libérée et la libérera automatiquement quand elle
deviendra accessible. Pourquoi ?



"Faites face à votre pile..."

(Ou un autre jeu de mot débile dans le même genre)



Pour comprendre ce phénomènene, plongeons nous au coeur du fonctionnement de FARMALLOC et FARFREE.

Si vous compilez un des programmes fournis et lancez le TurboDebuger, vous vous apercevrez que FARMALLOC ne fait pas que demander de la mémoire au DOS. En fait, il va au travers d'une procèdure à la fois simple et utile, une sorte de gestion de pile : LIFO (Last In, First Out).
Comme dans une pile de livres, si vous ne voulez pas que tout s'écroule, vous n'avez accès qu'à celui au-dessus de la pile. Imaginez maintenant que chaque livre représente une zone de mémoire. Si vous libérez le livre du dessus, c'est cool, vous
le jetez, et celui d'en dessous devient celui du dessus. Ok ? Mais là on ‡a se complique, c'est lorsque vous voulez atteindre le livre tout en bas de la pile, donc le premier livre qui a constitué la pile. C'est un peu comme jouer au mikado avec des Séquoias, c'est complexe.
Pour éviter d'avoir à tout désempiler (ce qui reviendrai à faire un Hanoi, pour ceux qui connaissent), vous "marquez" simplement que celui d'en dessous est "virtuellement" libéré. Quand tout ceux au-dessus seront partis, on dégagera
physiquement ce livre devenu accessible.

Seulement, ce système, qui reste pratiquement transparent tant pour le programmeur que l'utilisateur a un gros défaut : c'est qu'entre une "mémoire libre" et une "mémoire libérée" il y a une grosse différence. La mémoire d'en-dessous est
libérée ("Marquée libre") mais n'est pas libre en réalité.
Résultat ? Imaginez que vous avez 400 Ko de mémoire libre. Vous allouez un bloc A de 399 Ko, puis un bloc B de 1Ko.
Vous libérez alors le bloc A. Théoriquement, vous disposez de 399 Ko de libre. Mais essayez d'allouer ne serait-ce qu'1 Ko : PAF ! Eh oui, tant que le bloc B existe (qui est au-dessus du bloc A) le bloc B ne sera pas réellement libre.


La folie du LIFO

L'inventeur de la pile (Mr Volta) mangeait de la viande bovine britannique.



Les solutions sont alors variées. On pourrait reprogrammer entièrement la gestion de la mémoire en rempla‡ant la pile LIFO par une liste chaînée (ou double chaînée) qui nous permettrait de libérer n'importe quelle zone n'importe quand. Mais bon. Nous aborderont les listes chainées dans quelques numéros, dans les cours de C++.

A priori, si le programme veut libérer une zone se trouvant au milieu de la pile, c'est qu'il a été mal programmé. Pensez donc lorsque vous programmez à l'ordre d'allocation. La durée de vie de la mémoire dynamique doit être proportionnelle à sa profondeur dans la pile.

Mais dans les grands programmes, difficile de s'y retrouver. Le programme suivant permet de détecter qu'une mémoire n'a pas vraiment été libéré. On aurait également pu faire plus compliqué, en vérifiant la mémoire qu'on libère est au-dessus de la pile.

Dans un prochain numéro, on verra comment parcourir toute la pile pour améliorer encore sa gestion.
Merci, de votre attention, bon courage à tous. N'hésitez pas à nous écrire pour tous commentaires, corrections, idées...









#include <alloc.H>            // Permet d'utiliser la m‚moire en C
#include <stdio.H>


void main(void)
{
void far *Buffer;             // Pointeur sur la zone allou‚e

Buffer=farmalloc(16000);      // Allocation de 16000 octets

printf("%p %lu\n",Buffer,farcoreleft());  // Affichage du pointeur
                                          // et de la m‚moire restante

farfree(Buffer);                          // Lib‚ration de la m‚moire

printf("%p %lu\n",Buffer,farcoreleft());  // Affichage du pointeur
                                          // et de la m‚moire restante
}
//



#include <alloc.H>            // Permet d'utiliser la m‚moire en C
#include <stdio.H>


void main(void)
{
void far *Buffer;             // Pointeur sur la zone allou‚e

Buffer=farmalloc(16000);      // Allocation de 16000 octets

printf("%p %lu\n",Buffer,farcoreleft());  // Affichage du pointeur
                                          // et de la m‚moire restante

farfree(Buffer);  Buffer = NULL;          // Lib‚ration de la m‚moire

printf("%p %lu\n",Buffer,farcoreleft());  // Affichage du pointeur
                                          // et de la m‚moire restante
}
//



#include <alloc.H>
#include <stdio.H>


void main(void)
{
void far *Buffer1,*Buffer2,*Buffer3;

Buffer1=farmalloc(16000);
Buffer2=farmalloc(10000);
Buffer3=farmalloc(16000);

farfree(Buffer2);  Buffer2 = NULL;
printf("%p %lu\n",Buffer2,farcoreleft());

farfree(Buffer3);  Buffer3 = NULL;
printf("%p %lu\n",Buffer3,farcoreleft());

farfree(Buffer1);  Buffer1 = NULL;
printf("%p %lu\n",Buffer1,farcoreleft());
}
//



#include <alloc.H>
#include <stdio.H>
#include <process.H>
void far *Personnal_farfree(void far *zone)
{
long MemRest;
MemRest=farcoreleft();
farfree(zone);
if (MemRest==farcoreleft())   // La m‚moire dispo n'a pas chang‚e : Erreur
	{
	printf("Memory LIFO error\n");exit(1);
	}
	return NULL;
}
void main(void)
{
void far *Buffer1,*Buffer2;
Buffer1=farmalloc(16000);
Buffer2=farmalloc(10000);
Buffer1 = Personnal_farfree(Buffer1);
Buffer2 = Personnal_farfree(Buffer2);
}//