9 Entrées Sorties

Comme nous l'avons vu (c.f. Section 8.1.3) un programme dispose de canaux d'entrées sorties stdin, stdout et stderr. Pour faciliter la vie du programmeur, les bibliothèques standard offrent toute une panoplie de fonctions pour formater les sorties et interpréter les entrées.

La fonction phare est bien entendu printf pour le formatage de chaîne de caractères et scanf pour la lecture de chaînes de caractères. Ces dernières fonctions se déclinent en plusieurs variantes :

  • depuis/vers les canaux standards printf, scanf

  • depuis/vers un fichier quelconque fprintf, fscanf

  • depuis/vers une chaîne de caractères sprintf, sscanf

La liste citée est non exhaustive, mais largement documentée ici: <stdio.h>.

9.1 Sorties non formatées

Si l'on souhaite simplement écrire du texte sur la sortie standard, deux fonctions sont disponibles :

putchar(char c)

Pour imprimer un caractère unique: putchar('c')

puts(char[] str)

Pour imprimer une chaîne de caractères

Exercice 9.1

Écrire un programme qui retourne un mot parmi une liste de mot, de façon aléatoire.

#include <time.h>
#include <stdlib.h>

char *words[] = {"Albédo", "Bigre", "Maringouin", "Pluripotent", "Entrechat",
    "Caracoler" "Palinodie", "Sémillante", "Atavisme", "Cyclothymie",
    "Idiosyncratique", "Entéléchie"};

#if 0
    srand(time(NULL));   // Initialization, should only be called once.
    size_t r = rand() % sizeof(words) / sizeof(char*); // Generate random value
#endif

9.2 Sorties formatées

Convertir un nombre en une chaîne de caractères n'est pas trivial. Prenons l'exemple de la valeur 123. Il faut pour cela diviser itérativement le nombre par 10 et calculer le reste :

Etape  Opération  Resultat  Reste
1      123 / 10   12        3
2      12 / 10    1         2
3      1 / 10     0         1

Comme on ne sait pas à priori combien de caractères on aura, et que ces caractères sont fournis depuis le chiffre le moins significatif, il faudra inverser la chaîne de caractères produite.

Voici un exemple possible d'implémentation :

#include <stdlib.h>
#include <stdbool.h>

void swap(char* a, char* b)
{
    char old_a = a;
    a = b;
    b = old_a;
}

void reverse(char* str, size_t length)
{
    for (size_t start = 0, end = length - 1; start < end; start++, end--)
    {
        swap(str + start, str + end);
    }
}

void my_itoa(int num, char* str)
{
    const unsigned int base = 10;
    bool is_negative = false;
    size_t i = 0;

    if (num == 0) {
        str[i++] = '0';
        str[i] = '\0';
        return;
    }

    if (num < 0) {
        is_negative = true;
        num = -num;
    }

    while (num != 0) {
        int rem = num % 10;
        str[i++] = rem + '0';
        num /= base;
    }

    if (is_negative)
        str[i++] = '-';

    str[i] = '\0';

    reverse(str, i);
}

Cette implémentation pourrait être utilisée de la façon suivante :

#include <stdlib.h>

int main(void)
{
    int num = 123;
    char buffer[10];

    itoa(num, buffer);
}

9.3 printf

Vous conviendrez que devoir manuellement convertir chaque valeur n'est pas des plus pratique, c'est pourquoi printf rend l'opération bien plus aisée en utilisant des marques substitutives (placeholder). Ces spécifié débutent par le caractère % suivi du formatage que l'on veut appliquer à une variable passée en paramètres. L'exemple suivant utilise %d pour formater un entier non signé.

#include <stdio.h>

int main()
{
    int32_t earth_perimeter = 40075;
    printf("La circonférence de la terre vaut vaut %d km", earth_perimeter);
}

Le standard C99 défini le prototype de printf comme étant :

int printf(const char *restrict format, ...);

