10. Fichiers
Pour les systèmes d'exploitation des familles Unix-Linux, un fichier est généralement considéré comme un ensemble de bytes accédés séquentiellement. Dans la documentation, il est fréquemment question de stream (flot ou flux en français) lorsque l'on parle de ces fichiers.
Le présent chapitre traite de la manipulation des fichiers par des pointeurs sur les structures FILE et des fonctions standards qui y sont associées. L'utilisation des appels système et des file descriptors sera abordée dans le cours de Unix.
Nous verrons que le monde Unix-Linux reconnaît 2 types de fichiers : les fichiers constitués uniquement de caractères imprimables (fichiers text) et les autres (fichiers binaires).
Descripteur de fichier
Les applications C gèrent les fichiers en stockant les informations utiles (nom, count pointer, attributs, etc.) dans une structure FILE. Pour chaque application, ces structures sont stockées dans une table, où une structure peut être accédée soit par son indice, appelé file descriptor, soit par un pointeur. Les 3 premiers éléments de la table sont réservés aux flots de données standard (cf. point suivant).
Lorsqu'il est nécessaire d'utiliser un fichier particulier, la première étape est d'ouvrir le fichier pour créer et remplir une structure FILE et pour stocker celle-ci dans la table des descripteurs de fichiers. Lorsqu'on accède à la structure au moyen de son file descriptor, les opérations de lecture, écriture, etc. sont réalisées au travers des appels système propres à l'OS. Mais il est également possible de manipuler les fichiers grâce aux fonctions standards, dont les prototypes se trouvent dans le header stdio.h. Ces fonctions référencent un fichier grâce à un pointeur sur une structure FILE.
Les flots standard
Trois flots standard peuvent être utilisés en C sans qu'il soit nécessaire de les ouvrir ou de les fermer:
-
stdin (standard input): unité d'entrée standard (par défaut, le clavier) ;
-
stdout (standard output): unité de sortie standard (par défaut, l'écran) ;
-
stderr (standard error): unité d'affichage des messages d'erreur (par défaut, l'écran).
Notez qu'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.
Les flots de données standard sont de type FILE*. Ils occupent les trois premières places de la table des file descriptors: à l'indice 0 se trouve la structure reprenant les informations du fichier stdin (entrée standard = clavier), à l'indice 1 du fichier stdout (sortie standard = écran) et à l'indice 2 stderr (sortie des messages d'erreur = écran).
Ouverture - Fermeture
La première étape lorsqu'on désire utiliser des fichiers dans une application est l'ouverture du fichier. En C, la fonction qui réalise cette action est la fonction fopen().
Cette fonction nécessite deux paramètres: le premier, de type char*, est le nom du fichier dans le "file system" et le second, également un char*, permet de spécifier le mode d'ouverture, i.e. la façon d'exploiter le fichier. Les principaux modes sont :
-
"r" → ouverture en lecture (read) ; si le fichier n'existe pas, il y a erreur
-
"w" → ouverture en écriture (write) ; si le fichier existe, il est écrasé
-
"a" → ouverture en ajout (append) ; si le fichier n'existe pas, il est créé, si le fichier existe, il est continué
S'agissant des fichiers binaires, les principaux modes d'ouverture sont :
-
"rb" → ouverture en lecture binaire (read binary) ; si le fichier n'existe pas, il y a erreur
-
"wb" → ouverture en écriture binaire (write binary) ; si le fichier existe, il est écrasé
-
"ab" → ouverture en ajout binaire (append binary) ; si le fichier n'existe pas, il est créé, si le fichier existe, il est continué
En cas de réussite, la fonction retourne un pointeur sur une structure FILE. C'est ce pointeur qu'il faut utiliser dans les fonctions de traitements du fichier. Si un problème empêche l'ouverture du fichier, la fonction renverra un pointeur NULL et positionnera la variable système errno à une valeur non nulle, représentant le type d'erreur détectée (cf. traitement des erreurs).
FILE *fin = NULL;if ((fin = fopen(nom, "r")) == NULL) {perror("Problème lors de l'ouverture du fichier fin");return 1;}
Lorsque le fichier n'est plus utilisé dans l'application, il est préférable de le fermer pour libérer les ressources. La fermeture d'un fichier se fait à l'aide de la fonction fclose():
fclose(fin);
Les fichiers text
Un fichier text est un fichier constitué uniquement de caractères imprimables, où chaque ligne est terminée par le caractère '\n'. Il est possible de le traiter caractère par caractère grâce aux macros ou fonctions de lecture getchar(), fgetc(), getc() et d'écriture putchar(), fputc(), putc(), ou ligne par ligne grâce à la fonction de lecture fgets() et la fonction d'écriture fputs(),
Lecture
La lecture est bufferisée par le système, càd. que le système lit un bloc de bytes en une seule opération et fournit les bytes demandés par la fonction de lecture en les extrayant du buffer. Il peut fournir un caractère, une ligne ou une partie de ligne.
- int fgetc (FILE * stream)
- Fonction qui lit un caractère du fichier stream et le retourne ; elle renvoie la valeur EOF (-1) si la fin de fichier est rencontrée (bien que le caractère renvoyé soit de type unsigned char, il est casté en int à cause de cette valeur négative).
- int getc (FILE * stream)
- Macro (ou fonction) qui lit un caractère du fichier stream et le retourne ; elle renvoie la valeur EOF (-1) si la fin de fichier est rencontrée (le type int est nécessaire à cause de cette valeur négative).
- int getchar ()
- Macro (ou fonction) qui lit un caractère du fichier sdtin et le retourne ; elle renvoie la valeur EOF (-1) si la fin de fichier est rencontrée (le type int est nécessaire à cause de cette valeur négative).
- int ungetc (int c, FILE * stream)
- Replace le caractère lu dans le buffer du fichier stream.
- int fgets (char* s, int n, FILE * stream)
-
Lit une (partie de) ligne du fichier stream et renvoie le pointeur NULL si la fin de fichier est rencontrée. Le 1er paramètre est l'adresse
d'une zone mémoire (par exemple un tableau de caractères) dont la longueur est suffisante pour stocker n caractères lus sur stream
(en ce compris le caractère '\0' marquant la fin de la chaîne). Par exemple,
lit une ligne du fichier fin. Si la taille de la ligne en cours de lecture est supérieure à n, la fonction ne lira pas l'entièreté de la ligne mais se limitera à n-1 caractères (dans ce cas, le '\0' sera stocké dans le nième caractère du buffer).char ligne[256];while (fgets(ligne, 256, fin)) {...}
Ecriture
En général, les écritures sont bufferisées pour réduire le nombre d'accès disques et donc augmenter les performances du système car ces accès sont très lents. Il est possible de forcer la vidange du buffer grâce à la fonction fflush().
- int fputc (int c, FILE * stream)
- Fonction qui écrit un caractère dans le fichier stream.
- int putc (int c, FILE * stream)
- Macro (ou fonction) qui écrit un caractère dans le fichier stream.
- int putchar (int c)
- Macro (ou fonction) qui écrit un caractère dans le fichier sdtout.
- int puts (char *s)
- Fonctions qui écrivent la chaîne de caractères s dans le fichier stream (fputs) ou stdout (puts).
- int fprintf (FILE * stream, const char * format, ...)
- Ecriture formatée dans le fichier stream.
printf
L'écriture formatée permet de convertir des valeurs en une chaîne de caractères avec beaucoup de souplesse. Trois fonctions utilisent l'écriture formatée :
- int fprintf (FILE *stream, const char * format, ...)
- L'output de la fonction est écrit sur le fichier stream
- int printf (const char * format, ...)
- L'output de la fonction est écrit sur le fichier stdout
- int sprintf (char *str, const char * format, ...)
- L'output de la fonction est écrit dans le buffer de caractères pointé par str
Le format permet de préciser comment l'output doit être formaté. Tout caractère est repris tel quel dans l'output sauf en présence du caractère '%' qui précise comment doit être interprété le contenu du paramètre correspondant dans la liste spécifiée après le format. Le format général d'un parmètre est de la forme %cn.ptl où
- c
- Le flag est un des caractères: 0 (padding à gauche par des zéros '0'), - (justification à gauche ; par défaut elle se fait à droite), + (toujours afficher le signe d'un nombre), ' ' (mettre un espace avant un nombre positif), # (employer une variable de formatage).
- n
- Taille minimale de la zone écrite ('*' si la taille est spécifiée dans un paramètre).
- p
- Indique la précision qui est fonction du type de conversion ('*' si la taille est spécifiée dans un paramètre).
- t
- Modificateur de taille ('h' short, 'l' long ou 'L' long double).
- l
- Indique la conversion ('c', 'd', 'e', 'f', 'g', 'G', 'i', 'n', 'o', 'p','s', 'u', 's', 'x' ou 'X').
Voici les interprétations des conversions :
| Type de conversion | Signification | Flags permis | Modificateurs de tailles possibles | Type des arguments | Précision par défaut |
|---|---|---|---|---|---|
| d i | décimal | '0' '-' '+' ' ' | [rien] | int | 1 |
| h | short | ||||
| l | long | ||||
| u | non signé | '0' '-' '+' ' ' | [rien] | unsigned | 1 |
| h | unsigned short | ||||
| l | unsigned long | ||||
| o | octal | '0' '-' '+' ' ' | [rien] | unsigned | 1 |
| h | unsigned short | ||||
| l | unsigned long | ||||
| x X | hexa | '0' '-' '+' ' ' | [rien] | unsigned | 1 |
| h | unsigned short | ||||
| l | unsigned long | ||||
| f | réel normal | '0' '-' '+' ' ' | [rien] 'l' | double | 6 |
| h | unsigned short | ||||
| e E | scientifique | '0' '-' '+' ' ' | [rien] 'l' | double | 6 |
| h | unsigned short | ||||
| g G | le plus court | '0' '-' '+' ' ' | [rien] 'l' | double | 6 |
| h | unsigned short | ||||
| c | caractère | '-' | [rien] | int | 1 |
| s | chaîne | '-' | [rien] | char * | ? |
| p | pointeur | [rien] | void * | 1 | |
| n | pointeur | [rien] | int * | n/a | |
| h | short * | ||||
| l | long * | ||||
| % | % | [rien] | [rien] | % |
Quelques remarques concernant ce tableau :
-
La taille minimale ne peut pas commencer par '0' pour ne pas confondre avec le flag '0'
-
La taille minimale indique le nombre minimal de caractères écrits:
- si la taille est trop grande par rapport à la valeur, il y a remplissage (padding) avec ' ' ou '0'
- si la taille est trop petite, la zone est étendue
-
La précision a une signification adaptée à la conversion :
- - pour les conversions entières (d i o u x X)
- = nombre minimum de chiffres
- - pour les conversions de nombres réels (e E f)
- = nombre de chiffres décimaux (à droite du .)
- - pour les conversions de nombres réels (g G)
- = nombre de chiffres significatifs
- - pour les conversions de chaînes
- = nombre maximum de caractères à écrire (donc troncage éventuel)
-
Si le '.' est indiqué sans précision, celle-ci est considérée comme 0
Les fichiers binaires
Outre les fichiers text, il existe un second type de fichiers: les fichiers binaires. Ces fichiers sont très pratiques puisqu'ils permettent de charger ou de sauver différents types de données définis en C (par exemple, une bibliothèque définie comme un tableau de struct Livre peut être sauvée dans un fichier binaire pour être par la suite directement chargée depuis ce fichier).
Les fichiers binaires sont lus et écrits à l'aide de deux fonctions qui traitent un nombre précis de bytes:
Lecture
La fonction de lecture: size_T fread (void *base, size_t size, size_t nmemb, FILE * stream)
lit (size * nmemb) bytes dans le fichier stream et les place en mémoire à l'adresse base. La zone pointée par base doit être suffisamment grande. Si nmemb est différent de 1, base sera l'adresse d'un tableau.
La fonction fread renvoie le nombre d'enregistrements lus. Si ce nombre est inférieur à nmemb, cela signifie que la fin du fichier est atteinte. Si la fonction renvoie la valeur 0, cela indique un problème de lecture (à tester grâce à la fonction ferror) ou la fin du fichier (à vérifier grâce à la fonction feof).
Ecriture
La fonction d'écriture: size_T fwrite (void *base, size_t size, size_t nmemb, FILE * stream)
écrit (size * nmemb) bytes dans le fichier stream à partir de la zone mémoire située à l'adresse base. Si nmemb est différent de 1, base sera l'adresse d'un tableau d'éléments de taille size.
La fonction fwrite renvoie le nombre d'enregistrements écrits. Si la fonction renvoie une valeur différente de nmemb, cela indique un problème d'écriture (à tester grâce à la fonction ferror).
Notez que des problèmes de portabilité peuvent apparaître si vous lisez un fichier binaire créé sur une autre machine. Ces problèmes sont dûs aux éventuelles différences d'architecture entre les deux machines. L'architecture détermine en effet la taille des types (par exemple 2 ou 4 bytes pour un int), l'ordre de codage utilisé (boutisme ou endianness en anglais) et la disposition des champs d'une structure en mémoire (cf. padding). Bien entendu, si vous travaillez sur une seule machine, tous ces problèmes ne se poseront pas.
D'autres fonctions liées aux fichiers
La librairie standard (stdio.h) comprend quelques autres fonctions utilisées dans le traitement des erreurs, le positionnement dans le fichier, certains traitements système, ainsi que la fonction fflush() pour vider le buffer d'écriture.
La fonction fflush
La fonction fflush (FILE * stream) permet de vider le buffer d'écriture (i.e. transférer son contenu sur le disque), même si celui-ci n'est pas complètement rempli. En effet, les écritures sont bufferisées par le système, càd. que l'écriture physique sur le disque n'est réalisée que lorsque l'une des conditions suivantes est remplie:
-
le buffer est plein ;
-
le caractère '\n' est rencontré dans le cas des fonctions d'écriture dans des fichiers de type text ;
-
la fonction fflush est appelée: elle permet de forcer la vidange du buffer d'écriture ;
-
la fonction fclose est appelée: elle vide le buffer d'écriture avant de fermer le fichier.
Traitement des erreurs
Les fonctions suivantes peuvent être utilisées pour vérifier le bon résultat des fonctions "système". Rappelons qu'en cas d'erreur système, la variable globale errno est positionnée à une valeur représentant le type d'erreur détectée. La page de manuel man errno fournit des explications sur les différents codes d'erreur. Ne pas oublier d'inclure le fichier d'entête errno.h.
- int feof (FILE * stream)
- Renvoie la valeur vrai si la fin du fichier est atteinte (càd. si la dernière lecture effectuée a atteint la fin de fichier; on a donc essayé de lire au-delà du dernier byte)
- int ferror (FILE * stream)
- Renvoie la valeur vrai si une erreur "système" s'est produite
- void clearerr (FILE * stream)
- Réinitialise la variable errno (les fonctions qui se terminent normalement ne modifient pas errno)
- void perror (const char * mess)
- Affiche sur sdterr un message d'erreur système (la signification textuelle de la valeur de errno), en plus du message passé en paramètre
- char* strerror (int errnum)
- Renvoie la signification textuelle du code d'erreur de errno.
Positionnement dans le fichier
La lecture/écriture d'un fichier utilise un pointeur de fichier positionné à l'endroit où l'action (lecture/écriture) va s'effectuer. Normalement, cette position est gérée par le traitement de lecture/écriture et ne doit pas être géré par l'application. Cependant, si un traitement non séquentiel est requis sur le fichier, il est possible de modifier directement ce pointeur grâce aux fonctions suivantes:
- int fseek (FILE * stream, long deplacement, int origine)
-
Fonction qui recalcule la valeur du pointeur de fichier, où origine spécifie l'une des trois positions:
- SEEK_SET (=0) : depuis le début du fichier
- SEEK_CUR (=1) : depuis la position courante du fichier
- SEEK_END (=2) : depuis la fin du fichier
C'est la position à partir de laquelle le deplacement (offset en anglais, exprimé en bytes) est effectué. - long ftell (FILE * stream)
- Renvoie la position courante (exprimée en bytes) dans le fichier stream.
- void rewind (FILE * stream)
- Replace le pointeur au début du fichier.
Traitement "système"
C permet également de manipuler le nom des fichiers grâce aux fonctions suivantes:
- int rename (const char* ancien, const char* nouveau)
- Change le nom d'un fichier et retourne 0 en cas de réussite.
- int remove (const char* nom)
- Supprime le fichier dont le nom est passé en paramètre.