Probleme – Un format de fichier qui survit à l'aller-retour
Exercice · niveau 3 (difficile) · informatique (MP2I/MPI), chapitre 8 — Mémoire, fichiers et entrées-sorties
Énoncé
On veut sauver une table de couples (nom, valeur) et la relire à l'identique.
- Écrire
sauveretrelireavecfprintfetfscanf, au format « une entrée par ligne :nom valeur». - Cette solution échoue. Sur quelle entrée, et pourquoi ?
- Concevoir un format qui résiste, et prouver que l'aller-retour est fidèle.
Corrigé
1. La solution naturelle.
typedef struct { char nom[NOM_MAX]; int valeur; } entree;
/* Ecrit les n entrees de t dans le fichier chemin. true en cas de succes. */
bool sauver(const char* chemin, const entree t[], int n) {
assert(chemin != NULL && t != NULL && n >= 0);
FILE* f = fopen(chemin, "w");
if (f == NULL) { return false; }
fprintf(f, "%d\n", n); /* on ecrit d'abord le COMPTE */
for (int i = 0; i < n; i = i + 1) {
fprintf(f, "%s %d\n", t[i].nom, t[i].valeur);
}
return fclose(f) == 0; /* fclose peut ECHOUER : disque plein */
}
/* Relit au plus max entrees dans t. Renvoie le nombre lu, ou -1 en cas d'echec. */
int relire(const char* chemin, entree t[], int max) {
assert(chemin != NULL && t != NULL && max >= 0);
FILE* f = fopen(chemin, "r");
if (f == NULL) { return -1; }
int n = 0;
if (fscanf(f, "%d", &n) != 1 || n > max) { fclose(f); return -1; }
for (int i = 0; i < n; i = i + 1) {
if (fscanf(f, "%31s %d", t[i].nom, &t[i].valeur) != 2) {
fclose(f); return -1;
}
}
fclose(f);
return n;
}
Deux précautions y sont déjà : le %31s borne la lecture à la taille du champ — sans lui, un nom de caractères déborderait le tableau —, et le compte est vérifié contre max avant la boucle.
2. L'échec, mesuré. Sur la table {"pi",3}, {"e",2}, {"nombre d'or",1}, le fichier écrit est correct :
3
pi 3
e 2
nombre d'or 1
mais relire rend . La conversion %s s'arrête au premier blanc : elle lit nombre, puis %d tente de convertir d'or en entier, échoue, et la fonction abandonne.
Le vrai défaut n'est pas dans le lecteur, il est dans le format. L'espace y sert à la fois de séparateur et de caractère de donnée : le fichier est ambigu, et aucun lecteur ne peut le lever. C'est le point de méthode du problème — on ne corrige pas un format ambigu à coups de cas particuliers.
3. Un format sans ambiguïté. On écrit la longueur du nom avant le nom : le lecteur sait alors exactement combien d'octets prendre, et n'a plus à deviner où le champ s'arrête. On place aussi la valeur avant, pour que le nom — seul champ de longueur variable — termine la ligne.
/* Format : une ligne "n", puis n lignes "valeur longueur nom". */
bool sauver2(const char* chemin, const entree t[], int n) {
/* ... comme sauver, avec : */
fprintf(f, "%d %d %s\n", t[i].valeur, (int) strlen(t[i].nom), t[i].nom);
}
int relire2(const char* chemin, entree t[], int max) {
/* ... pour chaque entree : */
int lg = 0;
if (fscanf(f, "%d %d ", &t[i].valeur, &lg) != 2) { fclose(f); return -1; }
if (lg < 0 || lg >= NOM_MAX) { fclose(f); return -1; } /* borne VERIFIEE */
for (int k = 0; k < lg; k = k + 1) {
int c = fgetc(f);
if (c == EOF) { fclose(f); return -1; }
t[i].nom[k] = (char) c;
}
t[i].nom[lg] = '\0'; /* sentinelle POSEE */
}
Noter l'espace final dans "%d %d " : il consomme le blanc qui suit la longueur, de sorte que fgetc commence sur le premier caractère du nom.
L'aller-retour est fidèle. Écriture puis relecture de {"pi",3}, {"e",2}, {"nombre d'or",1}, {"",0} donne le fichier
4
3 2 pi
2 1 e
1 11 nombre d'or
0 0
et la relecture rend les quatre entrées identiques aux originales — vérifié par strcmp sur chaque nom, y compris le nom vide. Le cas du nom vide est celui qui distingue un format correct d'un format « qui marche » : avec la longueur , la boucle de lecture ne tourne pas et la sentinelle est posée en position . Aucun cas particulier n'a été nécessaire.
Preuve de fidélité. Le lecteur reconstitue le nom en lisant exactement lg octets, où lg est la valeur qu'a écrite strlen ; comme fprintf a écrit ces lg octets sans les altérer, la chaîne relue est la chaîne écrite. La récurrence sur donne la table entière, et le compte écrit en tête garantit qu'on lit ni plus ni moins d'entrées.
Ce que ce problème illustre au-delà de C. La longueur qui précède la donnée est le procédé qu'emploient tous les formats binaires sérieux, et l'alternative — un caractère séparateur qu'il faut alors échapper dans les données — est celle des formats textuels. Il n'y en a pas de troisième, et le choix se fait au moment de concevoir le format, jamais après.
Les autres exercices de ce chapitre Le cours du chapitre
Un blocage sur cet exercice ? Le tuteur d'Adloun guide par questions, sans donner la réponse.