Il définit que la fonction printf prend en paramètre un format suivi de .... La fonction printf comme toutes celles de la même catégorie sont dites variadiques, c'est-à-dire qu'elles peuvent prendre un nombre variable d'arguments. Il y aura autant d'arguments additionnels que de marqueurs utilisés dans le format. Ainsi le format "Mes nombres préférés sont %d et %d, mais surtout %s" demandera trois paramètres additionnels :

La fonction retourne le nombre de caractères formatés ou -1 en cas d'erreur.

La construction d'un marqueur est loin d'être simple, mais heureusement on n'a pas besoin de tout connaître et la page Wikipedia printf format string est d'une grande aide. Le format de construction est le suivant :

%[parameter][flags][width][.precision][length]type
parameter (optionnel)

Numéro de paramètre à utiliser

flags (optionnel)

Modificateurs: préfixe, signe plus, alignement à gauche ...

width (optionnel)

Nombre minimum de caractères à utiliser pour l'affichage de la sortie.

.precision (optionnel)

Nombre minimum de caractères affichés à droite de la virgule. Essentiellement, valide pour les nombres à virgule flottante.

length (optionnel)

Longueur en mémoire. Indique la longueur de la représentation binaire.

type

Type de formatage souhaité

../_images/formats.svg

Fig. 9.1 Formatage d'un marqueur

9.3.1 Exemples

Tableau 9.1 Exemple de formatage avec printf

Exemple

Sortie

Taille

printf("%c", 'c')

c

1

printf("%d", 1242)

1242

4

printf("%10d", 42)

:code:` 42`

10

printf("%07d", 42)

0000042

7

printf("%+-5dfr", 23)

+23   fr

6

printf("%5.3f", 314.15)

314.100

7

printf("%*.*f", 4, 2, 102.1)

102.10

7

printf("%8x", 57005)

:code:` dead`

6

printf("%s", "Hello")

Hello

5

Exercice 9.2

Indiquez les erreurs dans les instructions suivantes :

printf("%d%d\n", 10, 20);
printf("%d, %d, %d\n", 10, 20);
printf("%d, %d, %d, %d\n", 10, 20, 30, 40.);
printf("%*d, %*d\n", 10, 20);
printf("%6.2f\n", 10);
printf("%10s\n", 0x9f);

9.4 Entrées formatées

À l'instar de la sortie formatée, il est possible de lire les saisies au clavier ou parser une chaîne de caractères, c'est-à-dire faire une analyse syntaxique de son contenu pour en extraire de l'information.

La fonction scanf est par exemple utilisée à cette fin :

#include <stdio.h>

int main()
{
    int favorite;

    printf("Quelle est votre nombre favori ? ");
    scanf("%d", &favorite);

    printf("Saviez-vous que votre nombre favori, %d, est %s ?\n",
        favorite,
        favorite % 2 ? "impair" : "pair");
}

Cette fonction utilise l'entrée standard stdin. Il est donc possible soit d'exécuter ce programme en mode interactif :

$ ./a.out
Quelle est votre nombre favori ? 2
Saviez-vous que votre nombre favori, 2, est pair ?

soit d'exécuter ce programme en fournissant le nécessaire à stdin :

$ echo "23" | ./a.out
Quel est votre nombre favori ? Saviez-vous que votre nombre favori, 23, est impair ?

On observe ici un comportement différent, car le retour clavier lorsque la touche enter est pressée n'est pas transmis au programme, mais c'est le shell qui l'intercepte.

9.4.1 scanf

Le format de scanf se rapproche de printf mais en plus simple. Le man scanf ou même la page Wikipedia de scanf renseigne sur son format.

Cette fonction tient son origine une nouvelle fois de ALGOL 68 (readf), elle est donc très ancienne.

La compréhension de scanf n'est pas évidente et il est utile de se familiariser sur son fonctionnement à l'aide de quelques exemples.

Le programme suivant lit un entier et le place dans la variable n. scanf retourne le nombre d'assignements réussis. Ici, il n'y a qu'un placeholder, on s'attend naturellement à lire 1 si la fonction réussit. Le programme écrit ensuite les nombres dans l'ordre d'apparition.

#include <stdio.h>

