15.4. Les flux d'entrée / sortie

La plupart des fonctionnalités des flux d'entrée / sortie sont implémentées au niveau des classes template basic_ostream et basic_istream. Ces classes dérivent toutes deux directement de la classe basic_ios, dont elles héritent de toutes les fonctionnalités de gestion des tampons et de gestion d'état.

Les classes basic_ostream et basic_istream seront sans doute les classes de flux que vous utiliserez le plus souvent, car c'est à leur niveau que sont définies toutes les fonctionnalités de lecture et d'écriture sur les flux, aussi bien pour les données formatées telles que les entiers, les flottants ou les chaînes de caractères, que pour les écritures de données brutes. De plus, les flux d'entrée / sortie standards cin, cout, cerr et clog sont tous des instances de ces classes.

La bibliothèque standard définit également une classe capable de réaliser à la fois les opérations de lecture et d'écriture sur les flux : la classe basic_iostream. En fait, cette classe dérive simplement des deux classes basic_istream et basic_ostream, et regroupe donc toutes les fonctionnalités de ces deux classes.

15.4.1. La classe de base basic_ostream

La classe basic_ostream fournit toutes les fonctions permettant d'effectuer des écritures sur un flux de sortie, que ces écritures soient formatées ou non. Elle est déclarée comme suit dans l'en-tête ostream :

template <class charT,
    class traits = char_traits<charT> >
class basic_ostream : virtual public basic_ios<charT, traits>
{
public:
// Les types de données :
    typedef charT                     char_type;
    typedef typename traits::int_type int_type;
    typedef typename traits::pos_type pos_type;
    typedef typename traits::off_type off_type;
    typedef traits                    traits_type;

// Le constructeur et le destructeur :
    explicit basic_ostream(basic_streambuf<char_type, traits> *tampon);
    virtual ~basic_ostream();

// Les opérations d'écritures formatées :
    basic_ostream<charT, traits> &operator<<(bool);
    basic_ostream<charT, traits> &operator<<(short);
    basic_ostream<charT, traits> &operator<<(unsigned short);
    basic_ostream<charT, traits> &operator<<(int);
    basic_ostream<charT, traits> &operator<<(unsigned int);
    basic_ostream<charT, traits> &operator<<(long);
    basic_ostream<charT, traits> &operator<<(unsigned long);
    basic_ostream<charT, traits> &operator<<(float);
    basic_ostream<charT, traits> &operator<<(double);
    basic_ostream<charT, traits> &operator<<(long double);
    basic_ostream<charT, traits> &operator<<(void *);
    basic_ostream<charT, traits> &operator<<
          (basic_streambuf<char_type, traits> *tampon);

// Classe de gestion des exceptions pour les opérateurs d'écritures formatées :
    class sentry
    {
    public:
        explicit sentry(basic_ostream<charT, traits> &);
        ~sentry();
        operator bool();
    };

// Les opérations d'écritures non formatées :
    basic_ostream<charT, traits> &put(char_type);
    basic_ostream<charT, traits> &write(const char_type *p, streamsize taille);

// Les opérations de gestion du tampon :
    basic_ostream<charT, traits> &flush();
    pos_type tellp();
    basic_ostream<charT, traits> &seekp(pos_type);
    basic_ostream<charT, traits> &seekp(off_type, ios_base::seekdir);

// Les opérations de gestion des manipulateurs :
    basic_ostream<charT, traits> &operator<<
          (basic_ostream<charT, traits> & (*pf)(
	      basic_ostream<charT, traits> &));
    basic_ostream<charT, traits> &operator<<
          (basic_ios<charT, traits> & (*pf)(basic_ios<charT, traits> &));
    basic_ostream<charT, traits> &operator<<
          (ios_base & (*pf)(ios_base &));
};

Comme vous pouvez le constater, le constructeur de cette classe prend en paramètre un pointeur sur l'objet tampon dans lequel les écritures devront être réalisées. Vous pouvez donc construire un flux de sortie à partir de n'importe quel tampon, simplement en fournissant ce tampon en paramètre au constructeur. Cependant, il ne faut pas procéder ainsi en général, mais utiliser plutôt les classes dérivées de la classe basic_ostream et spécialisées dans les écritures sur fichiers et dans des chaînes de caractères.

Les écritures formatées sont réalisées par l'intermédiaire de différentes surcharges de l'opérateur d'insertion operator<<, dont il existe une version pour chaque type de donnée de base du langage. Ainsi, l'écriture d'une valeur dans le flux de sortie se fait extrêmement simplement :

