Année universitaire 2002-2003 |
Licence d'informatique
|
On a déjà vu, dans la séance 2 de travaux pratiques, qu'il existe deux catégories de fichiers, et ce indépendamment de tout langage de programmation :
Pour savoir si un fichier est un fichier de texte ou un fichier binaire, un moyen simple est de l'éditer à l'aide d'un éditeur de texte (par exemple asedit) :
Certains fichiers sont partiellement de texte et partiellement binaires. Par exemple, une image au format ppm (cf. la séance 6 de travaux pratiques) a un en-tête de type texte, mais le reste du fichier est de type binaire.
Tout fichier, de texte ou binaire, est composé d'octets, mais une même information est codée différemment dans un fichier de texte ou dans un fichier binaire.
L'entier 24 est codé par :
Par conséquent :
Rien n'interdit a priori d'utiliser les fonctions fread et fwrite pour lire ou écrire dans un fichier de texte, ni d'utiliser les fonctions fscanf et fprintf pour lire ou écrire dans un fichier binaire, mais de telles utilisations peuvent être sources d'erreurs difficiles à corriger.
Pour effectuer des lectures dans un fichier de texte à l'aide de la fonction
fscanf, on ouvrira ce fichier sous le mode "rt".
Pour effectuer des écritures dans un fichier de texte à l'aide de
la fonction fprintf, on ouvrira ce fichier sous les modes
"wt" (c'est-à-dire qu'on écrasera l'ancien contenu de ce fichier)
ou "at" (c'est-à-dire que les écritures seront concaténées
à l'ancien contenu de ce fichier). Pour effectuer des lectures dans un fichier
binaire à l'aide de la fonction fread, on ouvrira ce fichier
sous le mode "rb". Enfin, pour effectuer des
écritures dans un fichier binaire à l'aide de la fonction fwrite,
on ouvrira ce fichier sous les modes "wb" (écriture en début de fichier) ou "ab" (écriture en fin de fichier). Là
encore, ces règles peuvent être transgressées, mais
il peut s'ensuivre des erreurs difficiles à corriger.
Pour effectuer des lectures dans un fichier image au format ppm, qui comporte
une partie de texte et une partie binaire, suivant qu'on manipule l'une
ou l'autre partie, on utilisera les fonctions fscanf ou
fread, et donc on ouvrira le fichier sous le mode "rt"
ou sous le mode "rb" (cf. la séance
6 de travaux pratiques).
On peut se demander la raison pour laquelle il existe plusieurs types de fichiers. Plutôt que de disserter sur leur nécessaire coexistence, on se contentera ici de citer quelques avantages comparés de ces deux types de fichiers dans le cas de valeurs numériques (comme par exemple les images), puisque des valeurs numériques peuvent être stockées, a priori, sous forme de caractères (les chiffres plus le caractère .) ou sous forme binaire :
La lecture d'un bloc d'octets dans un fichier binaire s'effectue à l'aide de la fonction fread, d'en-tête :
size_t fread(void *pt,size_t taille,size_t nb_infos,FILE *id_fich)
où :
La fonction fread renvoie le nombre d'informations effectivement lues. Ce nombre d'informations peut être inférieur au nombre requis, dans le cas où la fin du fichier est atteinte.
L'écriture d'un bloc d'octets dans un fichier binaire s'effectue à l'aide de la fonction fwrite, d'en-tête :
size_t fwrite(void *pt,size_t taille,size_t nb_infos,FILE *id_fich)
La fonction fwrite renvoie le nombre d'informations effectivement écrites. Ce nombre d'informations peut être inférieur au nombre requis, dans le cas où l'écriture est effectuée sur un disque saturé.
#include <stdio.h> #define N 256 int main(void) { FILE *f1,*f2; /* Identificateurs de fichiers. */ char mat[N][N]; if ((f1=fopen("image.brt","rb"))==NULL) printf("Fichier original introuvable.\n"); else { if (fread(mat,1,N*N,f1)<N*N) { printf("Manque de données dans le fichier original.\n"); fclose(f1); } else { if ((f2=fopen("image_copie.brt","wb"))==NULL) { printf("Création du fichier copie impossible.\n"); fclose(f1); } else { if (fwrite(mat,1,N*N,f2)<N*N) { printf("Erreur à l'écriture.\n"); fclose(f1); fclose(f2); } else { printf("Copie terminée. Tout s'est bien passé.\n"); fclose(f1); fclose(f2); } } } } return(0); } |
Il peut être utile d'accéder à un fichier de manière directe ("par accès direct"), pour y effectuer des lectures ou des écritures, mais il faut alors gérer explicitement le pointeur d'octets qui donne la "position courante" dans le fichier. Cela est possible, grâce à la fonction fseek, d'en-tête :
int fseek(FILE *id_fich,long decalage,int depart)
où :
La fonction fseek renvoie 0 si tout s'est bien passé et -1 sinon, en particulier dans le cas où on essaierait de revenir en arrière avant le début d'un fichier. Néanmoins, lorsqu'on essaie d'avancer au-delà de la fin d'un fichier, la valeur retournée par fseek vaut 0 et non pas -1, comme on pourrait s'y attendre (tout se passe comme si on "piétinait" à la fin du fichier).
La fonction feof, d'en-tête :
int feof(FILE *id_fich)
renvoie une valeur non nulle si la fin du fichier (d'identificateur id_fich) est atteinte, et 0 sinon.
L'arrêt de l'exécution d'un programme s'effectue à l'aide d'un appel à la fonction exit, d'en-tête :
void exit(int code_retour)
Par convention, il est conseillé de retourner la valeur 0 lorsque tout s'est bien passé, et une valeur entière non nulle lorsqu'il y a eu un problème. Cette valeur s'appelle le "code de retour". Il ne faut pas confondre l'appel exit(code_retour); avec les instructions break; (cette instruction permet de sortir d'un bloc), return(valeur); ou return valeur; (ces instructions, placées dans une fonction, permettent de sortir de cette fonction, en renvoyant la valeur valeur non forcément entière, sans arrêt de l'exécution du programme). En revanche, placées dans le programme principal main, les instructions return(code_retour); ou return code_retour; provoquent l'arrêt du programme, ainsi que le renvoi de l'entier code_retour (sauf dans le cas où la fonction main est appelée récursivement).
On peut maintenant compléter l'exemple du paragraphe 4, en codant les différents types d'erreur, par exemple de la manière suivante (les rajouts par rapport à l'exemple du paragraphe 4 apparaissent en rouge) :
#include <stdio.h> #include <stdlib.h> #define N 256 int main(void) { FILE *f1,*f2; /* Identificateurs de fichiers. */ char mat[N][N]; if ((f1=fopen("image.brt","rb"))==NULL) { printf("Fichier original introuvable.\n"); exit(1); } else { if (fread(mat,1,N*N,f1)<N*N) { printf("Manque de données dans le fichier original.\n"); fclose(f1); exit(2); } else { if ((f2=fopen("image_copie.brt","wb"))==NULL) { printf("Création du fichier copie impossible.\n"); fclose(f1); exit(3); } else { if (fwrite(mat,1,N*N,f2)<N*N) { printf("Erreur à l'écriture.\n"); fclose(f1); fclose(f2); exit(4); } else { printf("Copie terminée. Tout s'est bien passé.\n"); fclose(f1); fclose(f2); exit(0); } } } } } |
Un programme exécutable est l'équivalent d'une commande UNIX (un certain nombre de commandes UNIX ont déjà été énumérées lors de la première séance de travaux pratiques). Les noms stdin, stdout, stderr désignent respectivement "l'entrée standard", la "sortie standard" et la "sortie standard des erreurs" d'une commande. Par défaut, l'entrée standard est associée au clavier, la sortie standard et la sortie standard des erreurs sont associées à l'écran. Il n'y a donc pas besoin d'utiliser la fonction fopen pour pouvoir lire des caractères tapés au clavier ou afficher des caractères à l'écran.
À l'intérieur d'un programme, pour effectuer une lecture sur l'entrée standard, on écrit (en supposant que a est une variable de type int) :
À l'intérieur d'un programme, pour effectuer une écriture sur la sortie standard, on écrit :
À l'intérieur d'un programme, pour effectuer une écriture sur la sortie standard des erreurs, on écrit :
On peut "rediriger" l'entrée standard, la sortie standard ou la sortie standard des erreurs d'une commande, et donc en particulier d'un programme, sur des fichiers. La redirection de l'entrée standard de la commande nom_commande sur le fichier de nom f_entree se fait grâce à la syntaxe suivante, au moment du lancement de cette commande :
> nom_commande < f_entree |
La redirection de la sortie standard de la commande nom_commande sur le fichier de nom f_sortie se fait grâce à la syntaxe suivante :
> nom_commande > f_sortie |
La redirection de la sortie standard des erreurs de la commande nom_commande sur le fichier de nom f_erreurs se fait grâce à une syntaxe qui dépend du shell utilisé. Avec le shell par défaut sur marine, qui est tcsh, c'est :
> nom_commande >& f_erreurs |
En revanche, avec le "shell de Bourne", qui seul sera étudié dans le cadre du module 4, la syntaxe est la suivante (remarquer le nouveau prompt, caractéristique du shell de Bourne) :
$ nom_commande 2> f_erreurs |
Écrire un programme permettant de recopier le fichier fich1
dans un fichier fich2, par paquets de 512
octets (sauf, le cas échéant, pour le dernier paquet).
Exemple d'appel du programme exécutable
:
> recopie < fich1 > fich2 |
On codera les différents types d'erreurs de la façon suivante :
Il est conseillé d'utiliser l'algorithme suivant, écrit en pseudo-langage, dans lequel les codes de retour ne sont pas précisés :
tant qu'on peut lire 512 octets : { écrire 512 octets ; si écriture impossible, alors sortir ; } s'il n'y a plus d'octets à écrire, alors sortir ; sinon : { écrire les octets restants ; si écriture impossible, alors sortir ; sinon, sortir ; } |
Dans chaque cas, on fera afficher un message explicite sur la sortie standard des erreurs.
1) Écrire une fonction paquet, permettant de lire n octets dans un fichier d'identificateur id_fich à partir d'une position pos (on suppose que ce fichier a déjà été ouvert en lecture dans le programme principal), et de les stocker dans un tableau tab :
On sortira de cette fonction avec :
2) Écrire le programme principal appelant cette fonction.