Cours STD
Cours STD
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
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).
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 :
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 = ∈
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.
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 ;
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 :
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
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 :
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).
8
Programme 1: Définition de tableaux et initialisations
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).
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.
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 :
10
Tableau 2: Soustraction de deux pointeurs
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.
Ceci nous amène à regarder l’utilisation de pointeurs pour manipuler des tableaux, en prenant les
variables : long i, tab[10], *pti ;
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
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().
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.
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.
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 :
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
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.
- 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.
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
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 *strrchr(const char *, int); idem que strchr, recherche à partir de la fin
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 *strpbrk(const char *s, const trouve la première occurrence d'un caractère
char *accept); d'accept dans s
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.
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.
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.
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.
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}};
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} };
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).
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.
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
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 */
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.
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.
- Simplement chaînées
- Doublement chaînées
- Circulaires (chaînage simple ou double)
Chaque élément d’une liste simplement chaînée est formé de deux parties :
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).
- 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.
35
Figure 13 : TDA d'une Liste simplement chaînée
- 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 :
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 :
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 :
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.
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).
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.
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).
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 ».
42
On considère que dans la pile :
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.
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.
- 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).
- 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 :
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].
Prog1 | Prog2
- 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 :
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).
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 :
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.
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 :
II.4. Exemple
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 != ' ')) ;
- 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.
Lit un caractère dans le fichier référencé par le pointeur <PointeurFichier>. La fonction retourne
soit :
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 :
Ecrit dans le fichier référencé par le pointeur <PointeurFichier> le caractère placé dans la variable
<Caractere>. La fonction retourne :
Fonction putc
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)
Lit une ligne de caractères dans le fichier référencé par <PointeurFichier>. Cette ligne est stockée
dans <Chaine>.
La fonction retourne :
Fonction fputs :
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 :
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.
55
La fonction retourne :
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>.
La fonction retourne :
Cet exemple :
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 :
ou bien par :
La fonction écrit les données formatées dans un fichier, elle fonctionne ainsi :
Elle retourne :
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.
La fonction retourne :
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.
A l'aide de fscanf, il est impossible de lire toute une phrase dans laquelle les mots sont séparés par des
espaces.
Place le pointeur du flux indiqué à un endroit défini comme étant situé à noct octets de l’« origine »
spécifiée par org :
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
58