• Aucun résultat trouvé

Ecriture formatée dans flux

Dans le document Programmation système en C sous (Page 117-121)

L'une des tâches premières des programmes informatiques est d'afficher des messages lisibles par les utilisateurs sur un périphérique de sortie, généralement l'écran. La preuve en est donnée dans le célèbre hello.c [KERNIGHAN 1994], dont l'unique rôle est d'afficher

«hello world !» et de se terminer normalement 1. #include <stdio.h>

main( ) {

printf ("Hello, world \n");

}

Lorsqu'il s'agit d'une chaîne de caractères constante, comme dans ce fameux exemple, le travail est relativement simple – il suffit d'envoyer les caractères l'un après l'autre sur le flux de sortie –, mais les choses se compliquent nettement quand il faut afficher des valeurs numériques. La conversion entre la valeur 1998, contenue dans une variable de type int, et la série de caractères « 1 », « 9», «9» et «8» n'est déjà pas une tâche simple. Ce qui se présente comme un exercice classique des premiers cours d'assembleur se corse nettement lorsqu'il faut gérer les valeurs signées, puis différentes bases d'affichage (décimal, hexa, octal). Imaginez alors la complexité du travail qui est nécessaire pour afficher le contenu d'une variable en virgule flottante, avec la multitude de formats possibles et le nombre de chiffres significatifs adéquat.

Heureusement, la bibliothèque C standard nous offre les fonctions de la famille printf(

), qui permettent d'effectuer automatiquement les conversions requises pour afficher les données. Ces routines sont de grands classiques depuis les premières versions des bibliothèques standard du langage C, aussi nous ne détaillerons pas en profondeur chaque possibilité de conversion. On pourra, pour avoir plus de renseignements, se reporter à la page de manuel printf(3).

Il existe quatre variantes sur le thème de printf( ), chacune d'elles étant disponible en deux versions, suivant la présentation des arguments.

La fonction la plus utile est bien souvent fprintf( ), dont le prototype est déclaré dans

<stdio.h> ainsi :

int fprintf (FILE * flux, const char * format, ...);

Les points de suspension indiquent qu'on peut fournir un nombre variable d'arguments à cet emplacement. Le premier argument est le flux dans lequel on veut écrire ; il peut s'agir bien entendu de stdout ou stderr, mais nous verrons ultérieurement qu'il peut s'agir aussi de n'importe quel fichier préalablement ouvert avec la fonction open( ).

Le second argument est une chaîne de caractères qui sera envoyée sur le flux indiqué, après avoir remplacé certains caractères spéciaux qu'elle contient. Ceux-ci indiquent la conversion à apporter aux arguments situés à la fin de l'appel avant de les afficher. Par exemple, la séquence «%d» dans le format sera remplacée par la représentation décimale de l'argument de type entier situé à la suite du format. On peut bien entendu placer plusieurs arguments à afficher, en indiquant dans la chaîne de format autant de caractères de conversion.

Les conversions possibles avec la GlibC sont les suivantes :

Conversion But

%d Afficher un nombre entier sous forme décimale signée.

%i Synonyme de %d.

%u Afficher un nombre entier sous forme décimale non signée.

%o Afficher un nombre entier sous forme octale non signée.

%x Afficher un entier non signé sous forme hexa avec des minuscules.

%X Afficher un entier non signé sous forme hexa avec des majuscules.

%f Afficher un nombre réel en notation classique (3.14159).

%e Afficher un réel en notation ingénieur (1.602e-19).

%E Afficher un réel en notation ingénieur avec E majuscule.

%g Afficher un réel le plus lisiblement possible entre %f et %e suivant sa valeur.

%G Comme %g , mais en choisissant entre %f et %E.

%a Afficher un réel avec la mantisse en hexa et l'exposant de 2 en décimal.

%A Comme %a , mais le « P » indiquant l'exposant de 2 est en majuscule.

%c Afficher un simple caractère.

%C Afficher un caractère large (voir chapitre 23).

%s Afficher une chaîne de caractères.

%S Afficher une chaîne de caractères larges.

%p Afficher la valeur d'un pointeur.

%n Mémoriser le nombre de caractères déjà écrits (voir plus bas).

%m Afficher la chaîne de caractères décrivant le contenu de errno.

