• Aucun résultat trouvé

Chapitre 12 : Allocation dynamique, liste linéaire chaînée A) Mémoire dynamique :

N/A
N/A
Protected

Academic year: 2022

Partager "Chapitre 12 : Allocation dynamique, liste linéaire chaînée A) Mémoire dynamique :"

Copied!
1
0
0

Texte intégral

(1)

Chapitre 12 : Allocation dynamique, liste linéaire chaînée

A) Mémoire dynamique : 1) Allocation statique :

Avec les tableaux, il faut prévoir l'espace de mémoire nécessaire pour mémoriser ces tableaux (#define MAX_PERS 175, etc ...).

Désavantages :

a) Dans la plupart des cas, c'est un gaspillage de mémoire si on utilise une petite partie de l'espace réservé:

Exemple :

#define MAX_ETUD 1000;

...

Etudiant Etud[MAX_ETUD] ;

Si le nombre d'étudiants lus est 465 par exemple, on gaspille de la mémoire réservée pour le tableau d'étudiants.

b) Souvent, on a une limite sur la taille d'un tableau quand on travaille sur micro-ordinateur:

Soit un programme qui contient les déclarations suivantes : #define MAX_EMPL 2000

#define LONG_NP 30 typedef struct

{ char nomPre[LONG_NP +1] ; int numero ; float taille, salHebdo ; }

Employe ;

Employe emp [MAX_EMPL] ;

Ces déclarations provoquent une erreur à la compilation : Error .... : Array size too large (tableau est trop gros!) L'espace mémoire "statique" est allouée dès la compilation.

On demande de l'espace pour mémoriser 2000 employés. Avec l'opérateur "sizeof", on a :

sizeof(Employe) vaut

31 (pour nom et prénom) + 2 (pour numero, un entier) + 4 (pour taille, un réel) + 4 (pour salHehdo , un réel) ---

41 octets

(2)

emp, qui mémorise 2000 employés, occupera : 2000 x 41 = 82000 octets > 64 Ko

qui dépasse le nombre d'octets "maximum" réservés pour la zone statique ( <= 64 Ko )

Conclusion :

Avant d'utiliser des tableaux pour gérer les informations d'un problème à programmer, il faut examiner la taille des données.

2) Allocation dynamique :

On alloue de la mémoire quand on en a besoin et on peut la libérer quand on n'a plus besoin. L'espace mémoire allouée est dans une zone dynamique (on peut atteindre 1 meg dépendant du modèle de mémoire).

Cette zone est plus grande que la zone statique.

a) Allocation :

On utilise souvent la fonction "malloc" (memory allocation) qui est déclarée dans le fichier d'en-tête <alloc.h>.

Exemples :

1. char * nomPre ;

nomPre = (char *) malloc (30) ;

On demande d'allouer 30 octets pour mémoriser un nom et prénom.

2. int * p ;

p = (int *) malloc (sizeof(int));

On demande d'allouer 2 octets (sizeof(int) vaut 2) pour mémoriser *p qui est de type entier. La valeur de P est l'adresse de *P en zone dynamique.

3. typedef struct { ...

}

Etudiant ; Etudiant * p ;

p = (Etudiant *) malloc ( sizeof(Etudiant)) ;

On demande d'allouer le nombre d'octets nécessaires pour mémoriser *p qui est de type Etudiant. La valeur de p est l'adresse de *p en zone dynamique.

(3)

Cas général :

Soit T le nom d'un type prédéfini (int, char, etc ...) ou inventé par l'usager (comme Etudiant, Employe, Personne, etc ...).

Avec la déclaration :

T * p ;

1. p est une variable de type pointeur qui pointe vers un élément de type T.

2. *p est une variable de type T.

Allocation dynamique se fait par l'affectation suivante :

p = (T *) malloc (sizeof(T));

sizeof(T) donne la taille de T en nombre d'octets (de bytes).

On demande l'espace de mémoire pour stocker *p et on dépose l'adresse de *p dans p:

p contient alors l'adresse de *p

( on dit aussi que P "pointe vers" *p ) Cette affectation est représentée par le schéma suivant:

Adresse ╔═══════╗ ╔═════════════╗ 58462 ║ 58462 ---> ║ ? ║

