Entrées - sorties en C++
Les termes entrée/sortie reflètent les échanges d'information entre la
mémoire centrale et l'extérieur.
Vu depuis l'ordinateur, une entrée, déclenchée par une instruction de
lecture, modifie une ou plusieurs zones de la mémoire centrale. Le
support extérieur est le clavier ou un fichier situé sur disque, bande
...
Dans l'autre sens, une sortie déclenchée par une instruction d'écriture,
va modifier le support extérieur. Le support peut être l'écran, une
imprimante, un fichier ...
Les échanges, avec un autre ordinateur ou par modem, qui ne sont pas
structurés en terme de fichier par le système d'exploitation, ne sont
pas concernés ici.
L'introduction concerne tous les langages; elle présente deux
particularités des entrée/sortie. Le reste concerne les instructions du
langage C++.
Le paragraphe suivant présente les instructions d'E/S, sans se
préoccuper de support extérieur mais en considérant que les écritures
envoient des octets dans un 'flot', et que les lectures reçoivent des
octets d'un 'flot'.
Il n'y est question que des types de base.
Le troisième paragraphe
présente des possibilités d'E/S pour des types de l'utilisateur, soit
par redéfinition d'opérateurs, soit par échanges binaires.
Le quatrième paragraphe est consacré aux instructions relatives aux
fichiers.
Le dernier paragraphe montre comment toutes les possibilités de
conversion mises en œuvre pour les échanges avec l'extérieur, sont
utilisables en 'interne'.
I. Introduction
Présentons deux particularités des opérations d'entrée/sortie.
-
Comparaison des durées d'une E/S et d'un cycle processeur
La première particularité concerne la durée. Une opération de lecture ou
d'écriture concernant un disque par exemple demande au moins 10-4
secondes pour être exécutée; cette valeur est évidemment
approximative, mais il faut la comparer avec le temps nécessaire à
l'exécution d'une addition par exemple, qui est de l'ordre de
10-9 secondes. On conçoit donc qu'il existe des mécanismes
pour que le processeur puisse continuer à travailler pendant qu'une E/S
se déroule, à sa vitesse propre.
Pour un accès disque par exemple, il faut déplacer le bras de lecture,
avant la transmission d'octets; ce déplacement est responsable de la
lenteur, alors que la transmission est rapide. Les temps pour recopier un
octet ou un millier d'octets sont quasiment les mêmes.
C'est pourquoi, même si l'instruction écrite par le programmeur porte
sur deux octets, le premier échange avec le support extérieur va se
faire sur un millier d'octets. En effet le système d'exploitation
anticipe sur les opérations suivantes, et les prochaines instructions de
lecture seront extrêmement rapides car les octets seront disponibles en
mémoire centrale. La zone mémoire contenant ces octets pas encore lus
par le programme, est appelée tampon (buffer).
Nous avons là, un mécanisme de transmission en plusieurs phases dont
l'une est 'lente' (échange entre le support et le tampon) et les autres
rapides (échanges entre le tampon et les zones mémoires concernées
par les instructions de lecture ... tant que le tampon contient des octets
non encore lus. Ce mécanisme est toujours transparent pour le
programmeur.
Dans le cas de lecture au clavier, la différence de vitesse est encore
plus flagrante, mais on ne demande pas à l'utilisateur de taper un
millier de caractères avant d'exploiter le tampon. Il existe un mécanisme
propre à un échange avec le clavier: quand le tampon reçoit l'octet
associé à la touche 'retour chariot' (noté \n dans le langage C), il
avertit le programme que de l'information est disponible et ce dernier
reprend l'exécution de l'instruction de lecture qui avait été
interrompue. Remarquons que si le tampon contenait déjà des caractères,
non encore exploités par les lectures précédentes, il n'y aurait pas eu
d'interruption.
Résumons en indiquant succinctement comment se déroule une opération de
lecture:
- examiner la zone tampon associée à cette lecture
- si elle est vide ou pas suffisamment remplie,
- interrompre l'exécution,
démarrer un échange avec l'extérieur, et passer à une autre tâche
- quand le tampon de lecture est plein ( ou reçoit l'octet
'retour chariot', dans le cas du clavier), réactiver le programme
interrompu, qui termine alors l'exécution de son instruction de lecture
- si la zone tampon est remplie, exécuter l'instruction de lecture et
passer à l'instruction suivante.
Changement de forme
Dans la majorité des instruction d'entrée-sortie, l'information
concernée change de forme. Par exemple une information numérique, fournie
au clavier comme la suite de caractères '3', '.' ,'1' et '4' (bref
3.14) va être codée dans un format binaire sur 8 octets (type double du
langage C), car il y a anticipation sur l'utilisation de cette valeur
dans un calcul numérique.
Les processeurs de calculs demandent des codages binaires spécifiques
pour effectuer rapidement les opérations, donc les instructions
d'entrée/sortie, de beaucoup de langages, modifient la forme de
l'information. A nouveau, ce mécanisme est en général transparent pour
le programmeur, mais pas toujours !
Dans le cas d'une écriture, la conversion d'un format numérique vers une
suite de caractères représentant cette valeur en numération décimale est
toujours possible. Par contre dans le cas d'une lecture il n'en va pas
de même; en effet si l'utilisateur tape un 'E' au clavier au lieu d'un
'3', la conversion est impossible et l'opération de lecture se termine
de façon inhabituelle en prévenant le programme qui traitera la
maladresse de l'utilisateur, si le programmeur y a pensé...
Suivant le langage, une telle erreur est signalée par l'activation d'une
exception ou, de façon plus discrète en positionnant un drapeau. Pour
les langages C et C++ un drapeau est levé; en outre, avec C++, toute
autre opération de lecture dans le même tampon est interdite avant
d'avoir examiné l'erreur.
On peut se demander pourquoi une valeur numérique placé sur un support
extérieur doit être sous forme d'une suite de caractères, plutôt que la
suite d'octets du format binaire ? Donnons deux raisons.
Premièrement, l'information numérique que peut lire un humain est
la suite des chiffres de la valeur décimale sous forme caractères, avec
éventuellement un signe et un '.'.
Deuxièmement, les formats binaires ne sont pas les mêmes pour tous
les processeurs; donc la suite d'octets peut avoir deux interprétations
différentes, donc deux valeurs différentes, sur des processeurs
différents. Si votre fichier doit changer de machine, il est préférable
que les valeurs numériques ne soient pas codées en format binaire, à
moins d'être sûr que les processeurs utilisés ont la même
interprétation.
II. Présentation C++
La programmation des E/S en C++ est particulièrement facile
pour la plupart des besoins du programmeur. En effet, avec les 2 opérateurs
'<<' et '>>' et les 2 mots 'cin' et 'cout' on gère les dialogues avec le
clavier et l'écran; et avec 2 mots supplémentaires, 'ofstream' et
'ifstream' on accède aux fichiers, car pour ces deux classes, l'ouverture
est réalisée dans le constructeur et la fermeture dans le destructeur.
Quelques besoins supplémentaires nécessitent cependant d'en savoir un
peu plus; citons:
- la mise en forme (ou formatage) de l'information;
- la lecture du caractère ' ', isolé ou dans une chaîne de caractères;
- les entrées-sorties en binaire;
- la gestion des errreurs;
- la programmation du mode accès direct dans un fichier;
L'ensemble des classes peut être présenté en plusieurs parties; nous ne
présentons pas toutes les classes dont les noms peuvent dépendre de
l'environnement de programmation utilisé. Signalons simplement:
- Les classes de base concernent la gestion des tampons (streambuf) d'une
part, et les opérations ou informations communes aux flots (class ios)
d'autre part.
- On trouve ensuite la modélisation des flots dans les trois classes
istream, ostream et iostream, respectivement pour les flots en entrée,
en sortie et entrée-sortie; istream et ostream dérivent de ios, et
iostream dérive de istream et ostream.
- La structuration des instructions sur fichiers se fait par:
- filebuf, classe qui dérive de streambuf et particularise les
tampons pour fichiers
- fstreambase dérive de ios et regroupe les opérations communes
aux fichiers d'entrée et de sortie
- ifstream dérive de istream et fstreambase pour les fichiers en lecture
- ofstream dérive de ostream et fstreambase pour les fichiers en écriture
- fstream dérive de iotream et fstreambase, pour les fichiers exploités
en lecture et écriture
- La structuration des instructions sur 'flots en mémoire':
- istrstream qui dérive de istream
- ostrstream qui dérive de ostream
Signalons les quatre flots mis à la disposition du
programmeur, déclarés dans iostream.h:
extern istream cin;
extern ostream cout;
extern ostream cerr;
extern ostream clog;
III. Flots
Presque toutes les lectures des types de base se font avec le seul operator>>
(et ses redéfinitions presque toutes les écritures des types de base également
avec operator<<.
Dans ce paragraphe sont exposées tout d'abord les instructions sur flot
de sortie, puis les instructions depuis le flot d'entrée; ensuite nous
voyons la gestion d'erreurs, puis les possibilités de mise en forme de
l'information (largeur, nombre de chiffres après la virgule ...) et
enfin l'utilisation des manipulateurs.
Instructions avec flot de sortie (class ostream)
Classes, opérations ... sont déclarées dans iostream.h. Signalons
simplement les classes ios, et ostream. Dans le langage
et sa bibliothèque standard, sont définies les opérations de sortie, sur
les types de base, par operator<<.
L'extrait ci-dessous de la classe ostream montre quelques
redéfinitions permettant d'écrire les types de base; les redéfinitions
pour les qualificatifs 'signed' ou 'unsigned' des types char ou int,
existent aussi, mais n'ont pas été recopiées.
class ostream : virtual public ios {
...
ostream & operator<<(char c)
ostream & operator<<(short ii)
ostream & operator<<(long n)
ostream & operator<<(int i)
ostream & operator<<(float xf)
ostream & operator<<(double x)
ostream & operator<<(long double xx)
ostream & operator<<(const char* ch);
ostream & operator<<(const void* ptr);
ostream & put(char c)
ostream & flush()
...
}
Remarquez des redéfinitions différentes pour les types const void *
et const char *
Le fait que operator<< renvoie un flot, donne un sens à une écriture
telle que:
cout << "Valeur : " << i;
Voici d'autres méthodes de la classe ostream:
- ostream& put(char c)
permet d'afficher un caractère, comme le fait
operator<<.
cout.put(car);
- ostream& flush()
permet de vider le tampon de sortie sur le support extérieur;
cette méthode est appelée quand on utilise endl
cout.flush();
Instructions avec flot d'entrée (class istream)
L'opérateur de lecture, operator>> est aussi simple à utiliser que son
équivalent en écriture, mais il a un comportement vis à vis
des espaces, qui empêche leur lecture. Par exemple, avec les
instructions:
char c;
cin >> c;
il ne peut pas y avoir d'espace dans la variable c, non plus qu'un des
octets équivalent à un espace du point de vue du langage C (\t \n \r \f
\v).
Ignorer ces caractères est pratique pour saisir des informations
numériques, mais l'est moins quand il est question de chaînes de caractères.
Précisons comment est exécutée l'intruction:
cin >> v;
- lire tous les octets équivalents à un espace; cela peut vider le
tampon de lecture, et le programme se met alors en attente de
nouvelles frappes au clavier;
- lire les octets suivants donc, à partir du premier caractère
différent d'un espace, et les interpréter d'après le type de v;
- arrêter la lecture au premier espace rencontré, ou quand
l'interprétation est impossible pour le type de v (ce qui peut arriver
si v est de type numérique);
- modifier la zone mémoire de v, variable concernée, s'il n'y a pas
d'erreur.
Suivant le type de v:
- short, int, long: toute suite pouvant commencer par
un signe, suivie de chiffres représentant un entier est lue, l'entier
est évalué et v est modifié s'il n'y a pas d'erreur;
- float, double, long double: toute suite pouvant
commencer par un signe, suivie de chiffres ou '.' ou 'e'
représentant une valeur décimale est lue, la valeur est évaluée et v est
modifié, s'il n'y a pas d'erreur;
- char: un seul octet est lu, qui est le caractère qui
suit les espaces initiaux;
- char *: la lecture s'arrête au premier espace
rencontré, et les octets lus sont placés dans la zone mémoire associée à
v, puis l'octet \0 est placé pour marquer la fin de la chaîne. Observez
qui, par défaut, il n'y a aucune vérification entre la longueur
de la zone mémoire prévue et celle de la zone modifiée; si la zone
modifiée est plus longue que la zone prévue par le programmeur le
comportement du programme sera indéfinie.
Quelques méthodes de la classe istream:
- istream& get(char& c)
permet de lire un caractère dans le flot, même si c'est un espace.
Cette méthode sert, par exemple, pour les analyses syntaxiques de
fichiers.
cin.get(car);
- int get()
ressemble à une fonction de la bibliothèque d'E/S en C.
int i=cin.get();
- istream& getline(char* tc, int taille, char delim='\n');
lit au plus taille-1 caractères, en s'arrétant si le délimiteur est
rencontré avant, puis place '\0' dans le tableau tc; si delim est
rencontré, il est enlevé du flot.
Dans l'exemple suivant, si on tape 20 caractères puis retour-chariot, le
tampon clavier est vide après exécution de getline().
char tc[80];
cin.getline(tc,sizeof(tc));
- istream& get(char* tc, int taille, char delim='\n');
lit au plus taille-1 caractères, en s'arrêtant si le délimiteur est
rencontré avant, puis place '\0' dans le tableau tc; si delim est
rencontré, il est toujours dans le flot.
Dans l'exemple suivant, si on tape 20 caractères puis
retour-chariot, le prochain octet à lire dans le flot est \n.
char tc[80];
cin.get(tc,sizeof(tc));
- int peek()
sert à examiner un caractère sans le lire; cela peut servir dans les
analyses syntaxiques.
char car=cin.peek ();
- istream& putback(char c);
permet de replacer dans le flot le dernier caractère lu; cela
peut servir dans les analyses syntaxiques.
cin.putback(car);
- istream& ignore(int n=1,int delim=EOF) ;
permet d'enlever des caractères d'un flot. Cela peut servir, par
exemple, quand, après détection d'une erreur, on veut ignorer la fin de
la ligne; on précisera alors le caractère '\n' comme délimiteur.
cin.ignore(1000,'\n');
- int gcount() ;
fournit le nombre d'octets lus
Voici les valeurs obtenues, après exécution de
cin.getline(tc,4);
à lire gcount() observation
a\n 2 \n n'est plus dans le tampon, et compte dans gcount
ab\n 3 \n n'est plus dans le tampon
abc\n 3 une nouvelle lecture est nécessaire pour évacuer \n
Remarque: après exécution de getline(tc,n) , on a toujours:
gcount()<=n-1
Conclusion: avec getline(tc,sizeof(tc) ,
on saura que la ligne est lue complètement, avec évacuation de \n,
avec le test:
gcount() > strlen(tc);
Lecture sécurisée de chaînes de caractères
Pour éviter les débordements de la zone mémoire réservée lors d'une
lecture de chaîne de caractères, il faut utiliser l'une des deux
méthodes:
istream& get(char* tc, int taille, char delim='\n');
istream& getline(char* tc, int taille, char delim='\n');
pour lesquelles on précise la taille de la zone recevant les octets lus.
A la différence de operator>>, ces méthodes permettent également de
mémoriser les espaces lus. Elles placent toujours en mémoire l'octet \0,
marque de fin de chaîne; le caractère delim n'est pas mémorisé.
Avec la méthode get() le caractère delim est toujours dans le
tampon, ce qui présente:
- un avantage, puisqu'on peut savoir, en lisant un caractère de
plus, si la ligne a été lue complètement; exemple:
char tc[80], c;
cin.get(tc,sizoeof(tc)); c=cin.peek();
if(c!='\n')cout<<"il reste des caractères à lire";
- et un inconvénient: il faut penser à enlever l'octet \n du tampon.
Précisons que lecture et mémorisation s'arrêtent au premier des
événements qui survient:
- la fin de fichier est dépassée
- le caractère delim est dans le flot d'entrée
- taille-1 caractères ont été lus
Gestion d'erreur
L'état de chaque flot est conservé dans un entier (type int) qu'on peut
consulter et modifier en utilisant des méthodes et des constantes
définies dans la classe ios; cet état peut être modifié par toute
opération sur le flot.
Quand le flot est en état d'erreur toute opération, autre que forcer le
changement d'état avec clear() ou setstate(), échoue; cela oblige à
considérer l'erreur.
On trouve dans la classe ios des constantes symboliques associées aux
bits d'erreur:
enum io_state {
goodbit = 0x00,
badbit = 0x01, // erreur matérielle sur E/S
eofbit = 0x02, // fin de flot dépassée
failbit = 0x04 // erreur logicielle sur E/S
}
On utilisera les constantes sous la forme: ios::goodbit par
exemple.
Tester un flot est réalisé par une des 4 fonctions dont
l'utilisation est évidente:
- int eof() const ;
- int fail() const ;
- int bad() const ;
- int good() const ;
Une opération ne peut réussir sur le flot f que si f.good() est non nul.
Pour tester l'état d'un flot, on peut, en outre, utiliser l'une des
formes suivantes, en remarquant que les deux premières sont équivalentes:
if(f.good()) ... // flot en état correct
if( f ) ... // flot en état correct
if( ! f ) ... // flot en état erreur
Cela est rendu possible grace à la définition de la conversion d'un flot
en void *, et par une redéfinition de operator! (voir
la classe ios).
Examen ou modification du mot d'état:
- int rdstate() const;
renvoie l'état d'erreur du flot
- void clear(int e = goodbit);
positionne le flot à l'état e; par défaut le flot est placé
dans l'état correct (goodbit).
- void setstate(int bits);
Positionne des bits de l'état, sans modifier les autres, comme dans
l'exemple suivant où le seul bit eofbit est modifié:
f.setstate(ios::eofbit);
Mise en forme
a) Un exemple de mise en forme
les informations suivantes concernant le numéro de compte et le solde
de trois clients de la banque Route, sont plus ou moins lisibles. Voici
deux présentations:
Pair Noël 87654 432.1
Delajaretière Amélie Antoinette 123 -43210.321
Lebon Max 456789 7.65432
Pair Noël ####87654 +432.10
Delajaretière Amélie Antoinette ######123 -43210.32
Lebon Max ###456789 +7.65
La deuxième utilise les éléments suivants de mise en forme:
- le nom occupe 17 positions;
- le prénom est cadré à gauche sur 18 positions;
- le numéro est écrit sur 9 positions, avec remplissage par des #;
- le solde est écrit sur 11 positions, avec 2 chiffres après la virgule
et la présence d'un signe obligatoire.
b) Programmation avec manipulateurs
La deuxième présentation est obtenu par le petit programme ci-dessous:
#include
#include
struct Enr{
char nom[20],prenom[20];
int num; double solde;
};
void aff(Enr e, ostream & f) {
f << setw(17) << e.nom << ' '
<< setw(18) << setiosflags(ios::left) << e.prenom
<< resetiosflags(ios::left) << setiosflags(ios::right)
<< setfill('#') << setw(9) << e.num
<< setiosflags(ios::fixed) << setprecision(2)
<< setfill(' ') << setw(11) << e.solde
<< endl;
}
Commentons successivement certaines lignes:
- f << setw(17) << e.nom << ' '
afficher le nom sur 17 positions
- << setw(18)<< setiosflags(ios::left)<< e.prenom
afficher le prénom sur 18 positions, cadré à gauche
- << resetiosflags(ios::left)<< setiosflags(ios::right)
annuler cadrage à gauche, revenir au cadrage à doite
- << setfill('#')<< setw(9)<< e.num
utiliser # comme caractère de remplissage et afficher sur 9
positions
- << setiosflags(ios::fixed)<< setprecision(2)
notation décimale fixe, avec 2 chiffres après la virgule
- << setfill(' ')<< setw(11)<< e.solde
utiliser ' ' comme remplissage et afficher sur 11 positions
c) Remarques
Voici quelques précisions relatives à la largeur de champ:
- si la largeur précisée est trop petite, l'information à afficher
n'est jamais tronquée
- par défaut la largeur est 0
- la largeur précisée n'est utilisée que pour un champ, puis elle
reprend sa valeur par défaut.
Les informations de formatage sont mémorisées dans
- un mot de formatage, modifiable par une des méthodes setf ou
unsetf ou un des manipulateurs setiosflags ou
resetiosflags
- un caractère, pour remplissage, modifiable par la méthode
fill ou le manipulateur setfill
- un entier, nombre de chiffres après la virgule, modifiable par la
méthode precision ou le manipulateur setprecision
- un entier, qui mémorise la largeur de champ, modifiable par la
fonction width ou le manipulateur setw
Les constantes suivantes, définies dans la classe ios, permettent
le formatage; les valeurs numériques associées ne sont pas nécessairement
les mêmes pour tout environnement.
typedef int fmtflags;
enum fmt_flags {
// Un ou plusieurs bits à utiliser dans .setf(int bits)
boolalpha = 0x0001,
showbase = 0x0200, // montrer la base de numération
showpoint = 0x0400, //
showpos = 0x0800, // afficher le signe
skipws = 0x1000, // ignorer les espaces
unitbuf = 0x2000,
uppercase = 0x4000, // chiffres hexadécimaux en majuscule
// Bit à utiliser dans .setf(int bit, int champ)
// Un seul des trois bits suivants est activé (champ:basefield)
dec = 0x0002,
hex = 0x0008,
oct = 0x0040,
// Un seul des trois bits suivants est activé (champ:ajustfield)
internal = 0x0010,
left = 0x0020,
right = 0x0080,
// Un seul des deux bits suivants est activé (champ:floatfield)
fixed = 0x0004,
scientific = 0x0100,
// Champs à utiliser dans .setf(int bit, int champ)
basefield = dec | oct | hex,
adjustfield = left | right | internal,
floatfield = scientific | fixed
};
d) Programmation sans manipulateur
La mise en forme peut s'écire sans utiliser de manipulateurs comme on le
voit ci-dessous où le programme précédent est réécrit, en utilisant
uniquement les méthodes de la classe ios.
void aff(Enr e, ostream & f) {
// nom sur 17 positions
f.width(17); f << e.nom;
// prénom sur 17 positions, cadré à droite
f.width(18); f.setf(ios::left,ios::adjustfield); f<
IV. Aspects divers
Redéfinition d'opérateurs
La classe Enreg ci-dessous regroupe le nom, le prénom, le numéro de
compte et le solde du client d'une banque par exemple; de plus la
méthode aff permet d'afficher ces informations sur un client.
Nous redéfinissons operator<<, puis nous l'utilisons
pour afficher un objet 'Enreg'.
class Enreg{
public:
char nom[20],prenom[20];
int num; double solde;
void aff(ostream & f){f<
E/S en binaire
Ces opérations sont de simples copies d'octets, sans interprétation;
elles sont donc immédiatement utilisables pour des types définies par
l'utilisateur.
Elles permettent par exemple d'avoir la notion d'enregistrements de
longueur fixe dans un fichier et donc de pouvoir effectuer des accès
directs.
Attention cependant car les fichiers ainsi créés ne sont pas
lisibles sur toute machine.
Il existe une opération d'écriture et une opération de lecture.
ostream& write(const char* s,int n);
permet de copier les n octets à l'adresse s vers le flot.
Voici un exemple de copie d'un tableau de 3 enregistrements en
une seule instruction.
Enreg te[]={{"Pair", "Noël", 87654, 432.1},
{"Delajaretière", "Amélie Antoinette", 123, -43210.321},
{"Lebon Max", 456789, 7.65432}
};
flotSortie.write((char *)te,sizeof(te));
istream& read(char* s,int n);
permet de copier n octets depuis un flot.
Voici un exemple de lecture des trois premiers enregistrements d'un
fichier, tous du type
Enreg, directement dans un tableau. La
lecture est effectuée en une seule instruction, puis on affiche dans une
boucle les trois enregistrements en utilisant la méthode
aff de
la classe Enreg.
ifstream flotEntree("testEnr");
if(flotEntree) {
Enreg tt[3]; int n=3;
flotEntree.read( (char *)tt, n*sizeof(Enreg));
flotEntree.close();
for(int i=0; i
Manipulateurs
En utilisant astucieusement la redéfinition des opérateurs << ou >>,
on peut préciser une action à effectuer.
On a déjà utilisé ceci, par exemple en écrivant:
cout << "Valeurs " << endl << 125;
on sait que 'endl' provoque, entre autre, le passage à la ligne suivante.
Ceci est possible car on trouve dans iostream.h:
- la définition du type OMANIP (ce nom peut changer):
typedef ostream& (*OMANIP)(ostream&);
- la surcharge de operator<< pour ce type:
ostream& operator<<(OMANIP func) { return (*func)(*this); }
- la définition de endl
ostream& endl(ostream& i) ;
Donnons un exemple de création d'un manipulateur, appelé SEP qui permet
de séparer des champs, en insérant un ';':
ostream & SEP(ostream & f) { f << ';'; return f;}
qui peut être utilisé comme suit:
cout<<"Pair"<<SEP<<"Jean"<<SEP<<4321
<<SEP<<123.4<<endl;
pour fournir le résultat:
Pair;Jean;4321;123.40
La fonction SEP est, comme endl, une fonction de prototype fixé,
qui ne prend qu'un paramètre (type ostream& ), le flot utilisé, et
renvoie ce flot.
Des types plus complexes, déclarés dans iomanip.h, permettent d'avoir
des actions paramétrées, comme par exemple celles des manipulateurs:
setfill(char remplissage), setprecision(int nb_chiffres)
Manipulateur |
Fonction |
ostream& endl(ostream& i) ;
| envoie \n sur le flot et vide le tampon |
ostream& ends(ostream& i) ; |
envoie \0 sur le flot |
ostream& flush(ostream&) ; |
vide le tampon |
ios& dec(ios&) ; |
passe en numération décimale |
ios& hex(ios&) ; |
passe en numération hexadécimale |
ios& oct(ios&) ; |
passe en numération octale |
istream& ws(istream&) ; |
enlèves les prochains espaes du flot d'entrée |
Les manipulateurs suivants sont plus complexes, car ils s'utilisent
avec un paramètre; ils nécessitent l'inclusion de iomanip.h.
Manipulateur |
Fonction |
resetiosflags(int bits)
|
Permet de remettre à 0 les bits précisés;
ces bits sont spécifiés par une combinaison logique de constantes
de type ios::fmtflags.
|
setiosflags(int bits)
| Permet d'activer les bits précisés |
setbase(int base) |
Fixe la base de numérotation à utiliser, qui peut être 8, 10 ou 16
cout << setbase(16)
<< setiosflags(ios::showbase|ios::uppercase) << 250;
|
setprecision(int nbchiffres)
|
Fixe le nombre de chiffres après le point décimal .
|
setw(int largeur)
|
Fixe la largeur minimale du champ pour l'écriture suivante;
n'est valable que pour une seule écriture.
|
setfill(char car)
|
Fixe le caractère de remplissage.
|
Entrée / sortie en C
Nous pouvons utiliser, en plus des instructions C++, les fonctions d'E/S
du langage C pour lire ou écrire. Cela peut poser un problème, par
exemple, pour les affichages à l'écran car deux
flots, stdout pour le langage C et cout pour C++ sont dirigés vers
l'écran, mais il n'y a aucune raison pour qu'ils soient synchronisés,
car ils utilisent des tampons distincts.
Pour résoudre ce problème, la méthode suivante de la classe streambuf,
static bool sync_with_stdio(int sync = 1);
permet d'activer la synchronisation, ce qui est fait par défaut.
Exemples:
cout.sync_with_stdio(false);
printf(" -> avec printf");
cout << " -> avec operator<< " << endl;
printf("\n");
cout << "cout.sync_with_stdio(false), puis operator<< et printf" << endl;
cout << " -> avec operator<< ";
printf(" -> avec printf \n");
cout << endl;
V. Fichiers
Les opérations sur fichiers se répartissent dans le temps.
On commence par localiser le fichier, vérifier que les accès demandés
sont compatibles avec les droits de l'utilisateur, puis préparer les
zones mémoires en vue des E/S ultérieures; c'est l'opération
d'ouverture.
Viennent ensuite une suite d'opérations d'échanges entre mémoire et
support extérieur, avec éventuellement des changements explicites de
position sur le support, comme recommencer une lecture au début ... il
y a toujours des changements de position implicites à chaque
transmission, mais le programmeur n'a pas à s'en préoccuper.
Enfin le programmeur peut indiquer qu'il a terminé l'exploitation du
fichier, pour libérer les ressources mobilisées et laisser le fichier
dans son état définitif; c'est l'opération dite de fermeture.
Nous allons voir comment ouvrir un fichier, comment s'y déplacer
explicitement et comment le fermer. Les opérations d'échanges ont déja
été vues car ce sont celles sur les flots.
Les déclarations nécessaires sont dans iostream.h et dans fstream.h; ce
dernier fichier regroupe les déclarations des 3 classes ifstream
(fichier en lecture) ofstream (fichier en écriture) et
fstream (fichier en lecture écriture).
déclaration, ouverture
Elle est faite soit par le constructeur, soit par un appel à la méthode
open.
Exemple de déclaration et d'ouverture d'un fichier en écriture, en
écrasant s'il existait, le contenu du fichier
ofstream fic("unfichier");
if(!fic) {
cerr << "Erreur en créant " << nomFic << endl;
}
Voici un exemple d'ouverture d'un fichier en lecture, en se positionnant
à la fin du fichier (ios::ate), puis en demandant la taille du
fichier pour l'afficher.
flotEntree.open("testEnr",ios::ate);
if(flotEntree) {
long taille=flotEntreep; tellg();
cout << "taille de testEnr: " << taille << endl;
flotEntree.close();
}
else cerr<< "Erreur à la lecture de testEnr" << endl;
ifstream( const char * nomFic, int mode=ios::in,
int prot=filebuf::openprot)
constructeur gérant l'ouverture dans le mode précisé; s'il s'agit d'une
création ( ios::nocreate est inactif) prot est utilisé
ofstream( const char * nomFic, int mode=ios::out,
int prot=filebuf::openprot)
constructeur gérant l'ouverture dans le mode précisé; s'il s'agit d'une
création ( ios::nocreate est inactif) prot est utilisé
void open( const char * nomFic, int mode,
int prot=filebuf::openprot)
ouverture dans le mode précisé; s'il y a création, prot est utilisé dans
les systèmes unix.
Dans les exemples, le fichier 'donnees1' est créé, et un fichier de
même nom est écrasé s'il existait; alors qu'avec l'autre cas, si 'donnees2'
existe, l'ouverture échoue, et on ne perdra pas son contenu.
f.open("donnees1", ios:out)
f.open("donnees2", ios:out | ios:noreplace)
void is_open( )
renvoie une valeur non nulle si le fichier est ouvert
fstream(const char *s, int mode=ios_base::in |
ios_base::out,
long protection= 0666 );
fichier en lecture et écriture
Modes d'ouverture
in |
fichier en lecture
|
out |
fichier en écriture
|
binary |
précise le contenu du fichier pour les systèmes
d'exploitation, comme dos ou windows, qui distinguent les
types de fichiers textes et binaires.
|
trunc |
fichier tronqué ou ecrasé à l'ouverture en écriture, donc
le contenu avant ouverture est perdu
|
app |
en écriture le fichier est ouvert à la fin; donc les
opérations vont ajouter des octets; le contenu avant
ouverture n'est pas perdu.
|
ate |
fichier ouvert à la fin; si l'ouverture est en écriture,
on va ajouter des octets en conservant le contenu antérieur.
Si l'ouverture est en lecture seule, cela permet d'avoir
la taille du fichier, puis il faut se repositionner au début
du fichier pour effectuer des lectures.
|
nocreate |
le fichier doit exister, sinon l'ouverture échoue; peut être utile
pour s'assurer qu'on ajoute effectivement à un fichier qui existe.
|
noreplace |
le fichier ne doit pas exister, sinon l'ouverture échoue; évite
d'écraser un fichier existant
|
position
A tout instant le flot associé à un fichier connait la position dans le
fichier, du prochain octet à lire pour un flot en lecture, ou à écrire
pour un flot en écriture. Deux fonctions permettent au programmeur
d'avoir cette position. Le type streampos est équivalent à un
entier sur 4 octets.
streampos tellg();
position du prochain octet à lire.
Dans l'exemple qui suit, on demande d'ouvrir et d'être placé à la fin
du fichier; dans ce cas la position fournie, correspond à la taille du
fichier.
flotEntree.open("testEnr",ios::ate);
if(flotEntree) {
long taille=flotEntree.tellg();
cout << "taille de testEnr: " << taille << endl;
flotEntree.close();
}
else cerr<< "Erreur à la lecture de testEnr" << endl;
streampos tellp() ;
position du prochain octet à écrire
déplacement
Il est possible de se déplacer dans le flot pour aller à une position
désirée, en utilisant les fonctions seekg() pour un flot en
lecture, et seekp()pour un flot en écriture. Deux versions pour
chaque fonction, la première s'utilisant avec un postion absolue,
la deuxième s'utilisant avec une position relative.
istream& seekg(streampos p)
istream& seekp(streampos p)
le prochain octet à lire pour seekg() ou à écrire pour seekp() se
trouve à p octets du début du fichier.
istream& seekg(streamoff dep, ios::seek_dir rel) ;
istream& seekp(streamoff dep, ios::seek_dir rel) ;
le premier paramètre est un déplacement en octets, le deuxième
paramètre indique comment doit être interprété le déplacement, par une
des trois valeurs (définies dans la classe ios):
- ios::beg dep octets par rapport au début
du fichier (appel équivalent à la fonction avec un seul paramètre)
- ios::cur dep octets par rapport à la
position courante
- ios::end dep octets par rapport à la fin du
fichier; dans ce cas dep est négatif.
fermeture
void close()
Dans l'exemple suivant, après ouverture, on écrit en une seule fois,
puis on ferme le fichier.
fstream flotSortie("testEnr");
if(flotSortie) {
flotSortie.write((char *)te,sizeof(te));
flotSortie.close();
}
else {
cerr<< "Erreur à la création de testEnr" << endl;
VI. Flots en mémoire
Toutes les possibilités de conversion entre les types de base et les
suites de caractères, vues ci-dessus pour les flots orientés fichiers
sont utilisables pour des suites de caractères exploitées en mémoire.
Ces outils sont utilisés quand le programmeur échange des
informations par le biais d'objets d'interface utilisateur, comme un
champ de saisie par exemple; dans ce cas les échanges se font entre les
variables du programme et les zone mémoires de ces objets graphiques qui
doivent toujours avoir les informations sous forme suite de caractères.
Il existe la classe ostrstream pour les conversions à partir
des types de base vers les suites de caractères, et la classe
istrstream pour les conversions dans l'autre sens.
L'usage de ces classes nécessite l'inclusion de strstream.h
classe ostrstream
Un objet ostrstream pourra recevoir, comme un flot, autant de caractères
qu'on le désire; cette suite, codée est accessible par la fonction
membre str qui renvoie un char *.
Si la suite d'octets ainsi constiutée doit être interprétée comme
une chaîne du langage C il faudra la faire suivre par \0, ce qui est
réalisée par ends, qui insère \0, comme 'endl' insére l'octet
\n.
Voici quelques méthodes de la classe ostrstream: deux constructeurs,
la méthode pcount() et la méthode str().
ostrstream()
Ce constructeur permet de préparer un objet à recevoir un flot. On
peut ensuite placer autant d'octets que l'on veut dans ce flot, sans se
soucier de gestion de place en mémoire.
ostrstream fm;
fm << "valeur: ";
fm << '+' << 32*32;
ostrstream( char *tab, int lgtab, ios::openmode=ios::out)
Avec ce constructeur, la zone mémoire recevant les octets est
précisée (adresse et longueur) par le programmeur. Comme on fournit la
longueur de cette zone, il n'y aura jamais de débordement réalisé; tous
les octets envoyé dans le flot, en plus de lgtab sont ignorés.
Dans l'exemple ci-dessous, le tablau ch est remplii, avec au maximum
25 octets.
indirectement par operator<< appliqué à l'objet fch.
char ch[25]; int nch=sizeof(ch);
ostrstream fch(ch,nch);
fch << "valeur: " << '+' << 32*32 << ends;
cout << ch << endl;
int pcount() const;
fournit le nombre total d'octets transmis au flot, incluant \0 si
celui-ci est transmis.
char ch[25]; int nch=sizeof(ch);
ostrstream fch(ch,nch);
fch << "valeur: " << '+' << 32*32 << ends;
cout << "Nombre d'octets: " << fch.pcount() << endl;
char *str();
fournit l'adresse du premier octet du flot constitué.
Après un appel à str() il n'est plus possible d'envoyer de
nouveaux octets à ce flot, et la zone de mémoire dynamique doit être
gérée par le programmeur.
S'il n'y pas pas eu d'appel à str, cette zone mémoire est gérée par le
destructeur de la classe ostrstream.
ostrstream fm;
fm << "valeur: " << '+' << 32*32 << ends;
char * txt=fm.str();
... utilisations de txt
delete txt;
classe istrstream
On peut récupérer toute variable d'un type de base à partir d'un objet
de la classe istrstream, comme on le ferait à partir du clavier, avec un
peu plus de souplesse cependant pour le programmeur.
istrstream(char *tc, long int taille);
Dans l'exemple ci-dessous, on récupère une entrée clavier dans le
tableau de caractères ligne , puis on teste à partir de là si
la valeur tapée est un entier court, en vérifiant que le caractère qui
suit la valeur numérique, doit être équivalent à un
espace ou \0.
Ainsi on détecte une entrée telle que 12e4 , dans
laquelle le carcatère e est incorrect.
Une valeur telle que 40000 est aussi détectée comme une
erreur (voir la variable cr), car elle dépasse la capacité de codage d'un
short int .
char ligne[80]; short int e; char car; int cr;
cout << "entier: "; cin.getline(ligne,sizeof(ligne));
istrstream fe(ligne,sizeof(ligne));
fe >> e; cr=fe.fail(); // compte rendu d'erreur
fe.get(car);
// Lecture correcte si la suite de chiffres est suivie par un
// espace ou par \n (alors car == marque de fin de chaine)
if( !cr && (isspace(car)||car=='\0')) cout << "e=" << e << endl;
else cout << "! erreur ! (" << cr << ')' << endl;
VII. Références
Les références signalées concernent C++ dans son ensemble.
- Des livres
- C. Delannoy - Programmer en langage C++ / Eyrolles
- B. Stroustrup - Le langage C++ / Addison Wesley
En ligne
-