Programmation Procédurale P2
Programmation Procédurale P2
Programmation Procédurale P2
"tableau" est l'identificateur de la variable tableau qui correspond à une zone mémoire où nous pouvons
stocker 3 entiers. Elle comporte 3 cases. Chaque case peut contenir une valeur entière de type int.
Il ne faut pas confondre le numéro d'un élément du tableau et l'indice de la case qu'il occupe. Vous
remarquerez qu'en langage C les indices de cases commencent à 0. Ainsi, la déclaration int t[3] crée 3
cases dont les indices sont 0, 1, 2. Nous avons bien 3 indices pour repérer les 3 cases.
Le compilateur alloue la zone mémoire, mais n'initialise pas le contenu des cases. C'est pourquoi nous
avons indiqué « ? » pour le contenu de chaque case.
25
Attention :
Le nombre de cases d'un tableau doit impérativement être connu à la compilation. En effet, il faut
pouvoir réserver la mémoire nécessaire à la zone de données contiguës.
À l'issue de la déclaration, le tableau est créé mais le contenu de ses cases n'est pas initialisé. Par
exemple « int truc[3] », déclare un tableau de 3 entiers, mais les entiers truc[0], truc[1] et truc[2] ne
sont pas initialisés.
26
{
machin[i] = 10-i;
}
}
Remplira les cases 0,1,2,3,4,5,6,7,8,9 du tableau avec dans l'ordre les valeurs 9,8,7,6,5,4,3,2,1.
iv. Initialisation par lecture
#include <stdio.h>
int main()
{
int i ;
float t_r [10] ;
printf("\nentrer 10 valeurs flottantes") ;
for (i = 0 ; i < 10 ; i ++)
{
scanf(" %f ", &t_r[ i ]);
}
}
« t_r[i] » est une variable de type flottante et &t_r[i] est bien son adresse mémoire. C'est ce dont a
besoin le « scanf » pour ranger l'information lue au clavier.
v. Techniques algorithmiques liées aux tableaux
L'usage de constantes pour coder le nombre de cases maximales du tableau est conseillé.
Considérons le code :
#include <stdio.h>
int main()
{
int machin[10];
for (i=0;i<10;i++)
{
machin[i] = 10-i;
}
}
Si nous souhaitions travailler avec un tableau de taille 50, nous aurions besoin de modifier 3 lignes.
Considérons maintenant le code :
#include <stdio.h>
#define MAX 10
int main()
{
int machin[MAX];
for (i=0;i<MAX;i++)
{
machin[i] = MAX-i;
}
}
Avec ce code, si l'on veut travailler avec un tableau de 50 cases, il suffit de modifier une ligne (le «
#define »). Le précompilateur fera le travail de modification à notre place. C'est une technique de
programmation qui évite bien des erreurs.
Nous rappelons que la taille d'un tableau doit être connue à la compilation et que la taille d'un tableau
ne peut être modifiée au cours de l'exécution d'un programme.
Soit le problème suivant posé : le nombre d'éléments que l'on veut ranger et utiliser dans un tableau
est inconnu, mais on sait qu'il n'y en aura pas plus que 1000.
Dans ce cas, il suffit d'utiliser la technique que nous illustrons dans le code suivant :
include <stdio.h>
#define MAX 1000
int main()
{
27
int machin[MAX];
int nb_machin; // le nombre d'entiers rangés dans le tableau machin
}
Nous déclarons le tableau de taille maximale.
Puis on ajoute la déclaration de la variable « nb_machin » qui indique le nombre d'éléments que l'on a
effectivement rangés dans le tableau « machin ».
Vous remarquerez le choix effectué pour construire l'identificateur de la variable qui gère le nombre
d'entiers rangés dans le tableau.
Bien évidemment, c'est au programmeur d'incrémenter « nb_machin» quand on ajoute un élément et
de le décrémenter quand on en retire un.
28
b. Les tableaux à deux dimensions
i. Problème de la linéarisation de la structure de donnée
Voici un tableau à deux dimensions tel qu'il est présenté en mathématique par exemple :
7 8 9
15 16 17
Nous remarquons immédiatement qu'il s'agit d'une donnée qui possède deux dimensions. Il faut un
indice de ligne et un indice de colonne pour accéder au contenu d'une case. Or, la mémoire d'une
machine est vue comme un très grand ruban d'octets, c'est une organisation linéaire avec une seule
dimension puisque l'on accède à un octet à l'aide de son adresse mémoire. Ce ruban peut être vu comme
un tableau à une dimension.
Il faut donc trouver une technique pour linéariser un tableau en deux dimensions afin qu'il puisse être
rangé et manipulé dans un tableau à une dimension.
Nous avons deux possibilités :
Linéarisation colonne/colonne
Linéarisation ligne/ligne
Dans le premier cas le tableau est découpé en colonnes, et les colonnes consécutives sont rangées les
unes derrière les autres. Dans le second cas le tableau est découpé en lignes, et les lignes sont rangées
les unes derrière les autres. Le langage C utilise la deuxième technique.
Le découpage et l'organisation en lignes donne :
7 8 9 15 16 17
Les valeurs 7, 8, 9, 15, 16, 17 peuvent se ranger les unes à la suite des autres dans la mémoire : nous
avons linéarisé le rangement du tableau.
Un tableau à deux dimension est aussi appelé matrice.
Permet de définir la variable "t" comme un tableau à deux dimensions de 2 lignes et 3 colonnes.
Pour accéder à une case il suffit d'utiliser la syntaxe :
t[1][2]
Ce qui permet d'accéder à la case qui contient un entier qui se trouve en deuxième ligne, troisième
colonne du tableau. (les indices commencent à zéro).
Bien que le tableau soit "linéarisé", pour accéder aux cases du tableau « t », l'opération d'indexation
s'écrit tout simplement « t[i][j] ». Le compilateur se charge de retrouver la bonne case, cette syntaxe
est transparente pour le programmeur.
1. Exemple d'initialisation à la déclaration
#define MAX_L 2
#define MAX_C 3
double td[MAX_L] [MAX_C] = {{37.2,37.5},{38.4, 40.5, 43.2}};
L'initialisation s'effectue ligne par ligne avec les listes de valeurs d'initialisation. Comme pour les
tableaux à une dimension, si une liste n'est pas complète, seules les premières cases sont remplies dans
l'ordre de la liste.
29
Ainsi, sur l'exemple ci-dessus la case td[0][2] n'est pas initialisée car il n'y a pas de troisième valeur
dans la liste d'initialisation de la première ligne.
iii. Initialisation par affectation
En considérant les déclarations :
#include<stdio.h>
#include <math.h>
#define MAX_L 2
#define MAX_C 3
#define PI 3.14159265
int main(void)
{
double td[MAX_L] [MAX_C];
int i,j;
for(i=0;i<MAX_L;i++)
{
for (j=0;j<MAX_C;j++)
{
td[i][j] = sin(PI/4);
printf("Les valeurs sont:%d \n",td[i][j]);
}
}
Nous avons initialisé toutes les cases du tableau "td" par calcul. Le type d'une case du tableau doit être
compatible avec le type de valeur retournée par l'expression à droite de l'opérateur d'affectation.
Va effectuer la lecture et affecter la valeur de toutes les cases du tableau « t ». Par souci de lisibilité,
nous vous conseillons d'écrire « &(t[i][j]) ». Cette écriture met plus en évidence que l'opérateur unaire
« & » (adresse de) s'applique à la case « t[i][j] ».
v. Débordement par excès et par défaut
30
Le débordement par excès survient quand on cherche à accéder à une case en dehors du tableau en
utilisant un indice plus grand que l'indice maximal possible.
include <stdio.h>
#define MAX 1000
int main()
{
int machin[MAX];
machin[1500] = 10;
}
La case d'indice 1500 n'existe pas puisque l'indice maximal pour le tableau qui a été déclaré est 999.
Attention :
Le débordement par défaut survient quand on cherche à accéder à une case en dehors du tableau en
utilisant un indice plus petit que l'indice minimal possible.
machin[-2] = 17;
La case d'indice -2 n'existe pas puisque l'indice minimal est 0.
Cela peut sembler très surprenant que le compilateur accepte ces instructions, mais c'est le cas. Plus
surprenant encore, le programme compilé peut aussi bien fonctionner que ne pas fonctionner. Les
débordements constituent évidemment des erreurs graves de programmation.
Car il est très dangereux d'accéder ainsi à une donnée qui n'existe pas et de la modifier.
En effet, on accède ainsi à des octets dans la mémoire et on les modifie. Cela peut faire "planter" le
code, comme cela peut ne pas le faire "planter".
Et puisque le C ne vérifie pas ce genre de chose, ce sont des erreurs très difficiles à détecter quand on
les commet.
31
Un des caractères du tableau doit être le marqueur de fin de chaîne '\0'. Dans ce cas tous les caractères
qui précèdent font partie de la chaîne de caractères et on ne se préoccupe pas de ceux qui suivent le
marqueur.
1. Les littéraux constants
Nous rappelons que la syntaxe 'a' désigne un unique caractère tandis que la syntaxe "a" désigne une
chaîne de caractères constante.
Dans le premier cas nous avons un unique octet, dans le second la chaîne de caractère occupe deux
octets dans une zone mémoire qui est un tableau de caractères. En effet, le premier octet contient le
codage du caractère 'a', le second le codage du '\0' qui marque la fin du littéral constant de type chaîne
de caractères "a".
i = 0;
while (s1[i]!='\0') putchar(s1[i++]);
Nous pouvons remarquer que la liste d'initialisation fait explicitement apparaître le caractère de
marquage '\0'. Cette initialisation par liste peut être fastidieuse, il existe une autre façon de faire dans
le cas des chaînes de caractères.
Cette valeur est calculée par le compilateur à partir des nombres de caractères utilisés pour
l'initialisation à la déclaration.
32
Initialement la chaîne « chai » contient le littéral constant de type chaîne de caractères « a ». Puis
l'usage de « scanf » avec le format %s effectue la lecture au clavier d'une chaîne de caractères qui va
écraser cette ancienne valeur.
Vous remarquerez que nous n'utilisons pas l'opérateur & (adresse de) devant l'identificateur « chai »
du tableau de caractères, ce n'est pas une erreur (se référer aux explications dans la section "lien entre
tableau, indice et pointeur")
Si l'on saisit plus de caractères que ne peut en contenir le tableau, seuls les premières cases seront
remplies, la chaîne saisie est alors tronquée et les caractères en surnombre sont perdus.
char * strcpy (chaîne1, chaîne2) : copie le contenu de « chaîne2 » dans « chaîne1 ». L'ancienne
valeur de chaîne1 est perdue. Retourne un « pointeur sur » chaîne1.
int strlen (chaîne) : renvoie le nombre de caractères de la chaîne. Cette fonction compte les
caractères jusqu'à trouver le ‘\0'. La valeur renvoyée est le nombre de caractères avant le ‘\0'
char * strcat (chaîne1, chaîne2) : copie le contenu de chaîne2 à la fin de chaîne1. Retourne un
« pointeur sur » chaîne1.
int strcmp ( chaîne1 , chaîne2 ) : compare les deux chaînes, retourne 0 en cas d'égalité, une
valeur négative si « chaîne1 » est "avant" « chaîne2 » dans l'ordre lexicographique, une valeur
strictement positive sinon.
char * strncat ( chaine1 , chaine2 , n ) : concatène au plus les « n » premiers caractères de «
chaine2 » à « chaine1 ». Retourne un pointeur sur « chaine1 ». Si n est plus grand que la
longueur de « chaine2 », la longueur de « chaine2 » est utilisée à la place de n. Retourne un «
pointeur sur » chaîne1.
char * strncpy ( chaine1 , chaine2 , n) : copie « n » caractères de « chaine2 » sur « chaine1 ».
Retourne « un pointeur sur chaine1 ». Si « n » est inférieur à longueur de « chaine2 » « \0 »
n'est pas ajouté en fin de « chaine1 ». Si « n » est supérieur à longueur de « chaine2 », « chaine1
» est complétée par des « \0 ». Retourne un « pointeur sur » chaîne1.
33
Pour les opérations de copies, si la chaîne de caractères cible « chaîne1 » ne contient pas assez de place
pour contenir « chaîne2 » alors seuls les caractères que l'on peut copier le sont. On dit que « chaîne2 »
est tronquée.
Nous ne les décrirons pas toutes dans le cadre de ce cours introductif. Connaissant leur nom, il vous
sera facile de retrouver ce qu'elles effectuent comme traitement en consultant les documentations
adhoc.
Attention :
Si vous ne leur fournissez pas de tableaux de caractères qui comportent quelque part le caractère de
marquage '\0', vous pouvez avoir des problèmes en les utilisant.
Attention :
« chaine1 » et « chaine2 » sont des chaînes de caractères c'est-à-dire qu'il faut transmettre aux fonctions
des « pointeurs sur char »
En C, on ne peut pas comparer deux chaînes de caractères avec les opérateurs >, >=, <, <=, ==. Il faut
utiliser la fonction « strcmp ».
La valeur de cette constante est l'adresse du premier octet du premier élément du tableau. Comme le
montre le schéma ci-dessous :
En résumé, « t » est l'identificateur d'un pointeur constant vers un entier et dont la valeur est l'adresse
du premier octet de la zone réservée. Puisque « t » est une constante, on ne peut pas modifier sa valeur.
int t[3];
int * point_sur_int;
int i = 5;
point_sur_int = &i;
t = point_sur_int;
/* t est l'identificateur d'un pointeur constant : cette instruction est INTERDITE !*/
t = t + 1;
/* t est l'identificateur d'un pointeur constant : cette instruction est INTERDITE */
L'opérateur d'affectation n'est pas directement utilisable entre les identificateurs de tableaux. Mais on
peut manipuler les tableaux case par case.
Par exemple, si on veut faire la somme de 2 vecteurs « v1 » et « v2 » dans « v3 », on ne peut pas écrire
« v3=v1+v2 ». Il faut ajouter les cases de « v1 » et « v2 » une à une à l'aide d'une boucle selon une
instruction du type « v3[i] = v1[i] + v2[i] ».
i. Cas des tableaux unidimensionnels
34
Puisque dans la déclaration :
int t[10];
« t » est l'identificateur d'un pointeur constant, nous pouvons introduire une autre manière d'accéder à
une case du tableau en utilisant l'arithmétique sur les pointeurs. En effet, les instructions suivantes sont
strictement identiques :
t[i] = 0;
*(t+i) = 0;
De la même façon : &t[i] et (t+i) fournissent la même valeur. C'est l'adresse du premier octet de la case
d'indice i du tableau t.
En fait, quand nous écrivons :
t[i] = 0;
Le compilateur génère des instructions machine que l'on peut écrire comme :
*(t+ i*sizeof(int))= 0;
Ce qui permet de remplir la case i à partir de son premier octet avec la valeur 0. Les 4 octets consécutifs
que l'on trouve à partir de l'adresse t+ i*sizeof(int)) sont donc initialisés.
C'est donc un décalage de i entiers à partir de l'adresse du début du tableau. Ceci est transparent pour
le programmeur.
float t[3] = {1.0, 2.0 ,3.0};
/* t est l'identificateur d'un pointeur constant, dont la valeur est l'adresse du
premier octet du tableau */
float * pt_sur_flottant;
/* « pt_sur_flottant » est l'identificateur d'une variable qui contient une
adresse vers un flottant */
/* les deux lignes suivantes sont equivalentes */
pointeur_sur_flottant = t; // on peut affecter la valeur d'une constante à une
variable
pointeur_sur_flottant = &t[0]; // c'est la même chose que l'instruction ci-dessus
/* car &*(t+0) équivanut à (t+0) donc t */
/* et les deux lignes suivantes sont aussi équivalentes */
*pointeur_sur_flottant = -273.15;
/* car c'est *t donc *(t+0) donc t[0] */
t[0] = -273.15; // c'est la façon la plus simple de procéder.
bidule[i] et *(bidule +i)
Nous avons donc 2 façons de réaliser l'accès à des cases d'un tableau "bidule" :
bidule[i] et *(bidule +i)
Mais la notation bidule[i] est beaucoup plus facile à comprendre, à lire et à écrire. C'est donc celle
qu'il faut privilégier.
ii. Cas des tableaux bi-dimensionnels ou à 2 dimensions
Considérons l'exemple :
#define MAX_L 2
#define MAX_C 3
int t[MAX_L] [MAX_C];
int i,j;
Cela correspond au dessin linéarisé suivant :
t
indices 0 1 2 3 4 5
t[0][0] t[0][1] t[0][2] t[1][0] t[1][1] t[1][2]
Tableau 1: Linéarisation d'un tableau à deux dimension
En effet, le tableau est linéarisé selon le schéma ligne/ligne. Dans ce cas, t est l'identificateur d'un
pointeur constant aussi. Mais ici c'est l'adresse d'une adresse.
35
C'est l'adresse de l'adresse de la première ligne. Et *t est donc l'adresse de la première ligne c'est en fait
*(t+0).
Donc :
(t+i) est l'adresse de l'adresse de la ligne d'indice i
*(t+i) est l'adresse de la ligne i
*(t+i) + j est l'adresse du premier octet de la case j de la ligne i car on a un décalage de j entiers
dans la ligne i
*(*(t+i)+j) est donc la valeur de l'entier qui se trouve là,
et c'est la même chose que t[i][j]
donc, si vous avez bien compris le cours sur les tableaux :
t[1][2] est la même chose que :
*(t[1]+2)
*(*(t + 1) + 2)
*((int *)t+5)
Qui sont des façons d'accéder à la case qui se trouve en ligne 1 colonne 2.
Explications particulière pour la notation « *((int *) t+5) » : il y a 3 colonnes et ((1*3)+2)=5 est bien
le nombre d'entiers depuis le début du tableau qui correspond à la case d'indices (1,2) avec la
linéarisation.
Mais il faut un « cast » car « t » est vu comme « un pointeur de pointeur vers entier » : il faut que le
compilateur le "voit" comme un « pointeur vers entier » pour ne pas avoir de « warning » à la
compilation.
Si on écrit directement l'expression (t+5) pour calculer l'adresse d'un entier (pointeur vers entier), le
compilateur va l'interpréter comme l'accès à la ligne d'indice 5, ce qui peut être gênant.
Attention
Quand on écrit dans le programme :
&(t[i][j])
La machine calcule en fait l'expression mathématique suivante pour avoir l'adresse :
t + ( i x MAX_C + j) x Taille
avec Taille = sizeof(int)
C'est-à-dire la taille en nombre d'octets d'une case du tableau.
Et forcément quand on écrit :
t[i][j] = 0;
Le compilateur effectue quelque chose comme :
*(t + (i x MAX_C + j) x Taille) = 0
Un tableau à deux dimensions peut donc être vu comme un tableau de tableau à une dimension.
Considérons la déclaration suivante :
float machin[2][4] = {{10.0,9.0,8.0,7.0},{51.0,52.0,53.0,54.0}};
Le compilateur le « voit » comme dans la figure suivante :
pointeur de ligne :
machin+0 -> 10.0 9.0 8.0 7.0
machin+1 -> 51.0 52.0 53.0 54.0
La valeur « machin[0] » est une valeur qui est un pointeur vers le premier octer de la ligne 0, ce qui
correspond à *(machin +0).
Ce qu'il faut retenir :
Le compilateur à absolument besoin de connaître le nombre de colonnes car il a besoin de la
valeur MAX_C pour effectuer ces calculs d'adresses
L’usage des pointeurs pour accéder aux tableaux à deux dimensions est compliqué.
1. Usage des tableaux multidimensionnels
N'utilisez pas les pointeurs avec des tableaux à 2 dimensions
Utilisez les notations :
36
tab[i][j] pour les opérations manipulant la valeur de la case (i,j)
&(tab[i][j]) pour les opérations qui ont besoin de l'adresse de la case comme le « scanf » par
exemple
En effet :
scanf("%d",&t[i][j]); sera toujours plus clair que scanf(%d",&*((int *)*(t+i)+j));
Les tableaux à « n » dimension (n>2) correspond à un tableau de tableaux de dimension n-1. Nous ne
présenterons pas en détail les tableaux à plus de 2 dimensions, sachez seulement que :
#define MAX_L 2
#define MAX_C 3
#define MAX_P 7
int t_3[MAX_L] [MAX_C] [MAX_P];
int i,j,k;
Déclare un tableau à 3 dimensions, et que :
t_3[i][j][k]= 7;
Pour mettre la valeur 7 dans la case d'indices (i,j,k) ainsi que :
scanf("%d",&(t_3[i][j][k]));
Pour saisir la case d'indices (i,j,k), fonctionnent parfaitement.
Et « t_3 » est l'identificateur d'une constante qui est vue comme une adresse d'adresse d'adresse d'entier
par le compilateur ...
37
IV. Structure des données
a. Définition
38
V. Allocation dynamique
a. Rappel
La mémoire centrale d'un ordinateur peut être vue comme un long ruban d'octets. Chaque octet possède
une adresse unique. Dans les chapitres précédents, les variables ont été présentées comme un couple
(identificateur, réceptacle), le réceptacle étant constitué de plusieurs octets contigus en mémoire
centrale.
Nous avons vu que, connaissant la taille (le nombre d'octets) d'un réceptacle et l'adresse de son premier
octet, il était possible de lire et d'écrire de l'information comme si c'était une variable : c'est une des
utilisations possibles des pointeurs. Cependant, nous n'avons pas précisé où se trouvaient rangées les
variables en mémoire centrale. Nous dirons simplement qu'il existe deux zones de la mémoire où on
peut ranger les variables :
Une zone 1 qui correspond à ce que nous avons vu jusqu'à présent
Une zone 2 qui peut être utilisée dynamiquement par le programmeur
La première zone correspond par exemple à la déclaration ci-dessous :
void main(void)
{
int i;
float t[50];
}
Note importante :
Nous avons utilisée ce type de déclaration dans tous nos exemples, depuis le début de ce cours
introductif. Nous allons vous présenter succinctement comment utiliser la zone 2 que l'on appelle le
"tas" (heap en anglais).
Important :
Nous avons volontairement simplifié la présentation en scindant la mémoire en deux zones, la réalité
est plus complexe. Les explications fournies dans ce cours introductif sont suffisantes pour
l'appréhender. Néanmoins, la « carte » d'un programme qui s'exécute en mémoire centrale est plus
complexe.
Ce cours introductif n'a pas pour objectif de prendre en considération des concepts trop complexes. A
ce stade, ce qu'il faut retenir, c'est que l'on peut dynamiquement demander la création de nouveaux
réceptacles pour des données. Ceci signifie que l'on peut allouer de l'espace pour les données au cours
de l'exécution du programme.
Cette création s'effectuera dans la zone de tas. De même, on peut récupérer dynamiquement les octets
qui ont été alloués. Pour cela, nous allons utiliser deux fonctions :
malloc
free
Ces fonctions sont définies dans le fichier "en-tête" « stdlib.h » qui fait partie de la bibliothèque
standard définie par la norme ANSI. Ce fichier contient aussi les déclarations de fonctions traitant de
conversion de chaînes de caractères en types numériques ("itoa" par exemple), de tirages aléatoires («
srand » et « rand ») et d'autres fonctions d'allocation mémoire.
b. La fonction « malloc »
Nous vous rappelons qu'il est impossible de déclarer une variable de type « void » et qu'une fonction
de type « void » ne retourne aucune valeur.
Par contre, « void * » correspond à une donnée de type pointeur vers octet. Nous pouvons donc déclarer
une variable de type « void * ».
Si une fonction est de type « void * » elle retournera une valeur de type « adresse d'un octet ».
40
#define MAX 50
struct eleve
{
char nom[MAX],
prenom[MAX],
float note_moyenne;
};
typedef struct eleve T_eleve;
int * allouer_tableau_eleve(int nb)
{
T_eleve * inter;
// variable locale de la fonction allouer_tableau_eleve
// qui contient une valeur qui est l'adresse d'un tableau alloué dynamiquement
inter = (T_eleve *)malloc(nb*sizeof(T_eleve));
//allocation de « nb » cases, chaque case étant de
// type « T_eleve ».
// La fonction « malloc » retourne un « void* », il ne faut
// donc pas oublier l'opération de « cast ».
return inter;
// on retourne la valeur du pointeur : elle n'est pas perdue
// bien que la variable locale soit détruite après l'appel de la fonction.
}
int main(void)
{
T_eleve * tab_eleve;
int nombre;
prinf("\n entrer le nombre d'eleves : ");
scanf("%d",&nombre);
tab_eleve = allouer_tableau_eleve(nombre);
// appel de la fonction « allouer_tableau_eleve » qui va
//dynamiquement créer
// un tableau de « nombre » cases. Chaque case étant
//une variable de type « T_eleve »
if (tab_eleve == NULL)
prinf("\n probleme d'allocation dynamique");
else
{
}
}
c. La fonction "free"
La fonction "free" sert à restituer l'espace que l'on avait alloué avec "malloc".
Nous vous conseillons d'utiliser cette fonction autant de fois que la fonction "malloc" pour libérer la
place allouée dynamiquement. Si "p" pointe sur le début d'une zone mémoire allouée dynamiquement
alors l'appel de la fonction « free(p) » libère cette zone mémoire.
Le gestionnaire (fictif) de mémoire centrale récupère cette place et peut éventuellement l'allouer à
nouveau.
Pour comprendre le fonctionnement de la fonction « free », il peut être utile de savoir que, au moment
d'une allocation dynamique, le gestionnaire (fictif) de mémoire réserve en fait une petite place
supplémentaire juste avant la zone allouée au programme pour y noter la longueur de la zone allouée.
Lorsque l'instruction « free(p) » est effectuée, il suffit alors au gestionnaire d'aller relire la longueur de
la zone allouée pour connaître la taille de la zone à "désallouer".
41
Si la mémoire allouée avec « malloc » n'est pas libérée avec « free », elle est quand même libérée à la
fin du programme, mais ce n'est pas de la programmation « propre ». Si l'on cherche à libérer une zone
mémoire non allouée avec « malloc », cela peut provoquer une erreur à l'exécution du code.
Nous avons, dans l'exemple qui suit, une structure avec deux champs : un entier et un pointeur vers
une structure de type cellule qui suit la cellule courante.
Nous utilisons les techniques d'allocation dynamique pour créer dynamiquement les zones mémoires
(réceptacles) qui correspondent aux cellules.
La liste est repérée simplement par un pointeur vers la tête de la liste. La tête de liste correspond à
l'adresse de la première cellule de la liste. Quand la liste est vide, ce pointeur vaut NULL. Sur le
dessin qui précède, la cellule de tête est celle qui contient la valeur 12.
Et la dernière cellule est celle qui contient la valeur 37. Le pointeur qui suit la dernière cellule pointe
sur une X, ce qui symbolise graphiquement la valeur particulière NULL. NULL est une valeur
particulière qui correspond à une adresse mémoire qui n'existe pas.
Il est donc facile de tester si la liste est vide. Nous comparons la valeur du pointeur de la liste avec
cette valeur particulière (opérateurs & et *).
Dans le code ci-dessous nous utilisons le prototypage de fonction.
Ce code permet :
D’ajouter des cellules en tête de la liste,
D’afficher les cellules de la liste,
De rechercher une valeur particulière dans la liste et
De récupérer tous les réceptacles alloués dynamiquement quand on désire détruire toute la
liste.
C'est une base, qui peut vous servir pour vos propres applications (voir code en annexe 1).
42
VI. Les entrées/sorties
43
Références
Cours et Exercices Pratiques — S3-PRC École Nationale d'Ingénieurs de Brest (ENIB) ;
https://www.w3schools.com/c/index.php;
http://ressources.unit.eu/cours/Cfacile/co;
Le langage C, IUT de Ngaoundére, 2010-2011.
44
Annexe 1
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
struct cellule
{
int valeur;
struct cellule * succ;
// pointeur vers la cellule suivante
};
typedef struct cellule T_cellule;
// definition de type, permet d'eviter
// d'avoir a ecrire a chaque fois "struct cellule"
// les prototypes des fonctions
char bon_choix(char *s);
int affiche_menu_et_saisie_choix(void);
void afficher_cellule(T_cellule * c);
void affiche_liste(T_cellule * c);
T_cellule * detruire_liste(T_cellule * c);
T_cellule * ajouter(T_cellule * c);
void rechercher(T_cellule * c);
on principale
int main(void)
{
char choix;
T_cellule * tete;
tete = NULL;
do {
switch (choix = affiche_menu_et_saisie_choix())
{
case 'A' : tete = ajouter(tete); break;
case 'V' : affiche_liste(tete); break;
case 'S' : tete = detruire_liste(tete); break;
case 'R' : rechercher(tete); break;
case 'Q' : { tete = detruire_liste(tete);
// desallouer toujours proprement avant de
sortir
printf("\n\nAu revoir\n");
}
printf("\n");
}
// les declarations completes des fonctions
char bon_choix(char *s) {
/* cette fonction teste si le premier caractère d'une chaîne correspond à un
caractère parmi ‘A','a', ‘V', ‘v', ‘S', ‘s', ‘R', ‘r', ‘Q', ‘q', elle retourne un
unique caractère majuscule dans ce cas et le caractère ‘\0' sinon. */
char r;
switch (s[0])
{
case 'A' :;
case 'a' : r = 'A';break;
case 'V' :;
case 'v' :r = 'V';break;
i
case 'S' :;
case 's' :r = 'S';break;
case 'R' :;
case 'r' :r = 'R';break;
case 'Q' :;
case 'q' : r = 'Q';break;
default: r='\0';
}
return r;
}
int affiche_menu_et_saisie_choix(void)
{
/* Permet d'afficher un menu, de lire une chaîne et de vérifier si le premier
caractère de cette chaîne correspond à un des choix possibles du menu.*/
// Elle affiche un message d'erreur si le choix n'est pas bon et ré-affiche
ensuite le menu.
char s_choix[2];
do {
printf("\n ajouter -> A");
printf("\n voir liste -> V");
printf("\n supprimer liste -> S");
printf("\n rechercher -> R");
printf("\n quitter -> Q");
printf("\n");
printf("\n votre choix : ");
scanf("%s",s_choix);
printf("\n");
if (!bon_choix(s_choix))
{
printf("\n\nVotre choix n'est pas bon ,recommencez SVP\n");
}
} while (!bon_choix(s_choix));
return (bon_choix(s_choix));
}
void afficher_cellule(T_cellule * c) {
// Fonction qui affiche le contenu d'une cellule
if (c==NULL)
{
printf("\rien a afficher, cellule vide\n");
}
else
{
printf("\nla cellule rangee a l'adresse %p contient la valeur %d ",c,c-
>valeur);
printf("\nla cellule qui suit est rangee a l'adresse %p ",c->succ);
printf("\n");
}
}
void affiche_liste(T_cellule * c) {
// Fonction qui affiche le contenu d'une liste cellule par cellule
if (c==NULL)
{
printf("\nrien a afficher, liste vide\n");
}
else {
do {
afficher_cellule(c);
c = c -> succ;
} while (c!=NULL);
}
ii
}
T_cellule * detruire_liste(T_cellule * c) {
// Fonction qui libère toute la place mémoire occupée par une liste que l'on veut
détruire
//initialement c contient la valeur du pointeur vers la cellule de tête de liste
à détruire
T_cellule * inter;
if (c==NULL)
{
printf("\nrien a detruire, liste vide\n");
}
else
{
do {
inter = c -> succ; //inter pointe sur la prochaine cellule a détruire
printf("\nrecuperation de la memoire de la cellule a l'adresse %p",c);
free(c);
c = inter;
} while (c!=NULL);
}
return NULL; //puisque toute la liste a été détruite on renvoie NULL
}
T_cellule * ajouter(T_cellule * c) {
// Fonction qui crée une cellule (allocation dynamique avec « malloc »).
T_cellule * nouvelle;
nouvelle = ( T_cellule *)malloc(sizeof( T_cellule));
printf("\nun entier SVP ");
scanf("%d",&(nouvelle->valeur));
nouvelle -> succ = c; return nouvelle;
}
void rechercher(T_cellule * c) {
// Fonction qui recherche une valeur particulière dans une liste.
// Affiche l'adresse de la cellule qui contient la valeur si cette dernière est
dans la liste et un message
// disant que la valeur n'est pas dans la liste sinon.
int val,trouve;
trouve = 0;
if (c==NULL)
{
printf("\nrien a rechercher, liste vide\n");
}
else {
printf("\nl'entier recherche SVP ");
scanf("%d",&val);
do {
if (c->valeur == val)
{
printf("\n\nl'entier recherche %d",val);
printf("\nse trouve dans la cellule a l'adresse %p",c);
c = NULL;
// pour stopper la boucle do while
trouve = 1;
// pour memorise le fait que l'on a trouve
}
else
{
printf("\npanparcours cellule a l'adresse %p, valeur %d",c,c->valeur);
c = c-> succ;
}
} while ((c!=NULL)&& (trouve !=1));
if (trouve ==0)
iii
{
printf("\n\nrecherche entier %d ",val);
printf("ne se trouve pas dans la liste");
printf("\n");
}
}
}
iv