// Écriture d'une chaîne de caractères sur le flux de sortie standard :
cout << "Voici la valeur d'un entier :\n";

// Écriture d'un entier sur le flux de sortie standard :
cout << 45;
Vous constaterez que, grâce aux mécanismes de surcharge, ces écritures se font exactement de la même manière pour tous les types de données. On ne peut faire plus simple...

Note : Les opérations de formatage prennent en compte toutes les options de formatage qui sont stockées dans la classe de base ios_base. En général, les opérations d'écriture ne modifient pas ces options. Toutefois, la largeur minimale des champs dans lesquels les résultats sont formatés est systématiquement réinitialisée à 0 après chaque écriture. Il est donc nécessaire, lorsque l'on réalise plusieurs écritures formatées dans un flux de sortie, de spécifier pour chaque valeur sa largeur minimale si elle ne doit pas être égale à 0. Autrement dit, il faut redire la largeur de chaque champ, même s'ils utilisent tous la même largeur minimale.

Les opérations de formatage utilisent également les conventions locales du pays où le programme est exécuté, conventions qui sont définies dans la locale incluse dans le flux de sortie. Les notions de locale seront détaillées dans le Chapitre 16.

Bien entendu, il est possible de définir de nouvelles surcharges de l'opérateur d'insertion pour les types définis par l'utilisateur, ce qui permet d'étendre à l'infini les possibilités de cette classe. Ces surcharges devront obligatoirement être définies à l'extérieur de la classe template basic_ostream et, si l'on veut les écrire de manière générique, elles devront également être des fonctions template paramétrées par le type de caractère du flux sur lequel elles travaillent.

Vous noterez la présence d'une classe sentry dans la classe basic_ostream. Cette classe est une classe utilitaire permettant de réaliser les initialisations qui doivent précéder toutes les opérations d'écriture au sein des surcharges de l'opérateur d'insertion. Entre autres opérations, le constructeur de cette classe peut synchroniser le flux de sortie standard encapsulé dans le flux grâce à la méthode tie de la classe de base basic_ios, et prendre toutes les mesures devant précéder les écritures sur le flux. Vous devrez donc toujours utiliser un objet local de ce type lorsque vous écrirez une surcharge de l'opérateur operator<< pour vos propres types. Les écritures ne devront être réalisées que si l'initialisation a réussi, ce qui peut être vérifié simplement en comparant l'objet local de type sentry avec la valeur true.

Exemple 15-4. Définition d'un nouvel opérateur d'insertion pour un flux de sortie

#include <iostream>
#include <string>

using namespace std;

// Définition d'un type de donnée privé :
struct Personne
{
    string Nom;
    string Prenom;
    int Age;       // En centimètres.
    int Taille;
};

// Définition de l'opérateur d'écriture pour ce type :
template <class charT, class Traits>
basic_ostream<charT, Traits> &operator<<(
    basic_ostream<charT, Traits> &flux,
    const Personne &p)
{
    // Inialisation du flux de sortie :
    typename basic_ostream<charT, Traits>::sentry init(flux);
    if (init)
    {
        // Écriture des données :
        int Metres = p.Taille / 100;
        int Reste = p.Taille % 100;
        flux << p.Prenom << " " << p.Nom <<
            " mesure " << Metres <<
            "m" << Reste << " (" <<
            p.Age << " an";
        if (p.Age > 1) flux << "s";
        flux << ")";
    }
    return flux;
}

int main(void)
{
    // Construit une nouvelle personne :
    Personne p;
    p.Nom = "Dupont";
    p.Prenom = "Jean";
    p.Age = 28;
    p.Taille = 185;
    // Affiche les caractéristiques de cette personne :
    cout << p << endl;
    return 0;
}

Note : L'utilisation de l'objet local de type sentry comme un booléen est autorisée parce que la classe sentry définit un opérateur de transtypage vers le type bool.

Le constructeur de la classe sentry est susceptible de lancer des exceptions, selon la configuration du masque d'exceptions du flux de sortie avec lequel on l'initialise.

Les écritures de données brutes ne disposent bien entendu pas de surcharges pour chaque type de donnée, puisqu'il s'agit dans ce cas d'écrire les données directement sur le flux de sortie, sans les formater sous forme textuelle. Ces écritures sont donc réalisées par l'intermédiaire de méthodes dédiées qui effectuent soit l'écriture d'un caractère unique, soit l'écriture d'un tableau de caractères complet. Pour écrire un unique caractère sur le flux de sortie, vous pouvez utiliser la méthode put. Pour l'écriture d'un bloc de données en revanche, il faut utiliser la méthode write, qui prend en paramètre un pointeur sur la zone de données à écrire et la taille de cette zone.

