Beaucoup de développeurs abordant le C++ utilisent les fonctions du C pour gérer les flux sur fichiers. Il existe pourtant dans la STL tout ce qu'il faut pour lire et écrire dans un fichier, ou un flux de façon plus générale. Il est également possible en utilisant le C++ d'associer un flux à une chaîne de caractères. Enfin, nous verrons à la fin de cet article comment définir nos opérateurs permettant d'envoyer/recevoir des objets dans des flux et terminerons par une présentation du concept d'amitié appliqué au C++.
Un lecteur m'a écrit pour me signaler une erreur dans l'article du n°46. J'ai affirmé que les structures ne gèrent pas l'encapsulation et l'héritage. Celà est faux... La seule différence entre les mots clés struct
et class
est que dans une structure, par défaut, les membres sont publics par défaut alors qu'ils sont privés dans le cas d'une classe. Merci à toi Benoît pour cette précision.
Après ce mea culpa, je vais poursuivre cet article avec quelques rappels sur les flux. Les notions que j'aborde ici sont simples, aussi, je ne fais que les citer, sans donner beaucoup d'exemples. Par ailleurs, afin de simplifier, je ne traite ici que les flux de caractères tenant sur un octet. Il existe des classes équivalentes permettant de gérer l'Unicode. Les prototypes que je vous fournis sont approximatifs mais vous pouvez néanmoins vous y fier. Pour connaître les prototypes exacts, je vous invite à consulter les fichiers d'en-tête de votre système...
Vous connaissez déjà les opérateurs <<
et >>
pour injecter des données dans des flux et en extraire. Ces opérateurs prennent en paramètres le flux et les données à y injecter. La valeur renvoyée est le flux lui même. C'est grâce à celà que l'on peut enchaîner les entrées/sorties : cout << 5 << " est un nombre ";
commence par ajouter 5
à cout
puis retourne cout
, ce qui permet d'y ajouter " est un nombre"
. En effet, une fois ajoutée la première sortie, la suite de la ligne est équivalente à un simple cout << " est un nombre";
ostream& put (char c)
ostream& write (char* c, int n)
Il existe aussi pour les flux de sortie, pris en charge par ostream
ou ses classes filles, les méthodes put
, pour injecter un caractère dans un flux de sortie ou write
, qui permet d'injecter n caractères.
int get()
istream& get(char& ch)
istream& get(char* ch, int length)
istream& get(char* ch, int length, char char_final)
istream& getline(char* ch, int length)
istream& getline(char* ch, int length, char char_final)
istream& read(char* ch, int length)
istream& ignore(int length=1, char char_final = eof())
istream
, on peut utiliser les méthodes get
permettant de lire un caractère. Deux versions surchargées existent. La première permet de lire au plus length-1 caractères, la lecture se stoppant sur le caractère de fin de ligne s'il est rencontré avant les length-1 caractères. La seconde est équivalente à ceci prêt que l'on peut spécifier le caractère mettant fin à la lecture. Toutes deux complètent la chaîne lue d'un 0 terminal. Pour ne pas avoir de caractère de fin et ne pas insérer de 0, on préfère la méthode read
, qui lit length
octets, point.get
et getline
est que get
laisse le caractère de fin dans le flux alors que getline
l'en enlève. Dans le cas de get
, on doit l'enlever à l'aide de >>, de get(), de get(char& ch), de read, ou l'ignorer grâce à ignore
...eof
) ou un autre caractère passé en deuxième paramètre.
Pour utiliser des fichiers dans un programme C++, vous devez inclure l'entête fstream
Pour manipuler des fichiers on utilise un objet de la classe fstream
. Il existe plusieurs constructeurs :
fstream()
créé un objet générique, non associée à un fichier. On peut ultérieurement ouvrir un fichier grâce à la méthode open
ou l'"attacher" à un fichier existant grâce à la méthode attach(int fd)
.fstream(int fd)
crée un objet fstream
associé à un fichier préalablement ouvert.fstream(const char* path, int mode)
permet d'ouvrir un fichier et de l'associer à un objet fstream
. Ce constructeur, tout comme la méthode open
, utilisée dans le deuxième exemple, prend un paramètre optionnel supplémentaire permettant de fixer les droits d'accès au fichier. Par défaut, ce paramètre vaut 0644
.Les paramètres à passer sont évidents pour deux d'entre eux. Il s'agit pour fd
d'un descripteur de fichier et dans le cas de path
du chemin vers le fichier à ouvrir. En revanche, le paramètre mode
mérite que l'on s'y attarde un peu. Ces modes sont théoriquement (d'après la "bible" de Stroustrup, citée en fin d'article) définis dans la classe ios_base
mais sur mon linux, le sont dans la classe ios
, il s'agit de :
app | ouverture pour ajout (append) |
ate | ouverture et positionnement en fin (at end) |
binary | ouverture en mode binaire (traitement différent des carcatères de fin de ligne) |
in | ouverture en lecture |
out | ouverture en écriture |
trunc | fichier tronqué à 0 octets |
nocreate | ne pas créer le fichier s'il n'existe pas |
noreplace | ne pas écraser le fichier s'il existe |
Dans la pratique, pour lire dans un fichier, on utilise un objet ifstream
(input file stream) et pour écrire dans un fichier, un objet ofstream
(output file stream). Les constructeurs sont à peu près les mêmes que pour fstream
à ceci près qu'un ifstream
est forcément ouvert en mode ios::in
et un ofstream
en ios::out
. Bien sûr, pour ouvrir un fichier en lecture et écriture, on codera fstream monfic("fichier", ios::in | ios::out)
.
Voyons un exemple simple. On va écrire quelques lignes de caractères dans un fichier et les y relire.
/* ex_6_1.c++ */ #include <fstream> #include <iostream> #include <string> using namespace std; int main (void) { ofstream ecrivain("test", ios::trunc); if (!ecrivain) { cout << "Erreur lors de l'ouverture du fichier écrivain." << endl; return 1; } for (int i=0; i<5; ++i) { ecrivain << "test" << i+1 << endl; } ecrivain.close(); ifstream lecteur("test"); if (!lecteur) { cout << "Erreur lors de l'ouverture du fichier lecteur." << endl; return 2; } string s; while(lecteur >> s) { cout << s << endl; } return 0; }
On se sert pour ce premier exemple des classes ifstream
et ofstream
. On remarque l'utilisation des constructeurs, celui de l'instance d'ofstream
s'assure que le fichier test, dans le répertoire courant, sera vidé s'il existe ou créé sinon, grâce au mode d'ouverture ios::trunc
. L'instance de classe ifstream
, quand à elle conserve le mode d'ouverture par défaut, à savoir uniquement ios::in
.
On remarque ensuite dans les boucles for
que l'on utilise un flux sur fichier de la même façon que les flux standards tels que cout
et cin
, grâce aux opérateurs <<
et >>
.
Après l'écriture, on doit fermer le fichier pour le rouvrir en lecture en étant sûr de l'intégrité de son contenu. On utilise pour cela la méthode close()
, qui ne prend aucun paramètre. Il est important de noter que la plupart du temps, vous n'avez pas à vous soucier de la fermetrure des fichiers, celle ci étant effectuée lorsque les flux associés sont hors de portée. C'est à dire que la destruction d'un flux associé à un fichier provoque la fermeture de ce dernier.
Cet article comportait, dans sa verion originale une erreur, la boucle était construite ainsi :
while(!lecteur.eof()) { lecteur >> s; cout << s << endl; }
Ce qui ne produit pas le résultat escompté. La méthode eof()
ne renvoit true que lorsque on a essayé de lire après la fin de fichier. On lisait bien les 5 lignes du fichier, lecteur.eof()
restait à false
puisqu'on était à la fin du fichier mais qu'on ne l'avait pas dépassée. L'itération suivant la fin du fichier ne mettait pas à jour s
et on traitait donc deux fois la dernière ligne du fichier avant de quitter la boucle... Il faut faire attention à ce piège, dans lequel je suis tombé par inattention (Voilà ce qu'on gagne à ne pas tester du code apparemment simple).
On se sert donc du fait que lecteur >> s
ne renvoit pas le flux s'il ne contient rien pour mettre fin à la boucle.
On peut mener à bien le même genre d'exercice avec un objet fstream
:
/* ex_6_2.c++ */ #include <fstream> #include <iostream> #include <string> using namespace std; int main (void) { fstream f; f.open("test", ios::out | ios::trunc); if (!f.is_open()) { cout << "Erreur lors de l'ouverture du fichier en écriture." << endl; return 1; } for (int i=0; i<5; ++i) { f << "test" << i+1 << endl; } f.close(); f.open("test", ios::in); if (!f.is_open()) { cout << "Erreur lors de l'ouverture du fichier en lecture." << endl; return 1; } string s; while(f >> s) { cout << s << endl; } return 0; }
La principale différence réside dans l'utilisation de la méthode open
pour réouvrir un fichier en lecture à partir du même objet fstream après l'appel de close
sur ce dernier pour fermer le fichier ouvert jusqu'alors. Bien sûr, ayant affaire à un objet fstream
générique, on doit préciser si le fichier est ouvert en lecture ou en écriture (ou éventuellement les deux).
Notez également l'utilisation de la méthode is_open()
pour vérifier la "validité" de notre objet fstream
.
Voyons rapidement deux autres méthodes utiles en termes de flux mais appliquées ici aux fichiers, get
et put
, qui permettent respectivement d'extraire un caractère et d'en injecter un depuis/dans un flux. Ce qui permet de coder une copie de fichier simple :
/* ex_6_3.c++ */ #include <fstream> #include <iostream> #include <string> using namespace std; int main (void) { ifstream input("test"); ofstream output("test_copie"); if (!(input && output)) return 1; char c; while(1) { input.get(c); if (input.eof()) break; output.put(c); } return 0; }
Tout se passe dans la boucle while
. On extrait un carcatère du fichier en entrée, test, on vérifie que la fin du fichier n'est pas atteinte, grâce à eof()
, auquel cas on sort de la boucle grâce à l'instruction break
, sinon, on ajoute le caractère au fichier test_copie.
stringstream
Les stringstream
remplacent avantageusement les fonctions sprintf
et sscanf
du C puisqu'elle permet d'associer un flux à une chaîne de caractères. On peut récupérer la chaîne correspondant à ce flux à tout instant grâce à une méthode str()
. Tout comme dans le cas des flux de fichiers, on trouve des istringstream
, des ostringstream
et des stringstream
.
On peut construire des objets strinstream
à partir de rien ou d'une string
, on peut spécifier en paramètre supplémentaire les mêmes paramètres que dans le cas des autres flux (ate
, in
, out
, ...). Par défaut, le mode est fixé à in | out
, soit lecture et écriture.
Nous allons voir un exemple simple d'utilisation de stringstream
.
/* ex_6_4.c++ */ #include <sstream> #include <iostream> using namespace std; string aff_hexa(int n) { ostringstream ost; ost << n << " = " ; ost.setf(ios::hex, ios::basefield); ost << n; return ost.str(); } int main (void) { cout << "Tapez un nombre :" << endl; int i; cin >> i; cout << aff_hexa(i) << endl; return 0; }
Nous n'avons pas abordé le formatage des sorties mais je vous propose tout de même un exemple permettant de prendre en entrée un nombre et de l'afficher sous forme décimale et hexadécimale en sortie à l'utilisateur. Si vous souhaiter en savoir plus sur le formatage des sorties (décimal, octal, héxa, justification, remplissage, mise en majuscule, etc, etc...) reportez vous à la documentation des méthodes setf
, setw
, ... de la classe ios
(enfin, théoriquement d'ios_base
mais, là encore, d'ios
chez moi ...).
Revenons à notre exemple. Il demande à l'utilisateur de taper un nombre, qu'il envoie à une fonction aff_hexa
. Cette dernière créé un ostringstream
dans lequel elle injecte l'entier et la chaîne " = ". Vient ensuite la déclaration du format hexadécimal, suite à quoi on réinjecte l'entier dans le flux. Enfin, on renvoie l'objet string
associé à ce flux. Celà conduit à l'affichage de la valeur hexadécimale correspondant à l'entier saisi par l'utilisateur.
Une fois que l'on sait ce qu'ils sont, l'utilisation des stringstream est évident et extrêmement simple mais il peut vous simplifier énormément la vie, en séparant la mise en forme de la sortie. Il est ainsi plus simple de passer d'une sortie écran à une sortie sur fichier ou syslog, etc ...
Un objet peut surcharger les opérateurs <<
et >>
pour permettre aux objets d'une classe d'être simplement envoyés dans ou extraits d'un flux. Il est normal de vouloir sauver un objet en tapant flux << objet
et de le restaurer en tapant flux >> objet
. Ca tombe bien, c'est extrêmement simple. Il suffit d'avoir vu une fois comment faire. "Envoyer un objet dans un flux", celà s'appelle la sérialisation.
En pratique, pour une classe uneClasse
, il suffit d'écrire deux fonctions, une pour l'entrée :
istream& operator>>(istream& in, uneClasse& obj);
et une pour la sortie :
ostream& operator<<(ostream& out, const uneClasse& obj);
On remarque d'ores et déjà que les opérateurs de sortie et d'entrée ont des prototypes très proches puisque la seule différence (outre les flux) consiste en un modificateur const devant la référence de l'objet dans le ca de la sortie (operator<<
). En y réfléchissant rapidement, c'est tout à fait normal, afficher une variable n'en change pas la valeur alors que le but de l'opérateur d'entrée (ou d'extraction du flux) consiste justement à mettre à jour une variable à partir du contenu d'un flux.
Prenons un exemple :
/* ex_6_5.c++ */ #include <string> #include <fstream> #include <iostream> using namespace std; class uneClasse { int unEntier; string uneChaine; public: uneClasse (int i = 0, string s = "") : unEntier(i), uneChaine(s) {}; int getEntier() const {return unEntier;}; string getChaine() const {return uneChaine;}; }; ostream& operator<< (ostream& os, const uneClasse& uc) { return (os << uc.getEntier() << ':' << uc.getChaine()); } istream& operator>> (istream& is, uneClasse& uc) { char c; int entier; string chaine; is >> entier >> c; if (c==':') while (c!='\n') { is.get(c); chaine+=c; } uc = uneClasse(entier, chaine); return is; } int main(void) { uneClasse obj; cin >> obj; cout << "Objet Saisi: " << obj; ofstream output("sauve"); if (output) { output << obj; output.close(); } ifstream input("sauve"); if (input) { input >> obj; cout << "Objet du fichier : " << obj; } return 0; }
La classe uneClasse
est composée de deux propriétés privées, un entier et une chaîne. On dispose d'un constructeur public pour créer des objets et des deux "accesseurs", deux méthodes permettant de consulter la valeur des propriétés privées.
On décide arbitrairement que la représentation des objets de cette classe doit être de la forme unEntier:uneChaine
et qu'on doit donc les afficher et les saisir ainsi.
La surcharge de l'opérateur de sortie est immédiate. Il s'agit d'afficher l'entier sur le flux passé en paramètre, puis ':' puis la chaîne. Ces deux propriétés pouvant être récupérées par les accesseurs, il n'y a aucun problème particulier. Comme dit plus haut, les opérateurs << et >> renvoient le flux sur lequel ils agissent (leur premier paramètre), on aurait pu mettre à la suite des sorties sur os
une ligne seule contenant return os;
, cela aurait été équivalent.
L'opérateur d'entrée est à peine plus complexe. On commence par lire un entier, on vérifie que le caractère suivant est bien ':', et, le cas échéant, on lit tous les caractères jusqu'à la fin de ligne. Ces derniers sont ajoutés à une chaîne de caractères. On utilise ensuite le constructeur pour mettre à jour le deuxième paramètre et on renvoit le flux.
La fonction main
montre l'utilisation simple des surcharges ainsi définies avec les flux standards et un fichier. Le même comportament est bien évidemment reconductible avec des stringstream
.
L'exécution de ce bout de code est rapporté ci-dessous :
[xavier@zaz1 C++_6]$ ./ex_6_5 4477:Un objet de la classe uneClasse Objet Saisi: 4477:Un objet de la classe uneClasse Objet du fichier : 4477:Un objet de la classe uneClasse [xavier@zaz1 C++_6]$
Un mot à propos de la position des opérateurs. De part leur prototypes, on ne peut pas les définir commes membres de la classe uneClasse
, le premier paramètre étant un flux. On pourrait en revanche les déclarer comme membres de la classe (i|o)stream
. Si cette approche semble logique, elle n'est bien évidemment pas à pratiquer, on ne doit pas modifier des classes existantes parce que ça nous arrange ... Qui plus est chaque développeur devrait alors fournir sa version de la libstdc++ et ces dernières seraient incompatibles entre elles. C'est pourquoi les surcharges de ces opérateurs sont des fonctions globales. En revanche, dans les cas ou c'est possible, il est conseillé de déclarer les opérateurs comme fonctions membres.
Si la classe uneClasse
n'avait pas fourni d'accesseurs, la définition d'un nouvel opérateur de sortie n'aurait pas été possible puisqu'on doit connaître la valeur des propriétés privées pour pouvoir les utiliser dans une fonction non membre.
On utilise pour contourner ce problème la notion de fonction amie, friend
en anglais (et donc en C++). On peut déclarer au sein d'une classe un certain nombre de fonctions et/ou de classes amies. Ces entités ont alors accès aux membres privés (et protégés, forcément) de la classe.
Cette approche nous permet également de modifier la construction utilisée dans la surcharge de l'opérateur d'extraction >>
, puisqu'on peut à présent accéder directement aux membres de l'objet. Quoiqu'il en soit, c'est la méthode couramment employée et celle que vous trouverez dans le livre de Bruce Eckel, cité en référence, que je vous conseille de vous procurer, en téléchargement par internet ou dans une bonne librairie.
Passons à l'exemple, on ajoute à la section public:
de la classe les prototypes des fonctions amies :
class uneClasse { [...] public: [...] string getChaine() const {return uneChaine;}; friend ostream& operator<< (ostream& os, const uneClasse& uc); friend istream& operator>> (istream& is, uneClasse& uc); };
On modifie légèrement les surcharges précédentes en tenant compte du fait que l'on peut à présent accéder aux membres privés :
ostream& operator<< (ostream& os, const uneClasse& uc) { return (os << uc.unEntier << ':' << uc.uneChaine); } istream& operator>> (istream& is, uneClasse& uc) { char c; uc.uneChaine = ""; is >> uc.unEntier >> c; if (c==':') while (c!='\n') { is.get(c); uc.uneChaine+=c; } return is; }
Il ne faut pas oublier de "purger" la propriété uneChaine
puisqu'elle n'est plus écrasée par une affectation mais que l'on y ajoute directement les caractères au fur et à mesure de leur lecture.
zaz2:~/Documents/articles/C++/6$ g++ -o ex_6_6 ex_6_6.c++ zaz2:~/Documents/articles/C++/6$ ./ex_6_6 4479:Nantes Niort Objet Saisi: 4479:Nantes Niort Objet du fichier : 4479:Nantes Niort zaz2:~/Documents/articles/C++/6$
Après compilation et exécution, comme ci-dessus, on constate que les nouveaux opérateurs utilisent et modifient librement les propriétés privées de la classe, comme le leur permet leur statut d'"amie".
Je ne rajoute pas d'exemple sur le sujet mais sachez que vous pouvez déclarer une classe comme amie, en ajoutant friend classeAmie
dans une classe uneClasse
. Cela permet à toutes les méthodes de la classe classeAmie
d'accéder à tous les membres de la classe uneClasse
.
C'est tout pour cette fois. Le mois prochain, nous verrons quelques types utiles de la librairie standard, les vecteurs et les maps, et plus si affinités.
<pub>Cet article a été écrit sur deux distributions différentes comme le montrent les captures de terminal, une Mandrake9.0 et une tcLinux1.0.</pub>
@+
Xavier Garreau - <xavier@xgarreau.org>
http://www.xgarreau.org/
Ingénieur de recherche PRIM'TIME TECHNOLOGY
http://www.prim-time.fr/
Membre fondateur du ROCHELUG
http://www.rochelug.org/
Précédent | Index | Suivant |
a+