0% ont trouvé ce document utile (0 vote)
10 vues59 pages

Cours STD

Ce document présente un cours sur les structures de données en C, en se concentrant sur les pointeurs et les tableaux. Il explique les concepts fondamentaux tels que l'utilisation des pointeurs, l'arithmétique d'adresse, et la gestion des chaînes de caractères. Des exemples pratiques illustrent comment les pointeurs peuvent être utilisés pour manipuler des données en mémoire.

Transféré par

yassine atiki
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
10 vues59 pages

Cours STD

Ce document présente un cours sur les structures de données en C, en se concentrant sur les pointeurs et les tableaux. Il explique les concepts fondamentaux tels que l'utilisation des pointeurs, l'arithmétique d'adresse, et la gestion des chaînes de caractères. Des exemples pratiques illustrent comment les pointeurs peuvent être utilisés pour manipuler des données en mémoire.

Transféré par

yassine atiki
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd

Cours Programmation

avancée
STRUCTURE DE DONNEES – CHAPITRE I

HONNIT BOUCHRA
EMSI-CASA | 2021/2022

0
Table des matières
1. Pointeur & tableau en C ......................................................................................................... 3

1.1 Introduction ...................................................................................................................... 3

1.1.1 Opérateur de référencement ....................................................................................... 4

1.1.2 Opérateur de déréférencement ................................................................................... 4

1.1.3 Utilisation de & et * ................................................................................................... 4

1.1.4 Utilisation de pointeurs en paramètres ....................................................................... 5

1.2 Tableaux et pointeurs........................................................................................................ 7

1.2.1 Arithmétique d’adresse et tableaux ............................................................................ 9

1.2.2 Pointeurs & tableaux .................................................................................................. 9

1.2.3 Tableau de pointeur .................................................................................................. 12

2. Les chaines de caractères ..................................................................................................... 13

2.1 Représentation de chaine de caractères .......................................................................... 13

2.2 Initialisation .................................................................................................................... 13

2.3 Lecture & écriture des chaines de caractères .................................................................. 15

2.4 Gestion d’entrée avec gets et sscanf ............................................................................... 16

2.5. Bibliothèque <string.h> ................................................................................................. 16

1
Chapitre I
Rappel

2
1. Pointeur & tableau en C
1.1 Introduction
Le pointeur est une variable destinée à contenir une adresse mémoire. Le compilateur connaissant la
taille de l’espace adressable de la machine, il connaît la taille nécessaire pour contenir un pointeur. Un
pointeur est reconnu syntaxiquement par l’étoile (symbole de la multiplication *) qui précéde son nom
dans sa définition.

Tout pointeur est associé à un type d’objet. Ce type est celui des objets qui sont manipulables grâce
au pointeur. Ce type est utilisé en particulier lors des calculs d’adresse qui permettent de manipuler
des tableaux à partir de pointeurs (voir Chap. 10).

Prenons les définitions suivantes :

int *ptint;

char *ptchar;

Dans cet exemple, ptint est une variable du type pointeur sur un entier. Cette variable peut donc
contenir des3 valeurs qui sont des adresses de variables du type entier (int).

De même, ptchar est une variable du type pointeur sur un caractère. Elle peut donc contenir des
valeurs qui sont des adresses de variables de type caractère (char).

Le compilateur C vérifie le type des adresses mises dans un pointeur. Le type du pointeur conditionne
les opérations arithmétiques (voir chap. 9 pointeurs et tableaux) sur ce pointeur.

Les opérations les plus simples sur un pointeur sont les suivantes :

- Affectation d’une adresse au pointeur ;


- Utilisation du pointeur pour accéder à l’objet dont il contient l’adresse.

Si l’on définit les variables de la manière suivante :

int in;
int tabint[10];
char car;
int *ptint;
char *ptchar;

Un pointeur peut être affecté avec l’adresse d’une variable ayant un type qui correspond à celui associé
au pointeur. Comme nous le verrons dans la partie sur les opérateurs, le et-commercial & donne
l’adresse du nom de variable qui le suit et les noms de tableau correspondent à l’adresse du premier
élément du tableau. Les pointeurs précédents peuvent donc être affectés de la manière suivante :

ptint = &in;
ptc = &car;

Une fois un pointeur affecté avec l’adresse d’une variable, ce pointeur peut être utilisé pour accéder
aux cases mémoires correspondant à la variable (valeur de la variable) :

*ptint = 12;

*ptc = ’a’;

3
La première instruction met la valeur entière 12 dans l’entier in ; la deuxième instruction met le
caractère « a minuscule » dans l’entier car.

Il est possible de réaliser ces opérations en utilisant le pointeur pour accéder aux éléments du tableau.
Ainsi les lignes :

ptint=tab;
*ptint=4;

affectent le pointeur ptint avec l’adresse du premier élément du tableau tabint équivalent (comme
nous le reverrons dans le chapitre sur pointeurs et tableaux 10) à &tabint[0] ; puis le premier élément
du tableau (tabint[0]) est affecté avec la valeur 4.

1.1.1 Opérateur de référencement

Le & est l’opérateur de référencement, il retourne l’adresse en mémoire (référence) de la variable dont
le nom le suit. &var donne l’adresse en mémoire de la variable var. Cette adresse peut être utilisée
pour affecter un pointeur (à la condition que le pointeur soit d’un type compatible avec l’adresse de la
variable). Comme le montre l’extrait de l’exemple : pint = &var ;

1.1.2 Opérateur de déréférencement


Le * est l’opérateur d’indirection. Il permet d’accéder à une variable à partir d’une adresse (le plus
souvent en utilisant la valeur contenue dans un pointeur). Dans notre programme 5.1, *&var donne la
valeur 10, de même que *pint, puisque pint a été initialisé avec l’adresse de var.

1.1.3 Utilisation de & et *


Prenons un exemple d’utilisation des opérateurs d’adresse et d’indirection en supposant que l’espace
mémoire associé aux données du programme débute en 0x20000 et que la taille d’un entier est de
quatre octets. Nous supposons de plus que la taille d’un pointeur est elle-aussi de quatre octets.

Figure 1: Exemple de relation entre pointeur et variable

Figure 2. Mise en relation d’un pointeur et d’une variable

La Figure 1 montre l’espace mémoire associé aux définitions du programme 5.1, et en particulier,
l’association entre le pointeur pint et la variable var.

4
L’instruction pint = &nvar ; est traduite dans la Figure 2.

Une fois cette instruction réalisée, l’expression *pint = var - 2 se traduit au niveau du processeur1 par
les opérations suivantes :

1. Mettre la valeur de var dans le registre R0 (R0 ← @ 0x20000, soit R0 ← 10) ;


2. Soustraire 2 à cette valeur (R0 ← R0 -2, soit R0 ← 8) ;
3. Mettre la valeur de pint dans le registre A0 (A0 ← @ 0x20004, soit A0 ← 0x20008) ;
4. Mettre la valeur qui est dans R0 à l’adresse contenue dans le registre A0, c’est-à-dire dans var
(@ A0 ← R0, soit 0x20008 ← 8).
5. Nous reparlerons des relations entre les pointeurs et les variables dans le chapitre 10 sur les
tableaux et les pointeurs.

1.1.4 Utilisation de pointeurs en paramètres


Il est possible, à partir d’une fonction, de modifier des objets de la fonction appelante. Pour cela, il faut
que la fonction appelante passe les adresses de ces objets1.

Les adresses sont considérées comme des pointeurs dans la fonction appelée. Comme pour les autres
constantes, il y a promotion de constante à variable lors du passage des paramètres par valeur. Il est
en effet possible d’appeler la fonction plus() avec des constantes et d’utiliser en interne de cette même
fonction des variables. Dans le cas d’une adresse, la promotion en variable rend un pointeur.

La Figure 3 est un exemple d’utilisation de la fonction add() qui accepte comme arguments deux entiers
et une adresse d’entier. Cette fonction utilise le troisième argument pour communiquer le résultat de
ses calculs.

La Figure 4, montre les différents états de la pile lors de l’exécution du premier appel add(x,y,&z); qui
modifie la variable z en lui affectant la valeur 12 (5+7), dans les étapes 0 à 5. Lors du deuxième appel
add(43, 4, &x) ; la variable x est modifiée et elle prend la valeur 47 (43+4). Seules les premières étapes
de cet appel sont représentées sur la figure. Ces étapes de 6 à 8 correspondent à l’appel de fonction
et l’exécution de l’affectation en utilisant le pointeur. Le retour de la fonction appelée à la fonction
appelante n’est pas représenté. Il suffit de reprendre les étapes numérotés 4 et 5 en changeant la
flèche qui va du pointeur à la variable associée (ici x au lieu de z) et la valeur de la case mémoire
correspondante (x doit contenir 47).

1
Vous trouvez ici l’explication rationnelle de la formule magique qui consiste à mettre un "et commercial" devant les noms
de variables lors de l’appel de la fonction scanf(). Il faut en effet passer l’adresse de la variable à modifier à cette fonction.

5
Figure 3 : Pile et passage de variables

6
Figure 4: Pile et passage de variables avec référence

1.2 Tableaux et pointeurs


La déclaration d’un tableau à une dimension réserve un espace de mémoire contiguë dans lequel les
éléments du tableau peuvent être rangés.

Comme le montre la Figure 5, le nom du tableau seul est une constante dont la valeur est l’adresse du
début du tableau. Les éléments sont accessibles par : le nom du tableau, un crochet ouvrant, l’indice
de l’élément et un crochet fermant.

L’initialisation d’un tableau se fait en mettant une accolade ouvrante, la liste des valeurs servant à
initialiser le tableau, et une accolade fermante. La Figure 6 montre l’espace mémoire correspondant à
la définition d’un tableau de dix entiers avec une initialisation selon la ligne :

int tab[10] = {9,8,7,6,5,4,3,2,1,0};

7
Comme le montrent les exemples du Programme 1, il est possible de ne pas spécifier la taille du tableau
ou (exclusif) de ne pas initialiser tous les éléments du tableau. Dans ce programme, tb1 est défini
comme un tableau de 6 entiers initialisés, et tb2 est défini comme un tableau de 10 entiers dont les 6
premiers sont initialisés. Depuis la normalisation du langage, l’initialisation des premiers éléments d’un
tableau provoque l’initialisation de l’ensemble des éléments du tableau y compris pour les tableaux
locaux.
Les éléments pour lesquels des valeurs ne sont pas précisées sont initialisés avec la valeur 0 (ensemble
des octets à zéro quelle que soit la taille des éléments).

Figure 5: Tableau de dix entiers

Figure 6: Tableau de dix entiers initialisé

Figure 7: Adresses dans un tableau de dix entiers

8
Programme 1: Définition de tableaux et initialisations

int tb1[] = {12,13,4,15,16,32000};


int tb2[10] = {112,413,49,5,16,3200};

