Vous êtes sur la page 1sur 52

SUPPORT DE COURS

Année académique / Semestre 2023-2024/ SEMESTRE 1


Intitulé du cours Structures de données avancées 2

Classe / Niveau GL/ Niveau 2


Enseignant M. NDJE MAN Dieudonné François
Coordonnées Tél : 654080512
Email : franoisndje@yahoo.fr
Volume Horaire global CM : TD : TP :
OBJECTIFS GÉNÉRAUX DU COURS :
Ce cours vise à fournir aux étudiants une compréhension approfondie des structures de
données avancées en utilisant le langage de programmation C.

OBJECTIFS SPECIFIQUES :

▪ Pour réussir ce cours, l'étudiant doit :


▪ Comprendre les structures et pointeurs
▪ Comprendre et manipuler les listes chainées
▪ Comprendre la gestion des fichiers
▪ Comprendre et implémenter une table de hachage
TABLE DES MATIERES
CHAPITRE 1 RAPPELS SUR LES STRUCTURES DE DONNEES .............................. 1
1.1 LA STRUCTURE .............................................................................................................. 1
1.1.1 DEFINITION ............................................................................................................... 1
1.1.2 MANIPULATION D’UNE STRUCTURE .......................................................... 4
1.1.3 STRUCTURE ET POINTEURS ........................................................................... 9
1.1.4 PORTEE ET DECLARATION ............................................................................ 14
1.1.5 PRINCIPE DES LISTES CHAINEES .............................................................. 16
1.2 EXERCICES DE FIN DE CHAPITRE ...................................................................... 21
CHAPITRE 2 LES FICHIERS .................................................................................................... 22
2.1 DEFINITION ..................................................................................................................... 22
2.1.1 EXTENSION DE FICHIER .................................................................................. 22
2.1.2 SYSTEME DE FICHIERS .................................................................................... 23
2.1.3 HIERARCHIE ........................................................................................................... 24
2.1.4 HIERARCHIE SOUS UNIX ................................................................................. 25
2.1.5 LE CHEMIN D'ACCES.......................................................................................... 26
2.1.6 METADONNEES..................................................................................................... 27
2.1.7 LES FLUX : UN PEU DE THEORIE ................................................................ 28
2.1.8 ÉCRITURE VERS UN FLUX DE TEXTE....................................................... 30
2.1.9 ÉCRITURE VERS UN FLUX BINAIRE .......................................................... 34
2.1.10 LECTURE DEPUIS UN FLUX BINAIRE ................................................... 35
2.1.11 EN RESUME......................................................................................................... 37
CHAPITRE 1 RAPPELS SUR LES STRUCTURES DE
DONNEES
Les structures de données en C sont des moyens permettant de regrouper différentes
variables sous un même nom. Elles offrent la possibilité de créer des types de données
personnalisés en regroupant des données hétérogènes sous une seule entité. Les structures
de données sont un élément fondamental de la programmation en C, car elles permettent de
représenter et de manipuler des informations de manière organisée. On en distingue plusieurs
parmi lesquelles nous pouvons citer :
Les structures de données en C sont des composants essentiels pour organiser et manipuler
des données de manière efficace. Voici une exploration plus détaillée des structures de
données couramment utilisées en C :
▪ Tableau (Array) : structure de données linéaire qui stocke des éléments du même type
sous un seul nom. Les éléments sont accessibles à l'aide d'indices entiers.
▪ Structure (Struct) : structure permet de regrouper des éléments de différents types sous
un même nom. Elle est utile pour représenter des entités complexes.
▪ Liste chaînée (Linked List) : structure de données dynamique qui consiste en des
nœuds liés les uns aux autres. Chaque nœud contient une valeur et une référence
(pointeur) vers le nœud suivant.
▪ Pile (Stack) : structure de données linéaire basée sur le principe Last In, First Out
(LIFO). Les éléments sont ajoutés et retirés uniquement du sommet de la pile.
▪ File (Queue) : structure de données linéaire basée sur le principe First In, First Out
(FIFO). Les éléments sont ajoutés à l'arrière et retirés du devant de la file.
▪ Arbre (Tree) : structure de données hiérarchique constituée de nœuds. Chaque nœud
a un parent et zéro ou plusieurs enfants.
▪ Graphe (Graph) : structure de données composée de nœuds (sommets) et d'arêtes qui
les relient. Les graphes peuvent être dirigés ou non dirigés.
Ces structures de données fournissent des outils puissants pour résoudre une variété de
problèmes algorithmiques. Le choix de la structure de données dépend du problème
spécifique que vous cherchez à résoudre et des opérations que vous prévoyez d'effectuer
fréquemment sur les données.

1.1 LA STRUCTURE

1.1.1 DEFINITION
Une structure est un moyen de regrouper des variables de types différents sous un seul nom.
Cela permet de créer des enregistrements complexes.
• Chaque élément déclaré à l’intérieur de la structure est appelé un champ ou membre.

1
• Le nom donné à la structure est appelé étiquette de la structure.

1.1.1.1 SYNTAXE DECLARATIVE


Les syntaxes suivantes permettent de déclarer une structure en C :
struct NomDeLaStructure {
// Membres de la structure
TypeDeMembre1 NomMembre1;
TypeDeMembre2 NomMembre2;
// ... (autres membres)
};
Ou
typedef struct NomDeLaStructure {
// Membres de la structure
TypeDeMembre1 NomMembre1;
TypeDeMembre2 NomMembre2;
// ... (autres membres)
} NomAlias;
Exemple 1.1 : Déclaration d’une structure nommée Etudiant qui représente les informations
associées à un étudiant. La structure a trois membres :
▪ nom: Un tableau de caractères (chaîne de caractères) de taille 50, utilisé pour stocker
le nom de l'étudiant.
▪ age: Une variable de type entier (int), utilisée pour stocker l'âge de l'étudiant.
▪ moyenne: Une variable de type flottant (float), utilisée pour stocker la moyenne de
l'étudiant.
#include <stdio.h>
// Définition de la structure
struct Etudiant {
char nom[50];
int age;
float moyenne;
};
Ou
typedef struct Etudiant
{
char nom[50];
int age;
float moyenne;
} Etudiant;
Le tableau ci-dessous compare ces deux approches pour la définition d'une structure en
langage C : l'utilisation de typedef et la définition traditionnelle sans typedef.

2
1.1.1.2 REPRESENTATION EN MEMOIRE

En C, la représentation en mémoire d'une structure dépend de plusieurs facteurs, tels


que l'alignement de la mémoire et le type de données que la structure contient. La
mémoire est généralement organisée de manière à être efficace en termes d'accès et
d'utilisation.
La mémoire pourrait être organisée comme suit pour une instance de la structure
Etudiant :
---------------------------------------------
| nom (50 octets) | age (4 octets) | moyenne (4 octets) |
---------------------------------------------
Chaque membre de la structure occupe une certaine quantité d'octets en mémoire.
Dans cet exemple, le tableau de caractères `nom` occupe 50 octets, l'entier `age`
occupe 4 octets, et le nombre à virgule flottante (réel) `moyenne` occupe également 4
octets. La taille totale de la structure est donc de 58 octets.

3
1.1.1.2.1 ALIGNEMENT DE LA MEMOIRE

Les compilateurs C peuvent introduire des octets de bourrage 1pour garantir que
chaque membre de la structure commence à une adresse mémoire compatible avec
son type de données. La taille totale de la structure peut être plus grande en raison de
l'alignement de la mémoire.
Par exemple, si l'alignement de la mémoire pour les entiers est sur 4 octets, le
compilateur pourrait ajouter 2 octets de bourrage après le tableau de caractères `nom`
(50 octets) et 2 octets de bourrage après l'entier `age` (4 octets) pour s'assurer que la
`moyenne` commence à une adresse qui est un multiple de 4.
Ainsi, la représentation exacte en mémoire dépend des spécificités de la plateforme,
du compilateur et des options de compilation.
1.1.1.2.2 ACCES AUX MEMBRES DE LA STRUCTURE

Lorsque vous accédez aux membres d'une structure en C, le compilateur utilise l'offset
de chaque membre 2par rapport au début de la structure pour accéder directement à
la bonne position en mémoire. Par exemple, pour accéder à la valeur de l'âge dans
une instance de la structure `Etudiant` :
struct Etudiant etudiant;
int valeurAge = etudiant.age;
Le compilateur sait que l'âge est situé à un certain offset par rapport au début de la
structure et génère le code nécessaire pour accéder directement à cette position en
mémoire.

1.1.2 MANIPULATION D’UNE STRUCTURE


La manipulation d'une structure en langage C implique l'accès et la modification des membres
de la structure via diverses opérations telles que l'initialisation, la modification et l'affichage
des valeurs de la structure. Considérons la structure Etudiant créé ci-dessus. La manipuler
revient à effectuer les opérations ci-dessous :

1Les "octets de bourrage" sont des octets supplémentaires ajoutés à une structure de données pour
garantir un alignement correct des membres de la structure en mémoire. Les architectures matérielles
ont souvent des exigences d'alignement pour améliorer l'efficacité des accès aux données. Ainsi, les
compilateurs insèrent des octets de bourrage entre les membres d'une structure afin de s'assurer que
chaque membre commence à une adresse mémoire qui est une multiple de la taille de son type. Bien
que cela puisse augmenter la taille totale de la structure, cela permet d'optimiser les performances
d'accès aux données, en particulier sur des architectures spécifiques.

2 L'offset d'un membre dans une structure représente la distance, en octets, entre le début de la
structure et le début du membre spécifié. C'est l'index ou la position relative d'un membre par rapport
au début de la structure. 'offset est important lors de l'accès aux membres d'une structure, car il permet
au compilateur de calculer où se trouve chaque membre en mémoire par rapport au début de la structure

4
a. INITIALISATION ET AFFICHAGE
Objectif : Créer une variable de la structure et afficher ses valeurs.
Dans la fonction main, nous allons créer une variable de type struct Etudiant nommée
etudiant1 et la déclarer puis l’initialiser avec des valeurs spécifiques, par la suite, accéder aux
membres de la structure accéder aux membres en utilisant l’opérateur (point) et afficher les
informations correspondantes. Nous utiliserons la fonction strcpy pour attribuer une nouvelle
valeur à la chaîne de caractères nom de la structure etudiant2. Avant l'appel à strcpy, la chaîne
de caractères nom de etudiant2 était initialement vide car la structure etudiant2 a été déclarée
sans initialisation explicite. Enfin, les informations de cette structure seront affichées à l'aide
de la fonction printf.
Implémentation sans typedef
#include <stdio.h>
// Définition de la structure
struct Etudiant {
char nom[50];
int age;
float moyenne;
};
int main() {
// Déclaration et initialisation d'une variable de la structure Etudiant
struct Etudiant etudiant1 = {"SANI NGATCHUI", 20, 18.5};
// Affichage des informations
printf("Nom: %s\n", etudiant1.nom);
printf("Age: %d\n", etudiant1.age);
printf("Moyenne: %.2f\n", etudiant1.moyenne);
// Autre manière d'initialiser
struct Etudiant etudiant2;
strcpy(etudiant2.nom, "SILINOU");
etudiant2.age = 20;
etudiant2.moyenne = 17.5;
// Accès aux membres et Affichage des informations de la première structure
printf("Informations de l'étudiant 1 :\n");
printf("Nom: %s\n", etudiant1.nom);
printf("Age: %d\n", etudiant1.age);
printf("Moyenne: %.2f\n\n", etudiant1.moyenne);
// Accès aux membres et Affichage des informations de la deuxième structure
printf("Informations de l'étudiant 2 :\n");
printf("Nom: %s\n", etudiant2.nom);
printf("Age: %d\n", etudiant2.age);
printf("Moyenne: %.2f\n", etudiant2.moyenne);
return 0;
}

