Lire un fichier octet par octet
Cette opération est à la base de tout traitement programmé en C.
Elle présente une petite difficulté technique qu'il est intéressant d'élucider complètement.
C'est le but de cette page.
La technique expliquée ci-dessous sera mise en œuvre systématiquement dans la suite.
- I. Du fichier et de son nom
- II. Octets, entiers et caractères
- L'octet ou sa valeur ?
- Ce qu'il ne faut pas faire !
- Entiers négatifs, interprétation du bit de poids fort
- Ce qu'il ne faut pas faire (bis)
- Ce qu'il faut faire (enfin !)
- Écriture d'un octet dans un fichier
- III. Programme complet illustrant cette discussion
I. Du fichier et de son nom
-
Le fichier est matérialisé par un pointeur sur fichier (de type FILE *),
obtenu à partir du nom du fichier (chaîne de caractères) par la fonction fopen.
Pour un fichier dans lequel on doit lire, fopen prend comme 2ème argument la chaîne
"r"
(comme read).
Par exemple, le fichier nommé "toto.txt" sera mis en œuvre à travers le pointeur fp
comme suit :
FILE *fp; /* déclaration de la variable fp, avec le type idoine */
fp = fopen("toto.txt", "r");
-
En général, le nom du fichier est passé comme argument à la commande d'exécution,
et il est donc connu comme argv[1] (si c'est le premier argument sur la ligne de commande :
par exemple dans l'exécution de la commande "./a.out toto.txt tata.txt",
la chaine "toto.txt" se trouve dans argv[1], et la chaine "tata.txt" se trouve dans argv[2]).
En outre, on se prémunit contre une erreur toujours possible en s'assurant que
l'opération d'ouverture
a réussi, d'où l'écriture traditionnelle :
if ( (fp = fopen(argv[1], "r")) == NULL ) {
printf("Je ne peux pas ouvrir %s", argv[1]);
exit(1);
}
II. Octets, entiers et caractères
-
L'octet ou sa valeur ?
Il y a ici un passage délicat auquel il faut faire très attention. Nous y retrouvons le problème fondamental que nous avons
déja abordé au cours n° 1 :
Les objets qui nous intéressent, à
savoir les octets, ne pourront pas être extraits du fichier de manière
directe.
et nous ne les obtiendrons que par le biais de leurs valeurs numériques (nombres entiers).
-
Pour lire un octet à partir du pointeur de fichier fp (et passer à l'octet suivant),
on utilise la fonction fgetc,
qui renvoie une valeur entière, de type int.
- Cette valeur est comprise entre 0 et 255, c'est l'entier dont
la représentation en binaire sur 8bits est l'octet qu'on vient de lire.
Écrite en hexadécimal, c'est elle qui sert couramment pour désigner les octets, notamment dans les logiciels comme hexdump.
Nous n'en voulons point d'autre...
-
Lorsqu'on arrive à l'extrémité du fichier, fgetc renvoie une valeur spéciale connue comme
EOF (end of file),
qui permet de mettre fin à la lecture (cette valeur spéciale est -1).
Donc tout va bien...
Mais cette valeur numérique renvoyée par fgetc se révèle protéiforme, en fonction du type de la variable où le programmeur la loge
(car il faut bien la loger quelque part pour pouvoir l'exploiter, et
pour cela définir une variable, à laquelle on devra donner un type !).
Sur ce point, il est facile de se tromper, et pour raisonner à coup sûr il faut prendre un peu de recul.
-
Ce qu'il ne faut pas faire !
On trouve dans la documentation la forme suivante pour la lecture d'un fichier octet par octet :
char ch; /* bien noter le type "char" pour la variable ch : hic jacet lepus ! */
while ( (ch = fgetc(fp)) !=EOF ) {
...exploiter ch...
}
Pour comprendre l'enjeu, rien de tel qu'un essai !
Notre fichier de test sera don.txt,
qui n'a pas d'autre intérêt que de contenir
des caractères représentés en UTF-8 par 1, 2, 3 et 4 octets.
Voici son contenu, en négligeant les espaces mais
avec commentaires entre crochets :
Aa [ASCII]
¡ [le point d'exclamation inversé, lettre espagnole figurant dans le bloc Latin-1]
éàœùçÇ [sans commentaire]
ႫႫ [deux fois le "m" géorgien (3 octets en UTF-8)]
ÿ [retour à Latin-1]
𐌰 𐌲 [deux caractères gothiques, hors du plan de base (4 octets en UTF-8)]
დ [le "d" géorgien (3 octets en UTF-8)]
zZ [ASCII]
Voyons ce que donne la boucle ci-dessus en définissant "exploiter" comme "imprimer" :
while ( (ch = fgetc(fp)) !=EOF ) {
printf("%d ", ch); /* "%d" comme "en décimal" */
}
jfp% ./a.out don.txt
65 97 32 -62 -95 32 -61 -87 -61 -96 -59 -109 -61 -71 -61 -89 -61 -121 32 -31 -126 -85 -31 -126 -85 32
-61 -65 32 -16 -112 -116 -80 32 -16 -112 -116 -78 32 -31 -125 -109 32 122 90
jfp%
Où sont nos valeurs entre 0 et 255 ? On trouve bien les valeurs des octets ASCII, y compris les blancs (32),
mais pourquoi ces nombres négatifs ?
Entiers négatifs, interprétation du bit de poids fort
Pour représenter en machine les entiers négatifs,
on convient qu'un mot
- dont le bit de poids fort (le bit "le plus à gauche) vaut 1
- sera interprété comme un nombre négatif,
- dont la valeur absolue est calculée par la méthode dite du complément à 2 (voyez Wikipédia
si nécessaire).
Par exemple, avec des mots de 32 bits, le mot 1111 1111 1111 1111 1111 1111 1011 1011
représente l'entier négatif -69.
Dès lors, pour un octet dont le bit de poids fort vaut 1, c'est à dire pour un octet "non-ASCII", (par exemple 1100 0010 )
la question se pose :
- doit-on l'interpréter comme un entier positif, de valeur supérieure à 127 (avec notre exemple, 194)
- ou comme un entier négatif, par complément à 2 (avec notre exemple, -62) ?
C'est une question de point de vue !
Et comment s'exprime le point de vue adopté ? En C, il est manifesté par le type de la variable
qui contient le mot à interpréter.
En C, la notion de type recouvre deux aspects :
- Le format de représentation (nombre d'octets utilisés) ;
on parle de la taille (size) du type, et cette quantité est accessible par la primitive sizeof(nom du type).
- La manière d'interpréter l'information :
en ce qui nous concerne, le bit de poids fort doit-il être traité comme un bit de signe ?
Le langage C connaît quatre formats pour loger des informations destinées à être interprétées comme des entiers :
par ordre de tailles croissantes
char (1 octet), short (2 octets), int (2 ou 4 octets), long (4 octets) - et récemment un cinquième long long (8 octets).
Sur les machines actuelles (janvier 2007), sizeof(int) = 4 et par conséquent int est équivalent à long.
Ce n'était pas le cas autrefois !
Chacun de ces formats peut être qualifié de signed ou unsigned (par défaut, c'est signed)
le premier qualificatif signale que oui, le bit de poids fort doit être traité comme un bit de signe, le second que non.
Voyons les conséquences du choix du type pour la variable logeant la valeur de notre octet lu,
en laissant de côté les types longs :
- Avec int, le format est réputé être de 32 bits, notre octet n'a aucune chance d'êre pris pour un nombre négatif.
- Avec short, le format est de 16 bits, idem
- Avec char, le format est de 8 bits, catastrophe ! Et voila pourquoi votre fille est muette...
Mais si c'est unsigned char, tout va bien, le passage en négatif est inhibé.
Allons-y !
-
Ce qu'il ne faut pas faire (bis)
Sur la base de l'analyse précédente, il paraît indiqué de rectifier notre programme ainsi :
unsigned char ch; /* je dis bien "unsigned" */
while ( (ch = fgetc(fp)) !=EOF ) {
printf("%d ", ch);
}
Mézalor le compilateur s'alarme ! En réponse à la demande gcc -Wall -pedantic lirOct.c
il s'exclame :
lirOct.c: In function 'main':
lirOct.c:27: warning: comparison is always true due to limited range of data type
En effet, la valeur -1 en unsigned char devient 255 !
Si vous désirez voir de vos yeux ce phénomène, faites fonctionner le petit programme que voici.
-
Ce qu'il faut faire (enfin !)
Il faut loger la valeur lue dans une variable de type int (ou short si on veut vraiment raffiner).
Et cela suffit à nos besoins.
Notre boucle de lecture s'écrit donc :
int val; /* bien noter le type "int" pour la variable val : hic jacet lepus */
while ( (val = fgetc(fp)) !=EOF ) {
...exploiter val...
}
Dans ces conditions, pourquoi la documentation dit-elle char ?
C'est que la tradition de la programmation en C est enracinée dans une culture purement ASCII !
Pour un programmeur C, un caractère ne saurait occuper plus de sept bits.
Nos préoccupations sont autres, nous devons donc rompre avec la tradition.
Si la chose vous intéresse, voici quelques détails supplémentaires, mais non nécessaires à notre but principal.
-
Écriture d'un octet dans un fichier
Maintenant que nous savons lire un octet dans un fichier d'entrée,
la question se pose de l'écriture d'un octet dans un fichier de sortie.
La fonction fputc prend comme arguments un entier et un pointeur sur fichier,
et elle envoie dans le fichier l'octet correspondant à cet entier :
- si l'entier est compris entre 0 et 255, l'octet est exactement sa représentation en binaire
- s'il est supérieur à 255 (donc occupant en binaire plus d'un octet) seul l'octet de poids faible
est envoyé.
Comme nos valeurs d'octets restent dans la plage des unsigned char ([0, 255]),
il n'y aura pas de perte d'information et nous pourrons passer ces valeurs à fputc sans inquiétude.
III. Programme complet illustrant cette discussion
Ce programme attend deux noms de fichier, l'un à lire et l'autre à écrire.
Il imprime sur la sortie standard les valeurs numériques des octets successifs du fichier d'entrée,
ainsi que ces mêmes valeurs converties en
char, et il envoie ces valeurs dans le fichier de sortie.
À la fin du calcul le fichier de sortie a exactement le même contenu que le fichier d'entrée.
fichier
lirOct.c ;
#include <stdio.h>
int main(int argc, char *argv[]) {
FILE *fentr, *fsort;
int val;
char ch;
if (argc!=3) {
printf("Il me faut deux noms de fichiers");
exit(1);
}
if ((fentr= fopen(argv[1], "r")) == NULL) {
printf("Je ne peux pas ouvrir %s", argv[1]);
exit(1);
}
if ((fsort= fopen(argv[2], "w")) == NULL) {
printf("Je ne peux pas ouvrir %s", argv[2]);
exit(1);
}
while ((val=fgetc(fentr)) !=EOF) {
printf("%d ", val);
fputc(val, fsort);
ch = val;
printf("%d\n", ch);
}
fclose(fentr);
fclose(fsort);
return 0;
}/* main */
En compilant ce programme par
gcc -Wall -pedantic lirOct.c
et en exécutant
./a.out don.txt dan.txt
on obtient à l'écran :
jfp% ./a.out don.txt dan.txt
65 65
97 97
32 32
194 -62
161 -95
32 32
195 -61
169 -87
195 -61
160 -96
197 -59
147 -109
195 -61
185 -71
195 -61
167 -89
195 -61
135 -121
32 32
225 -31
130 -126
171 -85
225 -31
130 -126
171 -85
32 32
195 -61
191 -65
32 32
240 -16
144 -112
140 -116
176 -80
32 32
240 -16
144 -112
140 -116
178 -78
32 32
225 -31
131 -125
147 -109
32 32
122 122
90 90
jfp%
et le fichier-résultat dan.txt se révèle identique au fichier-donnée don.txt.