1.2.1 Arithmétique d’adresse et tableaux


Comme nous venons de le dire le nom d’un tableau, correspond à l’adresse du premier élément du
tableau, de plus les éléments d’un tableau sont du même type. Ces éléments ont donc tous la même
taille, et ils ont tous une adresse qui correspond au même type d’objet (par exemple une adresse
d’entier pour chaque élément du tableau tb1 du Programme 1). La Figure 7 reprend l’exemple de la
Figure 6 en le complétant par les adresses de chaque élément. Ces adresses sont exprimées à partir du
début du tableau.
Ceci nous amène à considérer les opérations possibles à partir d’une adresse :
- Il est possible d’additionner ou de soustraire un entier (n) à une adresse. Cette opération calcule
une nouvelle adresse de la manière suivante :
▪ L’opération suppose que l’adresse de départ et l’adresse résultante sont les adresses de deux
variables contenues dans le même tableau.
▪ L’opération suppose aussi que le tableau est d’une taille suffisamment grande, c’est-à-dire
qu’elle suppose que le programmeur doit être conscient des risques de dépassement des
bornes du tableau.
Dans le cas du tableau tab pris en exemple dans la Figure 7, les adresses doivent être comprises
entre &tab[0] et &tab[9]. Pour une question de test de borne, l’adresse immédiatement
supérieure à la fin du tableau est calculable. Il est possible d’écrire &tab[10] mais rien ne
garantit que l’expression ne provoque pas une erreur si le rang dépasse la taille plus un. La
zone mémoire correspondante au rang du tableau plus un ne doit cependant pas être accédée.
Le compilateur garantit qu’il calcule correctement l’adresse mais il ne garantit pas que le fait
d’essayer d’accéder à la zone mémoire correspondante soit possible.
▪ Selon ces conditions, l’addition d’un entier à une adresse retourne une adresse qui est celle
du n ième objet contenu dans le tableau à partir de l’adresse initiale.
Dans ces conditions, tab + n est l’adresse du nième entier à partir du début du tableau. Dans
notre exemple de tableau de dix éléments, n doit être compris entre 0 et 10. L’opérateur
d’accès à la variable à partir de l’adresse (*) ne peut cependant s’appliquer que pour n valant
entre 0 et 9. Ainsi, *(tab +n) n’est valide que pour n compris entre 0 et 9.
▪ L’addition ou la soustraction d’un entier est possible pour toute adresse dans un tableau. Ainsi,
&tab[3] + 2 donne la même adresse que &tab[5]. De même, &tab[3] - 2 donne la même adresse
que &tab[1].
- Il est aussi possible de réaliser une soustraction entre les adresses de deux variables appartenant à
un même tableau. Cette opération retourne une valeur du type ptrdiff_t qui correspond au nombre
d’objets entre les deux adresses. Ainsi, &tab[5] - &tab[3] doit donner la valeur 2 exprimée dans le
type ptrdiff_t. De même, &tab[3] - &tab[5] retourne la valeur -2 exprimée dansle type ptrdiff_t.

1.2.2 Pointeurs & tableaux


Le pointeur est une variable destinée à contenir une adresse mémoire. Il est reconnu syntaxiquement
par l’* lors de sa déclaration. Comme les adresses, le pointeur est associé à un type d’objet. Ce type
est celui des objets qui sont manipulés grâce au pointeur. L’objet peut être une variable ou une

9
fonction. Contrairement à ce que beaucoup d’apprentis espèrent, la déclaration d’un pointeur
n’implique pas la déclaration implicite d’une variable associée et l’affectation de l’adresse de la
variable au pointeur. Il faut donc déclarer une variable du type correspondant et initialiser le pointeur
avec l’adresse de cette variable. Par convention, l’adresse 0 est invalide et si le programme cherche à
y accéder, il obtient une erreur d’exécution du type bus-error sur UNIX. Ce comportement implique
que l’utilisation de pointeurs globaux sans initialisation mène à ce résultat, car les pointeurs (comme
les autres variables) déclarés en variables globales sont initialisés à 0.

Les pointeurs déclarés en variable locale (comme toutes les variables locales) ont des valeurs initiales
dépendantes du contenu de la pile à cet instant, qui dépend de l’exécution précédente du programme
mais correspond en général à n’importe quoi. Le comportement du programme qui utilise un pointeur
local sans l’avoir affecté convenablement peut donner des comportements tels que violation de
l’espace mémoire, mais parfois le pointeur reçoit une adresse valide et le programme se déroule sans
erreurs flagrante mais en donnant des résultats faux (ce que d’aucuns appellent un effet de bord
indésirable, ce que d’autres appellent un bug difficile à reproduire).

Voici deux exemples de définition de pointeurs :

- int *ptint; pointeur sur un entier


- char *ptchar; pointeur sur un caractère.

Le compilateur C vérifie le type des adresses qui sont affectées à un pointeur. Le type du pointeur
conditionne les opérations arithmétiques sur ce pointeur.

Les opérations possibles sur un pointeur sont les suivantes :

- Affectation d’une adresse au pointeur ;


- Utilisation du pointeur pour accéder à l’objet dont il contient l’adresse ;
- Addition d’un entier (n) à un pointeur ; la nouvelle adresse est celle du nieme objet à partir de
l’adresse initiale ;
- Soustraction de deux pointeurs du même type. Le calcul est réalisé dans les mêmes conditions
que la différence entre deux adresses de variables contenues dans un même tableau. La
soustraction calcule le nombre de variables entre les adresses contenues dans les pointeurs.
Le résultat de type ptrdiff_t n’est valide que si les adresses contenues dans les deux pointeurs
sont bien des adresses de variables appartenant à un même tableau, sinon le résultat est
indéfini.

Tableau 1: Addition d’un entier à un pointeur

Le Tableau 1 est un exemple de manipulations en relation avec les pointeurs et les tableaux en utilisant
les variables : long x[10], *px , y;

L’addition décrite dans la dernière ligne du Tableau 1 se traduit par les conversions suivantes :

px = (long *) ( (int) px + i * sizeof (long));

10
Tableau 2: Soustraction de deux pointeurs

Le Tableau 2 est un exemple de soustraction à partir des définitions de variables suivantes :


int tab[20], *pt1, *pt2, ptrdiff_t i;

Par convention, le nom d’une variable utilisé dans une partie droite d’expression donne le contenu de
cette variable dans le cas d’une variable simple. Mais un nom de tableau donne l’adresse du tableau
qui est l’adresse du premier élément du tableau.

Nous faisons les constatations suivantes :

- Un tableau est une constante d’adressage ;


- Un pointeur est une variable d’adressage.

Ceci nous amène à regarder l’utilisation de pointeurs pour manipuler des tableaux, en prenant les
variables : long i, tab[10], *pti ;

