précédent suivant haut Contents Index P.Trau

Les fichiers de données


Les données stockées en mémoire sont perdues dès la sortie du programme. Les fichiers sur support magnétique (bande, disquette, disque) sont par contre conservables, mais au prix d'un temps d'accès aux données très supérieur. On peut distinguer les fichiers séquentiels (on accède au contenu dans l'ordre du stockage) ou à accès direct (on peut directement accéder à n'importe quel endroit du fichier). Les fichiers sont soit binaires (un float sera stocké comme il est codé en mémoire , d'où gain de place mais incompatibilité entre logiciels), soit formaté ASCII (un float binaire sera transformé en décimal puis on écrira le caractère correspondant à chaque chiffre). Les fichiers étant dépendants du matériel, ils ne sont pas prévus dans la syntaxe du C mais par l'intermédiaire de fonctions spécifiques.

Fichiers bruts

C'est la méthode la plus efficace et rapide pour stocker et récupérer des données sur fichier (mais aussi la moins pratique). On accède au fichier par lecture ou écriture de blocs (groupe d'octets de taille définie par le programmeur). C'est au programmeur de préparer et gérer ses blocs. On choisira en général une taille de bloc constante pour tout le fichier, et correspondant à la taille d'un enregistrement physique (secteur, cluster...). On traite les fichiers par l'intermédiaire de fonctions, prototypées dans stdio.h (ouverture et fermeture) et dans unistd.h (les autres), disponibles sur la plupart des compilateurs, pour Turbo C il faut inclure io.h.

La première opération à effectuer est d'ouvrir le fichier. Ceci consiste à définir le nom du fichier (comment il s'appelle sous le système) et comment on veut l'utiliser. On appelle pour cela la fonction :

int open(char *nomfic, int mode);

