Recovered PDF 13
Recovered PDF 13
Recovered PDF 13
2 - Bufferisation .....................................................................................................................2
4 - Accès direct.......................................................................................................................6
6 - Conclusion ......................................................................................................................11
2 - Bufferisation
Les fichiers ouverts par association avec une variable de type ofstream sont bufferisés, c’est à
dire que le système d’exploitation ne procède pas nécessairement à une écriture physique sur
le disque concerné après chaque insertion de données dans le fichier. Ce mécanisme permet au
système d’obtenir une exécution plus rapide en gardant en mémoire les écritures demandées,
et en les effectuant "en bloc", lorsque le volume de données à transférer est suffisant.
Il arrive que cette façon de fonctionner pose des problèmes, notamment lorsqu'une fonction
cherche à lire dans un fichier des données qui n'y ont pas encore été réellement écrites :
1 void demo()
2 {
3 ofstream fichier("essai.txt");
4 if(!fichier)
5 return;
6 fichier << 1789;
7 maFonction(); //surprise : le fichier est toujours vide !
8 }
1 void maFonction()
2 {
3 int revolution;
4 ifstream fichier("essai.txt");
5 if(fichier)
6 fichier >> revolution; //la lecture échoue
7 }
Pour forcer le système à effectuer les écritures en attente, on peut fermer explicitement le
fichier (à l'aide de la fonction close(), avant d'appeler maFonction()) ou utiliser le
manipulateur flush. La ligne 6 de la fonction demo() devient alors :
fichier << 1789 << flush;
Le manipulateur endl provoque, lui aussi, le transfert sur le disque des données en attente
d'écriture. La ligne
fichier << 1789 << endl;
1 Même en présence d'un environnement graphique, il reste possible d'écrire des programmes fonctionnant en mode
"console". Le seul intérêt réel de cette approche est qu'elle permet d'obtenir des programmes pouvant facilement être
recompilés sous divers systèmes. Dans le cas de programmes dont l'interface utilisateur est minimale, ce bénéfice
justifie parfois le sacrifice esthétique qui l'accompagne.
3 - Images mémoire
Comme nous l’avons vu, l’usage de l’opérateur d’insertion permet de placer dans un fichier une
description alphanumérique de la valeur concernée, et non une copie binaire de la
représentation mémoire de cette valeur. Cette façon de procéder crée des fichiers au format
texte, qui obtiennent l’avantage d’être facilement interprétables au prix d’une utilisation peu
efficace de l’espace de stockage disponible. Outre les inconvénients liés à leur volume, les
fichiers textes présentent un défaut qui peut parfois pousser à leur préférer l'usage de fichiers
bruts : ces derniers permettent de stocker des données correspondant à des instances de
classes qui ne sont pas capables de se décrire elles-mêmes sous forme textuelle lorsqu'elles
sont confiées à l'opérateur d'insertion dans un flux.
Cette façon de procéder pose toutefois un problème si l'un des membres est un pointeur : la
valeur insérée dans le fichier sera l'adresse contenue dans ce pointeur, et non la valeur
correspondant à l'état de la zone de mémoire située à l'adresse en question. Par la suite,
lorsque le contenu du fichier sera utilisé, il est très improbable que cette adresse redonne
naissance à un pointeur valide, et il serait franchement surprenant que la zone de mémoire
concernée ait retrouvé (par magie ?) son état initial.
Lorsqu'une classe comporte des variables membre qui sont des pointeurs, une sortie brute
risque de ne pas insérer dans le fichier toute l'information souhaitée.
La classe ofstream comporte une fonction nommée write() qui permet d'effectuer une copie
d'une zone de mémoire dans le fichier associé. La fonction write() nécessite deux arguments :
un "pointeur sur char" contenant l'adresse de la zone à copier, et la taille de la zone à copier.
Ces deux paramètres appellent plusieurs commentaires.
Tout d'abord, il est fort peu vraisemblable que la zone de mémoire faisant l'objet d'une sortie
brute contienne effectivement des caractères. En effet, dans le cas de caractères, il n'y a
justement aucune différence entre sortie brute et sortie formatée ! Si la fonction write()
attend un "pointeur sur char", c'est simplement parce que le type char occupe un seul octet,
et que "pointeur sur char" est la meilleure façon de dire "pointeur sur octets" dans un contexte
comme celui-ci, où on ne s'intéresse aucunement à la signification de l'état de la zone de
mémoire dont on manipule l'adresse.
Puisqu'il s'agit simplement de recopier un certain nombre d'octets dans un fichier, la fonction
write() n'a pas du tout besoin de savoir de quel type de données elle est en train de
s'occuper. Il serait bien entendu possible d'avoir plusieurs fonction write(), l'une recevant
un pointeur sur int et permettant la sortie brute d'int, une autre recevant un pointeur sur
double et permettant la sortie brute de double, et ainsi de suite. Mais cette approche ne
permettrait pas la sortie brute de données de types non-standards : il n'est pas envisageable
d'avoir, chaque fois que l'on crée un nouveau type, à modifier la classe ofstream pour lui
rajouter une fonction membre write() recevant le bon type de pointeur !
Mais comment un "pointeur sur char" peut-il contenir l'adresse d'une zone de mémoire
représentant autre chose que des char ? Le langage C++ s'oppose normalement à une telle
abomination, parce que déréférencer un pointeur dont le type diverge de celui des données sur
lesquelles il pointe conduit le plus souvent à des catastrophes. Dans certains cas particuliers,
dont le cas présent n'est qu'un exemple, des pointeurs de ce genre s'avèrent cependant
indispensables. Le langage C++ permet donc de les obtenir, à condition toutefois que leur
aspect pathologique soit explicitement mentionné dans le code source, et, donc, consciemment
assumé par un programmeur agissant en toute connaissance de causes.
L'opération qui permet de changer le type d'une valeur s'appelle un transtypage (cast, en
anglais).
Dans le cas présent, l'opérateur qu'il convient d'utiliser est reinterpret_cast<>(). Le type
désiré doit figurer entre les chevrons, et la valeur à convertir doit figurer entre les parenthèses.
L'exemple suivant donne à un pointeur sur char une valeur qui correspond en fait à l'adresse
d'une variable de type int.
1 int unEntier = 4;
2 char * ptr = reinterpret_cast<char *> (& unEntier);
Dans cet exemple, l'évaluation de (& unEntier) conduit à une valeur de type "pointeur sur
int", cette valeur voit son type modifié par le transtypage, et c'est donc bien une valeur de type
"pointeur sur char" qui est utilisée pour initialiser ptr.
Nous aurons l'occasion de revenir sur les opérations de transtypage (au cours de la Leçon 21,
en particulier). Notez bien, dès à présent, qu'il s'agit seulement de "changer le type d'une
VALEUR". L'opération qui consisterait à changer le type d'une VARIABLE n'existe pas, et
n'existera jamais en C++. Une opération de transtypage produit un résultat, mais elle n'a
aucun effet sur la variable éventuellement utilisée pour indiquer la valeur à convertir.
La fonction write() ne se contente pas d'exiger l'adresse de la zone de mémoire qu'elle doit
copier dans le fichier. Son second paramètre doit indiquer la taille de cette zone de mémoire, et
cette taille doit être obtenue d'une façon particulière. En effet :
Vous ne devez JAMAIS utiliser votre connaissance éventuelle de la quantité de mémoire utilisée
par tel ou tel type de donnée. Vous devez TOUJOURS obtenir ce renseignement en le
demandant au compilateur à l'aide de l'opérateur sizeof().
Entre autres talents, cet opérateur2 possède celui d'être capable de fournir la taille occupée par
une variable d'un type quelconque, même s'il s'agit d'un type que vous venez de créer.
Pourquoi utiliser sizeof() plutôt que votre connaissance de la taille des variables ?
Parce que sizeof() ne se trompe jamais. Il arrive que la place occupée par une variable d'un
certain type change (soit parce qu'on a ajouté ou supprimé une variable membre, ou changé
son type, soit parce qu'on a changé de compilateur, soit parce qu'on produit un exécutable
pour un système d'exploitation différent, soit parce que…). Lorsque le programme est
recompilé, l'opérateur sizeof() produit infailliblement les valeurs correctes dans le contexte
en vigueur, ce qui n'est pas le cas de vos connaissances personnelles. Signalons, en outre,
que l'utilisation de sizeof() n'ajoute pas la moindre nanoseconde à la durée d'exécution du
programme, car ce type d'expression est évalué lors de la compilation
Nous pouvons donc obtenir une sortie brute de la façon suivante :
1 int unEntier = 10;
2 ofstream monFichier ("essai.bin",ios::binary);
3 if (monFichier)
4 {
5 char * ptr = reinterpret_cast <char *> (& unEntier);
6 monFichier.write(ptr, sizeof(int));
7 }
A l'issue de l'exécution de ce fragment de code, le fichier essai.bin contient la représentation
binaire de l'entier 10.
Si vous utilisez l'environnement de développement Visual C++, vous pouvez observer
directement le contenu de ce fichier car, dans le cas d'un fichier portant l'extension .bin,
l'éditeur intégré n'interprète pas simplement les octets du fichier comme des codes ASCII,
mais affiche leur valeur en hexadécimal. Dans notre exemple, l'affichage obtenu est
000000 0A 00 00 00 ....
La première colonne indique la position dans le fichier (la valeur 000000 correspond au début
du fichier). 0A 00 00 00 sont les quatre octets que contient le fichier (si ce résultat vous
surprend, revoyez la Leçon 1...) et les quatre points qui figurent dans la colonne de droite
indiquent que ces quatre octets n'ont pas de représentation visuelle en tant que caractères.
2 Ne vous laissez pas abuser pas sa syntaxe : il s'agit bel et bien d'un opérateur appliqué à un opérande, et non d'une
fonction à laquelle on transmettrait un argument. Une fonction ne peut pas accepter un type comme argument.
Un fichier faisant l'objet de sorties brutes doit toujours être ouvert en mode binaire.
La classe ifstream comporte une fonction membre nommée read() qui permet de copier dans
une zone de mémoire un certain nombre d'octets extraits d'un fichier. Comme la fonction
write(), la fonction read() utilise deux paramètres : l'un est de type "pointeur sur char" et
contient l'adresse de la zone de mémoire qui recevra les octets extraits du fichier, alors que
l'autre indique le nombre maximum d'octets qui seront transférés en mémoire.
Les commentaires faits à propos du premier paramètre de la fonction write() restent valables
ici : il n’est pas certain que la zone de mémoire qui reçoit les octets extraits du fichier soit
effectivement destinée à stocker des caractères. La valeur de ce premier paramètre est donc
fréquemment obtenue grâce à un transtypage appliqué à l'adresse d'un autre type de donnée.
Pour ce qui est du second paramètre, il faut remarquer qu'il s'agit bien du nombre maximum
d'octets qui seront transférés : la fonction read() ne peut évidemment pas lire plus d'octets
qu'il n'y en a réellement dans le fichier ! Il est possible de déterminer combien d'octets ont été
effectivement copiés en mémoire lors de la dernière extraction, en appelant une autre fonction
membre de la classe ifstream, nommée gcount(), qui renvoie ce nombre.
La fin d'un fichier binaire n'est pas matérialisée par la présence d'un octet ayant une valeur
particulière, comme dans des fichiers texte. La fonction read() est capable de lire des séries
d'octets présentant toutes les valeurs possibles, ce qui n'est pas le cas de getline() qui doit
accepter qu'une valeur particulière entraîne, lorsqu'elle est rencontrée, la fin de l'extraction.
Si nous avons défini une classe nommée ClasseDemo dont aucune des variables membre n'est
un pointeur, nous savons qu'il est possible de stocker l'état d'une de ses instances (nommée
instanceDemo) dans un fichier à l'aide d'une section de code telle que :
1 char * ptr = reinterpret_cast<char *> (& instanceDemo);
2 ofstream monFichier("demo.bin", ios::binary);
3 if(monfichier)
4 monFichier.write(ptr, sizeof(ClasseDemo));
Ce fichier peut ensuite servir à placer une instance de ClasseDemo dans l'état qu'il décrit :
1 ClasseDemo uneInstance;
2 char * ptr = reinterpret_cast<char *> (& uneInstance);
3 ifstream unFichier("demo.bin", ios::binary);
4 if(unFichier)
5 {
6 unFichier.read(ptr, sizeof(ClasseDemo));
7 if (unFichier.gcount() != sizeof(ClasseDemo))
//alors c'est que quelque chose ne va pas du tout !
8 }
Lorsqu’un fichier contient un texte3 représenté comme une séquence de codes ASCII, une
entrée "formatée" ne s'accompagne d'aucune opération de conversion, puisqu'une
"représentation alphanumérique" du texte correspond exactement à la "représentation
mémoire" de ce texte. Pour des raisons pratiques, il arrive souvent qu'on choisisse de lire un
tel fichier à l'aide de la fonction read() plutôt qu'à l'aide de l'opérateur d'extraction, ou de la
fonction getline(), même si le texte a été initialement placé dans le fichier à l'aide de
3 Le mot "texte" est ici à prendre dans son sens commun : une suite de mots d'une langue naturelle, a priori destinée à
être lue par un être humain. Dans ce contexte, une suite de code ASCII correspondant à des caractères numériques et
représentant des valeurs destinées à être traitées par l'ordinateur n'est pas un texte, même s'il s'agit indiscutablement
d'un fichier au format texte.
4 - Accès direct
En l'absence de spécification contraire, les fichiers sont manipulés de façon séquentielle, ce qui
signifie que chaque opération de lecture (ou d'écriture) déplace la position courante dans le
fichier, de façon à ce que la lecture (ou l'écriture) suivante ait lieu à la suite de celle qui vient
d'être effectuée. Les informations apparaissent donc dans le fichier dans l'ordre chronologique
des écritures, et c'est dans cet ordre que les opérations d'extraction les récupéreront.
Il arrive cependant qu'il soit préférable de gérer explicitement la position à laquelle une
opération doit avoir lieu à l'intérieur du fichier, ce qui nécessite d'une part la possibilité de
modifier cette position et d'autre part la possibilité de la déterminer. La maîtrise de ces
techniques d'accès direct ouvre, en outre, la possibilité d'utiliser un même fichier
simultanément en lecture et en écriture.
La classe ofstream dispose d'une fonction membre nommée seekp() qui permet de déplacer4
la position d'écriture, et donc de remplacer des informations déjà présentes dans le fichier.
La position d'insertion désirée peut être indiquée soit de manière absolue soit de manière
relative. Pour l'indiquer de façon absolue, il suffit de transmettre à seekp() une valeur
indiquant la position dans le fichier de l'octet où doit commencer la prochaine insertion.
Comme d'habitude, le premier octet du fichier correspond à la position 0, et l'instruction
suivante place la position d'insertion sur le dixième octet de monFichier :
monFichier.seekp(9);
Pour spécifier une position d'insertion de façon relative, il faut indiquer deux choses : la
position de référence choisie, et la distance par rapport à cette position de référence à laquelle
la prochaine insertion doit avoir lieu. La position de référence peut être l'une des trois
suivantes :
ios::beg le début du fichier (beginning)
ios::cur la position d'insertion actuelle (current)
ios::end la fin du fichier (end)
Dans le cas des références ios::cur et ios::end, la distance spécifiée peut être négative, de
façon à placer la prochaine insertion avant le point de référence choisi. Les trois lignes
suivantes illustrent l'usage de ces différentes possibilités :
1 monFichier.seekp(10,ios::beg); //on va écrire sur le 11° octet du fichier
2 monFichier.seekp(-5,ios::cur); //on "recule" la position d'insertion
3 monFichier.seekp(0,ios::end); //on se place à la fin du fichier
Un détail important :
Il n'est pas possible de déplacer la position d'insertion dans un fichier qui a été ouvert en
utilisant le mode ios:append.
Ce mode d'ouverture offre donc une sécurité importante : le contenu antérieur du fichier ne
peut en aucun cas être écrasé par les opérations d'écriture concernant le fichier. Si vous
souhaitez ouvrir un fichier en positionnant le point d'insertion à la fin (comme avec
ios::append), mais en conservant la possibilité de déplacer par la suite le point d'insertion,
utilisez le mode d'ouverture ios::ate (ate est l'acronyme de at the end).
En dépit de ce que son nom pourrait laisser espérer, une opération d’insertion effectuée alors
que la position d’écriture ne se situe pas en fin de fichier ne s’accompagne pas d’un
déplacement des informations avales, déplacement qui permettrait de "faire de la place" aux
informations à "insérer". Les octets écrits viendront au contraire "écraser" un nombre égal
d'octets du fichier, dont les valeurs seront définitivement perdues.
4 Le verbe "to seek" signifie "rechercher", et son apparition ici est assez étrange (on attendrait plutôt "set"). Quant au 'p'
final du nom "seekp", c'est l'initial du mot "put", un autre verbe anglais exprimant l'idée d'insertion…
L'accès direct à un fichier d'entrée est tout à fait analogue à l'accès direct à un fichier de sortie,
mais la fonction membre utilisée, si elle utilise les mêmes arguments que son homologue, porte
un nom légèrement différent5 : seekg()
Les trois lignes suivantes illustrent diverses façons de modifier la position de lecture d'un
fichier supposé ouvert :
monFichier.seekg(10,ios::beg);
//on va lire le 11° octet du fichier
monFichier.seekg(5,ios::cur);
//on "avance" la position de lecture de 5 octets
monFichier.seekg(-3,ios::end);
//on se place 3 octets avant la fin du fichier
La position à laquelle aurait lieu une opération (si une opération était effectuée) peut être
obtenue en faisant appel à une fonction membre qui renvoie le nombre d'octets séparant la
position courante du début du fichier. Cette fonction est nommée tellp() dans le cas de la
classe ofstream, et tellg() dans le cas de la classe ifstream.
Déterminer la position courante est une opération qui présente parfois un intérêt réel, même si
le fichier concerné n'est pas véritablement utilisé comme un fichier à accès direct. Le fragment
de code suivant, par exemple, permet d'obtenir la longueur d'un fichier :
1 ofstream unFichier("essai.txt", ios::nocreate | ios::app);
2 if(unFichier)
3 unsigned long longueur = unFichier.tellp();
En effet, le mode d'ouverture ios::app positionne d'emblée le point d'insertion à la fin du
fichier, et tellp() donne alors la taille du fichier en octets.
Il existe bien entendu des moyens plus directs pour déterminer la longueur d'un fichier, sans
avoir à l'ouvrir. Ces moyens présentent toutefois l'inconvénient de ne pas être identiques sur
tous les systèmes (puisque, justement, il s'agit en définitive de demander la taille du fichier à
celui qui la connaît le mieux, à savoir le système d'exploitation lui-même). Le recours à
seekg() (ou seekp()), pour rustique qu'il puisse paraître, présente l'immense avantage
d'être relativement portable.
Veillez à ne pas confondre la taille d'un fichier contenant du texte avec le nombre de
caractères de ce texte : la représentation des passages à la ligne peut occuper deux octets
dans le fichier et un seul en mémoire.
Outre les classes ofstream et ifstream, le fichier fstream.h définit également une classe
nommée simplement fstream. Lorsqu'on ouvre un fichier à l'aide d'une variable de type
fstream, il est possible d'utiliser deux spécifications supplémentaires de mode d'écriture (les
spécifications ios::app, ios::ate, ios::nocreate, ios::noreplace et ios::binary restent
bien entendu disponibles) :
ios::in Il sera possible d'extraire des données du fichier
ios::out Il sera possible d'insérer des données dans le fichier
On peut donc ouvrir un fichier à la fois en lecture et en écriture en écrivant :
fstream monFichier("essai.txt", ios::in | ios::out);
5 Le 'g' final est évidemment l'initiale du mot "get", rappelant ainsi qu'il s'agit de la position de lecture.
Un tel fichier cumule les possibilités des fichiers ouverts en écriture et celles des fichiers
ouverts en lecture. L'usage en est toutefois assez délicat, et exige une gestion particulièrement
rigoureuse de la logique des opérations d'écriture et de lecture. Il convient donc de ne recourir
à cette possibilité que lorsque les bénéfices qu'elle apporte sont manifestes. L'approche
alternative, qui consiste à ouvrir et refermer le fichier, en lecture ou en écriture, selon le
besoin, ne doit être écartée que si elle conduit vraiment à des performances inacceptables.
Ce n'est sans doute que dans le cas de fichiers très volumineux et faisant l'objet de nombreux
accès directs que le choix d'un fichier ouvert simultanément en entrée et en sortie peut
s'avérer payant. Il convient toutefois de souligner que cette approche conduit
vraisemblablement à laisser le fichier ouvert en écriture pendant une période assez longue, ce
qui constitue une prise de risques importante. A la complexité de la gestion de la logique
d'entrée/sortie, il faudra donc sans doute ajouter celle d'une sécurisation accrue des
opérations d'écriture.
Du point de vue de leur manipulation, les fichiers d'entrée/sortie ne présentent guère qu'une
particularité vraiment notable : s'ils disposent bien des fonctions seekg() et tellg() (comme
les fichiers d'entrées) et des fonctions seekp() et tellp() (comme les fichiers de sortie), la
position manipulée ou indiquée par ces fonctions est unique.
Un fichier d'entrée/sortie ne peut à aucun moment avoir une position d'insertion et une
position d'extraction différentes.
En d'autres termes, les valeurs renvoyées par tellg() et tellp() sont toujours identiques, et
utiliser seekg() a le même effet qu'utiliser seekp() : dans les deux cas, la position d'insertion
et la position d'extraction sont toutes deux déplacées.
Ce léger détail est insuffisamment souligné par certains des ouvrages qui traitent de cette
question, et il arrive même que le contraire soit clairement suggéré. La documentation livrée
avec Visual C++ 6.0 n'est pas extraordinairement explicite à ce propos, mais elle précise
toutefois la chose suivante :
An fstream object is a single stream with two logical substreams, one for
input and one for output. Although the underlying buffer contains separately
designated positions for reading and writing, those positions are tied
together.
Ainsi, si les positions de lecture et d'écriture possèdent des désignations distinctes, elles sont
bel et bien indissociables
Les erreurs d'écriture sur un disque sont un phénomène assez rare pour que certains
programmeurs oublient de les prévoir, et assez fréquent pour que de nombreux utilisateurs
pâtissent de cette négligence.
Un cas typique est celui du programmeur qui ne teste son programme que sur son disque
dur, alors que les utilisateurs travaillent avec des disquettes (qui sont souvent pleines,
défectueuses, ou même éjectées) et un réseau (qui fonctionne correctement un jour sur dix).
La gestion correcte des cas d'erreurs d'écriture dans un fichier n'est pas facile à mettre en
place, car une opération d'écriture avortée n'est pas toujours réversible (une partie du fichier
peut avoir été perdue). Une politique envisageable est de procéder à une copie du fichier avant
de commencer à le modifier, et de se contenter de prévenir l'utilisateur lorsqu'une opération
d'écriture ne s'est pas déroulée normalement.
Même si vous ne jugez pas réaliste d'essayer de gérer totalement les erreurs d'écriture, prenez
au moins la peine de les détecter et de prévenir l'utilisateur que ses données sont corrompues !
Si votre programme ne fait pas automatiquement une copie de sauvegarde des fichiers dans
lesquels il va écrire, prévenez l'utilisateur que ces fichiers sont susceptibles d'être détruits, et
qu'il doit en assurer la sauvegarde lui-même, ou assumer les conséquences.
La détection d'une erreur d'écriture est rendue possible par une fonction membre de la classe
ofstream nommée good(), qui renvoie une valeur logiquement vraie si tout s'est bien passé,
et fausse sinon.
Lorsqu'un fichier subit une erreur d'écriture, certaines des variables membre de la variable
associée gardent trace de l'événement, ce qui permet ensuite au programme de tenter de
diagnostiquer l'origine du problème. Si un programme entreprend de prendre des mesures
palliatives après une erreur d'écriture, il doit prendre soin d'effacer les traces de cette erreur
avant de faire une nouvelle tentative (faute de quoi cette nouvelle tentative est, de toute façon,
vouée à l'échec par le fait que le fichier n'est plus en "état de marche"). Les indicateurs d'erreur
d'un fichier sont effacés en appelant la fonction clear().
Une gestion rudimentaire des erreurs d'écriture pourrait donc être mise en place ainsi :
1 double unNombre = 3.14;
2 message = "";
3 ofstream monFichier = "essai.txt";
4 if (monFichier)
5 {
6 monFichier << unNombre;
7 if (! monFichier.good())
8 message = "Erreur d'écriture dans essai.txt";
9 }
10 else
11 message = "Impossible d'ouvrir le fichier essai.txt";
12 if (message != "")
13 {
14 monFichier.clear();
//il faut afficher le message et faire quelque chose de radical,
//comme tout recommencer ou mettre fin au programme
L'expérience montre toutefois que la séquence d'événements découlant d'une erreur d'écriture
est assez difficilement prévisible et dépend largement du système d'exploitation utilisé. Les
échecs d'ouverture sont beaucoup plus facilement maîtrisables, et comme il est finalement
assez rare qu'il soit impossible d'écrire dans un fichier qui vient d'être ouvert sans problème,
nous pouvons formuler la règle empirique suivante :
Ne laissez pas un fichier durablement ouvert en écriture. Refermez-le dès que plus rien ne
laisse supposer qu'une nouvelle opération d'écriture va avoir lieu dans un futur très proche.
Les conséquences d'une erreur de lecture sont beaucoup moins graves que celles d'une erreur
d'écriture, puisque l'intégrité des fichiers n'est pas menacée. On peut donc raisonnablement
envisager de ne pas refermer systématiquement les fichiers ouverts en lecture dans les cas où
le gain en rapidité d'exécution ainsi obtenu semble justifier le risque accepté.
Si les erreurs de lecture peuvent, comme les erreurs d'écriture, être causées par des
défaillances techniques (perte de connexion réseau, éjection d'une disquette, support
défectueux, etc.), il en existe que le programme peut envisager de contourner sans que
l'utilisateur n'ait à intervenir. Ceci suppose, bien entendu, que le programme soit en mesure
d'identifier la nature du problème, et qu'il s'agisse d'une situation qu'il peut gérer. Deux
sources d'erreurs tombent parfois dans cette catégorie : la fin du fichier et la présence de
données ininterprétables. Différentes fonctions de diagnostic peuvent être utilisées pour
déterminer quelle doit être la réaction du programme lorsqu'une erreur se produit.
La classe ifstream possède (tout comme la classe ofstream) une fonction membre nommée
good(), mais celle-ci renvoie une valeur logiquement fausse non seulement lorsque la fin du
fichier est atteinte, mais aussi lorsque l'erreur a une autre origine. Si l'on veut considérer la fin
du fichier non comme une erreur, mais comme un événement signifiant simplement que la
lecture est terminée, il faut donc faire appel à d'autres fonctions de diagnostic, capables de
distinguer le problème causé par la fin du fichier de ceux ayant d'autres causes.
Cette distinction est faite par la fonction eof(), qui renvoie une valeur logiquement vraie si la
dernière extraction a échoué en raison d'un dépassement de la fin du fichier, et une valeur
logiquement fausse dans le cas contraire.
Le comportement de la fonction eof() s'avère parfois un peu frustrant, car cette fonction
signale une erreur (elle ne renvoie une valeur vraie qu'après qu'une lecture a échoué). Il est
souvent intéressant de savoir si la dernière lecture effectuée avec succès a ou non extrait la
dernière des données présentes dans le fichier. Ce renseignement peut être obtenu à l'aide de
la fonction peek(), qui simule l'extraction d'un caractère (c'est à dire que ce caractère reste
disponible pour l'extraction suivante). La fonction peek() renvoie le caractère6 EOF si elle
rencontre la fin du fichier, ce qui permet d'écrire des choses du genre :
1 monFichier >> maVariable; //extraction d'une donnée
2 if (monFichier.peek() == EOF)
//maVariable contient la dernière des données présentes dans monFichier
La fonction fail() renvoie une valeur logiquement vraie si la dernière extraction ne s'est pas
déroulée correctement et une valeur logiquement fausse dans le cas contraire (que la fin du
fichier soit ou non atteinte à l'issue de cette extraction). Deux cas principaux peuvent conduire
la fonction fail() à renvoyer une valeur vraie : l'impossibilité d'extraire des données (soit
parce que la fin du fichier était déjà atteinte AVANT la tentative d'extraction, soit par suite d'un
problème matériel) et l'impossibilité pour l'opérateur d'extraction d'interpréter les données
extraites (comme, par exemple, lorsque l'on rencontre du texte ou une ligne vide, alors que l'on
cherche à extraire une valeur numérique).
La fonction bad() ne renvoie une valeur logiquement vraie que si la dernière extraction s'est
soldée par l'impossibilité d'extraire des données. Les cas où fail() est vraie et bad() fausse
correspondent donc à l'extraction de données ininterprétables.
La fonction suivante reçoit un pointeur sur une variable associée à un fichier, dont elle
suppose que la fonction appelante s'est assurée qu'il a été correctement ouvert. Elle extrait de
ce fichier des valeurs entières (qu'elle transmet à une fonction nommée traiterValeur)
jusqu'à ce quelle rencontre un problème. Elle renvoie alors la valeur true si elle s'est arrêtée à
cause de la fin du fichier ou d'une donnée ne correspondant pas à une valeur entière, et la
valeur false si elle s'est arrêtée à cause d'un problème plus grave.
1 bool traiteLesProchainsEntiers(ifstream *monFichier)
2 {
3 int valeurLue;
4 do {
5 *monFichier >> valeurLue;
6 if(!monFichier->fail())
7 traiterValeur(valeurLue); //la valeurLue est correcte
8 } while (monFichier->good()); //tant qu'il n'y a aucun problème
9 return (!monFichier->bad());
10 }
Cette fonction fournit une solution simple au classique problème dit "du retour chariot final",
puisqu'elle exploite indifféremment des fichiers se terminant par une ligne vide
(ininterprétable en tant que valeur numérique) et des fichiers qui n'en comportent pas. Elle
fonctionne aussi (sur ces deux types de fichiers) dans le cas où un appel à getline() est
utilisé en lieu et place de l'opérateur d'extraction (ligne 5). La seule fonction de diagnostic à
laquelle elle ne fait pas appel est, paradoxalement, eof().
Lorsqu'une extraction a échoué (c'est à dire lorsque fail() est vraie), la position d'extraction
n'est pas modifiée. Pour ignorer les lignes ininterprétables et continuer le traitement du fichier,
il faut donc non seulement effacer les indicateurs d'erreur à l'aide de la fonction clear() (pour
6 Attention à ne pas confondre la fonction eof() (qui renvoie une valeur booléenne) avec le caractère EOF. La valeur
exacte du caractère EOF ne vous concerne pas (elle peut varier d'un système à l'autre). Utilisez les trois lettres "EOF" et
laissez votre compilateur s'occuper des détails, il se trompe moins souvent que vous.
que la lecture puisse continuer), mais aussi avancer le point de lecture (faute de quoi l'échec
d'interprétation se renouvelle, et le traitement du fichier "patine" sur place). Il convient donc de
modifier la position de lecture, de façon à "sauter" les données qui ne conviennent pas.
Cette technique est illustrée par la fonction suivante, qui reçoit un pointeur sur un fichier15
dont elle extrait des valeurs entières (qu'elle transmet à une fonction nommée
traiterValeur). La fonction poursuit le traitement jusqu'à ce quelle rencontre un problème
technique ou la fin du fichier, en ignorant les éventuelles données ininterprétables en tant
qu'int. Elle renvoie la valeur true si elle s'est arrêtée à cause de la fin du fichier, et la valeur
false si elle s'est arrêtée à cause d'un problème plus grave.
1 bool traiteTousLesEntiers(ifstream * monFichier)
2 {
3 int valeurLue;
4 do {
5 * monFichier >> valeurLue;
6 if(!monFichier->fail())
7 traiterValeur(valeurLue);
8 else
9 if(!monFichier->bad() && !monFichier->eof())
10 {//on vient de rencontrer quelque chose qui n'est pas un int :
11 monFichier->clear(); //on remet le fichier en état de marche et
12 monFichier->seekg(1,ios::cur); //on avance la position de lecture !
13 }
14 } while (monFichier->good());
15 return (!monFichier->bad());
16 }
Confrontée à un fichier dont le contenu serait
25 @!#§ 12 il était une fois 4
cette fonction appellerait trois fois traiterValeur(), en lui communiquant successivement
les valeurs 25, 12 et 4.
On voit donc que, si les erreurs d'écriture annoncent rarement de bonnes nouvelles, les erreurs
de lecture peuvent, elles, être interprétées et exploitées utilement par le programme au cours
de l'exécution duquel elles surviennent. Les diverses fonctions utilisables pour cette
interprétation peuvent être résumées par le tableau suivant :
good() eof() peek() fail() bad()
Extraction réussie true false Le prochain char false false
Données ininterprétables false false Le prochain char true false
Fin du fichier atteinte true false EOF false false
Fin du fichier dépassée false true true true
Autres problèmes false false false true
Les valeurs renvoyées par les fonctions de diagnostic, en fonction du résultat de la dernière tentative de lecture
6 - Conclusion
Les programmes "jetables" (petits programmes rapidement mis au point pour un usage
ponctuel, et dont le seul utilisateur est leur auteur) peuvent souvent se contenter des outils de
gestion de fichiers présentés dans la Leçon 10.
Dès qu'un programme vise une utilisation moins éphémère, la prise en compte de situations
inattendues devient nécessaire, et certaines des fonctions de diagnostic offertes par les classes
ifstream et ofstream doivent généralement être sollicitées.
L'utilisation de fichiers bruts est sans doute moins fréquente, car elle répond à des besoins
plus spécifiques (minimisation de la taille des fichiers de données, illisibilité par des tiers, etc.)
La même remarque s'applique à l'accès direct aux fichiers, dont l'usage est le plus souvent très
partiel (saut à la fin d'un fichier pour en déterminer la taille, par exemple).