Implémentation avec typedef


#include <stdio.h>
#include <string.h> // Inclure la bibliothèque pour utiliser la fonction
strcpy
// Définition de la structure

5
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;

int main() {
// Déclaration et initialisation d'une variable de la structure
Etudiant
Etudiant etudiant1 = {"SANI NGATCHUI", 20, 18.5};
// Affichage des informations
printf("Nom: %s\n", etudiant1.nom);
printf("Age: %d\n", etudiant1.age);
printf("Moyenne: %.2f\n", etudiant1.moyenne);
// Autre manière d'initialiser
Etudiant etudiant2;
strcpy(etudiant2.nom, "SILINOU");
etudiant2.age = 20;
etudiant2.moyenne = 17.5;
// Accès aux membres et Affichage des informations de la première
structure
printf("Informations de l'étudiant 1 :\n");
printf("Nom: %s\n", etudiant1.nom);
printf("Age: %d\n", etudiant1.age);
printf("Moyenne: %.2f\n\n", etudiant1.moyenne);

// Accès aux membres et Affichage des informations de la deuxième


structure
printf("Informations de l'étudiant 2 :\n");
printf("Nom: %s\n", etudiant2.nom);
printf("Age: %d\n", etudiant2.age);
printf("Moyenne: %.2f\n", etudiant2.moyenne);

return 0;
}

Comparaison des deux codes

Si nous voulons afficher les informations de plusieurs étudiants, il faudrait créer un tableau de
structures Etudiant et initialiser chaque élément du tableau avec des données spécifiques.
6
Ensuite, écrire une boucle for qui va parcourir le tableau et afficher les informations pour
chaque étudiant.
#include <stdio.h>
// Définition de la structure
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;

int main() {
// Déclaration et initialisation d'un tableau de structures Etudiant
Etudiant etudiants[3] = {
{"SANI NGATCHUI", 20, 19.7},
{"NJOYA SAMUEL", 22, 19.5},
{"FOMO NDANDA", 21, 19.2}
};
// Affichage des informations pour chaque étudiant
for (int i = 0; i < 3; ++i) {
printf("Étudiant %d:\n", i + 1);
printf("Nom: %s\n", etudiants[i].nom);
printf("Age: %d\n", etudiants[i].age);
printf("Moyenne: %.2f\n", etudiants[i].moyenne);
printf("\n");
}

return 0;
}

b. MODIFICATION DES VALEURS


Objectif : Modifier les valeurs des membres d'une structure après son initialisation.
Dans le code ci-dessous nous allons modifier les valeurs de la structure initiale pour l’étudiant
1 et afficher les informations mises à jour en changeant le nom en "NJOYA SAMUEL", l'âge
est modifié à 21, et la moyenne est mise à jour à 19.5.
#include <stdio.h>
#include <string.h> // inclure la bibliothèque pour utiliser strcpy
// Définition de la structure
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;

