Entrées/ sorties formatées et manipulateurs

Il arrive fréquemment qu'un programme doive se préoccuper de la précision et de l'exactitude de l'affichage des valeurs qu'il doit manipuler lorsque celles-ci doivent être présentées à un intervenant humain.

Nous examinerons donc ici les techniques et outils qui nous permettront d'obtenir l'affichage souhaité sur un flux standard en C++.

Représentation décimale et alternatives

L'écriture de nombres sous forme décimale est la façon de faire par défaut en C++, mais ce n'est pas la seule façon possible d'écrire des nombres dans ce langage :

Ne vous méprenez pas: représenter un nombre sous forme octale, décimale ou hexadécimale ne change rien au nombre en soi. De toute façon, pour la machine, la représentation du nombre est binaire (base 2). Les variantes dans d'autres bases sont prises en charge par le compilateur lorsque celui-ci traduit vos programmes et ne servent qu'à aider les informaticien(ne)s dans leur tâche.

Le tableau suivant présente quelques nombres entiers (encodés sur 16 bits[1]) sous leur forme octale, décimale et hexadécimale, selon la notation du langage C++.

Base 2 Base 8 Base 10 (non signé) Base 10 (signé) Base 16
00000000 00000000
0
0
0
0x0000
00000000 00000001
01
1
1
0x0001
00000000 00001010
012
10
10
0x000a
00000000 01100100
0144
100
100
0x0064
00000000 11111111
0377
255
255
0x00ff
01111111 11111111
077777
32767
32767
0x7fff
10000000 00000000
0100000
32768
-32768
0x8000
11111111 11111111
0177777
65535
-1
0xffff

Avant C++ 14, C++ n'offrait pas de notation pour représenter les nombres sous forme binaire. Cela dit, il est très facile de traduire un nombre d'une notation binaire en une notation hexadécimale et inversement (référez-vous à vos notes de mathématiques ou à cet article si vous ne vous souvenez pas des détails).

Exercice 0 – Complétez le tableau suivant en utilisant la notation C++, sachant qu'on parle d'entiers sur huit bits :

Binaire Décimal (signé) Décimal (non signé) Octal Hexadécimal
01001011
        
        
        
        
11110000
        
        
        
        
10000001
        
        
        
        
01111111
        
        
        
        
11111111
        
        
        
        
00010000
        
        
        
        

Réponse

Exercice 1 – Soit le programme C++ suivant. Est-ce un programme valide? Expliquez votre réponse.

int main()
{
   int bizarre = 09;
}

Réponse

Exercice 2 – Qu'affichera le programme suivant s'il est compilé avec Visual Studio?

#include <iostream>
using std::cout;
int main()
{
   if (0101 == 65 && 0xf0 == 0360)
      cout << "Bingo!";
   else
      cout << "Zut!";
}

Réponse

Base de numération

Par défaut, l'affichage d'un nombre entier s'effectue dans le système décimal. Ceci signifie que l'affichage d'un nombre restera décimal même si un programme représente ce nombre dans le système octal ou hexadécimal.

À titre d'exemple, l'extrait de programme se trouvant à droite affichera "123,123,123" puisque les trois nombres valent tous 123 en base 10.

Ce sont en fait trois façons différentes d'écrire le même nombre

cout << 123  << ","
     << 0173 << ","
     << 0x7B << endl;

Manipulateurs de flux

Pour forcer l'affichage d'un nombre avec des flux standard tels que std::cout dans un système de numération choisi, que ce soit en base 8, 10 ou 16, il est possible de faire appel aux manipulateurs de flux std::oct (pour l'affichage octal), std::dec (pour l'affichage décimal) ou std::hex (pour l'affichage hexadécimal).

Par exemple, en s'inspirant de l'exemple précédent, l'instruction à droite (qui présume que les divers using exposant std::hex et autres ont été faits) affichera cette fois-ci "7b,173,123", donc (dans l'ordre) l'équivalent hexadécimal, octal et décimal de .

cout << hex << 123 << ","
     << oct << 123 << ","
     << dec << 123
     << endl;

Le mode d'affichage choisi restera en vigueur jusqu'à ce qu'un autre système de numération soit imposé. Il faut donc être prudent(e) et prendre soin de rétablir le mode initial une fois l'affichage terminé, pour éviter les erreurs d'inattention!

Manipulateurs de flux paramétriques

Il existe aussi d'autres manipulateurs permettant de forcer l'affichage dans différents formats. L'un d'entre eux est le manipulateur std::setiosflags (et sa contrepartie directe std::resetiosflags).