nomfic pointe sur le nom du fichier (pouvant contenir un chemin d'accès). Mode permet de définir comment on utilisera le fichier. On utilise pour cela des constantes définies dans fcntl.h :

O_RDONLY lecture seule, O_WRONLY écriture seule, O_RDWR lecture et écriture. On peut combiner cet accès avec d'autres spécifications, par une opération OU (|) :

O_APPEND positionnement en fin de fichier (permet d'augmenter le fichier), O_CREAT crée le fichier s'il n'existe pas, au lieu de donner une erreur, sans effet s'il existe (rajouter en 3ème argument S_IREAD | S_IWRITE | S_IEXEC déclarés dans stat.h pour être compatible UNIX et créer un fichier lecture/ écriture/ exécution autorisée, seul S_IWRITE utile sur PC), O_TRUNC vide le fichier s'il existait, O_EXCL renvoie une erreur si fichier existant (utilisé avec O_CREAT).

Deux modes spécifiques au PC sont disponibles : O_TEXT change tous les \n en paire CR/LF et inversement, O_BINARY n'effectue aucune transformation.

La fonction rend un entier positif dont on se servira par la suite pour accéder au fichier (HANDLE), ou -1 en cas d'erreur. Dans ce cas, le type d'erreur est donné dans la variable errno, détaillée dans errno.h.

On peut ensuite, suivant le mode d'ouverture, soit lire soit écrire un bloc (l'opération est alors directement effectuée sur disque) :

int write(int handle, void *bloc, unsigned taille);

On désigne le fichier destination par son handle (celui rendu par open), l'adresse du bloc à écrire et la taille (en octets) de ce bloc. Le nombre d'octets écrits est retourné, -1 si erreur.

int read(int handle, void *bloc, unsigned taille);

lit dans le fichier désigné par son handle, et le met dans le bloc dont on donne l'adresse et la taille. La fonction retourne le nombre d'octets lus (<=taille, <si fin du fichier en cours de lecture, 0 si on était déjà sur la fin du fichier, -1 si erreur).

int eof(int handle)

dit si on se trouve (1) ou non (0) sur la fin du fichier.

Lorsque l'on ne se sert plus du fichier, il faut le fermer (obligatoire pour que le fichier soit utilisable par le système d'exploitation, entre autre mise à jour de sa taille :

int close(int handle)

fermeture, rend 0 si ok, -1 si erreur.

Le fichier peut être utilisé séquentiellement (le "pointeur de fichier" est toujours placé derrière le bloc que l'on vient de traiter, pour pouvoir traiter le suivant). Pour déplacer le pointeur de fichier en n'importe que autre endroit, on appelle la fonction :

long lseek(int handle, long combien, int code);

déplace le pointeur de fichier de combien octets, à partir de : début du fichier si code=0, position actuelle si 1, fin du fichier si 2. La fonction retourne la position atteinte (en nb d'octets), -1L si erreur.

long filelength(int handle);

rend la taille d'un fichier (sans déplacer le pointeur de fichier).

Exemple : copie de fichier (les noms de fichiers sont donnés en argument du programme)

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys\stat.h>
#define taillebloc 1024
int main(int argc,char *argv[])
 {
  int source, destination;
  char buffer[taillebloc];
  int nb_lus,nb_ecrits;
  if (argc!=3) {puts("erreur arguments");return(1);}
  if((source=open(argv[1],O_RDONLY|O_BINARY))<0)
   {puts("erreur ouverture");return(2);}
  if((destination=open(argv[2], O_WRONLY| O_CREAT| O_TRUNC| O_BINARY,
    S_IREAD| S_IWRITE| S_IEXEC))<0)
         {puts("erreur ouverture");return(2);}
  do
   {
    nb_lus=read(source,(char *)buffer,taillebloc);
    if (nb_lus>0) nb_ecrits= write(destination,(char*)buffer, nb_lus);
   }
  while ((nb_lus==taillebloc)&&(nb_ecrits>0));
  close(source);
  close(destination);
  return(0);
 }

Fichiers bufférisés

Les opérations d'entrée / sortie sur ces fichiers se font par l'intermédiaire d'un "buffer" (bloc en mémoire) géré automatiquement. Ceci signifie qu'une instruction d'écriture n'impliquera pas une écriture physique sur le disque mais dans le buffer, avec écriture sur disque uniquement quand le buffer est plein.

Les fichiers sont identifiés non par un entier mais par un pointeur sur une structure FILE (définie par un typedef dans stdio.h). Les fonctions disponibles (prototypées dans stdio.h) sont :

FILE *fopen(char *nomfic, char *mode) : ouvre le fichier, suivant le mode : r (lecture seule), w (écriture, si le fichier existe il est d'abord vidé), a (append : écriture à la suite du contenu actuel, création si inexistant), r+ (lecture et écriture, le fichier doit exister), w+ (lecture et écriture mais effacement au départ du fichier si existant), a+ (lecture et écriture, positionnement en fin de fichier si existant, création sinon). Sur PC, on peut rajouter t ou b au mode pour des fichiers texte (gestion des CR/LF, option par défaut) ou binaires, ou le définir par défaut en donnant à la variable _fmode la valeur O_TEXT ou O_BINARY. fopen rend un identificateur (ID) qui nous servira pour accéder au fichier. En cas d'erreur, le pointeur NULL est retourné, le type d'erreur est donné dans une variable errno, détaillée dans errno.h. La fonction void perror(char *s mess) affichera le message correspondant à l'erreur, en général on lui donne le nom du fichier.

int fread(void *bloc, int taille, int nb, FILE *id) : lit nb éléments dont on donne la taille unitaire en octets, dans le fichier désigné par id, le résultat étant stocké à l'adresse bloc. La fonction rend le nombre d'éléments lus (<nb si fin de fichier), 0 si erreur.

int fwrite(void *bloc, int taille, int nb, FILE *id) : écriture du bloc sur fichier, si le nombre rendu est différent de nb, il y a eu erreur (tester ferror ou errno).

int fclose(FILE *id) : ferme le fichier, en y recopiant le reste du buffer si nécessaire. Cette fonction est obligatoire pour être sur d'avoir l'intégralité des données effectivement transférées sur le disque. Retourne 0 si tout s'est bien passé.

int fflush(FILE *id) : transfère effectivement le reste du buffer sur disque, sans fermer le fichier (à appeler par exemple avant une instruction qui risque de créer un "plantage").

int fseek(FILE *id, long combien, int mode) : déplace le pointeur de fichier de combien octets, à partir de : début du fichier (mode=0), position actuelle (mode=1) ou fin du fichier (mode=2). Retourne 0 si tout c'est bien passé. Cette fonction n'est utilisable que si l'on connaît la taille des données dans le fichier (impossible d'aller directement à une ligne donnée d'un texte si on ne connaît pas la longueur de chaque ligne).

int feof(FILE *id) dit si on est en fin de fichier ou non (0).

Les fichiers bufférisés permettent aussi des sorties formatées :

au niveau caractère : char fgetc(FILE *id), char fputc(char c, FILE *id), et même char ungetc(char c, FILE *id) qui permet de "reculer" d'un caractère. Cette fonction correspond donc à {fseek(id,-1,1);c=fgetc(id)}.

au niveau chaîne de caractères :

char *fgets(char *s, int max, FILE *id) : lit une chaîne en s'arrêtant au \n ou à max-1 caractères lus, résultat dans la zone pointée par s, et retour du pointeur s ou NULL si erreur.

char fputs(char *s, FILE *id) : écrit la chaîne dans le fichier sans ajouter de \n, rend le dernier caractère écrit ou EOF si erreur.

int fprintf(FILE *id, char *format, listearguments) : rend le nb d'octets écrits, ou EOF si erreur. Les \n sont transformés en CR/LF si fichier en mode texte (spécifique PC).

int fscanf(FILE *id, char *format, listeadresses) : rend le nombre de variables lues et stockées, 0 si erreur).

En général, on utilise les fichiers bufférisés :

- Soit en accès direct, en lecture et écriture, avec tous les éléments de même type et même taille (souvent une structure, en format binaire), ceci permettant d'accéder directement à un élément donné (le 48ème, le précédent, l'avant dernier...).

- Soit en accès séquentiel, avec des éléments de type différent, tous formatés sous forme ASCII, en lecture seule ou écriture seule (il est peu probable que le remplacement d'un élément se fasse avec le même nombre d'octets et nécessiterait un décalage dans le fichier), ces fichiers seront compréhensibles par n'importe quel autre programme (éditeur de texte, imprimante...). Un tel fichier s'utilise comme l'écran et le clavier, par des fonctions similaires.

Exercice (agenda) : modifier l'agenda de l'exercice tel en permettant de sauver les données sur disque. On utilisera un fichier binaire à accès direct. On pourra ensuite apporter les améliorations suivantes : recherche rapide par dichotomie sans lire tout le fichier (en le supposant classé par ordre alphabétique), création de fichiers index classés alphabétiquement sur les noms, département et ville pour accès rapide par dichotomie, les autres se faisant par recherche séquentielle, avec possibilité d'ajout, suppression, édition du fichier.

Exercice (fic_formaté) : modifier le programme de produit de matrices en lui permettant de donner sur la ligne de commande trois noms de fichiers : les deux premiers contiendront la description des matrices à multiplier, le dernier sera créé et contiendra le résultat. Les fichiers seront formatés, contiendront en première ligne le nombre de lignes puis le nombre de colonnes, puis chaque ligne du fichier contiendra une ligne de la matrice.

passer à la suite du cours (ou utilisez les icônes de navigation ci-dessous)


précédent suivant haut Contents Index P.Trau