Exemple 15-5. Écriture de données brutes sur un flux de sortie

#include <iostream>
using namespace std;

// Définition de quelques codes de couleurs
// pour les terminaux ANSI :
const char Rouge[] = {033, '[', '3', '1', 'm'};
const char Vert[] = {033, '[', '3', '2', 'm'};
const char Jaune[] = {033, '[', '3', '3', 'm'};
const char Reset[] = {033, '[', 'm', 017};

int main(void)
{
    // Écriture d'un message coloré :
    cout.write(Rouge, sizeof(Rouge));
    cout << "Bonjour ";
    cout.write(Vert, sizeof(Vert));
    cout << "tout ";
    cout.write(Jaune, sizeof(Jaune));
    cout << "le monde !" << endl;
    cout.write(Reset, sizeof(Reset));
    return 0;
}

Bien entendu, la classe basic_ostream fournit les méthodes nécessaires à la gestion du tampon sous-jacent. La méthode flush permet de synchroniser le tampon utilisé par le flux (en appelant la méthode pubsync de ce dernier). La méthode tellp permet de lire la position courante dans le flux de sortie, et les deux surcharges de la méthode seekp permettent de modifier cette position soit de manière absolue, soit de manière relative à la position courante. Nous verrons un exemple d'utilisation de ces méthodes dans la description des classes de flux pour les fichiers.

La classe basic_ostream définit également des surcharges de l'opérateur d'insertion capables de prendre en paramètre des pointeurs de fonctions. Ces méthodes ne constituent pas des opérations d'écriture à proprement parler, mais permettent de réaliser des opérations sur les flux de sortie plus facilement à l'aide de fonctions capables de les manipuler. En raison de cette propriété, ces fonctions sont couramment appelées des manipulateurs. En réalité, ces manipulateurs ne sont rien d'autre que des fonctions prenant un flux en paramètre et réalisant des opérations sur ce flux. Les opérateurs operator<< prenant en paramètre ces manipulateurs les exécutent sur l'objet courant *this. Ainsi, il est possible d'appliquer ces manipulateurs à un flux simplement en réalisant une écriture du manipulateur sur ce flux, exactement comme pour les écritures normales.

La bibliothèque standard définit tout un jeu de manipulateurs extrêmement utiles pour définir les options de formatage et pour effectuer des opérations de base sur les flux. Grâce à ces manipulateurs, il n'est plus nécessaire d'utiliser la méthode setf de la classe ios_base par exemple. Par exemple, le symbole endl utilisé pour effectuer un retour à la ligne dans les opérations d'écriture sur les flux de sortie n'est rien d'autre qu'un manipulateur, dont la déclaration est la suivante :

template <class charT, class Traits>
basic_ostream<charT, Traits> &endl(
    basic_ostream<charT, Traits> &flux);
et dont le rôle est simplement d'écrire le caractère de retour à la ligne et d'appeler la méthode flush du flux de sortie.

Il existe des manipulateurs permettant de travailler sur la classe de base ios_base ou sur ses classes dérivées comme la classe basic_ostream par exemple, d'où la présence de plusieurs surcharges de l'opérateur d'insertion pour ces différents manipulateurs. Il existe également des manipulateurs prenant des paramètres et renvoyant un type de données spécial pour lequel un opérateur d'écriture a été défini, et qui permettent de réaliser des opérations plus complexes nécessitant des paramètres complémentaires. Les manipulateurs sont définis, selon leur nature, soit dans l'en-tête de déclaration du flux, soit dans l'en-tête ios, soit dans l'en-tête iomanip.

Le tableau suivant présente les manipulateurs les plus simples qui ne prennent pas de paramètre :

Tableau 15-5. Manipulateurs des flux de sortie

ManipulateurFonction
endl

Envoie un caractère de retour à la ligne sur le flux et synchronise le tampon par un appel à la méthode flush.

ends

Envoie un caractère nul terminal de fin de ligne sur le flux.

flush

Synchronise le tampon utilisé par le flux par un appelle à la méthode flush.

boolalpha

Active le formatage des booléens sous forme textuelle.

noboolalpha

Désactive le formatage textuel des booléens.