%% Afficher le caractère de pourcentage.

Notons tout de suite que %m est une extension Gnu, qui correspond à afficher la chaîne de caractères strerror(errno). Signalons également que la conversion %n est très particulière puisqu'elle n'écrit rien en sortie, mais stocke dans l'argument correspondant, qui doit être un pointeur de type int *, le nombre de caractères qui a déjà été envoyé dans le flux de sortie. Cette fonctionnalité n'est pas couramment employée : on peut imaginer l'utiliser avec sprintf( ) , que nous verrons ci-dessous, pour mémoriser l'emplacement des champs de données successifs si on désire y accéder à nouveau par la suite.

Les autres conversions sont très classiques en langage C et ne nécessitent pas plus de détails ici. Voyons un exemple d'utilisation des diverses conversions.

exemple_fprintf_1.c : #include <stdio.h>

#include <limits.h>

int main (void)

{

int d = INT_MAX;

unsigned int u = UINT_MAX;

unsigned int o = INT_MAX;

unsigned int x = UINT_MAX;

unsigned int X = UINT_MAX;

double f = 1.04;

double e = 1500;

double E = 101325;

double g = 1500;

double G = 0.00000101325;

double a = 1.0/65536.0;

double A = 0.125;

char c = 'a';

char * s = "chaîne";

void * p = (void *) main;

fprintf (stdout, " d=%d \n u=%u \n o=%o \n x=%x \n X=%X \n"

" f=%f \n e=%e \n E=%E \n g=%g \n G=%G \n"

" a=%a \n A=%A \n c=%c \n s=%s \n p=%p \n", d, u, o, x, X, f, e, E, g, G. a, A, c, s, p);

return (0);

}

Bien sûr, les valeurs INT_MAX et UINT_MAX définies dans <limits.h> peuvent varier avec l'architecture de la machine.

$ ./exemple_fprintf_1 d=2147483647

u=4294967295 o=17777777777 x=ffffffff X=FFFFFFFF f=1.040000 e=1.500000e+03 E=1.013250E+05 g=1500

G=1.01325E-06 a=Oxlp-16 A=OX1P-3 c=a

s=chaîne p=0x80483f0

$

On peut incorporer, comme dans n'importe quelle chaîne de caractères en langage C, des caractères spéciaux comme \n, \r, \t..., qui seront interprétés par le terminal au moment de l'affichage.

Entre le symbole % et le caractère indiquant la conversion à effectuer, on peut insérer plusieurs indications permettant de modifier la conversion ou de préciser le formatage, en termes de largeur minimale ou maximale d'affichage.

Le premier indicateur qu'on peut ajouter concerne le formatage. Il sert principalement à spécifier sur quel côté le champ doit être aligné. A la suite de cet indicateur peut se trouver un nombre signalant la largeur minimale du champ. On justifie ainsi des valeurs en colonne. On peut encore inclure un point, suivi d'une deuxième valeur marquant la précision d'affichage de la valeur numérique. Un dernier modificateur peut être introduit afin de préciser comment la conversion de type doit être effectuée à partir du type effectif de la variable transmise en argument.

Pour les conversions entières (%d, %i, %u, %o, %x, %X) ou réelles (%f, %e, %E, %g, %G), on peut utiliser — en premier caractère — les indicateurs de formatage suivants :

Caractère Formatage

+ Toujours afficher le signe dans les conversions signées.

- Aligner les chiffres à gauche et non à droite

espace Laisser un espace avant les chiffres positifs d'une conversion signée.

0 (zéro) Compléter le chiffre par des zéros au début plutôt que par des espaces à la fin.

# Préfixer par 0x ou 0X les conversions hexadécimales, et par 0 les conversions octales. Le résultat peut ainsi être relu automatiquement.

Avec les conversions affichant un caractère (%c), une chaîne (%s) ou un pointeur (%p), seul l'indicateur « - » peut être utilisé, afin d'indiquer une justification à gauche du champ.

À la suite de ce modificateur, on indique éventuellement la largeur minimale du champ. Si la valeur à afficher est plus longue, elle débordera. Par contre, si elle est plus courte, elle sera alignée à droite ou à gauche, et complétée par des espaces ou par des zéros suivant le formatage vu précédemment.

