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é
Fig. 9.1 Formatage d'un marqueur¶
9.3.1 Exemples¶
Exemple |
Sortie |
Taille |
---|---|---|
|
|
1 |
|
|
4 |
|
:code:` 42` |
10 |
|
|
7 |
|
|
6 |
|
|
7 |
|
|
7 |
|
:code:` dead` |
6 |
|
|
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;
n = scanf("%1d%1d", &i, &j);
,12\n
n = scanf("%d%d", &i, &j);
,1 , 2\n
n = scanf("%d%d", &i, &j);
,-1 -2\n
n = scanf("%d%d", &i, &j);
,- 1 - 2\n
n = scanf("%d,%d", &i, &j);
,1 , 2\n
n = scanf("%d ,%d", &i, &j);
,1 , 2\n
n = scanf("%4d %2d", &i, &j);
,1 234\n
n = scanf("%4d %2d", &i, &j);
,1234567\n
n = scanf("%d%*d%d", &i, &j);
,123 456 789\n
n = scanf("i=%d , j=%d", &i, &j);
,1 , 2\n
n = scanf("i=%d , j=%d", &i, &j);
,i=1, j=2\n
n = scanf("%d%d", &i, &j);
,1.23 4.56\n
n = scanf("%d.%d", &i, &j);
,1.23 4.56\n
n = scanf("%x%x", &i, &j);
,12 2a\n
n = scanf("%x%x", &i, &j);
,0x12 0X2a\n
n = scanf("%o%o", &i, &j);
,12 018\n
n = scanf("%f", &x);
,123\n
n = scanf("%f", &x);
,1.23\n
n = scanf("%f", &x);
,123E4\n
n = scanf("%e", &x);
,12\n
Exercice 9.5¶
Saisir 3 caractères consécutifs dans des variables
i
,j
,k
.Saisir 3 nombres de type float séparés par un point-virgule et un nombre quelconque d'espaces dans des variables
x
,y
etz
.Saisir 3 nombres de type double en affichant avant chaque saisie le nom de la variable et un signe
=
, dans des variablest
,u
etv
.
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
ety1
d’un vecteurv1
.Saisir les coordonnées réelles
x2
ety2
d’un vecteurv2
.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;
}
À quelle ligne commence l'exécution de ce programme ?
Dans quel ordre s'exécutent les instructions ?
Décrivez ce que fait ce programme étape par étape
Que verra l'utilisateur à l'écran ?
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;
}
Que fait le programme étape par étape ?
Que verra l'utilisateur à l'écran ?
À quoi sert ce programme ?
Euh, mais ? Ce programme comporte des erreurs, lesquelles ?
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.