Il est né ?
Vous êtes ici : xgarreau.org >> aide >> devel >> cpp : Flux, fichiers, chaînes de caractères et copains (g++ - partie 6)
Version imprimable

Flux, fichiers, chaînes de caractères et copains (g++ - partie 6)

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++.

Correction d'erreur

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.

Les flux

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())

Pour les flux entrants, gérés par les 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.
La différence entre les méthodes 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...
Cette dernière méthode ignore par défaut un caractère du flux d'entrée. Si on lui spécifie un paramètre, il s'agit du nombre de caractères à  ignorer. La limite étant la fin de flux (eof) ou un autre caractère passé en deuxième paramètre.

Les Fichiers : fstream

Du texte

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 :

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 :

appouverture pour ajout (append)
ateouverture et positionnement en fin (at end)
binaryouverture en mode binaire (traitement différent des carcatères de fin de ligne)
inouverture en lecture
outouverture en écriture
truncfichier tronqué à  0 octets
nocreatene pas créer le fichier s'il n'existe pas
noreplacene 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.

La copie

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.

Les flux sur chaînes : les 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 ...

Serialisation

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.

Classes et fonctions amies

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/

Références :


Précédent Index Suivant

a+

Auteur : Xavier GARREAU
Modifié le 10.09.2004

Rechercher :

Google
 
Web www.xgarreau.org