std::setiosflags(long);
std::resetiosflags(long);

Chacune de ces deux fonctions modifie la manière d'afficher d'un flux (comme std::cout). Un appel à std::setiosflags() apporte une nouvelle spécification de format d'affichage, alors qu'un appel à std::resetiosflags() la retire.

Une spécification de format établie par une invocation de std::setiosflags() demeurera en vigueur jusqu'à ce qu'elle soit réinitialisé par l'appel à std::resetiosflags() correspondant.

Afin d'utiliser ces deux manipulateurs, vous devez inclure l'en-tête <iomanip>. Des exemples suivent sous peu.

Le paramètre flag

Le paramètre flag qui doit être passé aux fonctions std::resetiosflags() et std::setiosflags() sert à spécifier la modification à apporter au format courant d'affichage du flux.

Les constantes en question, que vous retrouverez dans le tableau ci-après, sont déclarées dans l'en-tête standard <ios>. Si vous êtes curieux/ curieuse, consultez l'aide en ligne à propos de std::ios_base::fmtflags.

flag Effet
std::ios_base::boolalpha

Les objets de type bool s'affichent "true" ou "false" plutôt que leur équivalent numérique.

std::ios_base::dec

Affichage des entiers en format décimal (base 10).

std::ios_base::fixed

Affichage des valeurs réelles à virgule flottante en format point fixe plutôt que point flottant.

std::ios_base::hex

Affichage des entiers en format hexadécimal (base 16).

std::ios_base::internal

Effectue le remplissage en fonction de la largeur d'affichage à l'intérieur du nombre affiché.

std::ios_base::left

Justification à gauche, remplissage à droite en fonction de la largeur (les nombres, par défaut, sont alignés à droite).

std::ios_base::oct

Affichage des entiers en format octal (base 8).

std::ios_base::right

Justification à droite, remplissage à gauche en fonction de la largeur.

std::ios_base::scientific

Affichage des valeurs réelles en format scientifique (avec exposant).

std::ios_base::showbase

Insère un préfixe qui révèle la base des entiers.

std::ios_base::showpoint

Affiche le point décimal de tout réel, peu importe la fraction.

std::ios_base::showpos

Affiche le signe + en tout temps pour les nombres positifs.

std::ios_base::skipws

Saute par-dessus les espaces lors de certaines lectures.

std::ios_base::unitbuf