- tab est l’adresse du tableau (adresse du premier élément du tableau &tab[0] ;


- pti = tab ; initialise le pointeur pti avec l’adresse du début de tableau. Le & ne sert à rien dans
le cas d’un tableau. pti = &tab est inutile et d’ailleurs non reconnu ou ignoré par certains
compilateurs ;
- &tab[1] est l’adresse du 2e élément du tableau.
- pti = &tab[1] est équivalent à :
▪ pti = tab ; où pti pointe sur le 1er élément du tableau.
▪ pti += 1 ; fait avancer, le pointeur d’une case ce qui fait qu’il contient l’adresse du 2ème
élément du tableau.

Nous pouvons déduire de cette arithmétique de pointeur que : tab[i] est équivalent à *(tab + i). De
même, *(pti+i) est équivalent à pti[i].

La Figure 8 est un exemple dans lequel sont décrites les différentes façons d’accéder aux éléments du
tableau tab et du pointeur pt après la définition suivante : int tab[8], *pt = tab;

11
Figure 8: Pointeur et tableau

1.2.3 Tableau de pointeur


La définition d’un tableau de pointeurs se fait par : type *nom[taille] ;

Le premier cas d’utilisation d’un tel tableau est celui où les éléments du tableau de pointeurs
contiennent les adresses des éléments du tableau de variables. C’est le cas des arguments argv et envp
de la fonction main().

Figure 9: Tableau de pointeurs sur des variables dans un tableau

La Figure 9 est un exemple d’utilisation de tableau de pointeurs à partir des définitions de variables
suivantes : int tab[8], *pt[8] = {tab,tab+1,tab+2,tab+3,tab+4,tab+5,tab+6,tab+7};

12
2. Les chaines de caractères
Cette section a été rédigée en se basant sur le livre « Programmer en langage C – cours & exercices
corrigés », édition 5, 2009.

En langage C, il n’existe pas de véritable type chaîne, dans la mesure où l’on ne peut pas y déclarer des
variables d’un tel type. En revanche, il existe une convention de représentation des chaînes. Celle-ci
est utilisée à la fois :

- par le compilateur pour représenter les chaînes constantes (notées entre doubles quotes) ;
- par un certain nombre de fonctions qui permettent de réaliser :
- les lectures ou écritures de chaînes ;
- les traitements classiques tels que concaténation, recopie, comparaison, extraction de sous-
chaîne, conversions...

Mais, comme il n’existe pas de variables de type chaîne, il faudra prévoir un emplacement pour
accueillir ces informations. Un tableau de caractères pourra faire l’affaire. C’est d’ailleurs ce que nous
utiliserons dans ce chapitre. Mais nous verrons plus tard comment créer dynamiquement des
emplacements mémoire, lesquels seront alors repérés par des pointeurs.

2.1 Représentation de chaine de caractères


En C, une chaîne de caractères est représentée par une suite d’octets correspondant à chacun de ses
caractères (plus précisément à chacun de leurs codes), le tout étant terminé par un octet
supplémentaire de code nul. Cela signifie que, d’une manière générale, une chaîne de n caractères
occupe en mémoire un emplacement de n+1 octets.

L’instruction char adr[10] = "bonjour" ; réserve simplement l’emplacement pour un pointeur sur un
caractère (ou une suite de caractères).

En ce qui concerne la constante : "bonjour" le compilateur crée en mémoire une suite d’octets. La
notation : "bonjour" a comme valeur, non pas la valeur de la chaîne elle-même, mais son adresse ; on
retrouve là le même phénomène que pour les tableaux. La Figure 10 représente ce phénomène. La
flèche en trait plein correspond à la situation après l’exécution de l’affectation : adr = "bonjour" ; les
autres flèches correspondent à l’évolution de la valeur de adr, au cours de la boucle.

Figure 10: schéma illustrant l'affectation de chaine de caractères

2.2 Initialisation
Comme nous l’avons dit, vous serez souvent amené, en C, à placer des chaînes dans des tableaux de
caractères.

13
Mais, si vous déclarez, par exemple : char ch[20] ; vous ne pourrez pas pour autant transférer une
chaîne constante dans ch, en écrivant une affectation du genre : ch = "bonjour" ;

En effet, ch est une constante pointeur qui correspond à l’adresse que le compilateur a attribuée au
tableau ch ; ce n’est pas une valeur; il n’est donc pas question de lui attribuer une autre valeur (ici, il
s’agirait de l’adresse attribuée par le compilateur à la constante chaîne "bonjour").

En revanche, C vous autorise à initialiser votre tableau de caractères à l’aide d’une chaîne constante.
Ainsi, vous pourrez écrire : char ch[20] = "bonjour" ;

Cela sera parfaitement équivalent à une initialisation de ch réalisée par une énumération de caractères
(en n’omettant pas le code zéro – noté \0) : char ch[20] = { 'b','o','n','j','o','u','r','\0' }

N’oubliez pas que, dans ce dernier cas, les 12 caractères non initialisés explicitement seront :

- soit initialisés à zéro (pour un tableau de classe statique) : on voit que, dans ce cas, l’omission
du caractère \0 ne serait (ici) pas grave ;
- soit aléatoires (pour un tableau de classe automatique) : dans ce cas, l’omission du caractère
\0 serait nettement plus gênante.

De plus, comme le langage C autorise l’omission de la dimension d’un tableau lors de sa déclaration,
lorsqu’elle est accompagnée d’une initialisation, il est possible d’écrire une instruction telle que :

char message[] = "bonjour" ;

Celle-ci réserve un tableau, nommé message, de 8 caractères (compte tenu du 0 de fin).

Tableau de pointeur sur une chaine de caractères :


Nous avons vu qu’une chaîne constante était traduite par le compilateur en une adresse que l’on
pouvait, par exemple, affecter à un pointeur sur une chaîne. Cela peut se généraliser à un tableau de
pointeurs, comme dans :

char * jour[7] = { "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche" } ;

Cette déclaration réalise donc à la fois la création des 7 chaînes constantes correspondant aux 7 jours
de la semaine et l’initialisation du tableau jour avec les 7 adresses de ces 7 chaînes.

La situation présentée ne doit pas être confondue avec la précédente. Ici, nous avons affaire à un
tableau de sept pointeurs, chacun d’entre eux désignant une chaîne constante (comme le faisait adr).
La Figure 11 récapitule les deux situations.

14
Figure 11: Tableau de pointeurs contenant des chaines de caractères

2.3 Lecture & écriture des chaines de caractères


Le langage C offre plusieurs possibilités de lecture ou d’écriture de chaînes :

- L’utilisation du code de format %s dans les fonctions printf et scanf ;


- Les fonctions spécifiques de lecture (gets) ou d’affichage (puts) d’une chaîne (une seule à la
fois)

char nom[20], prenom[20], ville[25] ;


printf ("quelle est votre ville : ") ;
gets (ville) ;
printf ("donnez votre nom et votre prénom : ") ;
scanf ("%s %s", nom, prenom) ;
printf ("bonjour cher %s %s qui habitez à ", prenom, nom) ;
puts (ville)

Les fonctions printf et scanf permettent de lire ou d’afficher simultanément plusieurs informations de
type quelconque. En revanche, gets et puts ne traitent qu’une chaîne à la fois.

De plus, la délimitation de la chaîne lue ne s’effectue pas de la même façon avec scanf et gets. Plus
précisément :

- avec le code %s de scanf, on utilise les délimiteurs habituels (l’espace ou la fin de ligne). Cela
interdit donc la lecture d’une chaîne contenant des espaces. De plus, le caractère délimiteur
n’est pas consommé : il reste disponible pour une prochaine lecture ;
- avec gets, seule la fin de ligne sert de délimiteur. De plus, contrairement à ce qui se produit
avec scanf, ce caractère est effectivement consommé : il ne risque pas d’être pris en compte
lors d’une nouvelle lecture.

Dans tous les cas, vous remarquerez que la lecture de n caractères implique le stockage en mémoire
de n+1 caractères, car le caractère de fin de chaîne (\0) est généré automatiquement par toutes les
fonctions de lecture (notez toutefois que le caractère séparateur – fin de ligne ou autre – n’est pas
recopié en mémoire). Ainsi, dans notre précédent programme, il n’est pas possible (du moins pas
souhaitable !) que le nom fourni en donnée contienne plus de 19 caractères.

15
Remarque :
Dans les appels des fonctions scanf et puts, les identificateurs de tableau comme nom, prénom ou ville
n’ont pas besoin d’être précédés de l’opérateur & puisqu’ils représentent déjà des adresses. La norme
prévoit toutefois que si l’on applique l’opérateur & à un nom de tableau, on obtient l’adresse du
tableau. Autrement dit, &nom est équivalent à nom.

La fonction gets fournit en résultat soit un pointeur sur la chaîne lue (c’est donc en fait la valeur de son
argument), soit le pointeur nul en cas d’anomalie. La fonction puts réalise un changement de ligne à la
fin de l’affichage de la chaîne, ce qui n’est pas le cas de printf avec le code de format %s.

Nous nous sommes limité ici aux entrées-sorties conversationnelles. Les autres possibilités seront
examinées dans le chapitre consacré aux fichiers.

Si, dans notre précédent programme, l’utilisateur introduit une fin de ligne entre le nom et le prénom,
la chaîne affectée à prenom n’est rien d’autre que la chaîne vide ! Ceci provient de ce que la fin de
ligne servant de délimiteur pour le premier %s n’est pas consommée et se trouve donc reprise par le
%s suivant...

Étant donné que gets consomme la fin de ligne servant de délimiteur, alors que le code %s de scanf ne
le fait pas, il n’est guère possible, dans le programme précédent, d’inverser les utilisations de scanf et
de gets (en lisant la ville par scanf puis le nom et le prénom par gets) : dans ce cas, la fin de ligne non
consommée par scanf amènerait gets à introduire une chaîne vide comme nom. D’une manière
générale, d’ailleurs, il est préférable, autant que possible, de faire appel à gets plutôt qu’au code %s
pour lire des chaînes.

2.4 Gestion d’entrée avec gets et sscanf


Nous avons vu, dans le chapitre concernant les entrées-sorties conversationnelles, les problèmes
posés par scanf en cas de réponse incorrecte de la part de l’utilisateur.

Il est possible de régler la plupart de ces problèmes en travaillant en deux temps :

- Lecture d’une chaîne de caractères par gets (c’est-à-dire d’une suite de caractères
quelconques validés par « return ») ;
- Décodage de cette chaîne suivant un format, à l’aide de la fonction sscanf. En effet, une
instruction telle que : sscanf (adresse, format, liste_variables)

effectue sur l’emplacement dont on lui fournit l’adresse (premier argument de type char *) le même
travail que scanf effectue sur son tampon. La différence est qu’ici nous sommes maîtres de ce tampon
; en particulier, nous pouvons décider d’appeler à nouveau sscanf sur une nouvelle zone de notre choix
(ou sur la même zone dont nous avons modifié le contenu par gets), sans être tributaire de la position
du pointeur, comme cela était le cas avec scanf.

2.5. Bibliothèque <string.h>


La librairie <string.h> (<cstring> en C++) offre deux grandes catégories de traitements :

- La manipulation de chaînes de caractères ;


- La manipulation de blocs mémoires.

En fait, il n'y a pas tant de différences entre les deux types de traitements dans le sens ou une chaîne
de caractères est un bloc d'octets en mémoire : la seule différence étant qu'une chaîne de caractères

16
possède un marqueur de fin particulier '\0' au contraire d'un bloc de mémoire pour lequel il faut
connaître sa taille.

Les fonctions de manipulation de chaînes de caractères sont préfixées par str. Les fonctions de
manipulations de blocs mémoires sont, quand à elles, préfixées par mem.

void *memcpy(void *dest, const void copie n octets entre deux zones mémoire, qui
*src, size_t n); ne doivent pas se superposer

void *memmove(void *dest, const copie n octets entre deux zones mémoire ; à
la différence de memcpy, les zones mémoire
void *src, size_t n);
peuvent se superposer

void *memchr(const void *s, int c, retourne en pointeur la première


occurrence c parmi les n premiers octets de s,
size_t n);
ou NULL si c n'est pas trouvé

int memcmp(const void *s1, const compare les n premiers caractères de deux
void *s2, size_t n); zones mémoire

void *memset(void *, int, size_t); remplit une zone mémoire de la répétition d'un
caractère

char *strcat(char *dest, const char


concatène la chaîne src à la suite de dest
*src);

char *strncat(char * dest, const concatène au plus n caractères de la


char * src, size_t n); chaîne src à la suite de dest

cherche un caractère dans une chaîne et


char *strchr(const char *, int); renvoie un pointeur sur le caractère, en
cherchant depuis le début

char *strrchr(const char *, int); idem que strchr, recherche à partir de la fin

int strcmp(const char *, const char


compare deux chaînes lexicalement
*);

int strncmp(const char *, const compare les n premiers octets au plus de


deux chaînes en utilisant l'ordre
char *, size_t n);
lexicographique

17
int strcoll(const char *, const compare deux chaînes en utilisant l'ordre
char *); lexicographique

char *strcpy(char *toHere, const copie une chaîne de caractères d'une zone à
char *fromHere); une autre

char *strncpy(char *toHere, const copie au plus n caractères d'une chaîne d'une
char *fromHere, size_t n); zone à une autre

char *strerror(int); retourne la chaîne de caractères


correspondant à un numéro d'erreur

size_t strlen(const char *); retourne la longueur d'une chaîne caractères

size_t strspn(const char *s, const détermine la taille de la sous-chaîne initiale


maximale de s ne contenant que des
char *accept);
caractères présents dans accept

size_t strcspn(const char *s, const détermine la taille de la sous-chaîne initiale


maximale de s ne contenant pas de
char *reject);
caractères de reject

char *strpbrk(const char *s, const trouve la première occurrence d'un caractère
char *accept); d'accept dans s

char *strstr(const char *haystack, trouve la première occurrence de la


const char *needle); chaîne needle dans la chaîne haystack

scinde une chaîne en éléments lexicaux.


char *strtok(char *, const char *); Note: la fonction modifie la chaîne passée en
paramètre.

transforme src de façon que le tri par ordre


size_t strxfrm(char *dest, const lexicographique de la chaîne transformée soit
char *src, size_t n); équivalent au tri par ordre lexicographique
de src.

void *memcpy(void *dest, const void copie n octets entre deux zones mémoire, qui
*src, size_t n); ne doivent pas se superposer

18
Chapitre II
Structure &
Enumération

19
Introduction
La structure permet de désigner sous un seul nom un ensemble de valeurs pouvant être de types
différents. L’accès à chaque élément de la structure (nommé champ, composant, ou attribut) se
fera, par son nom au sein de la structure.
Les énumérations est un cas particulier de type entier. Sa présentation (tardive) dans ce chapitre
ne se justifie que parce que sa déclaration et son utilisation sont très proches de celles du type
structure.

1. Déclaration d’une structure


Soit la déclaration suivante :
struct enreg {
int numero ;
int qte ;
float prix ;
};
Celle-ci définit un modèle de structure mais ne réserve pas de variables correspondant à cette
structure. Ce modèle s’appelle ici enreg et il précise le nom et le type de chacun des champs
constituant la structure (numero, qte et prix).
Une fois un tel modèle défini, nous pouvons déclarer des variables du type correspondant
(souvent, nous parlerons de structure pour désigner une variable dont le type est un modèle de
structure).
Par exemple : struct enreg art1 ;
➔réserve un emplacement nommé art1 « de type enreg » destiné à contenir deux entiers et un
flottant.
De manière semblable : struct enreg art1, art2 ;
➔réserve deux emplacements art1 et art2 du type enreg.
Bien que ce soit peu recommandé, sachez qu’il est possible de regrouper la définition du modèle
de structure et la déclaration du type des variables dans une seule instruction comme dans cet
exemple :
struct enreg {
int numero ;
int qte ;
float prix ;
} art1, art2 ;

Dans ce dernier cas, il est même possible d’omettre le nom de modèle (enreg), à condition, bien
sûr, que l’on n’ait pas à déclarer par la suite d’autres variables de ce type.

20
2. Utilisation d’une structure
En C, il est possible d’utiliser une structure de deux manières :
▪ en travaillant individuellement sur chacun de ses champs ;
▪ en travaillant de manière globale sur l’ensemble de la structure.

2.1. Utilisation des composants (champs) d’une


structure
Chaque champ d’une structure peut être manipulé comme n’importe quelle variable du type
correspondant. La désignation d’un champ se note en faisant suivre le nom de la variable
structure de l’opérateur « point » (.) suivi du nom de champ tel qu’il a été défini dans le modèle
(le nom de modèle lui-même n’intervenant d’ailleurs pas).
Exemples : utilisant le modèle enreg et les variables art1 et art2 déclarées de ce type.
- [Link] = 15 ; //affecte la valeur 15 au champ numero de la structure art1.
- printf ("%e", [Link]) ; //affiche, suivant le code format %e, la valeur du champ prix de la
structure art1.
- scanf ("%e", &[Link]) ; //lit, suivant le code format %e, une valeur qui sera affectée au
champ prix de la structure art2. Notez bien la présence de l’opérateur &.
- [Link]++ ; // incrémente de 1 la valeur du champ numero de la structure art1.
La priorité de l’opérateur « . » est très élevée, de sorte qu’aucune des expressions ci-dessus ne
nécessite de parenthèses (voyez le tableau du chapitre 3, « Les opérateurs et les expressions en
langage C »).
Il est possible d’affecter à une structure le contenu d’une structure définie à partir du même
modèle. Par exemple, si les structures art1 et art2 ont été déclarées suivant le modèle enreg
défini précédemment, nous pourrons écrire :
art1 = art2 ;

Une telle affectation globale remplace avantageusement :


[Link] = [Link] ;
[Link] = [Link] ;
[Link] = [Link] ;
Notez bien qu’une affectation globale n’est possible que si les structures ont été définies avec
le même nom de modèle ; en particulier, elle sera impossible avec des variables ayant une
structure analogue mais définies sous deux noms différents.
L’opérateur d’affectation et, comme nous le verrons un peu plus loin, l’opérateur d’adresse &
sont les seuls opérateurs s’appliquant à une structure (de manière globale).
Remarque : L’affectation globale n’est pas possible entre tableaux. Elle l’est, par contre, entre
structures. Aussi est-il possible, en créant artificiellement une structure contenant un seul champ
qui est un tableau, de réaliser une affectation globale entre tableaux.

21
2.2. Initialisation d’une structure
On retrouve pour les structures les règles d’initialisation qui sont en vigueur pour tous les types
de variables, à savoir :
▪ En l’absence d’initialisation explicite, les structures de classe statique sont, par défaut,
initialisées à zéro ; celles possédant la classe automatique ne sont pas initialisées par
défaut (elles contiendront donc des valeurs aléatoires).
▪ Il est possible d’initialiser explicitement une structure lors de sa déclaration. On ne peut
toutefois employer que des constantes ou des expressions constantes et cela aussi bien
pour les structures statiques que pour les structures automatiques, alors que, pour les
variables scalaires automatiques, il était possible d’employer une expression
quelconque (on retrouve là les mêmes restrictions que pour les tableaux).
Voici un exemple d’initialisation de notre structure art1, au moment de sa déclaration :
struct enreg art1 = { 100, 285, 2000 } ;
Vous voyez que la description des différents champs se présente sous la forme d’une liste de
valeurs séparées par des virgules, chaque valeur étant une constante ayant le type du champ
correspondant. Là encore, il est possible d’omettre certaines valeurs.

3. Types synonymes avec typedef


La déclaration typedef permet de définir ce que l’on nomme en langage C des types synonymes.
A priori, elle s’applique à tous les types et pas seulement aux structures. C’est pourquoi nous
commencerons par l’introduire sur quelques exemples avant de montrer l’usage que l’on peut
en faire avec les structures.
Exemple :
La déclaration :
typedef int entier ;
➔ signifie que entier est synonyme de int, de sorte que les déclarations suivantes sont
équivalentes : int n, p ; entier n, p ;
De même :
typedef int * ptr ;
➔ signifie que ptr est synonyme de int *. Les déclarations suivantes sont équivalentes :
int * p1, * p2 ; ptr p1, p2 ;
Notez bien que cette déclaration est plus puissante qu’une substitution telle qu’elle pourrait être
réalisée par la directive #define. Nous n’en ferons pas ici de description exhaustive, et cela
d’autant plus que son usage tend à disparaître. À titre indicatif, sachez, par exemple, qu’avec la
déclaration :
typedef int vecteur [3] ;

22
Les déclarations suivantes sont équivalentes : int v[3], w[3] ; vecteur v, w ;
Application aux structures
En faisant usage de typedef, les déclarations des structures art1 et art2 du paragraphe 1 peuvent
être réalisées comme suit :
struct enreg{
int numero ;
int qte ;
float prix ;
};
typedef struct enreg s_enreg ;
s_enreg art1, art2 ;
ou encore, plus simplement :
typedef struct{
int numero ;
int qte ;
float prix ;
} s_enreg ;
s_enreg art1, art2 ;
Par la suite, nous ne ferons appel qu’occasionnellement à typedef, afin de ne pas vous enfermer
dans un style de notations que vous ne retrouverez pas nécessairement dans les programmes
que vous serez amené à utiliser.

4. Imbrication des structures


Dans nos exemples d’introduction des structures, nous nous sommes limités à une structure
simple ne comportant que trois champs d’un type de base. Mais chacun des champs d’une
structure peut être d’un type absolument quelconque : pointeur, tableau, structure... Il peut
même s’agir de pointeurs sur des structures du type de la structure dans laquelle ils apparaissent.
Nous en reparlerons dans le chapitre 11, « Gestion dynamique de la mémoire », à propos de la
constitution de listes chaînées. De même, un tableau peut être constitué d’éléments qui sont
eux-mêmes des structures. Voyons ici quelques situations classiques.
Soit la déclaration suivante :
struct personne {
char nom[30] ;
char prenom [20] ;
float heures [31] ;
} employe, courant ;
Celle-ci réserve les emplacements pour deux structures nommées employe et courant. Ces
dernières comportent trois champs :
▪ nom qui est un tableau de 30 caractères ;
▪ prenom qui est un tableau de 20 caractères ;
▪ heures qui est un tableau de 31 flottants.

23
On peut, par exemple, imaginer que ces structures permettent de conserver pour un employé
d’une entreprise les informations suivantes :
▪ nom ;
▪ prénom ;
▪ nombre d’heures de travail effectuées pendant chacun des jours du mois courant.
La notation : [Link][4]
désigne le cinquième élément du tableau heures de la structure employe. Il s’agit d’un élément
de type float. Notez que, malgré les priorités identiques des opérateurs . et [], leur associativité
de gauche à droite évite l’emploi de parenthèses.
De même : [Link][0]
représente le premier caractère du champ nom de la structure employe.
Par ailleurs : &[Link][4]
représente l’adresse du cinquième élément du tableau heures de la structure courant.
Notez que, la priorité de l’opérateur & étant inférieure à celle des deux autres, les parenthèses
ne sont, là encore, pas nécessaires.
Enfin : [Link]
représente le champ nom de la structure courant, c’est-à-dire plus précisément l’adresse de ce
tableau.
À titre indicatif, voici un exemple d’initialisation d’une structure de type personne lors de sa
déclaration :
struct personne emp = {"Dupont", "Jules", { 8, 7, 8, 6, 8, 0, 0, 8}};

4.1. Tableau des structures


Voyez ces déclarations :
struct point {
char nom ;
int x ;
int y ;
};
struct point courbe [50] ;
La structure point pourrait, par exemple, servir à représenter un point d’un plan, point qui serait
défini par son nom (caractère) et ses deux coordonnées.
Notez bien que point est un nom de modèle de structure, tandis que courbe représente
effectivement un tableau de 50 éléments du type point.
Si i est un entier, la notation : courbe[i].nom
➔représente le nom du point de rang i du tableau courbe. Il s’agit donc d’une valeur de type
char. Notez bien que la notation :

24
[Link][i] ➔ n’aurait pas de sens.
De même, la notation : courbe[i].x
➔ désigne la valeur du champ x de l’élément de rang i du tableau courbe.
Par ailleurs : courbe[4]
représente la structure de type point correspondant au cinquième élément du tableau courbe.
Enfin courbe est un identificateur de tableau, et, comme tel, désigne son adresse de début. Là
encore, voici, à titre indicatif, un exemple d’initialisation (partielle) de notre variable courbe,
lors de sa déclaration :
struct point courbe[50]= { {'A', 10, 25}, {'M', 12, 28},, {'P', 18,2} };

4.2. Structures dans une autre structures


Supposez que, à l’intérieur de nos structures employe et courant définies dans le paragraphe
4.1, nous ayons besoin d’introduire deux dates : la date d’embauche et la date d’entrée dans le
dernier poste occupé. Si ces dates sont elles-mêmes des structures comportant trois champs
correspondant au jour, au mois et à l’année, nous pouvons alors procéder aux déclarations
suivantes :
struct date {
int jour ;
int mois ;
int annee ;
};
struct personne {
char nom[30] ;
char prenom[20] ;
float heures [31] ;
struct date date_embauche ;
struct date date_poste ;
} employe, courant ;
Vous voyez que la seconde déclaration fait intervenir un modèle de structure (date)
précédemment défini.
La notation :
employe.date_embauche.annee
➔représente l’année d’embauche correspondant à la structure employe. Il s’agit d’une valeur
de type int.
courant.date_embauche
représente la date d’embauche correspondant à la structure courant. Il s’agit cette fois d’une
structure de type date. Elle pourra éventuellement faire l’objet d’affectations globales comme
dans :
courant.date_embauche = employe.date_poste ;

25
4.3. Portée de la structure
À l’image de ce qui se produit pour les identificateurs de variables, la portée d’un modèle de
structure dépend de l’emplacement de sa déclaration :
▪ si elle se situe au sein d’une fonction (y compris, la fonction main), elle n’est accessible
que depuis cette fonction ;
▪ si elle se situe en dehors d’une fonction, elle est accessible de toute la partie du fichier
source qui suit sa déclaration ; elle peut ainsi être utilisée par plusieurs fonctions.
Voici un exemple d’un modèle de structure nommé enreg déclaré à un niveau global et
accessible depuis les fonctions main et fct.
struct enreg {
int numero ;
int qte ;
float prix ;
};
main () {
struct enreg x ;
....
}
fct ( ....) {
struct enreg y, z ;
....
}
En revanche, il n’est pas possible, dans un fichier source donné, de faire référence à un modèle
défini dans un autre fichier source. Notez bien qu’il ne faut pas assimiler le nom de modèle
d’une structure à un nom de variable ; notamment, il n’est pas possible, dans ce cas, d’utiliser
de déclaration extern. En effet, la déclaration extern s’applique à des identificateurs susceptibles
d’être remplacés par des adresses au niveau de l’édition de liens. Or un modèle de structure
représente beaucoup plus qu’une simple information d’adresse et il n’a de signification qu’au
moment de la compilation du fichier source où il se trouve.
Il est néanmoins toujours possible de placer un certain nombre de déclarations de modèles de
structures dans un fichier séparé que l’on incorpore par #include à tous les fichiers source où
l’on en a besoin. Cette méthode évite la duplication des déclarations identiques avec les risques
d’erreurs qui lui sont inhérents.
Le même problème de portée se pose pour les synonymes définis par typedef. Les mêmes
solutions peuvent y être apportées par l’emploi de #include.

26
5. Utilisation des structures comme paramètre &
retour de fonction
5.1. Passage comme argument
Jusqu’ici, nous avons vu qu’en C la transmission des arguments se fait par valeur, ce qui
implique une recopie de l’information transmise à la fonction. Par ailleurs, il est toujours
possible de transmettre la valeur d’un pointeur sur une variable, auquel cas la fonction peut, si
besoin est, en modifier la valeur. Ces remarques s’appliquent également aux structures (notez
qu’il n’en allait pas de même pour un tableau, dans la mesure où la seule chose qu’on puisse
transmettre dans ce cas soit la valeur de l’adresse de ce tableau).

5.1.1. Transmission de la valeur de la structure


Aucun problème particulier ne se pose. Il s’agit simplement d’appliquer ce que nous
connaissons déjà. Voici un exemple simple :
#include <stdio.h>
struct enreg {
int a ;
float b ;
};
void fct (struct enreg s){
s.a = 0; s.b=1;
printf ("\ndans fct : %d %e", s.a, s.b);
}
main(){
struct enreg x ;
void fct (struct enreg y) ;
x.a = 1; x.b = 12.5;
printf ("\navant appel fct : %d %e",x.a,x.b);
fct (x) ;
printf ("\nau retour dans main : %d %e", x.a, x.b);
}
avant appel fct : 1 1.25000e+01
dans fct : 0 1.00000e+00
au retour dans main : 1 1.25000e+01

Naturellement, les valeurs de la structure x sont recopiées localement dans la fonction fct lors
de son appel ; les modifications de s au sein de fct n’ont aucune incidence sur les valeurs de x.

5.1.2. Transmission de l’adresse d’une structure : l’opérateur ->


Cherchons à modifier notre précédent programme pour que la fonction fct reçoive effectivement
l’adresse d’une structure et non plus sa valeur. L’appel de fct devra donc se présenter sous la
forme :
fct (&x) ;
Cela signifie que son en-tête sera de la forme : void fct (struct enreg * ads) ;

27
Comme vous le constatez, le problème se pose alors d’accéder, au sein de la définition de fct,
à chacun des champs de la structure d’adresse ads. L’opérateur « . » ne convient plus, car il
suppose comme premier opérande un nom de structure et non une adresse. Deux solutions
s’offrent alors à vous :
▪ adopter une notation telle que (*ads).a ou (*ads).b pour désigner les champs de la
structure d’adresse ads ;
▪ faire appel à un nouvel opérateur noté ->, lequel permet d’accéder aux différents champs
d’une structure à partir de son adresse de début. Ainsi, au sein de fct, la notation ads ->
b désignera le second champ de la structure reçue en argument ; elle sera équivalente à
(*ads).b.
Voici ce que pourrait devenir notre précédent exemple en employant l’opérateur noté -> :
#include <stdio.h>
struct enreg {
int a ;
float b ;
};
void fct (struct enreg * ads){
ads->a = 0 ; ads->b = 1;
printf ("\ndans fct : %d %e", ads->a, ads->b);
}
main()
{
struct enreg x ;
void fct (struct enreg *) ;
x.a = 1; x.b = 12.5;
printf ("\navant appel fct : %d %e",x.a,x.b);
fct (&x) ;
printf ("\nau retour dans main : %d %e", x.a, x.b);
}
avant appel fct : 1 1.25000e+01
dans fct : 0 1.00000e+00
au retour dans main : 0 1.00000e+00

5.2. Retour d’une structure


Bien que cela soit peu usité, sachez que C vous autorise à réaliser des fonctions qui fournissent
en retour la valeur d’une structure. Par exemple, avec le modèle enreg précédemment défini,
nous pourrions envisager une situation de ce type :
struct enreg fct (...){
struct enreg s ; /* structure locale à fct */
.....
return s ; /* dont la fonction renvoie la valeur */
}
Notez bien que s aura dû soit être créée localement par la fonction (comme c’est le cas ici), soit
éventuellement reçue en argument.

28
Naturellement, rien ne vous interdit, par ailleurs, de réaliser une fonction qui renvoie comme
résultat un pointeur sur une structure. Toutefois, il ne faudra pas oublier qu’alors la structure en
question ne peut plus être locale à la fonction ; en effet, dans ce cas, elle n’existerait plus dès
l’achèvement de la fonction... (mais le pointeur continuerait à pointer sur quelque chose
d’inexistant !). Notez que cette remarque vaut pour n’importe quel type autre qu’une structure.

6. Les énumérations
Un type énumération est un cas particulier de type entier et donc un type scalaire (ou simple).
Son seul lien avec les structures présentées précédemment est qu’il forme, lui aussi, un type
défini par le programmeur.
Exemples :
Considérons cette déclaration :
enum couleur {jaune, rouge, bleu, vert} ;
Elle définit un type énumération nommé couleur et précise qu’il comporte quatre valeurs
possibles désignées par les identificateurs jaune, rouge, bleu et vert. Ces valeurs constituent les
constantes du type couleur.
Il est possible de déclarer des variables de type couleur :
enum couleur c1, c2 ; /* c1 et c2 sont deux variables, de type enum couleur */
Les instructions suivantes sont alors tout naturellement correctes :
c1 = jaune ; /* affecte à c1 la valeur jaune */
c2 = c1 ; /* affecte à c2 la valeur contenue dans c1 */
Comme on peut s’y attendre, les identificateurs correspondant aux constantes du type couleur
ne sont pas des lvalue et ne sont donc pas modifiables :
jaune = 3 ; /* interdit : jaune n’est pas une lvalue */

6.1. Propriétés du type énumération


Nature des constantes figurant dans un type énumération
Les constantes figurant dans la déclaration d’un type énumération sont des entiers ordinaires.
Ainsi, la déclaration précédente : enum couleur {jaune, rouge, bleu, vert} ;
➔ associe simplement une valeur de type int à chacun des quatre identificateurs cités. Plus
précisément, elle attribue la valeur 0 au premier identificateur jaune, la valeur 1 à
l’identificateur rouge, etc. Ces identificateurs sont utilisables en lieu et place de n’importe
quelle constante entière :
int n ;
long p, q ;
.....
n = bleu ; /* même rôle que n = 2 */
p = vert * q + bleu ; /* même rôle que p = 3 * q + 2 */
29
Une variable d’un type énumération peut recevoir une valeur quelconque
Contrairement à ce qu’on pourrait espérer, il est possible d’affecter à une variable de type
énuméré n’importe quelle valeur entière (pour peu qu’elle soit représentable dans le type int) :
enum couleur {jaune, rouge, bleu, vert} ;
enum couleur c1, c2 ;
.....
c1 = 2 ; /* même rôle que c1 = bleu ; */
c1 = 25 ; /* accepté, bien que 25 n’appartienne pas au */
/* type type enum couleur */
Qui plus est, on peut écrire des choses aussi absurdes que :
enum booleen { faux, vrai } ;
enum couleur {jaune, rouge, bleu, vert} ;
enum booleen drapeau ;
enum couleur c ;
.....
c = drapeau ; /* OK bien que drapeau et c ne soit pas d’un même type */
drapeau = 3 * c + 4 ; /* accepté */
Les constantes d’un type énumération peuvent être quelconques
Dans les exemples précédents, les valeurs des constantes attribuées aux identificateurs
apparaissant dans un type énumération étaient déterminées automatiquement par le
compilateur.
Mais il est possible d’influer plus ou moins sur ces valeurs, comme dans :
enum couleur_bis { jaune = 5, rouge, bleu, vert = 12, rose } ; /* jaune = 5, rouge = 6, bleu
= 7, vert = 12, rose = 13 */
Les entiers négatifs sont permis comme dans :
enum couleur_ter { jaune = -5, rouge, bleu, vert = 12 , rose } ; /* jaune = -5, rouge = -4,
bleu = -3, vert = 12, rose = 13 */
En outre, rien n’interdit qu’une même valeur puisse être attribuée à deux identificateurs
différents :
enum couleur_ter { jaune = 5, rouge, bleu, vert = 6, noir, violet } ; /* jaune = 5, rouge = 6,
bleu = 7, vert = 6, noir = 7, violet = 8 */
Remarques :
Comme dans le cas des structures ou des unions, on peut mixer la définition d’un type
énuméré et la déclaration de variables utilisant le type. Par exemple, ces deux instructions :
enum couleur {jaune, rouge, bleu, vert} ;
enum couleur c1, c2 ;
peuvent être remplacées par :
enum couleur {jaune, rouge, bleu, vert} c1, c2 ;
Dans ce cas, on peut même utiliser un type anonyme, en éliminant l’identificateur de type :

30
enum {jaune, rouge, bleu, vert} c1, c2 ;
Cette dernière possibilité présente moins d’inconvénients que dans le cas des structures ou des
unions, car aucun problème de compatibilité de type ne risque de se poser.
Compte tenu de la manière dont sont utilisées les structures, il était permis de donner deux
noms identiques à des champs de structures différentes. En revanche, une telle possibilité ne
peut plus s’appliquer à des identificateurs définis dans une instruction enum. Considérez cet
exemple :
enum couleur {jaune, rouge, bleu, vert} ;
enum bois_carte { rouge, noir } ; /* erreur : rouge déjà défini */
int rouge ; /* erreur : rouge déjà défini */
Bien entendu, la portée de tels identificateurs est celle correspondant à leur déclaration
(fonction ou partie du fichier source suivant cette déclaration).

31
Chapitre III
Structures de données
linéaires

32
Avant-propos
Type de Données Abstrait (TAD)

Un TDA est un ensemble de données organisé de sorte que les spécifications des objets et des
opérations sur ces objets (interface) soient séparées de la représentation interne des objets et de la
mise en œuvre des Operations

Exemple de TDA : le type entier muni des opérations +, −, ∗, %, /, >, <, <=, >=, == est un

TDA.

Une mise en œuvre d’un TDA est la structure de données particulière et la d´définition des
opérations primitives dans un langage particulier.

Les avantages des TDA sont :

- Prise en compte de types complexes.


- Séparation des services et du codage. L’utilisateur d’un TDA n’a pas besoin de connaître les
détails du codage.
- Ecriture de programmes modulaires.

L’objectif de ce cours est d’implémenter certains TDA en C. Pour chaque TDA implémenté,
certaines contraintes et conditions seront définies. En plus pour chacun d’eux un certain nombre
d’opération seront implémenter tel que le parcours, la réservation de mémoire, etc.

33
I. Liste

1.1 Généralité

Une liste est un ensemble fini / infini d’éléments notée 𝐿 = 𝑒1 , 𝑒2 , … , 𝑒𝑛 où 𝑒1 est le premier
élément, 𝑒2 est le deuxième, etc. Le 𝐿 représente le premier élément qui est 𝑒1 . Lorsque 𝑛 = 0 on dit
que la liste est vide.

Les listes servent à gérer un ensemble de données, un peu comme les tableaux. Elles sont
cependant plus efficaces et flexibles pour réaliser des opérations comme l’insertion et la suppression
d’éléments.

Elles utilisent par ailleurs l’allocation dynamique de mémoire et peuvent avoir une taille qui varie
pendant l’exécution. L’allocation (ou la libération) se fait élément par élément.

Les opérations sur une liste peuvent être :

- Créer une liste


- Supprimer une liste
- Rechercher un élément particulier en donnant son indice (position)
- Insérer un élément (en début, en fin ou au milieu)
- Supprimer un élément particulier
- Permuter deux éléments
- Concaténer deux listes
- ...

Les listes peuvent par ailleurs être :

- Simplement chaînées
- Doublement chaînées
- Circulaires (chaînage simple ou double)

1.2 Liste simplement chaînée


Une liste simplement chaînée est composée d’´éléments distincts liés par un simple pointeur.

Chaque élément d’une liste simplement chaînée est formé de deux parties :

- Un champ contenant la donnée (ou un pointeur vers celle-ci)


- Un pointeur vers l’élément suivant de la liste.

Le premier élément d’une liste est sa tête, le dernier sa queue. Le pointeur du dernier élément est
initialisé à une valeur sentinelle, par exemple la valeur NULL en C. Ceci pour indiquer que la liste est
vide.

Pour accéder à un élément d’une liste simplement chaînée, on part de la tête et on passe d’un
élément à l’autre à l’aide du pointeur suivant associé à chaque élément.

En pratique, les éléments étant créés par allocation dynamique, ne sont pas contigus en mémoire
contrairement à un tableau. La suppression d’un élément sans précaution ne permet plus d’accéder

34
aux éléments suivants. D’autre part, une liste simplement chaînée ne peut être parcourue que dans
un sens (de la tête vers la queue).

Pour résumer, la liste est une structure linéaire dans laquelle :

- Les éléments peuvent être traités les uns à la suite des autres ;
- Les éléments sont ajoutés et retirer n’importe où dans la liste ;
- Chaque élément de la liste est rangé à une certaine place ;
- Les éléments d'une liste sont donc ordonnés en fonction de leur place.

Figure 12. Liste simplement chaînée

1.3 TDA : liste simplement chaînée


La Figure 2 représente la définition d’une liste simplement chaînée en C. Dans ce qui suit le détail
de chaque fonction utilisée.

- liste_vide : permet de créer une liste de l’initialiser par NULL et de la retournée ;


- longueur : permet de retourner le nombre d’éléments dans la liste qui est passée comme
paramètre ;
- insérer : cette fonction prend trois paramètres qui sont : la liste où on va faire l’ajout, la valeur
à ajouter, et la position où on va ajouter la valeur. L’ajout ne peut pas être effectué si la
position donnée en paramètre est inférieure à zéro ou supérieur à la taille de la liste ;
- supprimer : prend deux paramètres à savoir la liste contenant les éléments et la position ou
l’indice de l’élément à supprimer ;
- Keme : prend comme paramètre deux paramètres : la liste et la position (ou indice) de
l’élément à récupérer. Cette fonction doit retourner l’élément qui se trouve dans la position
qui est passée comme paramètre à condition qu’elle ne soit pas inférieure de zéro et qu’elle
ne dépasse pas la taille de la liste ;
- accès : même chose que la fonction keme mais maintenant la fonction retourne l’adresse de
l’élément et non seulement l’élément ;
- succ : même chose que la fonction keme, mais maintenant la fonction retourne l’adresse de
l’élément suivant.

35
Figure 13 : TDA d'une Liste simplement chaînée

1.4 Implémentation des listes simplement chaînée en


C
Définition
La liste est définie comme étant un ensemble des éléments ou cellules dont chacun élément ou
cellule possède deux champs :

- La valeur ou données ;
- Un pointeur qui enregistre l’adresse de l’élément ou la cellule suivante.

Liste_vide
Consiste à créer une liste, de l’initialiser par NULL, et de la retourner.

Longueur

36
Cette fonction permet de calculer la taille d’une liste passée comme paramètre. L’idée consiste à
parcourir la liste de début (l’entête) jusqu’à la fin de la liste (indiquer normalement par NULL) en
comptant le nombre d’éléments rencontrés.

Insérer
La fonction reçoit en paramètre la liste où on va ajouter l’élément, la valeur (donnée) à ajouter, et la
position (indice) où la nouvelle valeur sera insérée. L’insertion ne peut pas être effectuée si l’indice ou
la position donnée est inférieur à zéro ou supérieur de la taille de la liste.

L’idée consiste à donner en entrée : une liste L, une valeur val, et une position pos où la valeur sera
insérée. La première étape dans l’ajout consiste à créer un élément ou cellule où la valeur sera
enregistrée. Ensuite, ce nouvel élément ou cellule sera inséré dans la liste L selon les cas suivantes :

- Si la liste est vide ➔ le nouvel élément sera le 1er élément de la liste ;


- Si la position = 0 ➔ le nouvel élément doit être inséré au début de la liste. Autrement, le nouvel
élément doit occuper le 1er emplacement de la liste ;
- Si la position est supérieure à zéro ➔ le nouvel élément doit être inséré au milieu de la liste.

L’implémentation en C de l’opération d’ajout dans le cas des listes simplement chaînés est la suivante :

37
Supprimer
Pour supprimer un élément dans une liste, il faudrait donner la position de l’élément à supprimer. La
suppression ne peut pas être effectué si la liste est vide ou si la position est inférieure à zéro ou
supérieur que la taille de la liste. L’implémentation en C est la suivante :

38
Les fonctions d’accès
Une liste est un ensemble d’éléments ou cellules liée par un pointeur suivant. D’où chaque élément
possède une adresse, et à l’intérieur de cette adresse, il se trouve la valeur enregistrée et un pointeur
ayant l’adresse de l’élément suivant. A cet effet, pour une position « pos » passée comme paramètres,
trois fonctions d’accès sont définies :

- Acces : retourne l’adresse de l’élément qui se trouve dans « pos » ;


- Keme : retourne la valeur enregistrée dans l’élément qui se trouve dans « pos » ;
- Succ : retourne l’adresse du suivant de l’élément qui se trouve dans « posé ».

L’accès ne peut pas être effectué si la liste est vide, si la position souhaitée est inférieure à zéro ou
supérieur à la taille de la liste. L’implémentation en C de ces trois fonctions est la suivante :

1.5 TDA : liste doublement chaînée


Une liste doublement chaînée est une liste chaînée dont chaque élément possède deux pointeurs
un vers l’élément ou cellule suivant, et l’autre vers l’élément précédant. Tout ce qui a été définit pour
les listes simplement chaînées reste valable même pour les listes doublement chaînées.

Les deux fonctions d’ajout et de suppression seront redéfinies pour prendre en considération la
nouvelle relation de précédant.

Définition

39
Insérer
La fonction insérer aura les mêmes paramètres à savoir une liste L, une valeur val, et une position pos.
Ensuite, le nouvel élément sera intégré dans la liste selon les cas suivantes :

- Si la liste est vide alors le nouvel élément sera le 1er élément de la liste avec un suivant et
précédant égales au NULL ;
- Si la liste n’est pas vide et pos égale à zéro dans ce cas, le précédant du nouvel élément aura
NULL, le suivant prendra l’adresse de L, le précédant de L prendra l’adresse du nouvel élément.
Et finalement le L prendre l’adresse du nouvel élément vue que c’est lui qui va occuper la 1 ère
position
;
- Si pos est un indice qui se trouve au milieu ou à la fin : dans ce cas il faut parcourir la liste
jusqu’à l’élément pos (et non pas pos-1 comme dans le cas de liste SC). Ensuite, on modifiera
les liens de suivant et précédant des trois éléments : le nouvel élément, l’élément qui se trouve
dans pos, et de celui qui se trouve dans pos-1. L’adresse de ce dernier est récupérable à partir
du champ précédant de l’élément qui se trouve dans pos.

L’implémentation en C de cette fonction est la suivante :

40
Supprimer
Même pour le cas d’une liste doublement chaînée, pour supprimer un élément de la liste, il faudrait
donner sa position (indice).

La suppression peut être effectué au début, au milieu, ou à la fin de la liste. L’implémentation en C


de cette fonction est la suivante :

Les fonctions d’accès définies pour les listes simplement chaînées sont aussi appliquées dans le cas
d’une liste doublement chaînée.

1.6 TDA : liste circulaire


Une liste circulaire est une liste qui peut être soit simplement chaînée ou doublement chaînée à
condition que le dernier élément de la liste soit lié au premier élément de la liste. A cet effet, y’a pas
de notion NULL dans les listes circulaires.

Les mêmes règles appliquées dans les listes simplement et doublement chaînées sont aussi
appliquées dans les listes circulaires.

41
1.7 Séries des exercices
Voir les séries :

- TD2
- TD3

1.8 Conclusion
Les listes sont un type abstrait de données qui existe dans plusieurs langages de développement et
qui est utilisé dans plusieurs processus et systèmes. L’idée général des listes consiste gérer
dynamiquement l’espace mémoire. L’espace mémoire n’est réservé que s’il y’a une nouvelle valeur à
insérer. En plus, chaque élément possède une adresse unique, une valeur (représente généralement
les données stockées dans une liste), une adresse suivant (dans le cas d’une liste simplement chaînée),
ou une adresse suivant et précédant (dans le cas d’une liste doublement chaînée).

II. Pile & File


Les piles & les files sont des structures linéaires dont les laquelle les éléments peuvent être traités
les uns à la suite des autres. Cependant, l’accès aux données ne peut pas être effectué directement
comme le cas des listes.

2.1 Pile
Une structure linéaire permettant de stocker et de restaurer des données selon un ordre LIFO
(Last In, First Out ou « dernier entré, premier sorti ».

Les opérations d’insertions (empilements) et de suppressions (dépilements) sont restreintes à


une extrémité appelée sommet de la pile

Les piles ont été utilisé dans plusieurs champs d’applications :

- Vérification du bon équilibrage d’une expression avec parenthèses ;


- Evaluation des expressions arithmétiques postfixées ;
- Gestion par le compilateur des appels de fonctions ;
- Etc.

Le type abstrait des piles est définie comme suit :

42
On considère que dans la pile :

- Les éléments sont enchaînés et liée par une adresse


précédant. Chaque élément sauvegarde l’adresse de
celui qui se trouve avant ;
- Quand on déclare « Pile P ; » : le P représente
l’adresse de l’élément dans le sommet de la pile. Qui
est normalement l’adresse du dernier élément ajouté dans la pile ;
- Une pile vide est représentée par pointeur NULL.

43
L’implémentation des piles en C :

44
2.2 File
Les files dont une structure linéaire permettant de stocker et de restaurer des données selon un
ordre FIFO (First In, First Out ou « premier entré, premier sorti »).

Les opérations d’insertions (enfilements) se font à une extrémité appelée queue de la file et de
suppressions (défilements) se font à l'autre extrémité appelée tête de la file.

Il existe plusieurs applications des files tel que :

- Gestion travaux d’impression d’une imprimante ;


- Ordonnanceur (dans les systèmes d’exploitation) ;
- Etc.

Le type abstrait des files est comme suit :

Dans les files, on considère que :

- Les éléments de la file sont chaînés entre eux ;


- Quand on déclare « File f ; », f représente deux pointeurs :
o Un pointeur sur le premier élément de la file et qui représente la tête de la file ;
o Un pointeur sur le dernier élément représente la queue de la file.
- Une file vide est représentée par un pointeur NULL.

45
L’implémentation des files en C :

46
Chapitre IV
Les fichiers texte

47
I. Généralité
Définition, propriété, type de fichiers en c.

I.1. Définition
Un fichier est une suite de données homogènes conservées en permanence sur un support externe
(disque dur, clef USB, …). Ses données regroupent, le plus souvent, plusieurs composantes (champs)
d'une structure. Par exemple : un fichier de clients, fichiers des entiers, etc. Le langage C offre la
possibilité de lire et d’écrire des données dans un fichier.

Il existe deux types de fonctions permettent de manipuler un fichier :

- Des fonctions de bas niveau : dépendent du système d'exploitation et font un accès direct sur
le support physique de stockage du fichier.

- Des fonctions de haut niveau : l'accès au fichier se fait par l'intermédiaire d'une zone mémoire
de stockage (la mémoire tampon). Ces fonctions sont construites à partir des fonctions de bas
niveau.

Dans ce cours, seules les fonctions de haut niveau seront étudiées et utilisées.

Pour permettre un accès rapide aux données d’un fichier, l’accès se fait par l’intermédiaire d’une
mémoire tampon (buffer), ce qui permet de réduire le nombre d’accès aux périphériques de stockage
(disque...). Il s'agit d'une zone de la mémoire centrale qui stocke une quantité, assez importante, de
données du fichier. Son rôle est d'accélérer les entrées/sorties à un fichier.

Pour pouvoir manipuler un fichier, un programme a besoin d’un certain nombre d’informations :
l’adresse de l’endroit de la mémoire-tampon où se trouve le fichier, la position de la tête de lecture,
le mode d’accès au fichier (lecture ou écriture), etc. Ces informations sont rassemblées dans une seule
structure qui est le type, FILE * défini dans la bibliothèque stdio.h. Un objet de type FILE * est appelé
un flot de données (en anglais, Stream).

I.2. Types de fichiers


Il existe deux types de fichier :

- Un fichier de texte est une suite de lignes ; chaque ligne est une suite de caractères terminée
par le caractère spécial '\n’ ;
- Un fichier binaire est une suite d'octets pouvant représenter toutes sortes de données. (le
système n'attribue aucune signification aux octets échangés).

Il existe des fichiers spéciaux, appelés fichiers standard, qui sont prédéfinis et ouverts
automatiquement lorsqu'un programme commence à s'exécuter. Ils peuvent être utilisés en C
directement sans qu’il soit nécessaire de les ouvrir ou de les fermer :

- stdin : entrée standard (par défaut, lié au clavier) ;


- stdout : sortie standard (par défaut, lié à l'écran)
- stderr : sortie d'erreur standard (par défaut, lié aussi à l'écran)

Ces fichiers peuvent être redirigés au niveau de l'interprète de commandes par l'utilisation de
symboles « > » et « < » à l'appel du programme.

48
Il est fortement conseillé d’afficher systématiquement les messages d’erreur sur stderr afin que
ces messages apparaissent à l’écran même lorsque la sortie standard est redirigée.

Par exemple : Soit le fichier de texte "c:\[Link]" et considérons les appels suivants du
programme exécutable Prog (un exécutable d’un programme en C) :

- Si on écrit Prog > c:\[Link] ➔ Prog écrira dans c:\[Link] au lieu de l'écran. Autrement,
tous les appels de printf seront exécutés dans [Link] au lieu de l’écran ;
- Si on écrit Prog < c:\[Link] ➔ Prog fera ses lectures dans c:\[Link]). Autrement, tous les
scanf vont lire les données non pas depuis le clavier mais depuis le fichier [Link].

Soient Prog1 et Prog2 deux programmes exécutables. En utilisant l'appel suivant

Prog1 | Prog2

➔Prog1 a sa sortie standard redirigée dans l'entrée standard de Prog2.

II. Manipulation d’un fichier (déclaration, ouverture, et fermeture)


Avant de lire ou d’écrire dans un fichier, on l’ouvre par la commande fopen. Cette fonction prend
comme argument le nom du fichier, pour initialiser le flot de données, et qui sera utilisé lors de
manipulation du fichier (lecture, écriture). A la fin du traitement, on coupe la liaison entre le fichier et
le flot de données grâce à la fonction fclose. D’où le principe de manipulation d’un fichier en C, est le
suivant :

1. Ouverture du fichier avec fopen ;


2. L’accès au fichier pour lecture, écriture, et/ou déplacement ;
3. Fermeture du fichier avec fclose.

Il existe deux techniques d’accès au fichier :

- Un accès séquentiel : pour atteindre l'information souhaitée, il faut passer par la première
puis la deuxième et ainsi de suite.
- Un accès direct : consiste à se déplacer directement sur l'information souhaitée sans avoir à
parcourir celles qui la précèdent.

II.1. Déclaration
Pour déclarer un fichier, il faut utiliser l’instruction suivante :

FILE *<PointeurFichier> ;

Le type FILE est défini dans <stdio.h> en tant qu’une structure. A l'ouverture d'un fichier, la
structure FILE contient un certain nombre d'informations sur ce fichier telles que :

- Adresse de la mémoire tampon ;


- Position actuelle dans le tampon ;
- Nombre de caractères déjà écrits dans le tampon, etc. ;
- Type d'ouverture du fichier : écriture, lecture, etc. ;
- Etc.

Pour pouvoir travailler avec un fichier dans un programme, ranger l'adresse de la structure FILE
dans le pointeur de fichier et tout accès ultérieur au fichier se fait par l'intermédiaire de ce pointeur.

49
II.2. Ouverture : fopen
Cette fonction, de type FILE* ouvre un fichier et lui associe un flot de données. Sa syntaxe est :

File* fopen("nom-de-fichier","mode")

La valeur retournée par fopen est un flot de données. Si l’exécution de cette fonction ne se déroule
pas normalement, la valeur retournée est le pointeur NULL. A cet effect, Il est recommandé de toujours
tester si la valeur renvoyée par la fonction fopen est égale à NULL afin de détecter les erreurs (lecture
d’un fichier inexistant...).

Le premier argument de fopen est le nom du fichier concerné, fourni sous forme d’une chaîne de
caractères. On préférera définir le nom du fichier par une constante symbolique au moyen de la
directive #define plutôt que d’expliciter le nom de fichier dans le corps du programme.

Le second argument, mode, est une chaîne de caractères qui spécifie le mode d’accès au fichier.
Les spécificateurs de mode d’accès se différent selon le type de fichier considéré (voir la partie II.2.
Types de fichiers).

Exemple : pf = fopen("[Link]", "rb") ; On dit pf est pointeur fichier ou un flot de données.

Les différents modes d’accès sont les suivants :

Fichier binaire
"rb" Ouverture en lecture
"wb" Ouverture en écriture
"ab" Ouverture en écriture à la fin
"r+b" Ouverture en lecture/écriture
"w+b" Ouverture en lecture/écriture
"a+b" Ouverture en lecture/écriture à la fin
Fichier texte
"r" Ouverture en lecture
"w" Ouverture en écriture
"a" Ouverture en écriture à la fin
"r+" Ouverture en lecture/écriture
"w+" Ouverture en lecture/écriture
"a+" Ouverture en lecture/écriture à la fin

Remarques :

- Si le mode contient la lettre r, le fichier doit exister ;


- Si le mode contient la lettre w, le fichier peut ne pas exister. Dans ce cas, il sera créé. Si le fichier
existe déjà, son ancien contenu sera perdu ;

50
- Si le mode contient la lettre a, le fichier peut ne pas exister. Dans ce cas, il sera créé. Si le fichier
existe déjà les nouvelles données seront ajoutées à la fin du fichier précédent.

Rappelant que cette fonction est utilisée uniquement pour les fichiers de type texte et binaire. Les
fichiers standard sont ouverts automatiquement dès l’exécution d’un programme.

II.3. Fermeture : fclose


Elle permet de fermer le flot qui a été associé à un fichier par la fonction fopen. Autrement, elle
détruit le lien pointeur de fichier et le nom de fichier. Sa syntaxe est :

int fclose(FILE *<PointeurFichier>)

Exemple : fclose(pf) ; /* pf est un pointeur de fichier */

La fonction fclose retourne un entier qui vaut zéro si l’opération s’est déroulée normalement (et
une valeur non nulle en cas d’erreur) souvent c’est la constante EOF.

Remarques :

- Quand un fichier ne sert plus, il est conseillé de le fermer ;


- Dès qu'un fichier est fermé, la mémoire tampon est libérée ;
- Après fclose(pf), le pointeur pf est invalide. Des erreurs graves pourraient donc survenir si ce
pointeur est utilisé par la suite.

II.4. Exemple

III. Traitement des données d’un fichier (lecture & écriture)


III.1. Fichiers standard
Les opérations possibles dans le cas des fichiers standard (stdin, stdout, stderr) sont :

1. Lecture et écriture caractère par caractère :

51
- Lecture : int getchar()
Permet de lire un caractère sur stdin, et retourne la valeur du caractère lu ou EOF (si fin du
fichier ou erreur).
Exemple :
while ((c = getchar() != EOF) && (c != ' ')) ;

- Ecriture : int putchar(int c) ;


Permet d'écrire le caractère c sur stdout, et retourne la valeur du caractère écrit c ou EOF en
cas d'erreur.

2. Lecture et écriture ligne par ligne ;


Une ligne est considérée comme une suite de caractères terminée par le caractère fin de ligne '\n'
ou par la détection de la fin du fichier.

- Lecture : char *gets(char *s) ;


Lit une ligne sur stdin et la place dans la chaîne s. Le caractère fin de ligne
'\n' est remplacé dans s par le caractère fin de chaîne '\0'. La fonction retourne NULL à la
rencontre de la fin de fichier ou en cas d'erreur.
- Ecriture : int puts(char *s) ;
Permet d'écrire la chaîne de caractères s, suivie d'un saut de ligne sur stdout. Retourne le
dernier caractère écrit ou EOF en cas d'erreur.

3. Lecture et écriture formatées


Dans ce type on utilise les fameuses fonctions : scanf et printf

III.2. Fichiers textes ou binaire


Une fois le fichier ouvert, C permet plusieurs types de traitement du fichier :

- par caractères ;
- par lignes ;
- par enregistrements ;
- par données formatées

Dans tous les cas, les fonctions de traitement du fichier (sauf les opérations de déplacement (voir
Parcours d’un fichier)) ont un comportement séquentiel. L'appel de ces fonctions provoque le
déplacement du pointeur courant relatif au fichier ouvert.

III.2.1. Traitement caractère par caractère


Fonction fgetc :

int fgetc(FILE *<PointeurFichier>) ;

Lit un caractère dans le fichier référencé par le pointeur <PointeurFichier>. La fonction retourne
soit :

- Le caractère lu sous forme d'un int ;


- EOF à la rencontre de la fin du fichier ou en cas d'erreur.

Fonction getc :

52
int getc(FILE *<PointeurFichier>) ;

Identique à fgetc() sauf que cette fonction est réalisée par une macro définie dans <stdio.h>. Pour
une macro, les instructions sont générées en ligne (et répétées à chaque appel) ce qui évite un appel
de fonction (coûteux).

Fonction fputc :

int fputc(int <Caractere>, FILE *<PointeurFichier>) ;

Ecrit dans le fichier référencé par le pointeur <PointeurFichier> le caractère placé dans la variable
<Caractere>. La fonction retourne :

- La valeur sous forme d'int du caractère écrit dans le fichier ;


- EOF en cas d'erreur.

Fonction putc

int putc(int <Caractere>, FILE *<PointeurFichier>) ;

Identique à fputc() sauf que cette fonction est réalisée par une macro.

Exemple :

53
Remarques :
- c = getchar() équivalente à c = getc(stdin) ou c = fgetc(stdin)
- putchar(c) équivalente à putc(c, stdout) ou fputc(c, stdout)

III.2.2. Traitement ligne par ligne (lecture d’une chaîne)


Fonction fgets :

char *fgets(char *<Chaine>,int<Nbre>,FILE *<PointeurFichier>);

Lit une ligne de caractères dans le fichier référencé par <PointeurFichier>. Cette ligne est stockée
dans <Chaine>.

<Nbre> est le nombre maximum de caractères à lire.

La fonction retourne :

- Un pointeur vers le début de la chaîne ;


- NULL en cas d'erreur ou à la rencontre de la fin de fichier.

La lecture s'arrête lorsque, un des événements se produit :

- Lecture de saut de ligne '\n' ('\n' est recopié dans <Chaine>) ;


- Lecture d'au plus (<Nbre> - 1) caractères (fgets termine <Chaine> par '\0') ;
- Rencontre de la fin de fichier

Fonction fputs :

int fputs(char *<Chaine>, FILE *<PointeurFichier>) ;

Cette fonction écrit la chaîne <Chaine> dans le fichier référencé par <PointeurFichier>.

54
Elle retourne :

- Une valeur positive (code ASCII du dernier caractère écrit) si l'écriture s'est correctement
déroulée ;
- EOF en cas d'erreur.

La chaîne <Chaine> doit être terminée par '\0'. Ce caractère n'est pas transféré dans le fichier. Il
faut mettre explicitement la fin de ligne dans la chaîne pour qu'elle soit présente dans le fichier.
Exemple :

III.2.3. Traitement par enregistrement


Permet de lire et écrire des objets, le plus souvent représentés par des structures (appelées
enregistrements) dans un fichier.

Pour ce type de traitement :

- Le fichier doit être ouvert en mode binaire ;


- Les données échangées ne sont pas traitées comme des caractères. Elles sont traitées sous
forme de blocs d'octets.

Fonction fread :

unsigned int fread(void *<pb>, unsigned <taille>, unsigned <nb>, FILE *<pf>) ;

Lit un certain nombre de données (des enregistrements) de taille identique depuis un fichier
référencé par <pf> vers un bloc mémoire.

- Le bloc mémoire d'adresse <pb> reçoit les enregistrements lus ;


- <taille> : taille d'un enregistrement en nombre octets ;
- <nb> : nombre d'enregistrements à échanger (lire) ;
- <pf> : fait référence à un fichier ouvert en mode binaire ;
- Le nombre d'octets lus est (<nb> * <taille>)

55
La fonction retourne :

- Le nombre d'enregistrements lus (et non le nombre d'octets) ;