Après la largeur minimale du champ. on peut placer un point suivi de la précision de la valeur numérique. La précision correspond au nombre minimal de chiffres affichés dans le cas d'une conversion entière et au nombre de décimales lors des conversions de nombres réels. Voici quelques exemples de formatage en colonne.

exemple_fprintf_2.c:

#include <stdio.h>

int main (void) {

int d;

fprintf (stdout, "| %%6d | %%+6d | %%-6d | %%-+6d| %% 6d | %%06d |\n");

fprintf (stdout, "+---+---+---+---+---+---+\n");

d=0;

fprintf (stdout, "|%6d|%+6d|%-6d|%-+6d|% 6d|%06d|\n", d, d, d, d, d, d); d = 1;

fprintf (stdout, "%6d|%+6d|%-6d|%-+6d|% 6d|%06d|\n", d, d, d, d, d, d);

d = -2;

fprintf (stdout, "|%6d|%+6d|%-6d|%-+6d|% 6d|%06d|\n", d, d, d, d, d, d);

d = 100;

fprintf (stdout, "|%6d|%+6d|%-6d|%-+6d|% 6d|%06d|\n", d, d, d, d, d, d);

d = 1000;

fprintf (stdout, "|%6d|%+6d|%-6d|%-+6d|% 6d|%06d|\n", d, d, d, d, d, d);

d = 10000;

fprintf (stdout, "|%6d|%+6d|%-6d|%-+6d|% 6d|%06d|\n", d, d, d, d, d, d);

d = 100000;

fprintf (stdout, "|%6d|%+6d|%-6d|%-+6d|% 6d|%06d|\n", d, d, d, d, d, d);

return (0);

}

$ ./exemple_fprintf_2

| %6d | %+6d | 1%-6d %-+6d1 % 6d 1 %06d +---+---+---+---+ + +

| 0 | +0|0 |+0 | 0|000000|

| 1| +1|1 |+1 | 1|000001|

| -2| -2|-2 |-2 | -2|-00002|

| 100| +100|100 |+100 | 100|000100|

| 1000| +1000|1000 |+1000 | 1000|001000|

| 10000|+10000|10000 |+10000| 10000|010000|

|100000|+100000|100000|+100000| 100000|100000|

$

Nous voyons que l'indication de largeur du champ correspond bien à une largeur minimale, un débordement pouvant se produire, comme c'est le cas sur la dernière ligne si le signe est affiché. L'exemple suivant montre l'effet de l'indicateur de précision sur des conversions entières et réelles.

exemple_fprintf 3.c : #include <stdio.h>

int main (void) int d;

double f;

fprintf (stdout, "| %%8.0d | %%8.2d | %%8.0f "

"| %%8.2f | %%8.2e | %%8.2g |\n");