int main(void)
{
    int i = 0, n;

    while (scanf("%d", &n) == 1)
        printf("%i\t%d\n", ++i, n);
    return 0;
}

Si le code est exécuté avec une suite arbitraire de nombres :

456 123 789     456 12
456 1
    2378

il affichera chacun des nombres dans l'ordre d'apparition :

$ cat << EOF | ./a.out
456 123 789     456 12
456 1
    2378
EOF
1       456
2       123
3       789
4       456
5       12
6       456
7       1
8       2378

Voyons un exemple plus complexe (c.f. C99 §7.19.6.2-19).

int count;
float quantity;
char units[21], item[21];

do {
    count = scanf("%f%20s de %20s", &quant, units, item);
    scanf("%*[^\n]");
} while (!feof(stdin) && !ferror(stdin));

Lorsqu'exécuté avec ce contenu :

2 litres de lait
-12.8degrés Celsius
beaucoup de chance
10.0KG de
poussière
100ergs d’énergie

Le programme se déroule comme suit :

quantity = 2; strcpy(units, "litres"); strcpy(item, "lait");
count = 3;

quantity = -12.8; strcpy(units, "degrees");
count = 2; // "C" échoue lors du test de "d" (de)

count = 0; // "b" de "beaucoup" échoue contre "%f" s'attendant à un float

quantity = 10.0; strcpy(units, "KG"); strcpy(item, "poussière");
count = 3;

count = 0; // "100e" échoue contre "%f", car "100e3" serait un nombre valable
count = EOF; // Fin de fichier

Dans cet exemple, la boucle do... while est utilisée, car il n'est pas simplement possible de traiter le cas while(scanf(...) > 0 puisque l'exemple cherche à montrer les cas particuliers où justement, la capture échoue. Il est nécessaire alors de faire appel à des fonctions de plus bas niveau feof pour détecter si la fin du fichier est atteinte, et ferror pour détecter une éventuelle erreur sur le flux d'entrée.

La directive scanf("%*[^\n]"); étant un peu particulier, il peut valoir la peine de s'y attarder un peu. Le flag *, différent de printf indique d'ignorer la capture en cours. L'exemple suivant montre comment ignorer un mot.

#include <assert.h>
#include <stdio.h>

int main(void) {
    int a, b;
    char str[] = "24 kayaks 42";

    sscanf(str, "%d%*s%d", &a, &b);
    assert(a == 24);
    assert(b == 42);
}

Ensuite, [^\n]. Le marqueur [, terminé par ] cherche à capturer une séquence de caractères parmi une liste de caractères acceptés. Cette syntaxe est inspirée des expressions régulières très utilisées en informatique. Le caractère ^ à une signification particulière, il indique que l'on cherche à capturer une séquence de caractères parmi une liste de caractères qui ne sont pas acceptés. C'est une sorte de négation. Dans le cas présent, cette directive scanf cherche à consommer tous les caractères jusqu'à une fin de ligne, car, dans le cas ou la capture échoue à C de Celsius, le pointeur de fichier est bloqué au caractère C et au prochain tour de boucle, scanf échouera au même endroit. Cette instruction est donc utilisée pour repartir sur des bases saines en sautant à la prochaine ligne.

Exercice 9.3

Considérant les déclarations :

int i, j, k;
float f;

Donnez les valeurs de chacune des variables après exécution. Chaque ligne est indépendante des autres.