- Si EOF ou erreur, une valeur inférieure à <nb> (ou même 0).

Fonction fwrite :

unsigned int fwrite(void *<pb>, unsigned <taille>, unsigned <nb>, FILE *<pf>) ;

L'espace mémoire d'adresse <pb> fournit les données à écrire dans les enregistrements.

La fonction écrit <nb> éléments (enregistrements) ayant chacun une taille de <taille> octets à la fin
d'un fichier référencé par <pf>.

Le nombre d'octets écrits est (<nb> * <taille>)

La fonction retourne :

- Le nombre d'enregistrement écrits (et non le nombre d'octets) ;


- Si erreur, une valeur inférieure à <nb> (ou même 0).

Exemple : Lecture d'enregistrements dans un fichier :

Cet exemple :

- Est une lecture du contenu d'un fichier appelé FichParcAuto ;


- Avec stockage du contenu de ce fichier dans un tableau en mémoire ParcAuto.

Les cases du tableau sont une structure contenante : un entier, une chaîne de 20 caractères et 3
chaînes de 10 caractères.

56
Remarque :

Il est possible de demander la lecture de 20 enregistrements en une seule opération, en


remplaçant la boucle for par :

fait = fread(ParcAuto, sizeof(struct automobile), 20, pf) ;

ou bien par :