hex

Formate les nombres en base 16.

oct

Formate les nombres en base 8.

dec

Formate les nombres en base 10.

fixed

Utilise la notation en virgule fixe pour les nombres à virgule.

scientific

Utilise la notation en virgule flottante pour les nombres à virgule.

left

Aligne les résultats à gauche.

right

Aligne les résultats à droite.

internal

Utilise le remplissage des champs avec des espaces complémentaires à une position fixe déterminée par la locale courante. Équivalent à right si la locale ne spécifie aucune position de remplissage particulière.

showbase

Indique la base de numérotation utilisée.

noshowbase

N'indique pas la base de numérotation utilisée.

showpoint

Utilise le séparateur de virgule dans les nombres à virgule, même si la partie fractionnaire est nulle.

noshowpoint

N'utilise le séparateur de virgule que si la partie fractionnaire des nombres à virgule flottante est significative.

showpos

Écrit systématiquement le signe des nombres, même s'ils sont positifs.

noshowpos

N'écrit le signe des nombres que s'ils sont négatifs.

uppercase

Écrit les exposants et les chiffres hexadécimaux en majuscule.

nouppercase

Écrit les exposants et les chiffres hexadécimaux en minuscule.

unitbuf

Effectue une opération de synchronisation du cache géré par le tampon du flux après chaque écriture.

nounitbuf

N'effectue les opérations de synchronisation du cache géré par le tampon du flux que lorsque cela est explicitement demandé.

Les paramètres suivants sont un peu plus complexes, puisqu'ils prennent des paramètres complémentaires. Ils renvoient un type de donnée spécifique à chaque implémentation de la bibliothèque standard et qui n'est destiné qu'à être inséré dans un flux de sortie à l'aide de l'opérateur d'insertion :

Tableau 15-6. Manipulateurs utilisant des paramètres

ManipulateurFonction
resetiosflags(ios_base::fmtflags)

Permet d'effacer certains bits des options du flux. Ces bits sont spécifiés par une combinaison logique de constantes de type ios_base::fmtflags.

setiosflags(ios_base::fmtflags)

Permet de positionner certains bits des options du flux. Ces bits sont spécifiés par une combinaison logique de constantes de type ios_base::fmtflags.

setbase(int base)

Permet de sélectionner la base de numérotation utilisée. Les valeurs admissibles sont 8, 10 et 16 respectivement pour la base octale, la base décimale et la base hexadécimale.

setprecision(int)

Permet de spécifier la précision (nombre de caractères significatifs) des nombres formatés.

setw(int)

Permet de spécifier la largeur minimale du champ dans lequel la donnée suivante sera écrite à la prochaine opération d'écriture sur le flux.

setfill(char_type)

Permet de spécifier le caractère de remplissage à utiliser lorsque la largeur des champs est inférieure à la largeur minimale spécifiée dans les options de formatage.

Exemple 15-6. Utilisation des manipulateurs sur un flux de sortie

#include <iostream>
#include <iomanip>

using namespace std;

int main(void)
{
    // Affiche les booléens sous forme textuelle :
    cout << boolalpha << true << endl;
    // Écrit les nombres en hexadécimal :
    cout << hex << 57 << endl;
    // Repasse en base 10 :
    cout << dec << 57 << endl;
    // Affiche un flottant avec une largeur
    // minimale de 15 caractères :
    cout << setfill('*') << setw(15) << 3.151592 << endl;
    // Recommence mais avec un alignement à gauche :
    cout << left << setw(15) << 3.151592 << endl;
}

15.4.2. La classe de base basic_istream

La deuxième classe la plus utilisée de la bibliothèque d'entrée / sortie est sans doute la classe template basic_istream. À l'instar de la classe ostream, cette classe fournit toutes les fonctionnalités de lecture de données formatées ou non à partir d'un tampon. Ce sont donc certainement les méthodes cette classe que vous utiliserez le plus souvent lorsque vous désirerez lire les données d'un flux. La classe basic_istream est déclarée comme suit dans l'en-tête istream :

template <class charT,
    class traits = char_traits<charT> >
