Entrées/Sorties

La gestion des flux de données (clavier, fichiers, chaîne de caractères) constitue un incontournable en programmation.

Des méthodes communes

Héritage

Qu’est ce que l’héritage et à quoi sert-il dans notre contexte ? Que nous traitions des données qui arrivent d’un fichier, du clavier ou d’une chaîne de caractères, au final, quelle importance a réellement le support ? Ce qui nous intéresse, c’est le traitement des informations. Ici apparaît le besoin de mettre en place une abstraction nous permettant de traiter les données sans tenir compte du support. Cette abstraction s’appelle un flux (stream) de données. Pour la mise en place, on associe à chaque type de support un type d’objet spécifique. Mais ces objets différents vont proposer un ensemble de fonctions identiques (même nom et mêmes paramètres) pour gérer les flux de données.

Du point de vue technique, cette abstraction a été réalisée en utilisant une hiérarchie de classes. Voici une représentation des classes de gestion des flux de la libraire standard du C++ :

../_images/heritage.png

La classe mère est la classe ios (input output stream). Elle offre des fonctions intéressantes comme :

  • good() : retourne true si le flux peut encore fournir des données.

  • fail() : retourne true si une erreur est survenue.

La sous-classe de la classe ios spécialisée dans les flux en entrée est la classe istream (input stream). Elle offre diverses fonctionnalités comme la redéfinition de l’opérateur >>. Elle hérite des fonctions good() et fail() de sa classe mère et proposent de clases dérivées :

  • La sous-classe istringstream spécialisée dans la lecture des chaînes de caractères.

  • La sous-classe ifilestream spécialisée dans la lecture des fichiers.

On trouve dans l’autre branche les classes symétriques pour la gestion des outputs : ostream et ses deux classes dérivées ostringstream et ofilestream.

Vous pouvez aussi trouver une fonction globale de la librairie standard : getline(), prenant en paramètre tout objet de type stream ou ses dérivés. Cette fonction permet ainsi de lire une ligne aussi bien sur un flux clavier, un flux fichier ou flux texte, en entrée ou en sortie.

On peut remarquer la présence de deux instances spécialisées sur les flux clavier entrée/sortie : cout et cin. L’instance cout (cin) est une instance de la classe ostream (istream) spécialisée dans le flux clavier sortie (entrée). Ces deux instances sont uniques pour chaque programme ce qui est normal car il y a une seule sortie et une seule entrée clavier associées à la fenêtre console de l’application. Les objets cout et cin sont instanciés avant le démarrage de votre programme. Ils sont accessibles par l’inclusion de la librairie <iostream> dans votre fichier de code. Ainsi, vous pouvez les utiliser comme des objets préexistants sans que vous ayez à les instancier dans votre code.

Note

Les exemples en bas des pages de cppreference.com sont assez pédagogiques, vous pouvez vous y référer pour voir comment mettre une méthode en pratique.

Note

Les exemples que nous présentons sur cette page utilisent des chaînes de caractères en guise de données d’entrée. En effet, les chaînes de caractères sont plus faciles à mettre en place. Vous trouverez un exemple de mise en place d’un flux sur fichier à la fin de cette page.

Avertissement

Si vous avez lu le chapitre sur les strings, vous savez que l’encodage des caractères spéciaux pose problème. Si vous traitez les flux par bloc en chargeant les données jusqu’à la fin de la ligne, les caractères spéciaux seront chargés sans problème. Si par contre vous traitez les flux caractère par caractère, vous aurez des problèmes dès que vous rencontrerez un é ou un ç. Dans tous les cas, si vos données n’utilisent que les caractères ASCII (voir page sur les strings) vous n’aurez aucun problème.

Options de sortie

Écriture avec l’opérateur <<

Si nous utilisons l’opérateur << avec l’objet cout, nous pouvons tout aussi bien l’utiliser pour écrire dans un fichier ou, chose étonnante, dans une chaîne de caractères. L’objet en question de type ostringstream accueille ainsi des écritures successives. Une fois les écritures terminées, on peut récupérer le résultat dans un objet de type string grâce à la fonction str(). Voici un exemple ci-dessous :

#include <string>
#include <iostream>
#include <sstream>
using namespace std;

int main()
{
    ostringstream c;  // output string stream

    int v = 10;
    c << "Var : " << v * 2  << " !";

    string s = c.str();  // convertit stream => string
    cout << s << endl;
}

Le terme endl produit un retour à la ligne et reste équivalent à l’écriture de "\n". On peut chaîner plusieurs affichages à la suite, cependant, les éléments sont accolés. Ainsi l’affichage de 18 et 13 produira 1813. Pour rendre lisible les résultats, il faut ainsi insérer des espacements supplémentaires :

cout << "Bonjour, votre âge est de " << age << " ans." << endl;

Formatage des nombres flottants

La librairie <iomanip> fournit une fonction setprecision() permettant de contrôler l’affichage des nombres flottants :

#include <iostream>
#include <iomanip>

using namespace std;
int main()
{
    cout << setprecision(5) << 3.141519 << endl;   // ==>> 3.1415  5 chiffres
    cout << setprecision(3) << 3.141519 << endl;   // ==>> 3.14    3 chiffres
}