fait = fread(ParcAuto, sizeof ParcAuto, 1, pf) ;

III.2.4. Lecture & écriture formatées


Dans ce cas les deux fonctions utilisées sont fprintf et fscanf. Ces fonctions permettent de réaliser
le même travail que printf et scanf sur des fichiers ouverts en mode texte.

Fonction fprintf (écriture formatée sur un fichier ouvert en mode texte)

int fprintf(FILE *<PointeurFichier>, char *<Format>, <Arguments>);

La fonction écrit les données formatées dans un fichier, elle fonctionne ainsi :

- Accepte une série d'arguments (les valeurs des données à écrire) ;


- Applique à chaque argument un spécificateur de format dans <Format> ;
- Envoie les données formatées dans un fichier.

Elle retourne :

- Le nombre de caractères écrits ;


- Une valeur négative en cas d'erreur.

Remarques :

Le nombre d'arguments doit satisfaire le nombre de formateurs car s'il y a trop d'arguments (pas
assez de formateurs), ceux en trop sont ignorés.

En pratique, les arguments représentent les rubriques qui forment un enregistrement et dont les
valeurs respectives sont écrites dans le fichier.

Fonction fscanf : (lecture formatée dans un fichier ouvert en mode texte)

int fscanf(FILE *<PointeurFichier>, char *<Format>, <Adresses>);