class basic_istream : virtual public basic_ios<charT, traits>
{
public:
// Les types de données :
    typedef charT                     char_type;
    typedef typename traits::int_type int_type;
    typedef typename traits::pos_type pos_type;
    typedef typename traits::off_type off_type;
    typedef traits                    traits_type;

// Le constructeur et destructeur :
    explicit basic_istream(basic_streambuf<charT, traits> *sb);
    virtual ~basic_istream();

// Les opération de gestion des entrées formatées :
    basic_istream<charT, traits> &operator>>(bool &n);
    basic_istream<charT, traits> &operator>>(short &n);
    basic_istream<charT, traits> &operator>>(unsigned short &n);
    basic_istream<charT, traits> &operator>>(int &n);
    basic_istream<charT, traits> &operator>>(unsigned int &n);
    basic_istream<charT, traits> &operator>>(long &n);
    basic_istream<charT, traits> &operator>>(unsigned long &n);
    basic_istream<charT, traits> &operator>>(float &f);
    basic_istream<charT, traits> &operator>>(double &f);
    basic_istream<charT, traits> &operator>>(long double &f);
    basic_istream<charT, traits> &operator>>(void * &p);
    basic_istream<charT, traits> &operator>>
        (basic_streambuf<char_type, traits> *sb);

// Classe de gestion des exceptions pour les opérateurs d'écritures formatées :
    class sentry
    {
    public:
        explicit sentry(basic_istream<charT, traits> &flux,
            bool conserve = false);
        ~sentry();
        operator bool();
    };

// Les opérations de lecture des données brutes :
    int_type get();
    basic_istream<charT, traits> &get(char_type &c);
    int_type peek();
    basic_istream<charT, traits> &putback(char_type c);
    basic_istream<charT, traits> &unget();
    basic_istream<charT, traits> &read(char_type *s, streamsize n);
    streamsize                   readsome(char_type *s, streamsize n);

    basic_istream<charT, traits> &get(char_type *s, streamsize n);
    basic_istream<charT, traits> &get(char_type *s, streamsize n,
        char_type delim);
    basic_istream<charT, traits> &get(
        basic_streambuf<char_type, traits> &sb);
    basic_istream<charT, traits> &get(
        basic_streambuf<char_type, traits> &sb, char_type delim);
    basic_istream<charT, traits> &getline(char_type *s, streamsize n);
    basic_istream<charT, traits> &getline(char_type *s, streamsize n,
        char_type delim);
    basic_istream<charT, traits> &ignore
        (streamsize n = 1, int_type delim = traits::eof());

    streamsize gcount() const;

// Les opérations de gestion du tampon :
    int sync();
    pos_type tellg();
    basic_istream<charT, traits> &seekg(pos_type);
    basic_istream<charT, traits> &seekg(off_type, ios_base::seekdir);

// Les opérations de gestion des manipulateurs :
    basic_istream<charT, traits> &operator>>
        (basic_istream<charT, traits> & (*pf)(
	    basic_istream<charT, traits> &));
    basic_istream<charT, traits> &operator>>
        (basic_ios<charT, traits> & (*pf)(basic_ios<charT, traits> &));
    basic_istream<charT, traits> &operator>>
        (ios_base & (*pf)(ios_base &));
};

Tout comme la classe basic_ostream, le constructeur de la classe basic_istream prend en paramètre un pointeur sur l'objet gérant le tampon dans lequel les écritures devront être effectuées. Cependant, même s'il est possible de créer une instance de flux d'entrée simplement à l'aide de ce constructeur, cela n'est pas recommandé puisque la bibliothèque standard fournit des classes spécialisées permettant de créer des flux de sortie orientés fichiers ou chaînes de caractères.

L'utilisation des différentes surcharges de l'opérateur d'extraction des données formatées operator>> ne devrait pas poser de problème. Le compilateur détermine la surcharge à utiliser en fonction du type des données à lire, déterminé par la référence de variable fournie en paramètre. Cette surcharge récupère alors les informations dans le tampon associé au flux, les interprète et écrit la nouvelle valeur dans la variable.

Bien entendu, tout comme pour la classe basic_ostream, il est possible d'écrire de nouvelles surcharges de l'opérateur d'extraction afin de prendre en charge de nouveaux types de données. Idéalement, ces surcharges devront être également des fonctions template paramétrées par le type de caractère du flux sur lequel elles travaillent, et elles devront également utiliser une classe d'initialisation sentry. Cette classe a principalement pour but d'initialiser le flux d'entrée, éventuellement en le synchronisant avec un flux de sortie standard dont la classe basic_ostream peut être stockée dans le flux d'entrée à l'aide de la méthode tie de la classe de base basic_ios, et en supprimant les éventuels caractères blancs avant la lecture des données.