fprintf (stdout, "+---+---+---"

d = 0;

f = 0.0;

fprintf (stdout, "|%8.0d|%8.2d|%8.0f|%8.2f|%8.2e|%8.28|\n", d, d, f, f, f, f);

d = 1;

f = 1.0;

fprintf (stdout, "|%8.0d|%8.2d|%8.0f|%8.2f|%8.2e|%8.28|\n", d, d, f, f, f, f);

d = -2;

f = -2.0;

fprintf (stdout, "|%8.0d|%8.2d|%8.0f|%8.2f|%8.2e|%8.2g|\n", d, d, f, f, f, f);

d = 10;

f = 10.1;

fprintf (stdout, "|%8.0d|%8.2d|%8.0f|%8.2f|%8.2e|%8.28|\n", d, d, f, f, f, f);

d = 100;

f = 100.01;

fprintf (stdout, "|%8.0d|%8.2d|%8.0f|%8.2f|%8.2e|%8.28|\n", d, d, f, f, f, f);

return (0);

$ ./exemple fprintf 3

| %8.0d %8.2d %8.0f %8.2f 1 %8.2e %8.2g

+---+---+---+---+---+---+

| | 00| 0| 0.00|0.00e+00| 0|

| | 01| 1| 1.00|1.00e+00| 1|

| -2| -02| -2| -2.00|-2.00e+00| -2|

| 10| 10| 10| 10.10|1.01e+01| 10|

| 100| 100| 100| 100.01|1.00e+02| 1e+02|

$

Notons là encore que la largeur indiquée peut être dépassée au besoin (comme avec -2 en notation exponentielle). La précision correspond bien au nombre minimal de chiffres affichés pour les entiers et au nombre de décimales pour les réels.

Enfin, le dernier indicateur est un modificateur qui précise le type réel de l'argument transmis, avant sa conversion. Avec les conversions entières. les modificateurs suivants sont autorisés :

Modificateur Effet

h L'argument est un short int ou un unsigned short int.

hh L'argument est un char ou un unsigned char.

l L'argument est un long int ou un unsigned long int.

ll, L, ou q L'argument est un long long int, parfois nommé « quad » sur d'autres systèmes.

t L'argument est de type ptrdiff_t.

z L'argument est de type size_t ou ssize_t .

Le choix entre le type signé ou non dépend du type de conversion qui suit le modificateur (%d ou %u , par exemple).

ptrdiff_t sert lorsqu'on effectue manuellement des opérations arithmétiques sur les pointeurs. Le type size_t ou sa version signée ssize_t servent à mesurer la taille des données.

Avec les conversions réelles, tout type de donnée est promue au rang de double avant d'être affichée. On peut éventuellement utiliser le modificateur L. qui indique que l'argument est de type long double. Il n'y a pas d'autre modificateur pour les conversions réelles.

Nous allons à présent observer quelques particularités moins connues de fprintf( ) : la largeur de champ variable et la permutation des arguments. Si on remplace la largeur minimale du champ ou la précision numérique par un astérisque, la valeur sera lue dans l'argument

suivant de fprintf( ). Cela permet de fixer la largeur d'un champ de manière dynamique. En voici une démonstration.

exemple_fprintf_4.c #include <stdio.h>

int main (void) {

int largeur;

int nb_chiffres;

for (largeur = 1; largeur < 10; largeur ++) fprintf (stdout, "|%*d|\n", largeur, largeur);

for (nb_chiffres = 0; nb_chiffres < 10; nb_chiffres ++) fprintf (stdout, "|%.*d|\n", nb_chiffres, nb_chiffres);

return (0);

}

$ ./exemple_fprintf_4

|1|

| 2|

| 3|

| 4|

| 5|

| 6|

| 7|

| 8|

| 9|

||

|1|

|02|

|003|

|0004|

|00005|

|000006|

|0000007|

|00000008|

|000000009|

$

L'intérêt de cette caractéristique est principalement de pouvoir fixer la largeur des colonnes d'un tableau pendant l'exécution du programme (éventuellement après avoir vérifié que la plus grande valeur y tient).

La permutation de paramètre est une deuxième particularité peu connue de fprintf( ).

On peut indiquer en tout début de conversion, juste après le symbole %, le numéro du paramètre qu'on désire convertir, suivi du signe $. Ce numéro doit être supérieur ou égal à 1, et inférieur ou égal au rang du dernier argument transmis. Si on utilise cette possibilité, il faut nécessaire-ment le faire pour toutes les conversions lors de l'appel de fprintf( ) , sinon le comportement est indéfini. L'utilité de cette fonctionnalité est de permettre de préciser l'ordre des arguments

au sein même de la chaîne de formatage. Une application évidente est d'ordonner correcte-ment les jours, mois et année de la date en fonction des désirs de l'utilisateur, uniquecorrecte-ment en sélectionnant la bonne chaîne de formatage.

exemple_fprintf_5.c #include <stdio.h>

#include <time.h>

int main (void) {

int i;

char * format [2] =

{ "La date est %3$02d/%2$02d/%1$02d\n", "Today is %1$02d %2$02d %3$02d\n"

time_t timer;

struct tm * date;

time (& timer);

date = localtime (& timer);

for (i = 0; i < 2; i++)

fprintf (stdout, format [i], date -> tm_year % 100, date -> tm mon + 1, date -> tm_mday);

return (0);

}

$ ./exemple_fprintf_5 La date est 14/09/99 Today is 99 09 14

$

On voit la puissance de cette fonctionnalité, qui permet de profiter de la phase de traduction des messages pour réordonner correctement les champs suivant la localisation.

Dans le document Programmation système en C sous (Page 117-121)