La fonction lit des données formatées dans un fichier :

- <PointeurFichier> fait référence au fichier ;


- <Format> : format de lecture des données ;
- <Adresses> : adresses des variables à affecter à partir des données ;
- Un formateur et une adresse doivent être fournis pour chaque variable.

La fonction retourne :

- Le nombre d'éléments lus (0 si aucun élément n'a été traité totalement) ;


- EOF si fin de fichier.

Remarque : les fonctions fprintf et fscanf peuvent être utilisées pour lire et écrire dans les fichiers
standard.

57
- fprintf(stdout, "Bonjour\n") équivalente à printf("Bonjour\n")

Dans les fichiers texte, il faut ajouter le symbole de fin de ligne '\n' pour séparer les données.

- fscanf(stdin, "%d", &N) équivalente à scanf("%d", &N)

A l'aide de fscanf, il est impossible de lire toute une phrase dans laquelle les mots sont séparés par des
espaces.

III.2.5. Parcours d’un fichier texte


int fseek (FILE * flux, long noct, int org)

Place le pointeur du flux indiqué à un endroit défini comme étant situé à noct octets de l’« origine »
spécifiée par org :

org = SEEK_SET correspond au début du fichier

org = SEEK_CUR correspond à la position actuelle du pointeur

org = SEEK_END correspond à la fin du fichier

Dans le cas des fichiers de texte (si l’implémentation les différencie des autres), les seules possibilités
autorisées sont l’une des deux suivantes :

▪ noct = 0
▪ noct a la valeur fournie par ftell (voir ci-dessous) et org = SEEK_SET

long ftell (FILE *flux)


Fournit la position courante du pointeur du flux indiqué (exprimée en octets par rapport au début du fichier) ou
la valeur -1L en cas d’erreur.

58

Vous aimerez peut-être aussi