int main() {
// Initialisation d'une structure Etudiant
Etudiant etudiant1 = {"SANI NGATCHUI", 20, 18.5};
// Modification des valeurs
strcpy(etudiant1.nom, "NJOYA SAMUEL");
etudiant1.age = 21;
etudiant1.moyenne = 19.5;
// Affichage des informations après modification
printf("Nom: %s\n", etudiant1.nom);

7
printf("Age: %d\n", etudiant1.age);
printf("Moyenne: %.2f\n", etudiant1.moyenne);

return 0;
}
Exercice d'Application : Gestion des Étudiants
Objectif : Écrire un programme qui permet à l'utilisateur de saisir les informations de plusieurs
étudiants, puis affiche ces informations à l’écran.
Instructions :
1. Déclaration de la structure :
- Définissez une structure nommée `Etudiant` avec les membres suivants :
- nom (tableau de caractères de taille 50),
- age (entier),
- moyenne (nombre à virgule flottante).
2. Programme principal :
- Demandez à l'utilisateur de saisir le nombre d'étudiants (n).
- Déclarez un tableau de structures `Etudiant` avec une taille égale à n.
- Utilisez une boucle pour permettre à l'utilisateur de saisir les informations de chaque
étudiant (nom, âge, moyenne).
- Utilisez une autre boucle pour afficher les informations de chaque étudiant après leur saisie.
3. Exemple d'exécution :
- Le programme doit demander à l'utilisateur de saisir le nombre d'étudiants.
- Ensuite, pour chaque étudiant, le programme doit permettre à l'utilisateur de saisir le nom,
l'âge et la moyenne.
- Enfin, le programme doit afficher les informations de tous les étudiants.
Remarques :
- Assurez-vous de gérer correctement les espaces dans les noms des étudiants lors de la
saisie.
- Vous pouvez ajouter des vérifications pour vous assurer que les données saisies sont valides
(par exemple, l'âge ne peut pas être négatif).

Note 1.1 : Nous pouvons écrire la structure dans un autre fichier hors du code main () principal.
Pour l’appeler, vous devez séparer la déclaration de la structure de sa définition. Vous déclarez
la structure dans un fichier d'en-tête (header file), et vous définissez la structure dans un fichier
source (source file). Ensuite, vous incluez le fichier d'en-tête dans le fichier source où vous
souhaitez utiliser la structure.
Fichier d'en-tête (etudiant.h)
#ifndef ETUDIANT_H
#define ETUDIANT_H
// Définition de la structure

8
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;
#endif // ETUDIANT_H

Fichier source (main.c)


#include <stdio.h>
#include <string.h>
#include "etudiant.h" // Inclut le fichier d'en-tête
int main() {
// Déclaration d'une variable de type Etudiant
Etudiant etudiant1;
// Initialisation des membres de la structure
strcpy(etudiant1.nom, "SANI NGATCHUI");
etudiant1.age = 20;
etudiant1.moyenne = 18.5;
// Affichage des informations
printf("Nom: %s\n", etudiant1.nom);
printf("Age: %d\n", etudiant1.age);
printf("Moyenne: %.2f\n", etudiant1.moyenne);
return 0;
}

1.1.3 STRUCTURE ET POINTEURS


Les structures et les pointeurs sont des concepts souvent associés. Une structure est un
moyen de regrouper des variables de types différents sous un même nom. Elle permet de
créer des types de données personnalisés regroupant des éléments apparentés. Un pointeur
est une variable qui contient l'adresse mémoire d'une autre variable. Il permet d'accéder et de
manipuler directement la mémoire, offrant des fonctionnalités avancées telles que la gestion
dynamique de la mémoire.

Les structures sont utilisées pour créer des objets complexes en regroupant plusieurs types
de données sous un seul nom, ce qui facilite la gestion et la manipulation de données
structurées. Les pointeurs sont utilisés pour des tâches avancées, telles que la gestion de la
mémoire dynamique, la manipulation d'adresses mémoire, et la création de structures de
données complexes.
Interaction entre Structures et Pointeurs
▪ Allocation Dynamique : Les pointeurs sont souvent utilisés avec des structures pour
allouer dynamiquement de la mémoire. Par exemple, l'allocation dynamique d'une
structure en C se ferait avec malloc ou calloc.
▪ Accès aux Éléments : Les pointeurs permettent d'accéder aux éléments d'une structure
de manière dynamique en utilisant l'opérateur de flèche (->). Par exemple, si p est un
pointeur vers une structure Point, p->x accède à la variable x de cette structure.

9
▪ Passage par Référence : Les pointeurs peuvent être utilisés pour passer des structures
par référence à des fonctions, permettant des modifications directes sur la structure
d'origine.
a. POINTEURS ET STRUCTURES
Objectif : Utiliser des pointeurs pour accéder et manipuler les membres d'une structure.
En nous servant de notre Structure Etudiant, nous allons utiliser un pointeur pour accéder aux
membres de la structure, ce qui va nous offrir une manière alternative d'interagir avec les
données de la structure. Le code ci-dessous illustre cela.
#include <stdio.h>
// Définition de la structure
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;

int main() {
// Initialisation d'une structure Etudiant
Etudiant etudiant1 = {"SANI NGATCHUI", 20, 18.5};

// Déclaration d'un pointeur vers la structure


Etudiant *ptrEtudiant = &etudiant1;
// Accès aux membres via le pointeur
printf("Nom: %s\n", ptrEtudiant->nom);
printf("Age: %d\n", ptrEtudiant->age);
printf("Moyenne: %.2f\n", ptrEtudiant->moyenne);

return 0;
}

1.1.3.1 ALLOCATION ET DESALLOCATION DE LA MEMOIRE


L'allocation de mémoire est le processus de réservation d'espace mémoire pour les données
nécessaires à l'exécution d'un programme. Elle peut être effectuée de deux manières :

▪ Allocation statique : la mémoire est allouée lors de la compilation du programme. Elle


est libérée automatiquement à la fin du programme.
▪ Allocation dynamique : la mémoire est allouée à l'exécution du programme. Elle doit
être libérée manuellement par le programmeur.
L'allocation et la désallocation de mémoire en langage C, en particulier en ce qui concerne les
structures et les pointeurs, peuvent être réalisées à l'aide des fonctions `malloc`, `calloc`,
`realloc` et `free`.
La désallocation de mémoire est le processus de libération de l'espace mémoire qui n'est plus
nécessaire. Elle est nécessaire pour éviter les fuites de mémoire, qui peuvent entraîner des
problèmes de performance et de stabilité. La désallocation de la mémoire allouée statiquement

10
est effectuée automatiquement par le système d'exploitation. La désallocation de la mémoire
allouée dynamiquement doit être effectuée manuellement par le programmeur.
Exemple : Le code ci-dessous crée dynamiquement un tableau d'entiers de taille 10 en utilisant
la fonction `malloc`, puis remplit ce tableau avec des valeurs correspondant à leurs indices à
l'aide d'une boucle. Enfin, il libère la mémoire allouée dynamiquement à l'aide de la fonction
`free` pour éviter les fuites de mémoire.
// Allocation de mémoire dynamique
int* tableau = (int*)malloc(10 * sizeof(int));

// Utilisation de la mémoire allouée


for (int i = 0; i < 10; i++) {
tableau[i] = i;
}
// Désallocation de la mémoire
free(tableau);
Le tableau ci-dessus permet de présenter les deux concepts.

Allocation Dynamique de Mémoire avec des Structures :


1. Allocation de mémoire pour une structure :

11
// Définition de la structure
typedef struct {
int x;
int y;
} Point;
// Allocation dynamique pour un objet de type Point
Point *dynamicPoint = (Point*)malloc(sizeof(Point));

Ici, `dynamicPoint` est un pointeur vers une structure `Point`, et `malloc` alloue de la mémoire
pour un objet de taille équivalente à celle de la structure `Point`.

2. Utilisation de la mémoire allouée :


if (dynamicPoint != NULL) {
dynamicPoint->x = 5;
dynamicPoint->y = 10;
}
On peut accéder aux membres de la structure via le pointeur `dynamicPoint` comme s'il
s'agissait d'une variable de type `Point`.
3. Désallocation de mémoire :
// Libération de la mémoire allouée
free(dynamicPoint);
Il est crucial de libérer la mémoire allouée avec `free` pour éviter les fuites mémoire.
Allocation Dynamique de Mémoire pour un Tableau de Structures :
1. Allocation de mémoire pour un tableau de structures :
// Allocation dynamique pour un tableau de 5 structures Point
Point *dynamicArray = (Point*)malloc(5 * sizeof(Point));

Cette fois, `dynamicArray` pointe vers le début d'un bloc de mémoire contenant un tableau de
5 structures `Point`.

2. Utilisation du tableau de structures :


if (dynamicArray != NULL) {
dynamicArray[0].x = 1;
dynamicArray[0].y = 2;
// ...
}
On peut accéder aux éléments du tableau de structures de manière similaire à un tableau
statique.
3. Désallocation de mémoire pour un tableau de structures :
// Libération de la mémoire allouée pour le tableau
free(dynamicArray);
Allocation Dynamique de Mémoire avec calloc :

12
La fonction `calloc` est similaire à `malloc`, mais elle initialise la mémoire allouée à zéro. Son
utilisation est la même que `malloc`.
Point *dynamicPoint = (Point*)calloc(1, sizeof(Point));
Allocation Dynamique de Mémoire avec realloc :
La fonction `realloc` permet de redimensionner une zone de mémoire précédemment allouée.
// Redimensionnement du tableau à 10 éléments
Point *resizedArray = (Point*)realloc(dynamicArray, 10 * sizeof(Point));

Remarque importante : Après toute utilisation, il est impératif de libérer la mémoire allouée
avec `free`. Ne pas le faire entraînerait des fuites de mémoire.
free(dynamicPoint);
free(resizedArray);

1.1.3.2 APPLICATION A LA STRUCTURE ETUDIANT


Dans la structure Etudiant que nous manipulons depuis, nous allons écrire un code qui définit
la structure Etudiant. Ensuite, il alloue dynamiquement de la mémoire pour une instance de
cette structure à l'aide de malloc. Après avoir vérifié que l'allocation a réussi, il utilise un
pointeur pour remplir les champs de la structure avec des valeurs spécifiques. Enfin, il affiche
ces informations et libère la mémoire allouée dynamiquement pour éviter les fuites de
mémoire. Si l'allocation échoue, un message d'échec est affiché..
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Définition de la structure Etudiant
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;

int main() {
// Allocation dynamique d'une structure Etudiant avec pointeur
Etudiant *etudiant1 = (Etudiant *)malloc(sizeof(Etudiant));
// Vérification de l'allocation
if (etudiant1 != NULL) {
// Utilisation de la structure via le pointeur
strcpy(etudiant1->nom, "BEYAS");
etudiant1->age = 21;
etudiant1->moyenne = 18.7;
// Affichage des informations
printf("Nom : %s, Age : %d, Moyenne : %.2f\n", etudiant1->nom,
etudiant1->age, etudiant1->moyenne);

// Libération de la mémoire allouée


free(etudiant1);
} else {
printf("Échec de l'allocation de mémoire.\n");

13
}
return 0;
}
Au final, l'allocation et la désallocation de mémoire en langage C avec des structures et des
pointeurs impliquent l'utilisation de `malloc`, `calloc`, `realloc` pour l'allocation, suivi de `free`
pour la libération de la mémoire allouée. Cela offre une gestion dynamique de la mémoire,
particulièrement utile pour des structures dont la taille peut varier pendant l'exécution du
programme. On peut donc dire que, les structures sont utilisées pour organiser des données,
tandis que les pointeurs permettent une manipulation plus avancée de la mémoire, notamment
avec les structures. Ensemble, ils offrent des moyens puissants de créer, organiser et
manipuler des données complexes en programmation.

1.1.4 PORTEE ET DECLARATION

1.1.4.1 PORTEE
Le choix d'utiliser une structure à l'intérieur ou à l'extérieur de la fonction main () dépend
généralement de la portée que vous souhaitez donner à cette structure dans votre programme.
Dans les exemples précédents, nous avons toujours placé notre définition de structure en
dehors de toute fonction. Cependant, sachez que celle-ci peut être circonscrite à un bloc de
sorte de limiter sa portée, comme pour les définitions de variables et les déclarations de
variables et fonctions.
Dans l’exemple ci-dessous, la structure Etudiant ne peut être utilisée que dans le bloc de la
fonction main() et les éventuels sous-blocs qui la composent.
#include <stdio.h>
int main(void) {
typedef struct {
char nom[50];
int age;
float moyenne;
} Etudiant;
Etudiant etudiant;
return 0;
}
Tableau comparatif illustrant les avantages de placer une structure à l'intérieur ou à l'extérieur
de la fonction main() :

14
Notez que le choix entre les deux dépendent du contexte spécifique de votre programme et
des principes de conception que vous souhaitez suivre. Il n'y a pas de solution unique, et la
meilleure approche dépend des besoins et de la structure de votre application.

1.1.4.2 DECLARATIONS
Jusqu’à présent, nous avons parlé de définitions de structures, toutefois, comme pour les
variables et les fonctions, il existe également des déclarations de structures. La déclaration
consiste à spécifier le nom de la structure et à indiquer au compilateur qu'elle existe, mais
sans fournir tous les détails de sa composition. La définition consiste à inclure la spécification
complète des membres de la structure et de leur type. Elle alloue également l'espace mémoire
nécessaire pour chaque instance de la structure.

L'inconvénient majeur d'utiliser des structures sans déclaration préalable est lié à la
déclaration et à l'allocation de la mémoire. En C, lorsque vous déclarez une structure, le
compilateur a besoin de connaître la taille de cette structure pour allouer l'espace mémoire
nécessaire. Si la structure contient des références à elle-même (comme dans le cas des
structures interdépendantes ou avec des membres pointeurs vers elles-mêmes), le
compilateur ne peut pas déterminer la taille de la structure tant que la définition complète de
la structure n'est pas connue.

Cela pose un problème lors de la création d'instances de ces structures ou lors de la


manipulation de ces structures dans votre programme. Si vous essayez de déclarer une
variable de la structure avant sa définition complète, le compilateur ne sait pas combien
d'espace mémoire allouer, ce qui entraîne une erreur de compilation.

15
Pour éviter cela, la plupart des programmeurs C utilisent des déclarations préalables
(prototypes) pour informer le compilateur de l'existence de la structure avant sa définition
complète. Par exemple, vous pourriez déclarer la structure avant la fonction main comme suit:
Une déclaration de structure est en fait une définition sans le corps de la structure.
// Déclaration préalable de la structure (sans la définition complète)
typedef struct Node Node;

int main() {
// Utilisation de la structure récursive
Node node1, node2, node3;
return 0;
}
// Définition complète de la structure
struct Node {
int data;
Node* next; // Membre pointeur vers la même structure
};

L’intérêt de cette déclaration vise à résoudre deux types de problèmes : les structures
interdépendantes3 et les structures comportant un ou des membres qui sont des pointeurs
vers elle-même4.

1.1.5 PRINCIPE DES LISTES CHAINEES


Une liste chaînée est une structure de données dynamique constituée d'éléments appelés
nœuds, où chaque nœud contient des données et un pointeur vers le nœud suivant dans la
séquence. Les listes chaînées permettent de stocker et d'organiser des données de manière
flexible, en allouant de la mémoire au fur et à mesure de l'ajout d'éléments.
Les listes chaînées sont des structures de données fondamentales en informatique, utilisées
pour stocker et organiser des données de manière dynamique. Une liste chaînée est
constituée d'éléments appelés nœuds, et chaque nœud contient des données et une référence
(ou pointeur) vers le nœud suivant dans la séquence. Explorons le principe des listes chaînées
en détail :

1.1.5.1 NŒUD D'UNE LISTE CHAINEE


Un nœud dans une liste chaînée est une structure qui contient deux composants principaux
• Données : Le contenu du nœud, c'est-à-dire l'information que vous souhaitez
stocker.

3Les structures interdépendantes se réfèrent à des situations où une structure contient des membres
qui sont du même type que la structure elle-même. Cela crée une dépendance récursive.

4 Les structures comportant un ou des membres qui sont des pointeurs vers elle-même se
réfèrent à une situation où une structure a un membre qui est un pointeur vers la même structure. La
principale différence avec les structures interdépendantes réside dans le fait que le membre pointeur
n'est pas obligatoirement du même type que la structure elle-même.

16
• Pointeur (ou référence) : Une référence vers le nœud suivant dans la liste.
Voici à quoi ressemble généralement la structure d'un nœud en C :
#include <stdio.h>
// Définition de la structure Node
typedef struct Node {
int data; // Données du nœud
struct Node* next; // Pointeur vers le nœud suivant
} Node;

int main() {
// Utilisation de la structure Node
Node myNode;
myNode.data = 42;
myNode.next = NULL;
// Affichage des données du nœud
printf("Data: %d\n", myNode.data);
printf("Next: %p\n", (void*)myNode.next);
return 0;
}
1.1.5.2 STRUCTURE GLOBALE DE LA LISTE CHAINEE
Une liste chaînée est simplement une séquence de nœuds connectés les uns aux autres. Le
premier nœud s'appelle généralement la tête de liste, et le dernier nœud peut avoir un pointeur
vers `NULL` pour indiquer la fin de la liste. Visuellement, une liste chaînée ressemble à ceci :
H -> [data1|next] -> [data2|next] -> [data3|next] -> ... -> [dataN|NULL]
▪ H : Tête de liste (pointe vers le premier nœud).
▪ data1, data2, data3, ..., dataN : Données stockées dans chaque nœud.
▪ next :Pointeur vers le nœud suivant.

Figure II.1. Schéma de deux listes chainées en C


L'image ci-dessus montre deux exemples de listes chainées : une liste vide et une liste non
vide. La tête de liste est un élément important de la liste chainée. Elle est utilisée pour accéder
au début de la liste et pour parcourir la liste. On pourrait également expliquer que les listes

17
chainées sont des structures de données dynamiques. Cela signifie qu'elles peuvent être
modifiées à la volée, sans avoir à redimensionner la mémoire allouée à la liste.
❖ Liste vide
La liste vide est représentée par un seul nœud. Le nœud a un pointeur vers NULL, ce qui
signifie qu'il n'y a pas d'autre nœud dans la liste.
❖ Liste non vide
La liste non vide est représentée par deux nœuds. Le premier nœud est la tête de la liste. Il
contient la donnée 7777 et un pointeur vers le deuxième nœud. Le deuxième nœud est le
dernier nœud de la liste. Il contient un pointeur vers NULL, ce qui signifie qu'il n'y a pas d'autre
nœud après lui.
❖ Légende
La légende de l'image explique les différents éléments d'une liste chainée.
▪ Tête de liste : le nœud qui pointe vers le début de la liste.
▪ Donnée : la valeur stockée dans un nœud.
▪ Lien (Pointeur) : un pointeur vers le nœud suivant dans la liste.

1.1.5.3 PRINCIPES DE BASE

• Accès Séquentiel : L'accès aux éléments d'une liste chaînée se fait séquentiellement
à partir de la tête. Pour accéder à un élément spécifique, vous devez traverser la liste
depuis la tête jusqu'à cet élément.
• Insertion : Pour insérer un nouvel élément dans une liste chaînée, vous ajustez les
pointeurs dans les nœuds environnants pour inclure le nouveau nœud.
• Suppression : Pour supprimer un nœud, vous ajustez les pointeurs dans les nœuds
environnants pour contourner le nœud à supprimer, puis vous libérez la mémoire du
nœud.

1.1.5.4 EXEMPLE APPROFONDI


Imaginez que vous ayez une liste de choses à faire, telles que vous rendre à l'école, faire vos
devoirs et ranger votre chambre. Vous pouvez écrire cette liste sur un papier ou la mémoriser.
Si vous écrivez la liste sur un papier, chaque élément de la liste est séparé par une virgule.
Par exemple, votre liste pourrait ressembler à ceci :
- Aller à l'école,
- Faire vos devoirs,
- Ranger votre chambre.

Pour trouver un élément de la liste, vous devez parcourir tous les éléments jusqu'à ce que
vous trouviez celui que vous cherchez. Par exemple, pour trouver l'élément "Faire vos devoirs"
dans cet exemple, vous devriez parcourir la liste de la première à la deuxième virgule, car il
s'agit du deuxième élément de la liste.

18
Si vous mémorisez la liste, vous devez vous souvenir de l'ordre de tous les éléments. Pour
trouver un élément de la liste, vous devez simplement vous rappeler de son ordre. Par
exemple, pour trouver l'élément "faire vos devoirs", vous devriez vous rappeler que c'est le
deuxième élément de la liste.
Les listes chaînées sont un moyen de stocker des données en mémoire de manière similaire
à une liste écrite sur un papier. Chaque élément de la liste chaînée est appelé un nœud. Un
nœud contient deux informations :
- La valeur que vous voulez stocker
- Une référence vers le nœud suivant
La référence vers le nœud suivant est un pointeur qui indique l'emplacement du nœud suivant
dans la liste.
Imaginez que vous ayez une liste chaînée de nombres. Le premier nœud pourrait contenir le
nombre 1, le deuxième nœud pourrait contenir le nombre 2, et ainsi de suite. La référence vers
le nœud suivant serait le pointeur vers le nœud suivant dans la liste.
Pour trouver un élément dans une liste chaînée, vous devez commencer par le premier nœud.
Si le nœud contient la valeur que vous cherchez, vous avez trouvé l'élément. Sinon, vous
devez suivre la référence vers le nœud suivant. Vous continuez à suivre les références jusqu'à
ce que vous trouviez l'élément que vous cherchez ou jusqu'à ce que vous arriviez à la fin de
la liste.
CODE DU PROBLEME
// Structure représentant un nœud de la liste chaînée
typedef struct node {
char *value;
struct node *next;
} node;
// Fonction pour créer un nouveau nœud
node *create_node(char *value) {
node *new_node = malloc(sizeof(node));
new_node->value = value;
new_node->next = NULL;
return new_node;
}
// Fonction pour ajouter un nœud à la fin d'une liste chaînée
void append_node(node **head, node *new_node) {
if (*head == NULL) {
*head = new_node;
} else {
node *current_node = *head;
while (current_node->next != NULL) {
current_node = current_node->next;
}
current_node->next = new_node;
}

19
}

// Fonction pour trouver un nœud dans une liste chaînée


node *find_node(node *head, char *value) {
node *current_node = head;
while (current_node != NULL) {
if (strcmp(current_node->value, value) == 0) {
return current_node;
}
current_node = current_node->next;
}
return NULL;
}
// Fonction principale
int main(){
// Création de la liste chaînée
node *head = NULL;
node *node_1 = create_node("Aller à l'école");
node *node_2 = create_node("Faire vos devoirs");
node *node_3 = create_node("Ranger votre chambre");
append_node(&head, node_1);
append_node(&head, node_2);
append_node(&head, node_3);
// Recherche du nœud "Faire vos devoirs"
node *found_node = find_node(head, "Faire vos devoirs");
if (found_node != NULL) {
printf("Le nœud trouvé est : %s\n", found_node->value);
} else {
printf("Le nœud n'a pas été trouvé.\n");
}
return 0;
}
Note : Un pointeur simple, déclaré comme `node *new_node`, est utilisé pour stocker l'adresse
d'un objet ou d'une structure, tandis qu'un pointeur double, noté `**head` dans le contexte
d'une liste chaînée, est un pointeur vers un pointeur. Ce dernier est couramment employé pour
passer l'adresse d'un pointeur en tant qu'argument à une fonction, permettant ainsi de modifier
le pointeur original à l'intérieur de la fonction et de refléter ces changements à l'extérieur. Dans
le cas d'une liste chaînée, `**head` représente un pointeur vers le pointeur de début de la liste,
permettant des modifications dynamiques de la structure de la liste, telles que l'ajout ou la
suppression de nœuds, avec une mise à jour cohérente de la tête de liste.

1.1.5.5 AVANTAGES DES LISTES CHAINEES


▪ Taille Dynamique : Les listes chaînées permettent d'ajouter ou de supprimer des
éléments sans avoir besoin de spécifier la taille à l'avance, contrairement à certains
tableaux.
▪ Insertions et Suppressions Efficaces : Les opérations d'insertion et de suppression
peuvent être plus efficaces qu'avec d'autres structures de données, notamment pour
des opérations au milieu de la liste.

20
1.1.5.6 INCONVENIENTS DES LISTES CHAINEES
▪ Accès Séquentiel : L'accès direct à un élément nécessite une traversée séquentielle
depuis le début de la liste.
▪ Utilisation de la Mémoire : Chaque nœud nécessite un espace supplémentaire pour
stocker le pointeur, ce qui peut entraîner une surcharge mémoire par rapport à un
tableau statique.
Les listes chaînées sont largement utilisées dans de nombreuses applications et offrent une
flexibilité considérable en termes d'ajout et de suppression d'éléments. Elles sont
particulièrement utiles lorsque la taille de la structure de données peut changer fréquemment
pendant l'exécution du programme.

1.2 EXERCICES DE FIN DE CHAPITRE

1.2.1.1 PROBLEME 1 : MANIPULATION DES STRUCTURES EN C


Objectif : Écrire un programme en langage C qui gère une liste d'étudiants. Chaque étudiant
est représenté par une structure comprenant son nom, son âge et sa moyenne. Le programme
doit permettre à l'utilisateur d'ajouter de nouveaux étudiants, d'afficher la liste des étudiants,
et de libérer la mémoire allouée dynamiquement.
1. Déclarez la structure "Etudiant" avec les membres nécessaires.
2. Implémentez une fonction qui ajoute un nouvel étudiant à la liste. Cette fonction devrait
allouer dynamiquement de la mémoire pour le nouvel étudiant.
3. Implémentez une fonction qui affiche la liste des étudiants.
4. Dans la fonction principale (main), utilisez un menu pour permettre à l'utilisateur
d'ajouter des étudiants et d'afficher la liste.
5. N'oubliez pas de libérer la mémoire allouée dynamiquement à la fin du programme.

1.2.1.2 PROBLEME : GESTION D'UNE FILE EN C


Objectif : Écrire un programme en langage C qui simule le fonctionnement d'une file (queue).
La file doit être implémentée à l'aide d'une liste chaînée. Le programme doit permettre à
l'utilisateur d'enfiler et de défiler des éléments de la file.
1. Déclarez la structure "ElementFile" avec les membres nécessaires.
2. Implémentez une fonction qui ajoute un nouvel élément à la file (enfiler).
3. Implémentez une fonction qui retire un élément de la file (défiler).
4. Implémentez une fonction qui affiche le contenu actuel de la file.
5. Dans la fonction principale (main), utilisez un menu pour permettre à l'utilisateur
d'enfiler, de défiler et d'afficher la file.
6. N'oubliez pas de libérer la mémoire allouée dynamiquement à la fin du programme.

21
CHAPITRE 2 LES FICHIERS
L'utilisation et la manipulation de fichiers occupent une place cruciale dans le domaine de la
programmation en langage C. Les fichiers constituent un moyen essentiel de stocker et
d'organiser des données de manière persistante, permettant ainsi aux programmes de
communiquer avec le monde extérieur. Que ce soit pour la lecture de fichiers existants,
l'écriture de nouveaux contenus ou la modification de données déjà enregistrées, la gestion
des fichiers en langage C revêt une importance fondamentale. Cette interaction avec les
fichiers offre aux programmeurs une puissante flexibilité pour créer des applications robustes
et polyvalentes. La bibliothèque standard du langage C propose une panoplie de fonctions
dédiées à la manipulation de fichiers, permettant aux développeurs de concevoir des
programmes capables de lire et d'écrire des informations structurées de manière efficace.

Dans cette exploration du monde des fichiers en langage C, nous plongerons dans les
différentes techniques pour ouvrir, fermer, lire et écrire dans des fichiers. Nous aborderons
également les concepts fondamentaux tels que les modes d'ouverture, les pointeurs de
fichiers, ainsi que la gestion appropriée des erreurs liées à la manipulation des fichiers. En
comprenant ces aspects essentiels, les programmeurs seront mieux équipés pour créer des
applications qui tirent pleinement parti de la gestion de fichiers en langage C.

2.1 DEFINITION
En informatique, un fichier est un ensemble d’informations stockées sur un support, réuni sous
un même nom et manipulé comme une unité.

2.1.1 EXTENSION DE FICHIER

En informatique, une extension de fichier est un suffixe de nom de fichier fait pour identifier
son format. Elle est généralement composée de trois ou quatre caractères, séparés du nom
de fichier par un point. Par exemple, le fichier "exemple.txt" a l'extension ".txt".
Les extensions de fichiers sont utilisées par les systèmes d'exploitation et les applications pour
identifier le type de données contenues dans un fichier. Elles permettent de déterminer quel

22
programme est capable d'ouvrir et d'afficher un fichier, ainsi que l'icône à utiliser pour le
représenter.
Chaque fichier possède une organisation interne déterminée par le format des données qu'il
contient. Ces formats sont divers et spécifiques à chaque type de données, comprenant, par
exemple :
- Audio : Ogg, MP3, MP4, FLAC, Wave, etc. ;
- Vidéo : Ogg, WebM, MP4, AVI, etc. ;
- Documents : ODT, DOC et DOCX, XLS et XLSX, PDF, Postscript, etc.

Afin d'aider l'utilisateur à identifier le contenu d'un fichier, ces formats sont généralement
indiqués par une extension, un suffixe ajouté au nom du fichier. Il est crucial de noter que cette
extension est purement indicative et facultative, n'affectant en rien le contenu du fichier. Son
unique but est d'assister l'utilisateur dans la reconnaissance du type de contenu d'un fichier.

Les extensions de fichiers peuvent être définies par les développeurs de logiciels ou par des
organisations standardisées. Il existe des centaines de milliers d'extensions de fichiers
différentes, chacune correspondant à un type de données spécifique. Elles sont un élément
important de l'organisation des fichiers sur un ordinateur. Elles permettent de faciliter la
recherche et l'ouverture des fichiers, ainsi que de garantir que les fichiers sont ouverts avec le
programme approprié.

2.1.2 SYSTEME DE FICHIERS

En informatique, un système de fichiers est un ensemble de règles et de structures de données


qui organisent les fichiers et les répertoires sur un périphérique de stockage. Il fait référence
à la manière dont les fichiers sont nommés et organisés logiquement à des fins de stockage
et de récupération et permet de localiser et d'accéder aux fichiers, de les créer, de les modifier
et de les supprimer. Un système de fichiers est généralement organisé en une hiérarchie de
répertoires. Un répertoire est un ensemble de fichiers et de sous-répertoires. Le répertoire
racine est le répertoire parent de tous les autres répertoires.

Par exemple, les systèmes d'exploitation (ou OS) DOS, Windows, OS/2, Mac OS et Unix
possèdent tous un système de fichiers, où les fichiers sont enregistrés à des emplacements
précis selon une structure hiérarchique (ou arborescente). Un fichier est placé dans un
répertoire (appelé dossier sous Windows) ou dans un sous-répertoire, à l'emplacement
souhaité dans l'arborescence.
Le système de fichiers définit les conventions de nommage des fichiers, notamment le nombre
maximal de caractères, les caractères autorisés et, dans certains systèmes, la taille du suffixe
du nom de fichier. Il détermine en outre le format permettant de déterminer le chemin d'accès
à un fichier à travers la structure de répertoires.

23
L’exemple de schéma d'arborescence de fichiers ci-dessous montre un diagramme de la
structure d'un dossier nommé "Tom". Le dossier contient quatre sous-dossiers : "Data",
"Thesis", "Notes.txt" et "Tools". Le dossier "Data" contient deux fichiers texte : "One.txt" et
"Two.txt". Le dossier "Tools" contient trois fichiers texte : "Format", "Stats" et "Old".

Figure 2.1. Exemple de schéma d'arborescence de fichiers

2.1.3 HIERARCHIE
• Chaque fichier doit avoir un nom univoque
o éventuellement plusieurs…
• L’espace de nom est structuré en hiérarchie :
o notions de répertoire et de chemin
o notion de répertoire racine (point d’entrée de la hiérarchie)
o notions de chemin relatif et de répertoire courant

2.1.3.1 HIERARCHIE SOUS MS DOS / WINDOWS


Chaque périphérique possède sa propre racine, identifiée par une lettre.

24
Figure 2.2. Exemple de schéma d'arborescence de fichiers

2.1.4 HIERARCHIE SOUS UNIX


On a une hiérarchie unique ; chaque périphérique ancre sa racine dans un répertoire
(montage).

Figure 2.3. Exemple de schéma d'arborescence de fichiers


Les fichiers sont des unités de données qui contiennent des informations. Ils peuvent être de
différents types, tels que des fichiers texte, des fichiers binaires, des fichiers images, des
fichiers audios ou des fichiers vidéo.

Le langage C fournit une bibliothèque standard de fonctions pour accéder aux fichiers. Ces
fonctions permettent de lire, d'écrire, de créer, de modifier et de supprimer des fichiers. Pour
faciliter la localisation et la gestion des fichiers, ceux-ci sont classés et organisés sur leur

25
support conformément à un système de fichiers. Ce système permet à l'utilisateur de structurer
ses fichiers dans une arborescence de dossiers et de les localiser à partir d'un chemin d'accès
spécifique.

2.1.5 LE CHEMIN D'ACCES

Le chemin d'accès d'un fichier est une suite de caractères décrivant sa position dans le
système de fichiers. Il se compose au minimum du nom du fichier visé et au maximum de la
suite de tous les dossiers qu'il est nécessaire de traverser pour l'atteindre depuis le répertoire
racine. Les dossiers sont séparés par un caractère spécifique : "/" sous Unix et "\" sous
Windows.

La racine d'un système de fichier est le point de départ de l'arborescence des fichiers et
dossiers. Sous Unix, c'est le répertoire "/", tandis que sous Windows, chaque lecteur constitue
une racine, comme par exemple "C :". Un chemin d'accès absolu commence par la racine,
permettant d'atteindre le fichier depuis n'importe quelle position dans l'arborescence. Un
chemin d'accès relatif, ne commençant pas par la racine, n'est valide que depuis un point
spécifique dans la hiérarchie des fichiers.

Ainsi, pour accéder à un fichier nommé "texte.txt" dans le dossier "Documents" situé dans le
dossier "utilisateur" à la racine, le chemin d'accès absolu serait /utilisateur/documents/texte.txt
sous Unix et C:\utilisateur\documents\texte.txt sous Windows (supposant qu'il soit sur le
lecteur C:). Cependant, si nous sommes déjà dans le dossier "utilisateur", un chemin relatif
comme documents/texte.txt ou documents\texte.txt serait utilisable. L’explication ci-dessus est
représentée par la démarche ci-dessous.

Implémentation : "exemple.txt" situé dans le dossier "Documents" du répertoire utilisateur sur


les systèmes Windows et Linux :
▪ Sous Windows :
- Chemin absolu : `C:\Users\NomUtilisateur\Documents\exemple.txt`
- Chemin relatif (si le répertoire de travail actuel est `C:\Users\NomUtilisateur\`) :
`Documents\exemple.txt`
▪ Sous Linux :
- Chemin absolu : `/home/NomUtilisateur/Documents/exemple.txt`
- Chemin relatif (si le répertoire de travail actuel est `/home/NomUtilisateur/`) :
`Documents/exemple.txt`

NB : le chemin absolu est spécifié depuis la racine du système, tandis que le chemin relatif
est défini par rapport au répertoire de travail actuel. L'utilisation de l'un ou l'autre dépend du
contexte et des besoins spécifiques de localisation des fichiers ou répertoires dans une
hiérarchie de fichiers.

26
2.1.6 METADONNEES
Les métadonnées sont des données qui décrivent d'autres données. Elles fournissent des
informations supplémentaires sur les données réelles, telles que leur contenu, leur format, leur
provenance, leur utilisation ou leur signification. Elles peuvent être utilisées à diverses fins,
notamment :

▪ Améliorer la découverte et l'accès aux données : les métadonnées peuvent être


utilisées pour indexer les données et les rendre plus faciles à trouver. Par exemple, les
moteurs de recherche utilisent les métadonnées pour indexer les pages Web.
▪ Faciliter la gestion des données : les métadonnées peuvent être utilisées pour
organiser et structurer les données. Par exemple, les bibliothèques utilisent les
métadonnées pour organiser leurs collections de livres et de documents.
▪ Soutenir l'analyse des données : les métadonnées peuvent être utilisées pour enrichir
les données et fournir des informations contextuelles. Par exemple, les analystes de
données peuvent utiliser les métadonnées pour comprendre le contexte dans lequel
les données ont été collectées.

Les métadonnées peuvent être structurées ou non structurées. Les métadonnées structurées
sont organisées dans un format défini, tel que XML ou JSON. Les métadonnées non
structurées ne sont pas organisées dans un format défini, comme le texte libre ou les images.

Exemples de métadonnées

Pour une image : le titre, la description, les mots-clés, la date de prise de vue, le lieu de prise
de vue, le format du fichier.

Pour un document : le titre, l'auteur, le sujet, la date de création, le format du fichier.

Pour une base de données : les tables, les colonnes, les types de données, les contraintes.

Les métadonnées sont un élément essentiel de la gestion des données. Elles peuvent être
utilisées pour améliorer la découverte, l'accès, la gestion et l'analyse des données.

En outre, le système de fichiers conserve généralement des métadonnées pour chaque fichier,
incluant des informations telles que sa taille, ses droits d'accès, la date de dernier accès, la
date de dernière modification, etc. Ces données supplémentaires contribuent à une gestion
plus complète des fichiers.

27
2.1.7 LES FLUX : UN PEU DE THEORIE
La bibliothèque standard fournit différentes fonctions pour manipuler les fichiers, toutes
déclarées dans l’en-tête <stdio.h>. Toutefois, celles-ci manipulent non pas des fichiers,
mais des flux de données en provenance ou à destination de fichiers. Ces flux peuvent être
de deux types :

▪ des flux de textes qui sont des suites de caractères terminées par un caractère de fin
de ligne (\n) et formant ainsi des lignes ;
▪ des flux binaires qui sont des suites de multiplets.

2.1.7.1 POURQUOI UTILISER DES FLUX ?


Les flux de données en programmation C, manipulés à travers la bibliothèque standard et
déclarés dans l'en-tête <stdio.h>, sont essentiels pour la manipulation de fichiers. Ces flux
peuvent être de deux types : les flux de texte, constitués de suites de caractères formant des
lignes, et les flux binaires, constitués de suites de multiplets. L'utilisation de flux plutôt que la
manipulation directe de fichiers est motivée par deux raisons majeures. Premièrement, les
disparités entre les systèmes d'exploitation dans la représentation des fins de ligne nécessitent
une gestion complexe si l'on manipule directement les fichiers. Deuxièmement, les opérations
de lecture et d'écriture impliquent souvent des accès au disque dur, la mémoire la plus lente
de l'ordinateur. Pour pallier cela, la bibliothèque standard utilise la temporisation, soit par blocs,
soit par lignes, minimisant ainsi les accès au disque et améliorant les performances.

La bibliothèque standard fournit également trois flux par défaut lors du démarrage d'un
programme : stdin (entrée standard), stdout (sortie standard) et stderr (sortie d'erreur
standard). Ces flux facilitent les interactions avec l'utilisateur et la gestion des erreurs. Stdin
est utilisé pour récupérer les informations fournies par l'utilisateur, stdout pour transmettre des
informations à l'utilisateur (affichées généralement dans le terminal), et stderr pour transmettre
des messages d'erreurs ou des avertissements, privilégiant une temporisation rapide pour une
transmission efficace des informations d'erreur à l'utilisateur.

2.1.7.2 FICHIER : OUVERTURE ET FERMETURE D'UN FLUX

2.1.7.2.1 LA FONCTION FOPEN


FILE *fopen(char *chemin, char *mode);
La fonction fopen() permet d’ouvrir un flux. Celle-ci attend deux arguments : un chemin d’accès
vers un fichier qui sera associé au flux et un mode qui détermine le type de flux (texte ou
binaire) et la nature des opérations qui seront réalisées sur le fichier via le flux (lecture, écriture
ou les deux). Elle retourne un pointeur vers un flux en cas de succès et un pointeur nul en cas
d’échec.

28
Le mode
Le mode est une chaîne de caractères composée d’une ou plusieurs lettres qui décrit le type
du flux et la nature des opérations qu’il doit réaliser.

Cette chaîne commence obligatoirement par la seconde de ces informations. Il existe six
possibilités reprises dans le tableau ci-dessous.

Mode Type(s) d’opération(s) Effets


r Lecture Néant
r+ Lecture et écriture Néant
w Écriture Si le fichier n’existe pas, il est créé.
Si le fichier existe, son contenu est effacé.
w+ Lecture et écriture Idem
a Écriture Si le fichier n’existe pas, il est créé.
Place les données à la fin du fichier
a+ Lecture et écriture Idem

Par défaut, les flux sont des flux de textes. Pour obtenir un flux binaire, il suffit d’ajouter la lettre
b à la fin de la chaîne décrivant le mode.

EXEMPLE
Le code ci-dessous tente d’ouvrir un fichier nommé « texte.txt » en lecture seule dans le
dossier courant. Notez que dans le cas où il n’existe pas, la fonction fopen() retournera un
pointeur nul (seul le mode r permet de produire ce comportement).

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
FILE *fp = fopen("texte.txt", "r");
if (fp == NULL)
{
printf("Le fichier texte.txt n'a pas pu être ouvert\n");
return EXIT_FAILURE;
}
printf("Le fichier texte.txt existe\n");
return 0;
}

Le code est correct, mais il serait prudent de s'assurer que l’on ferme le fichier après l'avoir
ouvert. D’où intervient la fonction fclose() pour éviter des fuites de mémoire. Cela garantit que
le fichier est correctement fermé avant que le programme ne se termine.

2.1.7.2.2 LA FONCTION FCLOSE


int fclose(FILE *flux);

29
La fonction fclose() termine l’association entre un flux et un fichier. S’il reste des données
temporisées, celles-ci sont écrites. La fonction retourne zéro en cas de succès et EOF en cas
d’erreur.

NB : EOF est une constante définie dans l’en-tête <stdio.h> et est utilisée par les fonctions
déclarées dans ce dernier pour indiquer soit l’arrivée à la fin d’un fichier (nous allons y venir)
soit la survenance d’une erreur. La valeur de cette constante est toujours un entier négatif.
Nous pouvons désormais compléter l’exemple précédent comme suit.
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
FILE *fp = fopen("texte.txt", "r");

if (fp == NULL)
{
printf("Le fichier texte.txt n'a pas pu être ouvert\n");
return EXIT_FAILURE;
}
printf("Le fichier texte.txt existe\n");

if (fclose(fp) == EOF)
{
printf("Erreur lors de la fermeture du flux\n");
return EXIT_FAILURE;
}
return 0;
}

Veillez qu’à chaque appel à la fonction fopen() corresponde un appel à la fonction fclose().

2.1.8 ÉCRITURE VERS UN FLUX DE TEXTE

2.1.8.1 ÉCRIRE UN CARACTERE


int putc(int ch, FILE *flux);
int fputc(int ch, FILE *flux);
int putchar(int ch);

Les fonctions putc() et fputc() écrivent un caractère dans un flux. Il s’agit de l’opération
d’écriture la plus basique sur laquelle reposent toutes les autres fonctions d’écriture. Ces deux
fonctions retournent soit le caractère écrit, soit EOF si une erreur est rencontrée. La fonction
putchar(), quant à elle, est identique aux fonctions putc() et fputc() si ce n’est qu’elle écrit dans
le flux stdout.

30
NB : Techniquement, putc() et fputc() sont identiques, si ce n’est que putc() est en fait le plus
souvent une macrofonction. Étant donné que nous n’avons pas encore vu de quoi il s’agit,
préférez utiliser la fonction fputc() pour l’instant.

L’exemple ci-dessous écrit le caractère « C » dans le fichier « texte.txt ». Étant donné que nous
utilisons le mode w, le fichier est soit créé s’il n’existe pas, soit vidé de son contenu s’il existe
(revoyez le tableau des modes à la section précédente si vous êtes perdus).
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp = fopen("texte.txt", "w");

if (fp == NULL)
{
printf("Le fichier texte.txt n'a pas pu être ouvert\n");
return EXIT_FAILURE;
}
if (fputc('C', fp) == EOF)
{
printf("Erreur lors de l'écriture d'un caractère\n");
return EXIT_FAILURE;
}
if (fclose(fp) == EOF)
{
printf("Erreur lors de la fermeture du flux\n");
return EXIT_FAILURE;
}
return 0;
}

2.1.8.2 ÉCRIRE UNE LIGNE


int fputs(char *ligne, FILE *flux);
int puts(char *ligne);

La fonction fputs() écrit une ligne dans le flux flux. La fonction retourne un nombre positif ou
nul en cas de succès et EOF en cas d’erreurs. La fonction puts() est identique si ce n’est
qu’elle ajoute automatiquement un caractère de fin de ligne et qu’elle écrit sur le flux stdout.

Maintenant que nous savons comment écrire une ligne dans un flux précis, nous allons pouvoir
diriger nos messages d’erreurs vers le flux stderr afin que ceux-ci soient affichés le plus
rapidement possible.

L’exemple suivant écrit le mot « Bonjour » suivi d’un caractère de fin de ligne au sein du fichier
« texte.txt ».

31
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp = fopen("texte.txt", "w");
if (fp == NULL)
{
fputs("Le fichier texte.txt n'a pas pu être ouvert\n", stderr);
return EXIT_FAILURE;
}
if (fputs("Bonjour\n", fp) == EOF)
{
fputs("Erreur lors de l'écriture d'une ligne\n", stderr);
return EXIT_FAILURE;
}
if (fclose(fp) == EOF)
{
fputs("Erreur lors de la fermeture du flux\n", stderr);
return EXIT_FAILURE;
}
return 0;
}
La norme garantit qu’une ligne peut contenir jusqu’à 254 caractères (caractère de fin de ligne
inclus). Aussi, veillez à ne pas écrire de ligne d’une taille supérieure à cette limite.

2.1.8.3 LECTURE DEPUIS UN FLUX DE TEXTE


2.1.8.3.1 RECUPERER UN CARACTERE
int getc(FILE *flux);
int fgetc(FILE *flux);
int getchar(void);

Les fonctions getc() et fgetc() sont les exacts miroirs des fonctions putc() et fputc() : elles
récupèrent un caractère depuis le flux fourni en argument. Il s’agit de l’opération de lecture la
plus basique sur laquelle reposent toutes les autres fonctions de lecture. Ces deux fonctions
retournent soit le caractère lu, soit EOF si la fin de fichier est rencontrée ou si une erreur est
rencontrée. La fonction getchar(), quant à elle, est identique à ces deux fonctions si ce n’est
qu’elle récupère un caractère depuis le flux stdin.

NB : Comme putc(), la fonction getc() est le plus souvent une macrofonction. Utilisez donc
plutôt la fonction fgetc() pour le moment.
L’exemple ci-dessous lit un caractère provenant du fichier texte.txt.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp = fopen("texte.txt", "r");

32
if (fp == NULL)
{
fprintf(stderr, "Le fichier texte.txt n'a pas pu être
ouvert\n");
return EXIT_FAILURE;
}
int ch = fgetc(fp);
if (ch != EOF)
printf("%c\n", ch);
if (fclose(fp) == EOF)
{
fprintf(stderr, "Erreur lors de la fermeture du flux\n");
return EXIT_FAILURE;
}
return 0;
}

2.1.8.4 RECUPERER UNE LIGNE


char *fgets(char *tampon, int taille, FILE *flux);

La fonction fgets() lit une ligne depuis le flux flux et la stocke dans le tableau tampon. Cette
dernière lit au plus un nombre de caractères égal à taille diminué de un afin de laisser la place
pour le caractère nul, qui est automatiquement ajouté. Dans le cas où elle rencontre un
caractère de fin de ligne : celui-ci est conservé au sein du tableau, un caractère nul est ajouté
et la lecture s’arrête.

La fonction retourne l’adresse du tableau tampon en cas de succès et un pointeur nul si la fin
du fichier est atteinte ou si une erreur est survenue.

L’exemple ci-dessous réalise donc la même opération que le code précédent, mais en utilisant
la fonction fgets().

NB : Étant donné que la norme nous garantit qu’une ligne peut contenir jusqu’à 254 caractères
(caractère de fin de ligne inclus), nous utilisons un tableau de 255 caractères pour les contenir
(puisqu’il est nécessaire de prévoir un espace pour le caractère nul).

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
char buf[255];
FILE *fp = fopen("texte.txt", "r");

if (fp == NULL)
{
fprintf(stderr, "Le fichier texte.txt n'a pas pu être
ouvert\n");

33
return EXIT_FAILURE;
}
if (fgets(buf, sizeof buf, fp) != NULL)
printf("%s\n", buf);
if (fclose(fp) == EOF)
{
fprintf(stderr, "Erreur lors de la fermeture du flux\n");
return EXIT_FAILURE;
}
return 0;
}

Toutefois, il y a un petit problème : la fonction fgets() conserve le caractère de fin de ligne


qu’elle rencontre. Dès lors, nous affichons deux retours à la ligne : celui contenu dans la chaîne
buf et celui affiché par printf(). Aussi, il serait préférable d’en supprimer un, de préférence celui
de la chaîne de caractères. Pour ce faire, nous pouvons faire appel à une petite fonction (que
nous appellerons chomp() en référence à la fonction éponyme du langage Perl) qui se
chargera de remplacer le caractère de fin de ligne par un caractère nul.
void chomp(char *s)
{
while (*s != '\n' && *s != '\0')
++s;

*s = '\0';
}

2.1.8.5 LA FONCTION FSCANF


La fonction fscanf() est identique à la fonction scanf() si ce n’est qu’elle récupère les données
depuis le flux fourni en argument (au lieu de stdin pour scanf()).

NB : Le flux stdin étant le plus souvent mémorisé par lignes, ceci vous explique pourquoi nous
lisions les caractères restant après un appel à scanf() jusqu’à rencontrer un caractère de fin
de ligne : pour vider le tampon du flux stdin.

La fonction fscanf() retourne le nombre de conversions réussies (voire zéro, si aucune n’est
demandée ou n’a pu être réalisée) ou EOF si une erreur survient avant qu’une conversion n’ait
eu lieu.

2.1.9 ÉCRITURE VERS UN FLUX BINAIRE


2.1.9.1 ÉCRIRE UN MULTIPLET
int putc(int ch, FILE *flux);
int fputc(int ch, FILE *flux);

34
Comme pour les flux de texte, il vous est possible de recourir aux fonctions putc() et fputc().
Dans le cas d’un flux binaire, ces fonctions écrivent un multiplet (sous la forme d’un int converti
en unsigned char) dans le flux spécifié.

2.1.9.2 ÉCRIRE UNE SUITE DE MULTIPLETS


size_t fwrite(void *ptr, size_t taille, size_t nombre, FILE *flux);

La fonction fwrite() écrit le tableau référencé par ptr composé de nombre éléments de taille
multiplets dans le flux flux. Elle retourne une valeur égale à nombre en cas de succès et une
valeur inférieure en cas d’échec.

L’exemple suivant écrit le contenu du tableau tab dans le fichier binaire.bin. Dans le cas où un
int fait 4 octets, 20 octets seront donc écrits.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int tab[5] = { 1, 2, 3, 4, 5 };
const size_t n = sizeof tab / sizeof tab[0];
FILE *fp = fopen("binaire.bin", "wb");

if (fp == NULL)
{
fprintf(stderr, "Le fichier binaire.bin n'a pas pu être
ouvert\n");
return EXIT_FAILURE;
}
if (fwrite(&tab, sizeof tab[0], n, fp) != n)
{
fprintf(stderr, "Erreur lors de l'écriture du tableau\n");
return EXIT_FAILURE;
}
if (fclose(fp) == EOF)
{
fprintf(stderr, "Erreur lors de la fermeture du flux\n");
return EXIT_FAILURE;
}

return 0;
}

2.1.10 LECTURE DEPUIS UN FLUX BINAIRE


2.1.10.1 LIRE UN MULTIPLET
int getc(FILE *flux);
int fgetc(FILE *flux);

35
Lors de la lecture depuis un flux binaire, les fonctions fgetc() et getc() permettent de récupérer
un multiplet (sous la forme d’un unsigned char converti en int) depuis un flux.

2.1.10.2 LIRE UNE SUITE DE MULTIPLETS


size_t fread(void *ptr, size_t taille, size_t nombre, FILE *flux);

La fonction fread() est l’inverse de la fonction fwrite() : elle lit nombre éléments de taille
multiplets depuis le flux flux et les stocke dans l’objet référencé par ptr. Elle retourne une valeur
égale à nombre en cas de succès ou une valeur inférieure en cas d’échec.

Dans le cas où nous disposons du fichier binaire.bin produit par l’exemple de la section
précédente, nous pouvons reconstituer le tableau tab.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int tab[5] = { 0 };
const size_t n = sizeof tab / sizeof tab[0];
FILE *fp = fopen("binaire.bin", "rb");

if (fp == NULL)
{
fprintf(stderr, "Le fichier binaire.bin n'a pas pu être
ouvert\n");
return EXIT_FAILURE;
}
if (fread(&tab, sizeof tab[0], n, fp) != n)
{
fprintf(stderr, "Erreur lors de la lecture du tableau\n");
return EXIT_FAILURE;
}
if (fclose(fp) == EOF)
{
fprintf(stderr, "Erreur lors de la fermeture du flux\n");
return EXIT_FAILURE;
}

for (unsigned i = 0; i < n; ++i)


printf("tab[%u] = %d\n", i, tab[i]);

return 0;
}
Résultat
tab[0] = 1
tab[1] = 2
tab[2] = 3

36
tab[3] = 4
tab[4] = 5

2.1.11 RESUME
L’extension d’un fichier fait partie de son nom et est purement indicative ;
Les fichiers sont manipulés à l’aide de flux afin de s’abstraire des disparités entre systèmes
d’exploitation et de recourir à la temporisation ;
Trois flux de texte sont disponibles par défaut : stdin, stdout et stderr ;
La fonction fopen() permet d’associer un fichier à un flux, la fonction fclose() met fin à cette
association ;
Les fonctions fputc(), fputs() et fprintf() écrivent des données dans un flux de texte ;
Les fonctions fgetc(), fgets() et fscanf() lisent des données depuis un flux de texte ;
Les fonctions fputc() et fwrite() écrivent un ou plusieurs multiplets dans un flux binaire ;
Les fonctions fgetc() et fread() lisent un ou plusieurs multiplets depuis un flux binaire.

2.2 EXERCICES DE FIN DE CHAPITRE


2.2.1 EXERCICE 1 : OBJECTIF : MANIPULATION DE FICHIERS TEXTE

EN LANGAGE C
1. Écrivez un programme en langage C qui crée un fichier texte nommé "donnees.txt" et y écrit
les entiers de 1 à 10, un entier par ligne.
2. Ensuite, lisez le contenu du fichier "donnees.txt" et affichez les entiers à l'écran.
3. Ajoutez une fonction au programme qui calcule la somme des entiers lus depuis le fichier
et affiche le résultat.

2.2.2 EXERCICE 2 : OBJECTIF : MANIPULATION DE FICHIERS

BINAIRES EN LANGAGE C
1. Créez une structure en langage C appelée "Etudiant" avec les champs suivants : nom
(chaîne de caractères), numéro d'étudiant (entier), et moyenne (nombre à virgule flottante).
2. Écrivez une fonction qui génère un tableau de structures "Etudiant" avec des données
fictives pour trois étudiants.
3. Écrivez ces structures dans un fichier binaire appelé "etudiants.dat".
4. Ensuite, lisez le contenu du fichier binaire "etudiants.dat" et affichez les informations des
étudiants à l'écran.
5. Ajoutez une fonction au programme qui calcule la moyenne générale des étudiants et affiche
le résultat.

37
2.2.3 EXERCICE 3 : GESTION DYNAMIQUE DE LA MEMOIRE
Objectif : Écrire un programme en langage C qui demande à l'utilisateur de saisir la taille d'un
tableau, alloue dynamiquement de la mémoire pour ce tableau, puis demande à l'utilisateur de
remplir le tableau avec des entiers. Enfin, le programme doit afficher la somme des éléments
du tableau et libérer la mémoire allouée.

2.2.4 EXERCICE 4 : PROGRAMME QUI PERMET DE LIRE UN FICHIER

TEXTE
Écrire un programme qui permet de lire un fichier texte et d’en afficher le contenu à l’écran, un
mot par ligne.
EXPLICATIONS
Pour réaliser cet exercice, il faudra utiliser les fonctions fopen(), fgets() et printf().
▪ La fonction fopen() permet d’ouvrir un fichier. Nous allons utiliser le mode r pour lire le
fichier en lecture seule.
▪ La fonction fgets() permet de lire une ligne depuis un fichier. Nous allons utiliser cette
fonction pour lire les mots du fichier un par un.
▪ La fonction printf() permet d’afficher des données à l’écran. Nous allons utiliser cette
fonction pour afficher les mots du fichier, un par ligne.

2.2.5 EXERCICE 2 : PROGRAMME QUI PERMET DE LIRE UN FICHIER

TEXTE ET D’EN COMPTER LE NOMBRE DE MOTS


Écrire un programme qui permet de lire un fichier texte et d’en compter le nombre de mots.

EXPLICATIONS

Pour réaliser cet exercice, il faudra utiliser les fonctions fopen(), fgets() et atoi().
▪ La fonction fopen() permet d’ouvrir un fichier. Nous allons utiliser le mode r pour lire le
fichier en lecture seule.
▪ La fonction fgets() permet de lire une ligne depuis un fichier. Nous allons utiliser cette
fonction pour lire les mots du fichier un par un.
▪ La fonction atoi() permet de convertir une chaîne de caractères en entier. Nous allons
utiliser cette fonction pour convertir chaque mot en entier.

38
CHAPITRE 3 LES LISTES CHAINEES

3.1 INTRODUCTION
Les listes chainées sont des structures de données dynamiques qui permettent de stocker des
données de manière non contiguë en mémoire. Elles sont composées d'éléments, appelés
nœuds, qui sont reliés entre eux par des pointeurs. Elles permettent de stocker et organiser
des données de manière dynamique, facilitant l'ajout, la suppression et la modification
d'éléments sans nécessiter une allocation statique de mémoire. Deux types courants de listes
chaînées sont les listes chaînées simples et les listes chaînées doubles, chacune avec ses
propres caractéristiques et avantages. Ces structures offrent une flexibilité accrue par rapport
aux tableaux statiques, car elles permettent l'insertion et la suppression efficaces d'éléments
sans nécessiter une allocation de mémoire préalable. En C, la mise en œuvre de listes
chaînées se base sur des nœuds qui contiennent les données ainsi que des pointeurs vers
les éléments suivants ou précédents de la liste.

3.2 LISTE CHAINEE SIMPLE (FILE)


Les listes chaînées simples, souvent appelées files, sont constituées de nœuds
successivement liés les uns aux autres. Chaque nœud contient un élément de données et un
pointeur vers le nœud suivant dans la séquence. La file commence par un nœud appelé "tête"
et se termine par un nœud particulier dont le pointeur vers le nœud suivant est nul, indiquant
ainsi la fin de la liste.

La file est une structure de données, qui permet de stocker les données dans l'ordre FIFO
(First In First Out) - en français Premier Entré Premier Sorti). La récupération des données
sera faite dans l'ordre d'insertion.

L'image ci-dessus montre un diagramme d'une file qui permet de stocker des données dans
un ordre FIFO (premier entré, premier sorti). Les données sont stockées dans une liste
chaînée, où chaque élément de la liste contient les données elles-mêmes et un pointeur vers

39
l'élément suivant dans la liste. Le premier élément de la file est appelé le premier entré (First
In). Le dernier élément de la file est appelé le dernier entré (Last In).

Dans une file, les éléments sont placés les uns à côtés des autres on sort les éléments du plus
ancien vers le plus récent. Cela correspond à ce qui se passe dans une file d'attente : si on
prend l'exemple d’un garagiste, il reçoit les clients le matin et réceptionne leurs fiches dans
une file d'attente. Pour effectuer les travaux sur le véhicule, le garagiste va récupérer les fiches
en commençant par le premier client arrivé, c'est à dire la première fiche entrée dans la file.
C'est la loi du premier arrivé, premier servi ou FIFO (First In First out). C'est le comportement
classique d'une file d'attente.

3.2.1 LA CONSTRUCTION DU PROTOTYPE D'UN ELEMENT DE LA

FILE
Pour définir un élément de la file, le type struct sera utilisé, comprenant un champ "donnee" et
un pointeur "suivant". Il est essentiel que le pointeur suivant soit du même type que l'élément
lui-même, garantissant ainsi sa capacité à pointer vers l'élément. Ce mécanisme de pointage
via le pointeur suivant facilitera l'accès au prochain élément de la file.
typedef struct ElementListe {
char *donnee;
struct ElementListe *suivant;
}Element;
Pour assurer le contrôle de la file, il est recommandé de sauvegarder certains éléments clés
tels que le premier élément, le dernier élément et le nombre total d'éléments. Pour mettre en
œuvre cette gestion, une autre structure sera utilisée, bien que l'utilisation de variables ne soit
pas exclue. La composition de cette structure est la suivante :
typedef struct ListeRepere{
Element *debut;
Element *fin;
int taille;
} File;

3.2.2 OPERATIONS SUR LES FILES

3.2.2.1 INITIALISATION
La fonction initialisation initialise les pointeurs debut et fin avec NULL ainsi que la taille avec
la valeur 0. Cela doit être effectué avant toute autre opération sur la file.
void initialisation (File * suite){
suite->debut = NULL;
suite->fin = NULL;
suite->taille = 0;
}

40
3.2.2.2 INSERTION D'UN ELEMENT DANS LA FILE
L'algorithme d'insertion implique la déclaration d'éléments à insérer, l'allocation de mémoire
pour le nouvel élément, le remplissage des données, la mise à jour des pointeurs debut et fin,
et l'ajustement de la taille de la file.
Etapes
▪ Déclaration d'élément(s) à insérer
▪ Allocation de la mémoire pour le nouvel élément
▪ Remplir le contenu du champ de données
▪ Mettre à jour le pointeur debut vers le 1er élément (le début de file)
▪ Mettre à jour le pointeur fin (ça nous servira pour l'insertion vers la fin de la file)
▪ Mettre à jour la taille de la file
La 1ère image affiche le début de l'insertion, donc la liste a la taille 1 après l'insertion.

Dans la file, l'élément à récupérer c'est le 1er entré. Pour cela, l'insertion se fera toujours à la
fin de la file. Il s'agit de l'ordre normal de l'insertion (1er, 2ème, 3ème ...... etc.).

41
LA FONCTION
Le code ci-dessous représente une fonction "inserer" qui prend en paramètres une file (File) représentée
par une structure, un élément courant (courant) et une donnée (donnee) à insérer dans la file. La
fonction crée un nouveau élément (nouveau_element) en allouant dynamiquement de la mémoire pour
la structure de l'élément ainsi que pour la donnée à stocker. Ensuite, la donnée est copiée dans le
nouveau élément. En fonction de la position du courant (tête de la file ou un élément existant), le
nouveau élément est inséré à la position appropriée dans la file, et les liens sont mis à jour en
conséquence. Enfin, la taille de la file est mise à jour, et la fonction renvoie 0 en cas de succès ou -1 en
cas d'échec (manque de mémoire).
int inserer(File *suite, Element *courant, char *donnee) {
// Création d'un nouveau_element et allocation mémoire
Element *nouveau_element;
if ((nouveau_element = (Element *)malloc(sizeof(Element))) == NULL)
return -1;
// Allocation mémoire pour la donnée
if ((nouveau_element->donnee = (char *)malloc(50 * sizeof(char))) ==
NULL) {
free(nouveau_element); // Libération de la mémoire en cas d'échec
return -1;
}
// Copie de la donnée dans le nouveau_element
strcpy(nouveau_element->donnee, donnee);

// Ajout du nouveau_element à la file


if (courant == NULL) {
// Cas où la file est vide
if (suite->taille == 0)
suite->fin = nouveau_element;
// Mise à jour des liens pour ajouter le nouveau_element en tête

42
nouveau_element->suivant = suite->debut;
suite->debut = nouveau_element;
} else {
// Cas où la file n'est pas vide
if (courant->suivant == NULL)
suite->fin = nouveau_element;
// Mise à jour des liens pour insérer le nouveau_element après le
courant
nouveau_element->suivant = courant->suivant;
courant->suivant = nouveau_element;
}
// Mise à jour de la taille de la file
suite->taille++;
return 0;
}
IMPLEMENTATION DE LA FONCTION DANS LE MAIN()

Dans l’exemple ci-dessous, la fonction main initialise une file, ajoute quelques éléments à
l'aide de la fonction enfiler, puis affiche le contenu de la file. Enfin, elle libère la mémoire
allouée pour les éléments de la file. Vous pouvez adapter ce code en fonction de vos besoins
spécifiques.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Structure pour un élément de la file
typedef struct Element {
char *donnee;
struct Element *suivant;
} Element;
// Structure pour la file
typedef struct File {
Element *debut;
Element *fin;
int taille;
} File;
// Prototype de la fonction d'insertion
int inserer(File *suite, Element *courant, char *donnee);
int main() {
// Initialisation de la file
File maFile;
maFile.debut = NULL;
maFile.fin = NULL;
maFile.taille = 0;
// Exemple d'utilisation de la fonction enfiler
char donnee1[] = "Premier element";
char donnee2[] = "Deuxieme element";
char donnee3[] = "Troisieme element";
inserer(&maFile, NULL, donnee1);
inserer (&maFile, maFile.debut, donnee2);
inserer (&maFile, maFile.fin, donnee3);
// Affichage des éléments de la file
Element *actuel = maFile.debut;
printf("Contenu de la file : ");
while (actuel != NULL) {
printf("%s -> ", actuel->donnee);

43
actuel = actuel->suivant;
}
printf("NULL\n");
// Libération de la mémoire allouée pour les éléments
actuel = maFile.debut;
while (actuel != NULL) {
Element *temp = actuel;
actuel = actuel->suivant;
free(temp->donnee);
free(temp);
}
return 0;
}

3.2.2.3 RETIRER UN ELEMENT DE LA FILE


La fonction "de_filer" supprime l'élément vers lequel pointe le pointeur début, décrémente la
taille de la file, et libère la mémoire associée à cet élément. La fonction renvoie -1 en cas
d'échec et 0 sinon.
ETAPES :
▪ Le pointeur supp_elem contiendra l'adresse du 1er élément
▪ Le pointeur debut pointera vers le 2ème élément (après la suppression du 1er
élément, le 2ème sera au début de la file)
▪ La taille de la file sera décrémentée d'un élément

LA FONCTION

Le code suivant représente une fonction "retirer" qui prend en paramètre une file (File)
représentée par une structure. La fonction vise à retirer l'élément en tête de la file. Tout
d'abord, elle vérifie si la file n'est pas vide (taille égale à zéro), renvoyant -1 en cas contraire.

44
Ensuite, un pointeur "supp_element" est utilisé pour pointer vers l'élément en tête de la file.
Les liens de la file sont mis à jour pour pointer vers l'élément suivant comme nouveau début
de file. Ensuite, la mémoire allouée pour la donnée de l'élément et pour l'élément lui-même est
libérée à l'aide de la fonction "free". Enfin, la taille de la file est réduite et la fonction renvoie 0
pour indiquer le succès de l'opération.
int retirer (File * suite){
Element *supp_element;
if (suite->taille == 0)
return -1;
supp_element = suite->debut;
suite->debut = suite->debut->suivant;
free (supp_element->donnee);
free (supp_element);
suite->taille--;
return 0;
}
IMPLEMENTATION DE LA FONCTION DANS LE MAIN()
Dans ce code, le programme principal initialise une file vide et tente de retirer un élément, affichant un
message indiquant le succès ou l'échec de l’opération (La fonction "retirer" retire l'élément en tête de la
file.).
#include <stdio.h>
#include <stdlib.h>
// Structure de l'élément de la file
typedef struct Element {
// Données de l'élément
int donnee;
// Pointeur vers l'élément suivant dans la file
struct Element *suivant;
} Element;
// Structure de la file
typedef struct File {
// Pointeur vers le début de la file
Element *debut;
// Pointeur vers la fin de la file
Element *fin;
// Taille de la file (nombre d'éléments)
int taille;
} File;
// Déclaration de la fonction retirer
int retirer(File *suite);

int main() {
// Initialisation de la file
File maFile;
maFile.debut = NULL;
maFile.fin = NULL;
maFile.taille = 0;
// Exemple d'utilisation de la fonction retirer
int resultat = retirer(&maFile);
if (resultat == 0) {
printf("Retrait réussi.\n");
} else {

45
printf("La file est vide, impossible de retirer.\n");
}
return 0;
}

3.2.2.4 AFFICHAGE DE LA FILE


Pour afficher la file entière, il faut se positionner au début de la file (le pointeur debut le
permettra). Ensuite en utilisant le pointeur suivant de chaque élément, la file est parcourue du
1er vers le dernier élément. La condition d'arrêt est donnée par la taille de la file. La fonction
"affiche" parcourt la file du premier au dernier élément en utilisant les pointeurs suivants.
LA FONCTION

La procédure `affiche` ci-dessous prend en entrée un pointeur vers une structure de file (`File`)
et parcourt ses éléments à l'aide d'une boucle `for`. Pour chaque élément, elle affiche la
donnée associée (supposée être une chaîne de caractères) en utilisant la fonction `printf`. La
fonction utilise un pointeur `courant` pour traverser la file, en partant du début, et se déplace
à l'élément suivant à chaque itération. Ainsi, l'affichage résultant est une ligne de texte
montrant les données de chaque élément de la file, dans l'ordre où ils sont stockés, séparés
par des espaces.
void affiche(File *suite){
Element *courant;
int i;
courant = suite->debut;

for(i=0;i<suite->taille;++i){
printf(" %s ", courant->donnee);
courant = courant->suivant;
}
}
IMPLEMENTATION DE LA PROCEDURE DANS LE MAIN()
Le code suivant définit une structure de données de file (`File`) implémentée comme une liste chaînée
de structures élémentaires (`Element`). Il crée une file (`suite`) dans la fonction `main`, ajoute deux
éléments à la file avec des données spécifiques, puis affiche le contenu de la file à l'aide de la fonction
`affiche`.
#include <stdio.h>

typedef struct Element {


char *donnee;
struct Element *suivant;
} Element;

typedef struct File {


Element *debut;
int taille;
} File;

void affiche(File *suite) {


Element *courant;

46
int i;
courant = suite->debut;

for (i = 0; i < suite->taille; ++i) {


printf(" %s ", courant->donnee);
courant = courant->suivant;
}
}
int main() {
File suite;
suite.debut = NULL;
suite.taille = 0;
// Ajout d'éléments à la suite
suite.debut = malloc(sizeof(Element));
suite.debut->donnee = "élément 1";
suite.debut->suivant = NULL;
suite.taille++;
suite.debut->suivant = malloc(sizeof(Element));
suite.debut->suivant->donnee = "élément 2";
suite.debut->suivant->suivant = NULL;
suite.taille++;
// Affichage de la suite
affiche(&suite);
return 0;
}
En C, la création d'une liste chaînée simple implique la définition d'une structure de nœud et la
manipulation des pointeurs pour gérer les liaisons entre les éléments. Les opérations courantes sur les
listes chaînées simples incluent l'insertion en tête, l'insertion en fin de liste, la suppression d'un élément
spécifique et la traversée de la liste.

3.3 LISTE CHAINEE SIMPLE (PILE)


Les listes chaînées simples, souvent utilisées pour implémenter des piles, sont des structures
de données linéaires qui stockent des éléments de manière séquentielle. Une liste chaînée se
compose d'éléments appelés "nœuds", où chaque nœud contient une valeur et une référence
(ou un pointeur) vers le nœud suivant dans la séquence. Les piles, qui suivent le principe
"dernier entré, premier sorti" (LIFO - Last In, First Out), peuvent être implémentées
efficacement à l'aide de listes chaînées.

Une pile est une structure de données qui permet de stocker des éléments de manière
ordonnée. Les éléments sont ajoutés et supprimés au sommet de la pile, ce qui signifie que le
dernier élément ajouté est le premier élément à être supprimé. Par analogie, elle peut être
représentée par une pile d'assiettes. Les assiettes sont ajoutées au sommet de la pile, et la
première assiette à être retirée est la dernière à avoir été ajoutée.

Le schéma ci-dessous illustre une pile composée de trois éléments : "Donnée 1", "Donnée 2",
"Donnée 3". "Donnée 1" occupe la position supérieure de la pile, tandis que "Donnée 3" se
trouve à la base. Chaque élément est représenté par un rectangle, relié aux autres par des

47
lignes pour indiquer leurs relations(pointeurs). Pour ajouter un élément, il suffit de le placer au
sommet, et pour le supprimer, il doit être retiré de la base de la pile.

3.3.1.1 LA CONSTRUCTION DU PROTOTYPE D'UN ELEMENT DE LA PILE


La mise en place du prototype d'un élément de la pile implique l'utilisation du type struct.
Chaque élément de la pile comprendra un champ "donnee" et un pointeur "suivant". Il est
essentiel que le type du pointeur suivant soit le même que celui de l'élément pour garantir un
pointage correct. Ce pointeur "suivant" facilitera l'accès au prochain élément. La définition de
cette structure est réalisée avec le typedef de la struct ElementListe.
typedef struct ElementListe {
char *donnee;
struct ElementListe *suivant;
}Element;
Pour effectuer des opérations sur la pile, des informations clés seront sauvegardées,
notamment le premier élément et le nombre total d'éléments. Le premier élément, situé en
haut de la pile, permettra l'opération de récupération des données en haut de la pile. Une autre
structure, ListeRepere, sera utilisée pour cette fin, composée d'un pointeur "debut" contenant
l'adresse du premier élément et d'une variable "taille" indiquant le nombre total d'éléments.
typedef struct ListeRepere{
Element *debut;
int taille;
} Pile;
Notons que contrairement aux listes simplement chaînées, nous n'utilisons pas de pointeur
"fin" ici, car le pointeur "debut" suffit pour travailler uniquement au début de la liste. Peu importe
la position dans la liste, le pointeur "debut" pointe toujours vers le premier élément en haut de

48
la pile, et le champ "taille" indique le nombre d'éléments, quelle que soit l'opération effectuée
sur la pile.

3.3.2 OPERATIONS SUR LES PILES

49

Vous aimerez peut-être aussi