i = sscanf("1 12.5", "%d %d, &j, &k);
sscanf("12.5", "%d %f", &j, %f);
i = sscanf("123 123", "%d %f", &j, &f);
i = sscanf("123a 123", "%d %f", &j, &f);
i = sscanf("%2d%2d%f", &j, &k, &f);

Exercice 9.4

Considérant les déclarations suivantes, donner la valeur des variables après l'exécution des instructions données avec les captures associées :

int i = 0, j = 0, n = 0;
float x = 0;
  1. n = scanf("%1d%1d", &i, &j);, 12\n

  2. n = scanf("%d%d", &i, &j);, 1 , 2\n

  3. n = scanf("%d%d", &i, &j);, -1   -2\n

  4. n = scanf("%d%d", &i, &j);, -  1  -  2\n

  5. n = scanf("%d,%d", &i, &j);, 1  ,  2\n

  6. n = scanf("%d ,%d", &i, &j);, 1  ,  2\n

  7. n = scanf("%4d %2d", &i, &j);, 1 234\n

  8. n = scanf("%4d %2d", &i, &j);, 1234567\n

  9. n = scanf("%d%*d%d", &i, &j);, 123 456 789\n

  10. n = scanf("i=%d , j=%d", &i, &j);, 1 , 2\n

  11. n = scanf("i=%d , j=%d", &i, &j);, i=1, j=2\n

  12. n = scanf("%d%d", &i, &j);, 1.23 4.56\n

  13. n = scanf("%d.%d", &i, &j);, 1.23 4.56\n

  14. n = scanf("%x%x", &i, &j);, 12 2a\n

  15. n = scanf("%x%x", &i, &j);, 0x12 0X2a\n

  16. n = scanf("%o%o", &i, &j);, 12 018\n

  17. n = scanf("%f", &x);, 123\n

  18. n = scanf("%f", &x);, 1.23\n

  19. n = scanf("%f", &x);, 123E4\n

  20. n = scanf("%e", &x);, 12\n

Exercice 9.5

  1. Saisir 3 caractères consécutifs dans des variables i, j, k.

  2. Saisir 3 nombres de type float séparés par un point-virgule et un nombre quelconque d'espaces dans des variables x, y et z.

  3. Saisir 3 nombres de type double en affichant avant chaque saisie le nom de la variable et un signe =, dans des variables t, u et v.

9.4.2 Saisie de chaîne de caractères

Lors d'une saisie de chaîne de caractères, il est nécessaire de toujours indiquer une taille maximum de chaîne comme %20s qui limite la capture à 20 caractères, soit une chaîne de 21 caractères avec son \0. Sinon, il y a risque de fuite mémoire :

int main(void) {
    char a[6];
    char b[10] = "Râteau";

    char str[] = "jardinage";
    sscanf(str, "%s", a);

    printf("a. %s\nb. %s\n", a, b);
}
$ ./a.out
a. jardinage
b. age

Ici la variable b contient age alors qu'elle devrait contenir râteau. La raison est que le mot capturé jardinage est trop long pour la variable a qui n'est disposée à stocker que 5 caractères imprimables. Il y a donc dépassement mémoire et comme vous le constatez, le compilateur ne génère aucune erreur. La bonne méthode est donc de protéger la saisie ici avec %5s.

En mémoire, ces deux variables sont adjacentes et naturellement a[7] est équivalent à dire la septième case mémoire à partir du début de ``a``.

     a[6]              b[10]
┞─┬─┬─┬─┬─┬─┦┞─┬─┬─┬─┬─┬─┬─┬─┬─┬─┦
│ │ │ │ │ │ ││R│â│t│e│a│u│ │ │ │ │
└─┴─┴─┴─┴─┴─┘└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

9.4.3 Saisie arbitraire

Comme brièvement évoqué plus haut, il est possible d'utiliser le marqueur [ pour capturer une séquence de caractères. Imaginons que je souhaite capturer un nombre en tetrasexagesimal (base 64). Je peux écrire :

char input[] = "Q2hvY29sYXQ";
char output[128];
sscanf(input, "%127[0-9A-Za-z+/]", &output);

Dans cet exemple je capture les nombres de 0 à 9 0-9 (10), les caractères majuscules et minuscules A-Za-z (52), ainsi que les caractères +, / (2), soit 64 caractères. Le buffer d'entrée étant fixé à 128 positions, la saisie est contrainte à 127 caractères imprimables.

Exercice 9.6

Parmi les instructions ci-dessous, indiquez celles qui sont correctes et celle qui comporte des erreurs. Pour celles comportant des erreurs, détaillez la nature des anomalies.

short i;
long j;
unsigned short u;
float x;
double y;
printf(i);
scanf(&i);
printf("%d", &i);
scanf("%d", &i);
printf("%d%ld", i, j, u);
scanf("%d%ld", &i, j);
printf("%u", &u);
scanf("%d", &u);
printf("%f", x);
scanf("%f", &x);
printf("%f", y);
scanf("%f", &y);

Exercice 9.7

Écrivez un programme déclarant des variables réelles x, y et z, permettant de saisir leur valeur en une seule instruction, et vérifiant que les 3 valeurs ont bien été assignées. Dans le cas contraire, afficher un message du type "données invalides".

Exercice 9.8

Écrire un programme effectuant les opérations suivantes :

  • Saisir les coordonnées réelles x1 et y1 d’un vecteur v1.

  • Saisir les coordonnées réelles x2 et y2 d’un vecteur v2.

  • Calculer le produit scalaire. Afficher un message indiquant si les vecteurs sont orthogonaux ou non.

Exercice 9.9

Votre collègue n'a pas cessé de se plaindre de crampes... aux doigts... Il a écrit le programme suivant avant de prendre congé pour se rendre chez son médecin.

Grâce à votre esprit affuté et votre œil perçant, vous identifiez 13 erreurs. Lesquelles sont-elles ?

#include <std_io.h>
#jnclude <stdlib.h>
INT Main()
{
int a, sum;
printf("Addition de 2 entiers a et b.\n");

printf("a: ")
scanf("%d", a);

printf("b: ");
scanf("%d", &b);

/* Affichage du résultat
somme = a - b;
Printf("%d + %d = %d\n", a, b, sum);

retturn EXIT_FAILURE;
}
}

Exercice 9.10

Considérez le programme suivant :

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    float a;
    printf("a = ");
    scanf("%f", &a);

    float b;
    printf("b = ");
    scanf("%f", &b);

    float x;
    printf("x = ");
    scanf("%f", &x);

    float y = a * x + b;

    printf("y = %f\n", y);

    return 0;
}
  1. À quelle ligne commence l'exécution de ce programme ?

  2. Dans quel ordre s'exécutent les instructions ?

  3. Décrivez ce que fait ce programme étape par étape

  4. Que verra l'utilisateur à l'écran ?

  5. Quelle est l'utilité de ce programme ?

Exercice 9.11

L'exercice précédent souffre de nombreux défauts. Sauriez-vous les identifier et perfectionner l'implémentation de ce programme ?

Exercice 9.12

Écrivez un programme demandant deux réels tension et résistance, et affichez ensuite le courant. Prévoir un test pour le cas où la résistance serait nulle.

Exercice 9.13

Considérons le programme suivant :

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main()
{
    printf("Quel angle mesurez-vous en visant le sommet du bâtiment (en degrés): ");
    float angle_degre;
    scanf("%f", &angle_degrees);
    float angle_radian = angle_degrees * M_PI / 45.;

    printf("À quelle distance vous trouvez vous du bâtiment (en mètres): ");
    float distance;
    scanf("%f", &distance);

    float height = distance / tan(angle_radian);
    printf("La hauteur du bâtiment est : %g mètres.\n", height);

    return 0;
}
  1. Que fait le programme étape par étape ?

  2. Que verra l'utilisateur à l'écran ?

  3. À quoi sert ce programme ?

  4. Euh, mais ? Ce programme comporte des erreurs, lesquelles ?

  5. Implémentez-le et testez-le.

Exercice 9.14

Hyperloop (aussi orthographié Hyperl∞p) est un projet ambitieux d'Elon Musk visant à construire un moyen de transport ultra rapide utilisant des capsules voyageant dans un tube sous vide. Ce projet est analogue à celui étudié en suisse et nommé Swissmetro, mais abandonné en 2009.

Néanmoins, les ingénieurs suisses avaient à l'époque écrit un programme pour calculer, compte tenu d'une vitesse donnée, le temps de parcours entre deux villes de Suisse.

Écrire un programme pour calculer la distance entre deux villes de suisse parmi lesquelles proposées sont :

  • Genève

  • Zürich

  • Bâle

  • Bern

  • St-Galle

Considérez une accélération de 0.5 g pour le calcul de mouvement, et une vitesse maximale de 1220 km/h.