Notez que, contrairement à la classe sentry des flux de sortie, le constructeur de la classe sentry des flux d'entrée prend un deuxième paramètre. Ce paramètre est un booléen qui indique si les caractères blancs présents dans le flux de données doivent être éliminés avant l'opération de lecture ou non. En général, pour les opérations de lecture formatées, ce sont des caractères non significatifs et il faut effecivement supprimer ces caractères, aussi la valeur à spécifier pour ce second paramètre est-elle false. Comme c'est aussi la valeur par défaut, la manière d'utiliser de la classe sentry dans les opérateurs d'extraction est strictement identique à celle de la classe sentry des opérateurs d'insertion de la classe basic_ostream.

Exemple 15-7. Écriture d'un nouvel opérateur d'extraction pour un flux d'entrée

#include <iostream>
#include <string>

using namespace std;

// Définition d'un type de donnée privé :
struct Personne
{
    string Nom;
    string Prenom;
    int Age;       // En centimètres.
    int Taille;
};

// Définition de l'opérateur de lecture pour ce type :
template <class charT, class Traits>
basic_istream<charT, Traits> &operator>>(
    basic_istream<charT, Traits> &flux, Personne &p)
{
    // Inialisation du flux de sortie :
    typename basic_istream<charT, Traits>::sentry init(flux);
    if (init)
    {
        // Lecture du prénom et du nom :
        flux >> p.Prenom;
        flux >> p.Nom;
        // Lecture de l'âge :
        flux >> p.Age;
        // Lecture de la taille en mètres :
        double Taille;
        flux >> Taille;
        // Conversion en centimètres ;
        p.Taille = (int) (Taille * 100 + 0.5);
    }
    return flux;
}

int main(void)
{
    // Construit une nouvelle personne :
    Personne p;
    // Demande la saisie d'une personne :
    cout << "Prénom Nom Âge(ans) Taille(m) : ";
    cin >> p;
    // Affiche les valeurs lues :
    cout << endl;
    cout << "Valeurs saisies :" << endl;
    cout << p.Prenom << " " << p.Nom << " a " <<
        p.Age << " ans et mesure " <<
        p.Taille << " cm." << endl;
    return 0;
}

Note : La classe sentry est également utilisée par les méthodes de lecture de données non formatées. Pour ces méthodes, les caractères blancs sont importants et dans ce cas le second paramètre fourni au constructeur de la classe sentry est true.

Comme pour la classe sentry de la classe basic_ostream, l'utilisation de l'objet d'initialisation dans les tests est rendue possible par la présence de l'opérateur de transtypage vers le type bool. La valeur retournée est true si l'initialisation s'est bien faite et false dans le cas contraire. Remarquez également que le constructeur de la classe sentry est susceptible de lancer des exceptions selon la configuration du masque d'exceptions dans la classe de flux.

Les opérations de lecture de données non formatées sont un peu plus nombreuses pour les flux d'entrée que les opérations d'écriture non formatées pour les flux de sortie. En effet, la classe basic_istream donne non seulement la possibilité de lire un caractère simple ou une série de caractères, mais aussi de lire les données provenant du tampon de lecture et de les interpréter en tant que « lignes ». Une ligne est en réalité une série de caractères terminée par un caractère spécial que l'on nomme le marqueur de fin de ligne. En général, ce marqueur est le caractère '\n', mais il est possible de spécifier un autre caractère.

La lecture d'un caractère unique dans le flux d'entrée se fait à l'aide de la méthode get. Il existe deux surcharges de cette méthode, la première ne prenant aucun paramètre et renvoyant le caractère lu, et la deuxième prenant en paramètre une référence sur la variable devant recevoir le caractère lu et ne renvoyant rien. Ces deux méthodes extraient les caractères qu'elles lisent du tampon d'entrée que le flux utilise. Si l'on veut simplement lire la valeur du caractère suivant sans l'en extraire, il faut appeler la méthode peek. De plus, tout caractère extrait peut être réinséré dans le flux d'entrée (pourvu que le tampon sous-jacent accepte cette opération) à l'aide de l'une des deux méthodes unget ou putback. Cette dernière méthode prend en paramètre le caractère qui doit être réinséré dans le flux d'entrée. Notez que la réinsertion ne peut être réalisée que si le caractère fourni en paramètre est précisément le dernier caractère extrait.