Vide le tampon après chaque sortie (normalement, avec un flux en sortie standard tel que std::cout, le tampon d'affichage ne se vide que sur demande (injecter le manipulateur std::endl dans le flux ou invoquer la méthode flush() du flux) ou lorsque le tampon est plein.

std::ios_base::uppercase

Affiche tous les caractères en majuscules.

std::ios_base::adjustfield

Identifie lequel d'entre std::ios_base::internal, std::ios_base::left et std::ios_base::right est en cours d'utilisation.

std::ios_base::basefield

Identifie lequel d'entre std::ios_base::dec, std::ios_base::hex et std::ios_base::oct est en cours d'utilisation.

std::ios_base::floatfield

Identifie lequel d'entre std::ios_base::scientific et std::ios_base::fixed est en cours d'utilisation.

À titre d'exemple, le programme suivant emploie les manipulateurs de sortie std::setiosflags() et std::resetiosflags() pour afficher un booléen de valeur false avec, puis sans l'application du manipulateur std::ios_base::boolalpha.

#include <iomanip>
#include <iostream>
int main()
{
   using namespace std;
   cout << setiosflags(ios_base::boolalpha)   << false << endl
        << resetiosflags(ios_base::boolalpha) << false << endl;
}

À l'exécution, ce programme affichera false suivi de 0.

Autre exemple : le programme suivant emploie les manipulateurs de sortie std::setiosflags() et std::resetiosflags() pour afficher un réel de valeur 3,14159 avec, puis sans l'application du manipulateur de flux std::ios_base::fixed.

#include <iomanip>
#include <iostream>
int main()
{
   using namespace std;
   cout << setiosflags(ios_base::fixed) << 3.14159f << endl
        << resetiosflags(ios_base::fixed) << 3.14159f << endl;
}

À l'exécution, ce programme affichera 3.141590 suivi de 3.14159. L'emploi du point fixe a pour effet ici de faire apparaître un 0 non significatif.

Modifier la largeur minimale de l'affichage

Il existe un manipulateur nommé std::setw() et servant à imposer une largeur minimale (en nombre de caractères) à la sortie.

Par défaut, la largeur d'affichage d'un nombre est de six chiffres. Le manipulateur std::setw(largeur_min) (pour Set Width, ou fixer la largeur) permet de changer cet état de fait. Ainsi, le programme à droite affichera "      3" (le chiffre trois, précédé de six espaces, pour un affichage total de sept caractères).

cout << setw(7) << 3 << endl;

Par défaut, si la largeur minimale demandée est plus grande que le nombre de chiffres à afficher, les chiffres seront alignés à droite et les espaces à gauche seront remplis par des caractères d'espacement (des blancs, par défaut).

La modification de la largeur d'affichage ne demeure effective que pour la prochaine sortie sur le flux ainsi manipulé. Toute sortie de largeur minimale non standard devra donc être précédée de sa propre requête à std::setw().

Modifier la précision de l'affichage

Il existe un manipulateur nommé std::setprecision() et permettant de choisir la précision lors de l'affichage d'un nombre à virgule flottante. La précision est le nombre de chiffres dans l'affichage résultant, sans compter le point décimal. La dernière décimale sera arrondie au besoin, et les zéros non significatifs seront tronqués. Par défaut, la précision d'affichage d'un nombre à virgule flottante est de 6 chiffres.

L'extrait de code en exemple (à droite) affiche 3.142, 13.14 et 3.14.

cout << setprecision(4)
     << 3.1415926535 << endl
     << 13.1415926535 << endl
     << 3.14 << endl;

Modifier le caractère de remplissage

Il existe un manipulateur nommé std::setfill() et servant à imposer un caractère choisi comme caractère de remplissage.

L'alignement à droite et les espaces ne sont que des paramètres par défaut qui peuvent être changés. Le caractère de remplissage se définit par le manipulateur setfill(), qui prend en paramètre le caractère désiré.

Par exemple, l'exemple proposé à droite affichera "*******55.54".

cout << setw(12) << setfill('*') << 55.54;

Exercice 3 – Essayez le programme suivant :

#include <iomanip>
#include <iostream>
int main()
{
   using namespace std;
   const long L= -16711936L;
   cout << setiosflags(ios_base::internal)
        << setw(20) << setfill('*') << L << endl
        << resetiosflags(ios_base::internal)
        << setw(20) << setfill('*') << L << endl;
}

Expliquez l'impact du manipulateur de flux std::ios_base::internal.

Exercice 4 – Essayez le programme suivant :

#include <iomanip>
#include <iostream>
int main()
{
   using namespace std;
   const float F= 3.14159f;
   cout << setiosflags(ios_base::scientific)
        << setw(20) << setfill('*') << F << endl
        << resetiosflags(ios_base::scientific)
        << setw(20) << setfill('*') << F << endl;
}

Expliquez l'impact du manipulateur de flux std::ios_base::scientific.

Exercice 5 – Écrivez la définition de la procédure C++ afficher_flottant(double) qui affiche le double sur une largeur de dix caractères, avec le signe visible (peu importe que le nombre soit positif ou négatif) et séparé du nombre en soi par des espaces. L'exécution du programme principal ci-après devrait afficher exactement la chaîne de caractères "+   45.176" (+ suivi de trois espaces puis du nombre) sur une ligne, puis "45.176" sur la seconde.

#include <iomanip>
#include <iostream>
void afficher_flottant(double);
int main()
{
   using namespace std;
   afficher_flottant(45.176);
   cout << endl << 45.176 << endl;
}

Reponse

Comment fonctionne un manipulateur de flux (brièvement)

Cette section est un peu plus costaude que les précédentes. Vous voilà averti(e)s!

Les manipulateurs de flux comme std::endl, std::setw, std::setprecision et ainsi de suite sont des créatures étranges qui implémentent une version fort intéressante du schéma de conception Visiteur. Utiliser un manipulateur sur un objet signifie travailler de l'intérieur, avec la permission expresse de l'objet.

La mention potentiellement amicale est placée ici au sens où le type d'un manipulateur pourrait être une classe ou une fonction globale qualifiée friend des types qu'elle manipule, ce qui lui permettrait d'obtenir un accès privilégié à ces types. Rien ne l'oblige, mais rien ne l'empêche non plus.

Contrairement à ce que certain(e)s pourraient penser, les manipulateurs ne sont pas des constantes (même std::endl n'est pas une constante!) mais bien des objets un peu particuliers, adjoints de manière potentiellement amicale aux classes dont ils permettent de manipuler les instances. Chaque manipulateur a un mandat précis à réaliser sur et dans l'objet qu'il manipule.

Le cas de std::endl est le plus simple à illustrer. Ce manipulateur a un type précis et, quand il sert d'opérande de droite pour l'opérateur << sur un flux en sortie, il y provoque deux opérations distinctes :

Il se trouve que std::endl est une fonction (oui, une simple fonction globale) d'une signature spécifique, prenant en paramètre une référence sur un flux de sortie. Le compilateur reconnaît que std::endl correspond à la signature d'un manipulateur et, sans savoir qu'il s'agit de std::endl, sollicite la fonction servant de manipulateur en lui passant en paramètre le flux sur lequel elle est appliquée.

En pratique, l'opérateur << prenant en paramètre une référence sur un flux en sortie et un pointeur de fonction prenant en paramètre un flux provoque une inversion de responsabilité puisque l'opérateur << en question appelle simplement la fonction en lui passant en paramètre le flux.

#include <iostream>
using namespace std;
// chic est un manipulateur extrêmement simple
ostream &chic(ostream &o)
   { return o << "coucou"; }
int main()
{
   // cout appliquera chic() sur lui-même
   cout << chic << endl;
}

Ceci permet à tous les manipulateurs d'avoir la même signature, celle d'une fonction ou d'un foncteur retournant une référence sur un flux en sortie et prenant en paramètre une référence sur un flux en sortie, et d'utiliser la même mécanique générique d'inversion de responsabilité pour chacun d'entre eux.

Un autre cas type, std::setw, permet de modifier la largeur de l'affichage des données numériques sur le flux qu'il manipule.

Dans ce cas, le manipulateur est simple: il s'agit d'une fonction prenant en paramètre un entier et retournant une instance de std::streamsize, que le flux considère comme une consigne de changement de largeur d'affichage. La valeur 0 passée en paramètre à std::setw() signifie largeur par défaut.

#include <iostream>
#include <iomanip>
int main()
{
   using namespace std;
   const float PI = 3.14159f;
   cout << PI << endl
        << setw(10) << PI << endl
        << setw(0)  << PI << endl;
}

Le fait qu'il s'agisse d'un manipulateur permet d'insérer les opérations de modification d'affichage à même la séquence de projection sur un flux en sortie plutôt que d'interrompre la séquence pour appeler une méthode du flux puis poursuivre les opérations.

Il aurait été possible d'écrire le même code sans avoir recours à un manipulateur pour changer la largeur de l'affichage. La différence entre les deux manières de procéder se situe principalement dans la notation, comme le montre l'exemple à droite, tout aussi fonctionnel mais plus verbeux que celui ayant recours à des manipulateurs.

En effet, les manipulateurs n'utilisent bien souvent que des fonctionnalités déjà disponibles dans le flux. En offrant une manière plus agréable de solliciter les méthodes du flux, toutefois, ils étendent en quelque sorte son interface et font du flux un objet plus riche, plus complet.

Ce que les manipulateurs apportent aux sorties est une syntaxe homogène et élégante, une plus grande concision dans la notation, et une approche plus près de l'idéal orienté objet en harmonisation données et opérations.

#include <iostream>
int main()
{
   using namespace std;
   const float PI = 3.14159f;
   cout << PI << endl;
   cout.width(10);
   cout << PI << endl;
   cout.width(0);
   cout << PI << endl;
}

La même technique peut être appliquée à n'importe quelle classe. Suffit d'un peu d'imagination...

Lectures complémentaire

Quelques liens d'autres sources :

Réponses aux exercices

Réponse à l'exercice 0

Binaire Décimal (signé) Décimal (non signé) Octal Hexadécimal
01001011
75
75
0113
0x4b
11110000
-16
240
0360
0xf0
10000001
-127
129
0201
0x81
01111111
127
127
0177
0x7f
11111111
-1
255
0377
0xff
00010000
16
16
020
0x10

Réponse à l'exercice 1

Non, ce programme n'est pas valide. Les nombres qui débutent par 0 sont des nombres en base 8; ainsi, le « nombre » 09 est invalide en C++ car le chiffre 9, en base 8 , n'existe pas (les unités vont de 0 à 7 inclusivement).

Réponse à l'exercice 2

La réponse est Bingo!.

Réponse à l'exercice 5

// includes et using...
void afficher_flottant(double r)
{
   using namespace std;
   cout << setiosflags(ios_base::internal)
        << setiosflags(ios_base::showpos)
        << setfill(' ') << setw(10) << r
        << resetiosflags(ios_base::showpos)
        << resetiosflags(ios_base::internal);
}

[1] On peut faire de même pour des entiers sur 32 bits, mais ça aurait pris plus de place sur la page sans vraiment donner plus d'information.


Valid XHTML 1.0 Transitional

CSS Valide !