Formatage des nombres entiers

La librairie <iomanip> fournit une fonction setw() (set width) permettant d’indiquer la place que doit prendre un affichage. Ainsi, si un nombre prend 2 chiffres et que le largeur est fixée à 5, trois caractères seront rajoutés. La fonction setfill() permet d’indiquer le caractère servant de remplissage. Voici un exemple :

#include <iomanip>
#include <iostream>

int main()
{
    std::cout << std::setfill('-');
    std::cout << std::setw(5) << 25 << std::endl;
    std::cout << std::setw(5) << 148;
}

==>> ---25
==>> --148

Options de lecture

Nous listons ici différentes options pour la lecture. Le choix le plus souple et le plus polyvalent est sans aucun doute la fonction getline() qui s’applique sur tout objet de type stream et qui retourne un string. Le second choix que nous présentons correspond à l’opérateur >> offrant une syntaxe simple lorsque les données sont séparées par des espaces. Les autres exemples sont données à titre anecdotique.

La fonction getline()

Une méthode fournissant une grande souplesse est la fonction getline(). Par défaut elle lit une ligne, c’est-à-dire qu’elle charge tous les caractères dans un objet string jusqu’à trouver un retour à la ligne. Mais, il est possible dans cette fonction de modifier le délimiteur de fin de ligne pour arrêter la lecture sur le caractère de son choix. Grâce à cette option, cela en fait un outil très pratique pour lire des informations séparées par des virgules ou par d’autres signes.

Voici un exemple décryptant la chaîne de caractères : "1,2,3,4;5,6,7,8;" :

#include <string>
#include <iostream>
#include <sstream>
using namespace std;

// Le flux va être décodé en utilisant la fonction getline()
// avec le caractère , comme délimiteur.
// Ainsi, vont être lus successivement 1 2 3 et 4.
// Convertis en valeurs entières, leur total est affiché.

void ProcessLigne(istream  & mystream)
{
    string number;
    int somme = 0;
    while (  getline( mystream, number, ',') )
    {
        cout << number << " - ";
        somme += stoi(number);
    }
    cout << endl;
    cout << "La somme est " << somme << endl << endl;
}

// On va d abord créer une string-stream mystream à partir des données.
// Ensuite, grâce à la fonction getline() utilisant le caractère ; comme délimiteur,
// nous allons lire la première ligne : 1,2,3,4 et la charger dans un objet string intitulé -ligne-.
// On crée un deuxième string-stream L à partir de cet objet -ligne- et on le passe à la fonction ProcessLigne

int main()
{
    string data = "1,2,3,4;5,6,7,8;";

    // traitement d'une string
    istringstream mystream;
    mystream.str(data);

    // lecture des blocs se terminant par un ;
    string ligne;
    while (  getline(mystream, ligne, ';') )
    {
        cout << "Ligne : " << ligne << endl;
        istringstream L(ligne);
        ProcessLigne(L);
    }
}

Opérateur >>

L’opérateur >> a été surchargé pour accepter sur sa gauche une instance de type istream (input stream) et sur sa droite une variable accueillant l’information lue. Ainsi, l’opérateur >> offre une syntaxe simple pour la lecture des données. Voici un exemple ci-dessous que vous pouvez tester :

#include <string>
#include <iostream>
#include <sstream>
using namespace std;

int main()
{
    // traitement d'une input string stream

    istringstream s("1 2 3 4 5 6 7 8 9");

    while ( s.good() )
    {
        int a;
        s >> a;
        cout << a << "    READ ";
        if (s.fail())  cout << " KO";  else  cout << " OK";
        cout << endl;
    }
}

Note

Cependant, l’opérateur >> est très contraignant car ses délimiteurs sont imposés : l’espace et le retour à la ligne. Si, vous devez lire des chiffres séparés par des points-virgules, vous ne pourrez donc pas l’utiliser.

Si le rôle de l’opérateur >>, de la fonction good() et de la fonction fail() se comprennent facilement, nous ne connaissons pas leur comportement dans une situation non-standard. Pour cela, le mieux reste de mettre en place des tests pour déterminer leur logique de fonctionnement. Lancez le code précédent en modifiant la chaîne par la version ci-dessous. Examinez le comportement de l’opérateur >>, de la fonction good() et de la fonction fail().

istringstream s("1 2\n 3 4 5.6 7 8 9");  // \n retour à la ligne

Le type de la variable de destination est utilisée par l’opérateur >> pour déterminer ce qu’il doit lire. Dans l’exemple précédent, changez le type de la variable a pour le type double et examinez comment se comporte l’opérateur >>.

Note

Vous pouvez aussi proposer d’autres scénarios en modifiant la chaîne de caractères à votre guise. Les cas particuliers ne manquent pas : nombre trop long pour rentrer dans un int, nombre commençant par un 0, utilisation de la notation exponentielle, utilisation de la virgule ou du point pour les nombres flottants…

Espacement fixe