╚═══════╝ ╚═════════════╝

p *p

L'espace de mémoire pour mémoriser *p se trouve dans le "tas" (heap).

C'est une zone de la mémoire dynamique.

On connaît l'emplacement de *p (son adresse) mais on ne connaît pas encore sa valeur. Pour cette raison, on a mis le point d'interrogation pour signaler que la valeur de *p est encore inconnue.

Cette affectation est largement utilisée pour la création des structures de données comme "liste linéaire chaînée", "arbre binaire", etc.

a) Désallocation (libération): free (P) ; 3) Exemples :

Exemple 1 :

Écrire les déclarations d'un tableau de 2000 pointeurs, chacun pointe vers le type Employe déclaré à la page 148.

(4)

Écrire un bloc d'instructions pour démontrer la faisabilité de l'allocation dynamique.

Solution :

#define MAX_EMPL 2000 #define LONG_NP 30 typedef struct

{ char nomPre[LONG_NP +1] ; int numero ; float taille, salHebdo ; }

Employe ;

Employe *p[MAX_EMPL] ;

void main()

{ int k , nbOctets = sizeof(Employe);

for (k = 0 ; k < MAX_EMPL ; k++)

p[k] = (Employe *) malloc(nbOctets);

...

}

Exemple 2 :

Peut-on déclarer un "tableau dynamique" des employés ? Si oui, comment ?

Solution :

#define MAX_EMPL 2000 #define LONG_NP 30 typedef struct

{ char nomPre[LONG_NP +1] ; int numero ; float taille, salHebdo ; }

Employe ; Employe *p ; void main()

{ int k , nbOctets = sizeof(Employe);

P = (Employe *) malloc(MAX_EMPL * nbOctets);

...

(5)

Dans ce cas, l'allocation est faite en zone dynamique. Le seul désavantage c'est qu'on demande de l'espace pour tout le tableau.

Il se peut que ce soit un gaspillage de mémoire.

4) Résumé du traitement de pointeurs :

Soit T le nom d'un type prédéfini (int, char, etc ...) ou inventé par l'usager (comme Etudiant, Employe, Personne, etc ...).

Soient les déclarations : T * p, * s ; Note 1 :

p est une variable de type pointeur qui pointe vers un objet de type T, *p est une variable de type T.

Note 2 :

Il existe une constante NULL pour spécifier qu'un pointeur qui pointe vers rien.

p = NULL ; /* p pointe vers rien */

Cette affectation est représentée par le schéma suivant : p

╔═════╗

║ X ║ ╚═════╝

On peut faire des tests pour vérifier si un pointeur vaut NULL ou non : a) if ( p != NULL ) printf("p pointe vers quelque chose");

On peut écrire une autre manière :

if ( p ) printf("p pointe vers quelque chose");

b) if ( p == NULL ) printf("p pointe vers rien \n");

On peut écrire une autre manière :

if ( !p ) printf("p pointe vers rien \n");

La constante NULL est la même pour n'importe quel pointeur : float * p = NULL ;

char * s = NULL ; Etudiant * v = NULL ;

Si p est non nul, la valeur de p est l'adresse de *p, on dit aussi que p pointe vers *p et on représente par le schéma suivant :

Adresse ╔═══════╗ ╔═════════════╗ 60020 ║ 60020 ---> ║ ... ║

╚═══════╝ ╚═════════════╝

p *p

(6)

Note 3 :

s = p ; implique que le pointeur à gauche va pointer vers la même place que celui à droite de l'affectation.

s pointe vers la même chose que P. Autrement dit, p et s contiennent la même adresse. On représente par le schéma suivant :

*p

╔═════════════╗

p ————————> ║ ... ║ ┌—————> ║ ... ║ │ ╚═════════════╝

│ *s s ——┘

Note 4 :

p = (T *) malloc (sizeof(T));

Cette affectation est représentée par le schéma suivant:

Adresse ╔═══════╗ ╔═════════════╗ 58462 ║ 58462 ---> ║ ? ║

╚═══════╝ ╚═════════════╝

p *p

Note 5 :

p + k <====> p + k * sizeof(*p) Le cas le plus fréquent est : p++ ;