Si l'on désire réaliser la lecture d'une série de caractères au lieu de les extraire un à un, il faut utiliser la méthode read. Cette méthode est la méthode de base pour les lectures non formatées puisqu'elle lit les données brutes de fonderie, sans les interpréter. Elle prend en paramètre un pointeur sur un tableau de caractères dans lequel les données seront écrites et le nombre de caractères à lire. Cette méthode ne vérifie pas la taille du tableau spécifié, aussi celui-ci doit-il être capable d'accueillir le nombre de caractères demandé. Il existe une variante de la méthode read, la méthode readsome, qui permet de lire les données présentes dans le tampon géré par le flux sans accéder au média que ce dernier prend en charge. Cette méthode prend également en paramètre un pointeur sur la zone mémoire devant recevoir les données et le nombre de caractères désiré, mais, contrairement à la méthode read, elle peut ne pas lire exactement ce nombre. En effet, la méthode readsome s'arrête dès que le tampon utilisé par le flux est vide, ce qui permet d'éviter les accès sur le périphérique auquel ce tampon donne accès. La méthode readsome renvoie le nombre de caractères effectivement lus.

Les méthodes de lecture des lignes sont à diviser en deux catégories. La première catégorie, constituée de plusieurs surcharges de la méthode get, permet d'effectuer une lecture des données du tampon jusqu'à ce que le tableau fourni en paramètre soit rempli ou qu'une fin de ligne soit atteinte. La deuxième catégorie de méthodes est constituée des surcharges de la méthode getline. Ces méthodes se distinguent des méthodes get par le fait qu'elles n'échouent pas lorsque la ligne lue (délimiteur de ligne compris) remplit complètement le tableau fourni en paramètre d'une part, et par le fait que le délimiteur de ligne est extrait du tampon d'entrée utilisé par le flux d'autre part. Autrement dit, si une ligne complète (c'est-à-dire avec son délimiteur) a une taille exactement égale à la taille du tableau fourni en paramètre, les méthodes get échoueront alors que les méthodes getline réussiront, car elles ne considèrent pas le délimiteur comme une information importante. Ceci revient à dire que les méthodes getline interprètent complètement le caractère délimiteur, alors que les méthodes get le traitent simplement comme le caractère auquel la lecture doit s'arrêter.

Dans tous les cas, un caractère nul terminal est inséré en lieu et place du délimiteur dans le tableau fourni en paramètre et devant recevoir les données. Comme le deuxième paramètre de ces méthodes indique la dimension de ce tableau, le nombre de caractères lu est au plus cette dimension moins un. Le nombre de caractères extraits du tampon d'entrée est quant à lui récupérable grâce à la méthode gcount. Remarquez que le caractère de fin de ligne est compté dans le nombre de caractères extraits pour les méthodes getline, alors qu'il ne l'est pas pour les méthodes get puisque ces dernières ne l'extraient pas du tampon.

Enfin, il est possible de demander la lecture d'un certain nombre de caractères et de les passer sans en récupérer la valeur. Cette opération est réalisable à l'aide de la méthode ignore, qui ne prend donc pas de pointeurs sur la zone mémoire où les caractères lus doivent être stockés puisqu'ils sont ignorés. Cette méthode lit autant de caractères que spécifié, sauf si le caractère délimiteur indiqué en deuxième paramètre est rencontré. Dans ce cas, ce caractère est extrait du tampon d'entrée, ce qui fait que la méthode ignore se comporte exactement comme les méthodes getline.

Exemple 15-8. Lectures de lignes sur le flux d'entrée standard

#include <iostream>
#include <sstream>

using namespace std;

int main(void)
{
    // Tableau devant recevoir une ligne :
    char petit_tableau[10];
    // Lit une ligne de 9 caractères :
    cout << "Saisissez une ligne :" << endl;
    cin.getline(petit_tableau, 10);
    if (cin.fail())
        cout << "Ligne trop longue !" << endl;
    cout << "Lu : ***" << petit_tableau << "***" << endl;
    // Lit une ligne de taille arbitraire via un tampon :
    cout << "Saisissez une autre ligne :" << endl;
    stringbuf s;
    cin.get(s);
    // Affiche la ligne lue :
    cout << "Lu : ***" << s.str() << "***";
    // Extrait le caractère de saut de ligne
    // et ajoute-le au flux de sortie standard :
    cout << (char) cin.get();
    return 0;
}

Note : Remarquez que le caractère de saut de ligne étant lu, il est nécessaire de saisir deux retours de chariot successifs pour que la méthode getline renvoie son résultat. Comme pour toutes les méthodes de lectures formatées, ce caractère interrompt la lecture dans le flux d'entrée standard du programme et se trouve donc encore dans le tampon d'entrée lors de la lecture suivante. Cela explique que dans le cas de lectures successives, il faut extraire ce caractère du flux d'entrée manuellement, par exemple à l'aide de la méthode get. C'est ce que cet exemple réalise sur sa dernière ligne pour l'envoyer sur le flux de sortie standard.

De plus, on ne peut pas prévoir, a priori, quelle sera la taille des lignes saisies par l'utilisateur. On ne procédera donc pas comme indiqué dans cet exemple pour effectuer la lecture de lignes en pratique. Il est en effet plus facile d'utiliser la fonction getline, que l'on a décrit dans la Section 14.1.8 dans le cadre du type basic_string. En effet, cette fonction permet de lire une ligne complète sans avoir à se soucier de sa longueur maximale et de stocker le résultat dans une basic_string.

La classe basic_istream dispose également de méthodes permettant de manipuler le tampon qu'elle utilise pour lire de nouvelles données. La méthode sync permet de synchroniser le tampon d'entrée avec le média auquel il donne accès, puisqu'elle appelle la méthode pubsync de ce tampon. Pour les flux d'entrée, cela n'a pas réellement d'importance parce que l'on ne peut pas écrire dedans. La méthode tellg permet de déterminer la position du pointeur de lecture courant, et les deux surcharges de la méthode seekg permettent de repositionner ce pointeur. Nous verrons un exemple d'utilisation de ces méthodes dans la description des classes de flux pour les fichiers.

Enfin, les flux d'entrée disposent également de quelques manipulateurs permettant de les configurer simplement à l'aide de l'opérateur operator>>. Ces manipulateurs sont présentés dans le tableau ci-dessous :

Tableau 15-7. Manipulateurs des flux d'entrée

ManipulateurFonction
boolalpha

Active l'interprétation des booléens sous forme de textuelle.

noboolalpha

Désactive l'interprétation des booléens sous forme textuelle.

hex

Utilise la base 16 pour l'interprétation des nombres entiers.

oct

Utilise la base 8 pour l'interprétation des nombres entiers.

dec

Utilise la base 10 pour l'interprétation des nombres entiers.

skipws

Ignore les espaces lors des entrées formatées.

noskipws

Conserve les espaces lors des entrées formatées.

ws

Supprime tous les espaces présents dans le flux d'entrée jusqu'au premier caractère non blanc.

Ces manipulateurs s'utilisent directement à l'aide de l'opérateur operator>>, exactement comme les manipulateurs de la classe basic_ostream s'utilisent avec l'opérateur d'insertion normal.

15.4.3. La classe basic_iostream

La bibliothèque standard définit dans l'en-tête iostream la classe template basic_iostream afin de permettre à la fois les opérations d'écriture et les opérations de lecture sur les flux. En fait, cette classe n'est rien d'autre qu'une classe dérivée des deux classes basic_ostream et basic_istream qui fournissent respectivement, comme on l'a vu, toutes les fonctionnalités de lecture et d'écriture sur un tampon.

La classe basic_iostream ne comporte pas d'autres méthodes qu'un constructeur et un destructeur, qui servent uniquement à initialiser et à détruire les classes de base basic_ostream et basic_istream. L'utilisation de cette classe ne doit donc pas poser de problème particulier et je vous invite à vous référer aux descriptions des classes de base si besoin est.

Note : Tout comme ses classes de base, la classe basic_iostream sera rarement utilisée directement. En effet, elle dispose de classes dérivées spécialisées dans les opérations d'écriture et de lecture sur fichiers ou dans des chaînes de caractères, classes que l'on présentera dans les sections suivantes. Ce sont ces classes que l'on utilisera en pratique lorsque l'on désirera créer un nouveau flux pour lire et écrire dans un fichier ou dans une basic_string.

Vous aurez peut-être remarqué que les classes basic_ostream et basic_istream utilisent un héritage virtuel pour récupérer les fonctionnalités de la classe de base basic_ios. La raison en est que la classe basic_iostream réalise un héritage multiple sur ses deux classes de base et que les données de la classe basic_ios ne doivent être présente qu'en un seul exemplaire dans les flux d'entrée / sortie. Cela implique que les constructeurs des classes dérivées de la classe basic_iostream prenant des paramètres doivent appeler explicitement les constructeurs de toutes leur classes de base. Voyez la Section 8.6 pour plus de détails sur les notions d'héritage multiple et de classes virtuelles.