XXXII. Saisie de données par un opérateur (stdin)▲
XXXII-A. Introduction▲
Il est courant en C standard d'utiliser le flux stdin pour acquérir des données en provenance d'un opérateur. (Mode conversationnel). On admettra pour la suite que stdin est connecté à la partie 'clavier' d'un périphérique console.
Le langage C offre plusieurs fonctions permettant de lire des données sur un flux en général et sur stdin en particulier :
- fgetc() ;
- getc() ;
- getchar() ;
- gets() ;
- scanf() ;
- fgets().
XXXII-B. fgetc(), getc(), getchar()▲
Ces trois fonctions extraient un caractère du flux entrant (pour getchar(), ce flux est stdin). C'est insuffisant pour saisir autre chose qu'un simple <ENTER>. Ces fonctions ne sont absolument pas adaptées à la saisie d'un caractère comme un choix de menu par exemple.
Par contre, ces fonctions peuvent être utilisées pour construire des fonctions d'entrées de plus haut niveau plus ou moins spécialisées.
XXXII-C. gets()▲
Pour des raisons évidentes de sécurité (pas de limitation du nombre de caractères saisis), la fonction gets() ne devrait pas être utilisée. Bien que, à ma connaissance, cette fonction ne soit pas officiellement dépréciée pour des raisons de compatibilité avec le code existant, il est fortement conseillé de ne pas l'utiliser pour de nouveaux développements.
XXXII-D. scanf()▲
Malgré ce que l'on constate dans l'abondante littérature consacrée à l'initiation au langage C, l'utilisation de scanf() n'est pas adaptée.
En effet, le 'f' de scanf() est là pour nous rappeler que l'entrée doit être formatée (formated), ce qui n'est évidemment pas le cas avec un opérateur humain qui peut entrer n'importe quoi. D'autre part, scanf() gère difficilement le '\n', ce qui entraîne des comportements aberrants dans les saisies si on ne prend pas certaines précautions d'usage.
L'utilisation correcte et sûre de scanf() est complexe, et n'est pas à la porté d'un débutant (ni même à celle de la plupart des programmeurs expérimentés). Néanmoins, il est possible d'utiliser correctement scanf() si on se forme correctement. En lisant par exemple l'article Scanf démythifiée.
XXXII-E. fgets()▲
Cette fonction est parfaitement adaptée à la saisie d'une ligne, (même de 1 caractère). Son usage est recommandé.
S'il faut saisir une valeur numérique, celle-ci sera d'abord saisie sous forme de ligne, puis traduite par la fonction appropriée (strtol(), strtoul(), strtod()) ou sscanf()) avec le filtre approprié :
#include <stdio.h>
#include <stdlib.h>
int
main
(
void
)
{
int
ret;
char
temp[20
];
do
{
char
saisie[20
];
printf
(
"
Entrez un nombre :
"
);
fflush (
stdout);
fgets (
saisie, sizeof
saisie, stdin);
/* Filtrage des caractères (entier décimal)
* Nota : la saisie s'arrête à la première erreur.
* Ce qui est saisi avant est considéré comme valide.
*
* "123a" -> "123" : ret = 1
*
* "a123" -> "" : ret = 0
*/
ret =
sscanf (
saisie, "
%[0-9-]s
"
, temp);
}
while
(
ret !=
1
);
{
long
n =
strtol (
temp, NULL
, 10
);
printf (
"
La chaîne est '%s', soit %ld
\n
"
, temp, n);
}
return
0
;
}
D'autres exemples dans le chapitre sur les fichiers.
XXXII-F. Ressources▲
XXXII-G. Comment fonctionne fgetc(stdin) alias getchar()▲
Cette fonction d'apparence simple a en fait un comportement plus complexe qu'il n'y parait. En effet, elle regroupe un certain nombre de comportements non triviaux qui sont rarement expliqués dans la littérature C.
XXXII-G-1. Comportement visible.▲
L'appel de cette fonction provoque une suspension de l'exécution du programme. Durant cette suspension, il est possible de rentrer des caractères (par exemple à l'aide du clavier) et même éventuellement de supprimer le ou les derniers caractères saisis à l'aide de la touche 'BackSpace'. La fin de saisie (et la reprise de l'exécution du programme) est marquée par la frappe de la touche <enter>.
XXXII-G-2. Comportement interne.▲
Les caractères saisis sont stockés dans le flux stdin. Lorsque l'on frappe la touche <enter>, le caractère '\n' est aussi placé dans stdin, et l'exécution reprend. Le caractère le plus ancien est alors extrait du flux et il est retourné. En cas d'erreur de lecture ou d'entrée d'un caractère spécial dit 'de fin de fichier' (Ctrl-D, Ctrl-Z etc. selon le système), la valeur EOF (int < 0) est retournée.
Ensuite, si on rappelle fgetc(), deux cas sont possibles. Soit le flux est vide, soit il ne l'est pas. Si le flux est vide, la fonction fgetc() suspend l'exécution, et on retrouve le comportement précédent. S'il n'est pas vide, l'exécution n'est pas suspendue, et le caractère le plus ancien est extrait et retourné.
Dans la grande majorité des cas la lecture du '\n' signifie que la ligne saisie a été complètement lue.
XXXII-G-3. Quelques expérimentations.▲
A l'aide de simples programmes, il est possible de vérifier un certain nombre de comportements décrits précédemment :
#include <stdio.h>
int
main (
void
)
{
int
x =
fgetc
(
stdin);
printf (
"
x = %d ('%c')
\n
"
, x, x);
return
0
;
}
Quelques essais de saisie :
<enter>
x = 10 ('
')
On voit que le caractère extrait est '\n' (ici, LF, soit le code ASCII 10)
a<enter>
x = 97 ('a')
On voit que le caractère extrait est 'a' (ici, le code ASCII 97). Le <enter> ('\n') n'a pas été extrait. Si on appelait fgetc() une nouvelle fois, il n'y aurait pas de suspension.
a<backspace>b<enter>
x = 98 ('b')
On constate que, bien que le premier caractère saisi fut 'a', le caractère extrait est 'b' (ici, le code ASCII 98). En effet, la touche <backspace> a permis de corriger la dernière saisie.
abcd<enter>
x = 97 ('a')
On voit que le caractère extrait est 'a', bien que d'autres caractères aient été saisis après. C'est donc bien le plus ancien caractère qui est extrait. Les autres caractères sont en attente de lecture. Une boucle de fgetc() permettrait de les extraire.
#include <stdio.h>
int
main (
void
)
{
int
x;
do
{
x =
fgetc
(
stdin);
printf (
"
x = %d ('%c')
\n
"
, x, x);
}
while
(
1
);
return
0
;
}
Je laisse au lecteur le soin de refaire les expériences précédentes et d'en tirer les conclusions qui s'imposent.
XXXIII. Les fichiers▲
XXXIII-A. Introduction▲
Le langage C n'offre pas, à proprement parler, de gestion de fichiers. Il définit plutôt des flux d'entrées / sorties (I/O streams) sur lesquels il peut agir (ouverture/fermeture, lecture/écriture). L'unité d'information gérée par un flux est le byte.
Certains de ces flux sont connectés à des périphériques permettant par exemple de réaliser une interface entre la machine et l'utilisateur (IHM) en mode texte. Mais la plupart du temps, le nom associé au flux est en fait un 'fichier', c'est-à-dire une sorte de mémoire (disque, flash) accessible en écriture et en lecture par l'intermédiaire du système. L'avantage évident est que les données sont permanentes, même après mise hors tension de la machine.
En conséquence, dans la pratique, les termes flux et fichiers sont souvent confondus.
XXXIII-B. Texte ou binaire ?▲
Le langage C fait la distinction entre les fichiers binaires et les fichiers textes. Cette distinction est historique. Elle dépend en fait du système utilisé. Sur certains systèmes, il n'existe aucune différence physique entre les fichiers textes et les fichiers binaires. Sur d'autres systèmes, il existe une différence. Par souci de portabilité, il est recommandé de respecter cette distinction.
Le choix entre fichier texte ou binaire provient du contenu de ce fichier.
XXXIII-B-1. Fichier texte▲
On appelle fichier texte un fichier qui contient des informations de type texte, c'est à dire des séquences de lignes.
Une ligne est une séquence de caractères imprimables terminée par une marque de fin de ligne.
Selon le système, la marque de fin de ligne est composée de un ou plusieurs caractères de contrôle (par exemple, CR, LF, ou une séquence de ces caractères)
:----------------:--------------:----------------:
: Système : Fin de ligne : Fin de fichier :
:----------------:--------------:----------------:
: Unix : : :
: Mac X : 0x0A LF : Sans objet :
: Linux : : :
:----------------:--------------:----------------:
: Mac (non unix) : 0x0D CR : Sans objet :
:----------------:--------------:----------------:
: MS-DOS : 0x0D CR : 0x1A :
: Windows : 0x0A LF : ^Z :
: Windows NT : : :
:----------------:--------------:----------------:
: VMS STREAM_CR : 0x0D CR : Sans objet :
:----------------:--------------:----------------:
: VMS STREAM_LF : 0x0A LF : Sans objet :
:----------------:--------------:----------------:
: VMS STREAM_CRLF: 0x0D CR : Sans objet :
: : 0x0A LF : :
:----------------:--------------:----------------:
L'ensemble des valeurs numériques des caractères (charset) dépend du système. La plupart du temps, il s'agit du codage ASCII (0-127) avec des extensions plus ou moins standards au delà de 127. Il existe d'autres codes, comme EBCDIC utilisé sur certains mainframes IBM.
Pour écrire une fin de ligne dans un fichier texte, il suffit d'écrire le caractère '\n'. Celui-ci sera alors automatiquement traduit en marqueur de fin de ligne.
De même, lors de la lecture d'un fichier texte, le marqueur de fin de ligne est automatiquement traduit en '\n', quel qu'il soit.
Nota : Certains systèmes marquent la fin des fichiers textes d'un caractère spécial. Par exemple MS-DOS ajoute un code 26 (^Z). Cela signifie que, pour ce système, la lecture d'un fichier texte s'arrête dès la rencontre de ce caractère.
XXXIII-B-2. Fichier binaire▲
N'importe quel fichier, y compris un fichier texte, peut être considéré comme binaire. Dans ce cas, l'écriture et la lecture des caractères se fait sans interprétation.
Par exemple, sur une plate-forme utilisant le jeu de caractères ASCII, CR vaut 13 ou 0x0D ou '\r'. De même, LF vaut 10 ou 0x0A ou '\n'.
XXXIII-B-3. Modes d'ouverture d'un fichier▲
La fonction d'ouverture de fichier est fopen(). Comme pour les autres fonctions de gestion des fichiers, le fichier d'interface est <stdio.h>.
FILE *
fopen (
char
const
*
filename, char
const
*
mode);
Le mode d'ouverture est déterminé par une chaîne de caractère. Voici les chaînes correspondant aux principaux modes :
"r" : mode texte en lecture
"w" : mode texte en écriture (création)
"a" : mode texte en écriture (ajout)
"rb" : mode binaire en lecture
"wb" : mode binaire en écriture (création)
"ab" : mode binaire en écriture (ajout)
XXXIII-B-4. Lecture d'un fichier▲
Le langage C offre plusieurs fonctions permettant de lire les données d'un fichier :
- fgetc() ;
- getc() ;
- fread() ;
- fscanf() ;
- fgets().
XXXIII-B-4-a. fgetc(), getc()▲
Ces fonctions sont identiques. Elles permettent de lire un caractère.
XXXIII-B-4-b. fread()▲
Cette fonction permet de lire un bloc de caractères d'une longueur donnée. Elle est tout à fait adaptée à la lecture des données binaires brutes (non interprétées).
XXXIII-B-4-c. fscanf()▲
Cette fonction permet de lire des données 'texte' formatées. Cette fonction est d'une utilisation complexe et son usage est peu recommandé.
XXXIII-B-4-d. fgets()▲
Cette fonction permet de lire une ligne de texte. Elle est tout à fait adaptée à la lecture d'un fichier texte ligne par ligne.
Sa simplicité d'utilisation et sa robustesse en font la fonction préférée des programmeurs qui doivent analyser des fichiers textes.
XXXIII-B-4-d-i. Exemple d'utilisation▲
Soit le fichier texte :
Ceci est un simple fichier
texte de 2 lignes.
et un petit programme permettant de lire ces deux lignes :
/* fichier1.c */
#include <stdio.h>
int
main (
void
)
{
/* ouverture du fichier en mode texte */
FILE *
fp =
fopen (
"
data.txt
"
, "
r
"
);
/* L'ouverture du fichier est-elle réalisée ? */
if
(
fp !=
NULL
)
{
/* définition d'un tableau de char destine a recevoir la ligne
* La taille est arbitraire. Elle doit être cependant adaptée * aux besoins courants.
* Pour les grandes tailles (disons > 256 char),
* il est préférable d'utiliser une allocation dynamique.
*/
char
ligne[32
];
/* lecture de la première ligne */
fgets (
ligne, sizeof
ligne, fp);
/* Affichage de la première ligne */
printf (
"
1: %s
\n
"
, ligne);
/* lecture de la deuxième ligne */
fgets (
ligne, sizeof
ligne, fp);
/* Affichage de la deuxième ligne */
printf (
"
2: %s
\n
"
, ligne);
/* Fermeture du fichier */
fclose (
fp);
}
else
{
printf (
"
Erreur d'ouverture du fichier
\n
"
);
}
return
0
;
}
On doit obtenir ceci sur la sortie standard (stdout) :
1: Ceci est un simple fichier
2: texte de 2 lignes.
XXXIII-B-4-d-ii. Explication▲
La ligne lue est stockée dans la variable ligne, y compris le '\n'. La fonction d'affichage printf() affiche le numéro de ligne, suivit de ': ', la ligne (avec son '\n') et un '\n' en plus, ce qui explique la présence de lignes "vides".
XXXIII-B-4-d-iii. Critique de cet exemple▲
Cet exemple de codage 'naïf' souffre d'un défaut majeur : Il fait l'hypothèse que le fichier fait 2 lignes, et il continue à lire le fichier même si une erreur de lecture s'est produite. En fait, tout simplement, il ne gère pas les erreurs de lecture.
Il est facile de gérer les erreurs de lecture. Toutes les fonctions de lecture retournent une valeur. Celle-ci peut prendre une valeur particulière qui signifie 'Arrêt de la lecture'. La cause n'est pas précisée. Ça peut être à cause d'une erreur (support en panne, données corrompu, fichier inexistant etc.) ou tout simplement par ce que la fin de fichier a été atteinte.
XXXIII-B-4-d-iv. Détection d'une erreur▲
La fonction fgets() retourne une valeur de type char *. Si la lecture a réussi, la valeur retournée est l'adresse du tableau de char passé en paramètre. En cas d'échec, la valeur NULL est retournée. Il suffit donc de surveiller cette valeur pour savoir si on peut continuer ou non. Comme une des causes d'échec est la "fin de fichier atteinte", on peut donc parfaitement intégrer ce test dans une boucle de lecture "ligne par ligne".
Une fois l'échec de la lecture constaté, il est possible d'en identifier la cause. Le langage C met à disposition les deux fonctions feof() et ferror() qu'il faut appeler après la boucle de lecture, mais avant la fermeture du fichier.
while
(
fonction_de_lecture
(
fp) !=
ERREUR)
{
...
}
if
(
feof
(
fp))
{
/* la fin de fichier a été détectée */
puts (
"
EOF
"
);
}
if
(
ferror
(
fp))
{
/* une erreur s'est produite */
perror (
NOM_DU_FICHIER);
}
fclose (
fp);
XXXIII-B-4-d-v. Gestion des fins de ligne▲
On constate que lorsque fgets() lit une ligne entière, un '\n' se retrouve à la fin de la chaîne saisie. La présence de '\n' est gênante ou non selon l'application.
Ceci dit, dans tous les cas, il est conseillé d'en détecter la présence. En effet, sa présence indique que la ligne a été lue entièrement, alors que son absence indique que la ligne a été tronquée, et que d'autres caractères (au minimum un '\n') attendent pour être lus. Il est donc conseillé d'écrire ces quelques lignes après un fgets() pour clarifier la situation :
#include <stdio.h>
#include <string.h>
...
{
char
ligne[123
];
/* test d'erreur omis */
fgets (
ligne, sizeof
ligne, fp);
{
/* chercher le '\n' */
char
*
p =
strchr
(
ligne, '
\n
'
);
if
(
p !=
NULL
)
{
/* si on l'a trouve, on l'elimine. */
*
p =
0
;
}
else
{
/* Le traitement depend de l'application.
* Par exemple, ici, on choisi d'ignorer
* les autres caracteres.
*/
/* sinon, on lit tous les caracteres restants */
int
c;
while
((
c =
fgetc
(
fp)) !=
'
\n
'
&&
c !=
EOF)
{
}
}
}
}
Il est clair que dans la pratique, l'ensemble de ce code devra être intégré dans une fonction unique de lecture d'une ligne à partir d'un flux.
XXXIII-B-4-d-vi. Exemple amélioré avec détection de la fin de lecture▲
/* fichier2.c */
#include <stdio.h>
int
main (
void
)
{
FILE *
fp =
fopen (
"
data.txt
"
, "
r
"
);
if
(
fp !=
NULL
)
{
char
ligne[32
];
/* définition d'un compteur de lignes et initialisation */
int
cpt =
0
;
/* lecture des lignes */
while
(
fgets (
ligne, sizeof
ligne, fp) !=
NULL
)
{
/* Mise a jour du compteur */
cpt++
;
/* Affichage des lignes */
printf (
"
%d: %s
\n
"
, cpt, ligne);
}
/* On peut ajouter ici la détection de la cause
* de l'erreur décrite ci-dessus
*/
fclose (
fp);
}
else
{
printf (
"
Erreur d'ouverture du fichier
\n
"
);
}
return
0
;
}
Cet exemple met en œuvre un mécanisme qui s'adapte automatiquement au nombre de lignes du fichier. Cependant, attention, le fonctionnement, bien qu'il reste sûr, risque d'être surprenant si la longueur de la ligne est supérieure à celle du tableau 'ligne'.
Par exemple, si on diminue la taille de 'ligne' à 16 au lieu de 32,
<
...>
char
ligne[16
];
<
...>
on obtient :
1: Ceci est un sim
2: ple fichier
3: texte de 2 lign
4: es.
XXXIII-B-4-d-vii. Explication▲
Rappelons que la taille du tableau de char a été transmise à la fonction fgets().
Celle-ci tente de lire la ligne, mais celle-ci est trop longue pour tenir dans le variable 'ligne'. fgets(), qui connaît la taille de la variable 'ligne', applique alors une stratégie d'adaptation qui consiste à stocker ce qui est possible dans la variable, en laissant une place pour le 0 final. En effet, fgets() a pour obligation de produire une chaîne de caractères valide dans tous les cas.
C'est pourquoi la première ligne est partiellement lue ainsi :
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
: Indice
'
C
'
'
e
'
'
c
'
'
i
'
'
'
'
e
'
'
s
'
'
t
'
'
'
'
u
'
'
n
'
'
'
'
s
'
'
i
'
'
m
'
0
: Données
Mais les caractères manquants ne sont pas perdus, et ils sont lus par l'appel suivant :
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
: Indice
'
p
'
'
l
'
'
e
'
'
'
'
f
'
'
i
'
'
c
'
'
h
'
'
i
'
'
e
'
'
r
'
'
\n
'
0
: Données
Cette fois, la place est suffisante, et l'ensemble de la chaîné est lue, y compris le '\n'.
XXXIII-B-5. Écriture dans un fichier▲
Le langage C offre plusieurs fonctions permettant d'écrire des données dans un fichier :
- fputc() ;
- putc() ;
- fwrite() ;
- fprintf() ;
- fputs().
XXXIII-B-5-a. fputc(), putc()▲
Ces fonctions sont identiques. Elles permettent d'écrire un caractère.
XXXIII-B-5-b. fwrite()▲
Cette fonction permet d'écrire un bloc de caractères d'une longueur donnée. Elle est tout à fait adaptée à l'écriture de données binaires brutes (non interprétées).
XXXIII-B-5-c. fprintf()▲
Cette fonction permet d'écrire des données 'texte' formatées. Elle comporte de nombreuses possibilités de conversion de valeurs numériques en texte. (Entiers, flottants etc.)
XXXIII-B-5-d. fputs()▲
Cette fonction permet d'écrire une chaîne de caractères.
XXXIII-B-6. Bien utiliser les formats de données▲
Il n'est pas rare que des données enregistrées dans un fichier par une machine soient lues par une autre machine, ou par autre programme ou par le même programme mais compilé avec des options différentes. Pour pouvoir récupérer les données, il faut qu'en aucun cas, le format des données enregistrées ne dépende de l'implémentation.
XXXIII-B-6-a. Format orienté texte▲
Le format texte est un bon choix, car il utilise une séquence de caractères simple et évidente (chronologique) et un codage très répandu (ASCII). Il peut y avoir quelques problèmes de transcodage pour les valeurs de 128 à 255 (ANSI, OEM etc.), mais rien qui ne soit insurmontable. D'autre part, la conversion ASCII/EBCDIC est triviale.
Il subsiste le problème des fins de ligne qui sont différentes d'un système à l'autre. Il existe des utilitaires bien connus (dos2unix, unix2dos etc.) généralement fournis avec ces systèmes qui font les conversions. Rappelons que la fonction system() permet d'appeler une commande extérieure. Si néanmoins, cet utilitaire n'existait pas, il serait facile de le faire soi-même. Bien sûr, il faudrait travailler en mode binaire de façon à contrôler les données du fichier de manière 'brute' (raw).
Les chaînes et les valeurs numériques sont encodées et éventuellement formatées avec fprintf(). Une organisation en ligne est souhaitable. Elles sont ensuite lues ligne par ligne avec fgets() et analysées soit par strtol(), strtoul() ou strtod() pour les cas les plus simples (valeurs numériques pures), soit par sscanf() pour les cas plus complexes, à condition que le formatage soit clairement défini. Il est souhaitable d'utiliser des formats simples à analyser et surtout sans ambiguïté quant aux séparateurs. Le format CSV est recommandé.
XXXIII-B-6-b. Format orienté binaire▲
Une mauvaise utilisation des formats binaires (raw) peut apporter des problèmes de portabilité. Il est recommandé d'utiliser des formats indépendants comme XDR (RFC 1832).
XXXIII-C. Supprimer un enregistrement dans un fichier binaire▲
Pour supprimer un enregistrement, le plus simple est de procéder ainsi :
- le fichier original est ouvert en lecture. Un nouveau fichier est ouvert en écriture. L'original est lu enregistrement par enregistrement (fread()), et recopié dans le nouveau fichier (fwrite()) en omettant l'enregistrement à supprimer (if …) ;
- par un jeu subtil de suppression et de renommage (remove(), rename()), on se retrouve avec une copie de l'original (genre .old ou .bak) et le nouveau fichier qui a maintenant le nom de l'ancien. L'opération reste simple, et a l'avantage de permettre l'annulation (par renommage de l'ancien fichier).
Toute autre opération basée sur l'écriture/lecture dans le même fichier est dangereuse, non portable et se traduit souvent par la destruction du fichier original sans recours possible.
XXXIII-D. En guise de conclusion▲
Il ne faut pas se tromper d'outil. Les flux du C sur disque sont très pratiques pour enregistrer quelques données statiques dans un fichier texte. En binaire, c'est déjà plus risqué à moins de passer par un format indépendant comme XDR. Pour gérer des enregistrements, les fichiers C sont trop rustiques. Il faut une véritable base de données (comme SQLite ouMySQL par exemple).
XXXIV. Pourquoi fflush (stdout) ?▲
Il arrive parfois de rencontrer ce genre de code…
printf
(
"
Entrez un nombre :
"
);
fflush (
stdout);
… et on se demande alors à quoi peut bien servir ce fflush (stdout).
Le printf() précédent envoie une chaîne de caractères à stdout. Or cette chaîne n'est pas terminée par un '\n'.
Il faut savoir que stdout est souvent un flux "bufferisé", ce qui signifie, en bon français, que les caractères sont placés dans un tampon (buffer) de sortie avant d'être réellement émis.
Il y a trois critères qui déclenchent l'émission réelle des caractères :
- le tampon d'émission est plein (incontrôlable) ;
- un '\n' a été placé dans le tampon[1] ;
- la commande de forçage a été activée.
La commande de forçage est activée par l'appel de la fonction fflush (stdout), ce qui explique sa présence dans le code mentionné.
[1] sauf en cas de redirection dudit flux vers un fichier.