c'est-à-dire : p = p + 1 ; /* k vaut 1 */

Dans ce cas, p pointe vers l'élément suivant. On utilise souvent p++ pour traiter des tableaux, des chaînes de caractères.

B) Liste linéaire chaînée :

1) Introduction :

Liste linéaire chaînée est une structure de données qui utilise la notion des pointeurs et l'allocation dynamique.

(7)

Les avantages de la liste linéaire chaînées sont :

1. on peut mémoriser beaucoup de données avec la zone dynamique ; 2. il est assez facile d'adapter un programme qui utilise une structure de données comme liste linéaire chaînée ou arbre binaire ;

3. on utilise une liste linéaire chaînée pour implanter une pile

(ordre LIFO : Last In First Out : dernière entrée, première sortie) ou une file d'attente (FIFO : First In First Out)

Rappels :

Il n'est pas préférable d'ajouter ou d'enlever un élément d'un tableau car il y a trop de décalage pour les indices à faire.

Exemple : Ajouter un nouvel étudiant après le 6 ième élément d'un tableau de 500 (nbEtud) étudiants.

...

indAjout = 6 ; /* indice à ajouter */

/* décaler les indices */

for ( i = nbEtud ; i > indAjout ; i-- ) etud[i] = etud[i-1] ;

etud[indAjout] = nouEtudiant ; nbEtud++ ;

...

Imaginez la lenteur que cela occasionne dans le cas d'un système d'inscription où l'on procède fréquemment à des ajouts et des modifications.

2) Schéma d'une liste linéaire chaînée :

Cette structure de données est la plus simple. Les données y sont organisées de fa‡on linéaire.

Supposons qu'on a les données suivantes : 10

15 20

Une liste linéaire chaînée représentée par le schéma suivant est en ordre LIFO (20 est la dernière valeur lue, elle est la valeur du premier élément de la liste) :

╔═════╗════╗ ╔═════╗════╗ ╔═════╗════╗

liste ---->║ 20 ║ ---> ║ 15 ║ --->║ 10 ║ X ║ ╚═════╝════╝ ╚═════╝════╝ ╚═════╝════╝

(8)

Une liste linéaire chaînée représentée par le schéma suivant est en ordre FIFO (10 est la première valeur lue, elle est la valeur du premier élément de la liste, c'est l'ordre de la lecture).

╔═════╗════╗ ╔═════╗════╗ ╔═════╗════╗

liste ---->║ 10 ║ ---> ║ 15 ║ --->║ 20 ║ X ║ ╚═════╝════╝ ╚═════╝════╝ ╚═════╝════╝

3) Déclaration d'une liste linéaire chaînée :

On constate que liste est un pointeur qui pointe vers son premier élément et que chaque élément est un enregistrement ayant deux champs d'information :

1. une valeur entière dans cet exemple (en pratique, c'est souvent une structure) ;

2. un pointeur qui pointe vers l'élément le suivant.

Ce qui incite à déclarer la liste (des entiers) comme suit :

typedef struct Elem

{ int valeur ; struct Elem * suivant ; }

Element ;

typedef Element * Pointeur ;

Pointeur liste ;

Remarques :

1. liste est un pointeur qui pointe vers un objet de type Element.

2. *liste est de type Element.

(9)

3. (*liste).valeur peut s'écrire liste->valeur (*liste).suivant peut s'écrire liste->suivant 4. Avec la déclaration : Pointeur tempo ;

L'affectation : tempo = (Element *) malloc (sizeof(Element));

est représentée par le schéma suivant :

╔═════════════╗

tempo ---> ║ ? | ? ║ ╚═════════════╝

*tempo

On ne connaît pas le champ "valeur" ni le champ "suivant"

de *tempo.

Exemple :

Écrire les déclarations d'une liste linéaire chaînée des employés.

Solution :

typedef struct

{ char nomPre[LONG_NP +1] ; int numero ; float taille, salHebdo ; }

Employe ; typedef struct Elem

{ Employe emp ; struct Elem * suivant ; }

Element ;

typedef Element * Pointeur ; Pointeur liste ;

Exercice :

Écrire les déclarations d'une liste linéaire chaînée des étudiants du cours IFT 1969 pour la gestion des notes.

4) Création d'une liste linéaire chaînée :