Dans certains fichiers, l’information occupe une largeur fixe. L’utilisation de la fonction get(…,n) permet de lire au plus n-1 caractères et de les charger dans un tableau de n char. Il reste à construire l’objet string à partir du tableau de char pour ensuite le convertir vers le type désiré :

#include <sstream>
#include <iostream>
using namespace std;

int main()
{
    // formatage : 1er code 4 caractères, 2eme 8 caractères, dernier 4 caractères
    string k("  11    1234 022");
    std::istringstream s(k);
    cout << k << endl;

    // lit les 4 premiers caractères
    char t[5];
    s.get(t,5);
    int a = stoi(string(t));
    cout << a << endl;

    // lit les 8 caractères suivants
    char u[9];
    s.get(u,9);
    int b = stoi(string(u));
    cout << b << endl;

    // lit les 4 derniers caractères
    char v[5];
    s.get(v,5);
    int c = stoi(string(v));
    cout << c << endl;
}

Lecture d’un caractère

Il est possible de lire un seul caractère. Cela peut parfois permettre de retrier des caractères spéciaux du flux comme dans la chaîne "%%123%%". Cette approche est généralement utilisée pour les entrées claviers, lorsque l’utilisateur doit entrer une valeur pour le choix d’un menu :

#include <sstream>
#include <iostream>
using namespace std;

int main()
{
        char c;

        cout << "(1) Choix 1" << endl;
        cout << "(2) Choix 2" << endl;
        cout << "(9) Quitter" << endl;

        while (  (c =cin.get()) != '9')
        {
            if ( c == '1') cout << "Choix 1" << endl;
            if ( c == '2') cout << "Choix 2" << endl;

        }
}

On peut trouver deux autres fonctions pratiques pour gérer un flux :

  • La fonction peek() qui lit le prochain caractère disponible sur le flux mais sans l’extraire. On peut par exemple ainsi vérifier si l’on a un chiffre ou une lettre pour déterminer le type du prochain élément à lire.

  • La fonction ignore() : qui exclut les prochains éléments sur le flux jusqu’à trouver un certain caractère.

Points particuliers

Gestion des entrées/sorties fichier

Pour gérer un fichier en entrée ou en sortie, il faut d’abord ouvrir un flux vers ce fichier et ensuite le refermer. Cela est important, car contrairement aux flux sous forme de string ou aux flux cin et cout qui appartiennent exclusivement à votre programme, les fichiers eux sont accessibles par tous.

Ainsi, lorsqu’un programme demande l’accès à un fichier, celui-ci est verrouillé et ainsi on ne peut plus, par exemple, modifier son nom dans l’explorateur de fichier. Il faut attendre que le flux soit refermé pour pouvoir à nouveau modifier son nom ou le déplacer.

Voici donc un exemple pour ouvrir et fermer un flux associé à un fichier :

#include <string>
#include <fstream>
using namespace std;

int main()
{
    // ecriture en sortie sur un fichier

    ofstream F;
    F.open("c:\\temp\\test.txt");
    F << "bonjour";
    F.close();
}

Note

Lorsque l’on écrit des données sur le disque, deux choix sont possibles. Soit on est écrit en mode standard ou en mode binaire. Lorsque le mode standard est choisi, les données sont écrites au format texte, ainsi un entier valant 123456 sera transcrit 123456 dans le fichier. Un fichier au format texte peut être ouvert et modifié dans n’importe quel éditeur de texte. Cependant, le format binaire utilise l’encodage mémoire de la donnée. Dans le mode binaire, si 123456 est un entier codé sur 4 octets, 4 octets du fichier sont utilisés pour encoder cette valeur. Le format binaire est utilisé pour compresser la taille du fichier en contrepartie il n’est pas lisible dans un éditeur de texte, on perd donc en souplesse. C’est souvent un format utilisé par certains programmes professionnels pour masquer les données.

Surcharger l’opérateur << pour vos objets

Nous avons utilisé l’objet cout avec des types fondamentaux. Pour les structures, on peut afficher leur contenu en écrivant une longue ligne listant tous les éléments. Cette approche est peu lisible et doit être reproduite à chaque fois. Il est plus pratique de surcharger l’opérateur << pour qu’il gère les objets de votre type particulier. Ainsi, vous fixez un format d’affichage unique pour tous les objets de ce type. Voici un exemple dans le code ci-dessous :

#include <iostream>
using namespace std;

struct Point
{
    int x,y;
    Point(int xx,int yy)  { x = xx; y = yy; }
};

ostream & operator << (ostream & stream, const Point & P)
{
    stream << "(" << P.x << "," << P.y << ")";
    return stream;
}

int main()
{
   Point P(4,5);
   cout << P;
}

Stream bidirectionnelle

Certains objets stream gèrent l’écriture et la lecture simultanément. Voici un exemple dans le code ci-dessous :

#include <string>       // std::string
#include <iostream>     // std::cout
#include <sstream>      // std::stringstream
using namespace std;

int main ()
{
    std::stringstream ss;

    int a = 1;
    ss << a << a << a; // écriture de 1  1  1

    int aa;
    ss >> aa;

    cout << aa;   // aa = 111
}