Les exemples suivants permettent de créer une liste linéaire chaînée des entiers dans l'ordre LIFO ou FIFO. Il est facile à adapter si on a une liste linéaire chaînée des structures (des employés, des étudiants, ...) à créer.

(10)

4.1) Création dans l'ordre LIFO :

Appel : creerLIFO (&liste, "Liste.Dta");

Créer une liste linéaire dans l'ordre LIFO à partir du fichier "Liste.Dta".

Fonction de création :

void creerLIFO ( Pointeur * p , char * nomDonnees) { FILE * donnees = fopen(nomDonnees, "r");

Pointeur laListe = NULL , /* liste vide au début */

tempo ; /* pour l'algorithme de création */

int nbOctets = sizeof(Element) ; while (!feof(donnees))

{

tempo = (Element *) malloc ( nbOctets) ; fscanf(donnees, "%d\n", &tempo->valeur);

tempo->suivant = laListe;

laListe = tempo ; }

fclose(donnees);

*p = laListe ; }

Simulation :

Pour comprendre le bon fonctionnement, on va simuler la fonction avec un fichier "Liste.Dta" qui ne contient que 2 entiers : 10

15

Initialisation : Les données La liste ( qui est vide! ) --> 10 ╔═════╗

15 ║ X ║ laListe = NULL ╚═════╝

laListe

Première tour de boucle while (!feof(Donnees) ... :

(11)

1 ère itération :

Avec : tempo = (Element *) malloc ( nbOctets) ; *tempo

╔═════════╗════╗

tempo ---> ║ ? ║ ? ║ (on ne connaît pas sa ╚═════════╝════╝ valeur,ni son suivant) Après : fscanf(donnees, "%d\n", &tempo->valeur);

on a :

Données ╔═════════╗════╗

10 tempo ---> ║ 10 ║ ? ║ -->15 ╚═════════╝════╝

Après : tempo->suivant = laListe;

on a :

Données ╔═════════╗════╗

10 tempo ---> ║ 10 ║ X ║ -->15 ╚═════════╝════╝

car laListe vaut NULL et ( s = p ; signifie que S pointe vers la même chose que P).

Finalement, laListe = tempo ; provoque que laListe pointe vers la même chose que tempo.

On a :

laListe │ │

Données └——————> ╔═════════╗════╗

10 ║ 10 ║ X ║ -->15 tempo ————-> ╚═════════╝════╝

On vient de faire le premier tour de la boucle while et on dispose d'une "liste partielle" qui ne contient qu'un seul élément.

Deuxième tour de boucle while (!feof(donnees) ... : Avant de faire ce deuxième tour, notre liste est : ╔═════════╗════╗

laListe ---> ║ 10 ║ X ║ ╚═════════╝════╝

Non fin du fichier " donnees " ? OUI

(12)

Avec : tempo = (Element *) malloc ( nbOctets) ; laListe ———————————————————————————┐

╔═════════╗════╗ └——> ╔═════════╗════╗

tempo ---> ║ ? ║ ? ║ ——————> ║ 10 ║ X ║ ╚═════════╝════╝ ╚═════════╝════╝

*tempo

Après : fscanf(Donnees, "%d\n", &tempo->valeur);

on a :

laListe ———————————————————————————┐ Données 10 │ 15 │ -->eof ╔═════════╗════╗ └——> ╔═════════╗════╗

tempo ---> ║ 15 ║ ? ║ ║ 10 ║ X ║ ╚═════════╝════╝ ╚═════════╝════╝

*tempo Après : tempo->suivant = laListe;

on a :

laListe ———————————————————————————┐ Données 10 │ 15 │ -->eof ╔═════════╗════╗ └——> ╔═════════╗════╗

tempo ---> ║ 15 ║ ——————————> ║ 10 ║ X ║ ╚═════════╝════╝ ╚═════════╝════╝

*tempo

car laListe pointe vers l'élément qui a la valeur 10.

Finalement, laListe = tempo ; provoque que laListe pointe vers la même chose que tempo.

On a :

laListe Données 10 │ 15 │ -->eof └—————————> ╔═════════╗════╗ ╔═════════╗════╗

tempo ———————> ║ 15 ║ ---> ║ 10 ║ X ║ ╚═════════╝════╝ ╚═════════╝════╝

*tempo

On vient de faire le deuxième tour de la boucle while et on dispose d'une liste représentée par le schéma :

╔═════╗════╗ ╔═════╗════╗

laListe ---> ║ 15 ║ --->║ 10 ║ X ║ ╚═════╝════╝ ╚═════╝════╝

Quand on revient à la boucle while, on rencontre déjà la fin du fichier. La liste est créée.

(13)

Exercice :

Supposons qu'on dispose d'un fichier binaire des employés (type structure du nom Employe). Adapter la fonction de création pour créer une liste linéaire chaînée à partir de ce fichier binaire.

Solution : On écrit seulement les changements.

void creerLIFO ( Pointeur * p , char * nomDonnees) { FILE * donnees = fopen(Nom_Donnees, "rb");

... pareils ...

int nbOctets = sizeof(Element) , k = sizeof(Employe) ; Employe unEmp ;

while ( fread(&unEmp, K, 1, donnees), !feof(donnees)) {

...

tempo->emp = unEmp ; /* emp est de type Employe. */

...

} ...

}

4.2) Création dans l'ordre FIFO :

Cette création est rarement présentée dans les livres. En réponse à la question d'une étudiante pour l'implantation d'une file d'attente (premier client à une caisse, premier servi), je donne une solution à cette création :

Appel : creerFIFO (&liste, "Liste.Dta");

Créer une liste linéaire dans l'ordre FIFO à partir du fichier "Liste.Dta".

Fonction de création :

void creerFIFO ( Pointeur * p , char * nomDonnees) { FILE * donnees = fopen(nomDonnees, "r");

Pointeur laListe = NULL , /* liste vide au début */

tempo, presentement ; /* pour l'algorithme de création */

int nbOctets = sizeof(Element) ;

(14)

while (!feof(donnees)) {

tempo = (Element *) malloc ( nbOctets) ; fscanf(donnees, "%d\n", &tempo->valeur);

if (laListe == NULL) laListe = tempo ; else

presentement->suivant = tempo ; presentement = tempo;

}

fclose(donnees);

/* fermer la liste : */

if (laListe) presentement->suivant = NULL ; *p = laListe ;

}

Simulation :

Pour comprendre le bon fonctionnement, on doit simuler la fonction avec un fichier "Liste.Dta" qui ne contient que 2 entiers :

10 15

Initialisation : Les données La liste ( qui est vide! ) --> 10 ╔═════╗

15 ║ X ║ laListe = NULL ╚═════╝

laListe Première tour de boucle while (!feof(donnees) ... : Non fin du fichier des données " ? OUI

1 ère itération :

Avec : tempo = (Element *) malloc ( nbOctets) ; *tempo

╔═════════╗════╗

tempo ---> ║ ? ║ ? ║ (on ne connaît pas sa valeur, ╚═════════╝════╝ ni son suivant)

Après : fscanf(donnees, "%d\n", &tempo->valeur);

on a :

données ╔═════════╗════╗

10 tempo ---> ║ 10 ║ ? ║

(15)

Après : if (laListe == NULL) laListe = tempo ; else

presentement->suivant = tempo ; on a le cas laListe == NULL, donc : laListe = tempo ; Ainsi :

On a :

laListe │

données └—————————> ╔═════════╗════╗

10 tempo ————————> ║ 10 ║ ║ -->15 ╚═════════╝════╝

Après : presentement = tempo;

on a :

laListe presentement (on est rendu ici) │ │

│ v

Données └—————————> ╔═════════╗════╗

10 tempo ————————> ║ 10 ║ ║ -->15 ╚═════════╝════╝

On vient de faire le premier tour de la boucle while et on dispose d'une "liste partielle" qui ne contient qu'un seul élément.

Deuxième tour de boucle while (!feof(Donnees) ... : Avant de faire ce deuxième tour, notre liste est : ╔═════════╗════╗

laListe ---> ║ 10 ║ ║ ╚═════════╝════╝

Non fin du fichier " donnees " ? OUI

Avec : tempo = (Element *) malloc ( nbOctets) ; laListe presentement

│ │ │ │ │ v

│ ╔═════════╗════╗ ╔═════════╗════╗

└—————————> ║ 10 ║ ? ║ ┌——> ║ ? ║ ? ║ ╚═════════╝════╝ │ ╚═════════╝════╝

│ *tempo tempo —————┘

(16)

Après : fscanf(donnees, "%d\n", &tempo->valeur);

on a :

laListe presentement données : 10 │ │ 15 │ │ -->eof │ v

│ ╔═════════╗════╗ ╔═════════╗════╗

└—————————> ║ 10 ║ ? ║ ┌——> ║ 15 ║ ? ║ ╚═════════╝════╝ │ ╚═════════╝════╝

│ *tempo tempo —————┘

Après : if (laListe == NULL) laListe = tempo ; else

presentement->suivant = tempo ; on a le cas laListe non NULL, on effectue donc presentement->suivant = tempo ; Ainsi :

laListe presentement données : 10 │ │ 15 │ │ -->eof │ v

│ ╔═════════╗════╗ ╔═════════╗════╗

└—————————> ║ 10 ║ ——————————> ║ 15 ║ ? ║ ╚═════════╝════╝ ┌——> ╚═════════╝════╝

│ *tempo tempo —————┘

Puis l'instruction : presentement = tempo ; donne :

laListe presentement │ │

│ │ │ v

│ ╔═════════╗════╗ ╔═════════╗════╗

└—————————> ║ 10 ║ ——————————> ║ 15 ║ ? ║ ╚═════════╝════╝ ┌——> ╚═════════╝════╝

│ *tempo tempo —————┘

Données : 10 15 --> eof

(17)

Ainsi, après 2 tours de boucle, on a :

presentement │

v

╔═════╗════╗ ╔═════╗════╗ laListe ---> ║ 10 ║ --->║ 15 ║ ? ║ ╚═════╝════╝ ╚═════╝════╝ C'est la fin du fichier. On effectue : if (laListe) presentement->suivant = NULL ; Comme laListe est non NULL (elle pointe vers quelque chose), la condition if (laListe) est vérifiée, on fait : presentement->suivant = NULL ; c'est-à-dire, on ferme la liste : presentement │ │ v

╔═════╗════╗ ╔═════╗════╗ laListe ---> ║ 10 ║ --->║ 15 ║ X ║ ╚═════╝════╝ ╚═════╝════╝ Exercice 1 : Supposons qu'on dispose d'un fichier binaire des employés (type structure du nom Employe). Adapter la fonction de création pour créer une liste linéaire chaînée dans l'ordre FIFO à partir de ce fichier binaire. Exercice 2 : Supposons que les données sont triées avant la création de la liste linéaire dans l'ordre FIFO. Les données de cette liste sont-elles triées ? Exercice 3 : Supposons qu'on dispose d'un fichier texte des entiers non triés : 20

15

50

30

Écrire une fonction permettant de créer une liste linéaire des entiers triés.

(18)

Références

Documents relatifs

tions permettant de caractériser le comportement statique d'un élément fluide, puis dans un deuxième temps nous aborderons les différentes modélisations dynamiques permet- tant

Le programme identifie les nombres comme étant des suites de chiffres successives, séparées par le reste du contenu du fichier par des espaces, des virgules, des point-virgule ou

En ce qui concerne la dynamique, nous utilisons la m´ ethode EMD (Empirical Mode Decompostion) pour estimer des spectres de puissance, et ´ etudier la dynamique multi-´ echelle via

Returns the index in this list of the first occurrence of the specified element, or -1 if the List does not contain this element. Retourner l’indice de la première occurence

Écrivez un constructeur de la classe Liste qui, à partir d’un tableau dynamique d’éléments, crée la liste contenant les mêmes éléments dans le même ordre.. Donnez un

Liste Cons(float tete, Liste queue) : Construit une nouvelle liste dont la valeur de la première cellule est tete et dont le suivant sera queue5. Il s’agit d’un insertion en tête

• Les piles sont utilisées pour implanter les appels de procédures (cf. pile système). • En particulier, les procédures récursives gèrent une pile

Allocation d’un bloc mémoire de 20 octets pour stocker 5 int. Ecriture de l’adresse de la 1ère case du bloc