Programmation C/Entrées/sorties

Les fonctions d'entrées/sorties sont celles qui vous permettent de communiquer avec l'extérieur, c'est-à-dire, la console, les fichiers, tubes de communication, socket IP, etc ... Pour utiliser ces fonctions, il faut inclure l'en-tête <stdio.h>, avec la directive d'inclusion :

#include <stdio.h>

Manipulation de fichiers modifier

En C, les fichiers ouverts sont représentés par le type FILE, qui est un type opaque : on ne connaît pas la nature réelle du type, mais seulement des fonctions pour le manipuler. Ainsi, on ne peut créer directement de variables de type FILE, seules les fonctions de la bibliothèque standard peuvent créer une variable de ce type, lors de l'ouverture d'un fichier. Ces données sont donc uniquement manipulées par des pointeurs de type FILE *.

Ce type est un flux de données, qui représente des fichiers, mais peut aussi représenter toute autre source ou destination de données. L'en-tête <stdio.h> fournit trois flux que l'on peut utiliser « directement » :

  • stdin, l'entrée standard ;
  • stdout, la sortie standard ;
  • stderr, la sortie standard des erreurs.

Souvent, l'entrée standard envoie au programme les données issues du clavier, et les sorties standard envoient les données que le programme génère à l'écran. Mais d'où viennent et où vont ces données dépend étroitement du contexte et de l'implémentation ; la bibliothèque standard fournit le type FILE comme une abstraction pour les manipuler tous de la même manière, ce qui libère le programmeur de certains détails d'implémentations, et permet à l'utilisateur d'un programme d'employer (suivant son implémentation) aussi bien son clavier qu'un fichier comme entrée standard.

Ouverture modifier

FILE * fopen(const char * restrict chemin, const char * restrict mode)

Ouvre le fichier désigné par le chemin et renvoie un nouveau flux de données pointant sur ce fichier. L'argument mode est une chaîne de caractères désignant la manière dont on veut ouvrir le fichier :

  • r : ouvre le fichier en lecture, le flux est positionné au début du fichier ;
  • r+ : ouvre le fichier en lecture/écriture, le flux est positionné au début du fichier ;
  • w : ouvre le fichier en écriture, supprime toutes les données si le fichier existe et le crée sinon, le flux est positionné au début du fichier ;
  • w+ : ouvre le fichier en lecture/écriture, supprime toutes les données si le fichier existe et le crée sinon, le flux est positionné au début du fichier ;
  • a : ouvre le fichier en écriture, crée le fichier s'il n'existe pas, le flux est positionné à la fin du fichier ;
  • a+ : ouvre le fichier en lecture/écriture, crée le fichier s'il n'existe pas, le flux est positionné à la fin du fichier.
Résumé des modes
mode lecture écriture crée le fichier vide le fichier position du flux
r X début
r+ X X début
w X X X début
w+ X X X X début
a X X fin
a+ X X X fin

Lorsqu'un fichier est ouvert en écriture, les données qui sont envoyées dans le flux ne sont pas directement écrites sur le disque. Elles sont stockées dans un tampon, une zone mémoire de taille finie. Lorsque le tampon est plein, les données sont purgées (flush), elles sont écrites dans le fichier. Ce mécanisme permet de limiter les accès au système de fichiers et donc d'accélérer les opérations sur les fichiers.

À noter une particularité des systèmes Microsoft Windows, est de traiter différemment les fichiers textes, des fichiers binaires. Sur ces systèmes, le caractère de saut de ligne est en fait composé de deux caractères (CR, puis LF, de code ASCII respectif 13 et 10, ou '\r' et '\n' écrit sous forme de caractère C). Lorsqu'un fichier est ouvert en mode texte (mode par défaut), toute séquence CRLF lue depuis le fichier sera convertie en LF, et tout caractère LF écrit sera en fait précédé d'un caractère CR supplémentaire. Si le fichier est ouvert en mode binaire, aucune conversion n'aura lieu.

Ce genre de comportement issu d'un autre âge, est en fait bien plus agaçant que réellement utile. Le premier réflexe est en général de désactiver ce parasitage des entrées/sorties, tant il est pénible. Pour cela deux cas de figure :

  1. Vous avez accès au nom du fichier : donc vous pouvez utiliser la fonction fopen(). Dans ce cas de figure, il suffit de rajouter la lettre b au mode d'ouverture:
    fopen("fichier.txt", "rb"); /* Ouverture sans conversion */
    fopen("fichier.txt", "wb"); /* L'écriture d'un '\n' n’entraînera pas l'ajout d'un '\r' */
    
  2. Vous n'avez pas accès au nom du fichier (par exemple stdout ou stdin). Il existe une fonction spécifique à Microsoft Windows, non portable sur d'autres systèmes, mais qui peut s'avérer nécessaire:
    /* Ce code qui suit n'est pas portable */
    #include <fcntl.h>
    
    setmode(descripteur, O_BINARY);
    

    Histoire de compliquer encore un petit peu, cette fonction travaille avec les descripteurs de fichier, plutôt que les flux stdio. On peut tout de même s'en sortir avec cette fonction faisant partie de la bibliothèque standard:

    int fileno(FILE *);
    

    Par exemple pour mettre la sortie standard en mode binaire:

    setmode(fileno(stdout), O_BINARY);
    

Fermeture modifier

int fclose(FILE * flux);

Dissocie le flux du fichier auquel il avait été associé par fopen. Si le fichier était ouvert en écriture, le tampon est vidé. Cette fonction renvoie 0 si la fermeture s'est bien passée (notamment la purge des zones en écriture), ou EOF en cas d'erreur (voir le paragraphe sur la gestion d'erreurs).

Suppression modifier

int remove(const char * path);

Supprime le fichier ou le répertoire nommé 'path'. La fonction renvoie 0 en cas de réussite et une valeur non nulle en cas d'erreur, ce qui peut inclure :

  • un répertoire n'est pas vide ;
  • vous n'avez pas les permissions pour effacer le fichier (média en lecture seule) ;
  • le fichier est ouvert ;
  • etc.

Renommage (ou déplacement) modifier

int rename(const char * ancien_nom, const char * nouveau_nom);

Cette fonction permet de renommer l'ancien fichier ou répertoire nommé 'ancien_nom' par 'nouveau_nom'. Elle peut aussi servir a déplacer un fichier, en mettant le chemin absolu ou relatif du nouvel emplacement dans 'nouveau_nom'.

La fonction renvoie 0 si elle réussit et une valeur non nulle en cas d'erreur.

Les causes d'erreur dépendent de l'implémentation, et peuvent être:

  • vous tentez d'écraser un répertoire par un fichier ;
  • vous voulez écraser un répertoire non vide ;
  • vous n'avez pas les permissions suffisantes ;
  • les deux noms ne sont pas sur la même partition ;
  • etc.

Déplacement dans le flux modifier

int fseek( FILE * flux, long deplacement, int methode );
long ftell( FILE * flux );

fseek permet de se déplacer à une position arbitraire dans un flux. Cette fonction renvoie 0 en cas de réussite.

deplacement indique le nombre d'octet à avancer (ou reculer si ce nombre est négatif) à partir du point de référence (methode) :

  • SEEK_SET : le point de référence sera le début du fichier.
  • SEEK_CUR : le point de référence sera la position courante dans le fichier.
  • SEEK_END : le point de référence sera la fin du fichier.

ftell permet de savoir à quelle position se trouve le curseur (ce depuis le début).

En cas d'erreur, ces deux fonctions renvoient -1.

Plusieurs remarques peuvent être faites sur ces deux fonctions :

  1. Sur une machine 32bits pouvant gérer des fichiers d'une taille de 64bits (plus de 4Go), ces fonctions sont limite inutilisables, du fait qu'un type long sur une telle architecture est limité à 32bits. On mentionnera les fonctions fseeko() et ftello() qui utilisent le type opaque off_t, à la place du type int, à l'image des appels systèmes. Ce type off_t est codé sur 64bits sur les architectures le supportant et 32bits sinon. La disponibilité de ces fonctions est en général limités aux systèmes Unix, puisque dépendantes de la spécification Single Unix (SUS).
  2. Il faut bien sûr que le périphérique où se trouve le fichier supporte une telle opération. Dans la terminologie Unix, on appelle cela un périphérique en mode bloc. À la différence des périphériques en mode caractère (stdin, stdout, tube de communication, connexion réseau, etc ...) pour lesquels ces appels échoueront.

Synchronisation modifier

int fflush ( FILE *flux );

Cette fonction purge toutes les zones mémoires en attente d'écriture et renvoie 0 si tout s'est bien passé, ou EOF en cas d'erreur. Si NULL est passé comme argument, tous les flux ouverts en écriture seront purgés.

À noter que cette fonction ne permet pas de purger les flux ouverts en lecture (Pour répondre à une question du genre « Voulez-vous effacer ce fichier (o/n) ? » ). Une instruction de ce genre sera au mieux ignorée, et au pire provoquera un comportement indéterminé :

  Ce code contient une erreur volontaire !
fflush( stdin );

Pour effectuer une purge des flux ouverts en lecture, il faut passer par des appels systèmes normalisés dans d'autres documents (POSIX), mais dont la disponibilité est en général dépendante du système d'exploitation.

Sorties formatées modifier

int printf(const char * restrict format, ...);
int fprintf(FILE * restrict flux, const char * restrict format, ...);
int sprintf(char * restrict chaine, const char * restrict format, ...);
int snprintf(char * restrict chaine, size_t taille, const char * restrict format, ...);

Ces fonctions permettent d'écrire des données formatées dans :

  • la sortie standard pour printf ;
  • un flux pour fprintf ;
  • une chaîne de caractères pour sprintf.

En retour elle indique le nombre de caractères qui a été écrit à l'écran, dans le flux ou la zone mémoire (caractère nul non compris pour sprintf).

Bien que cela ait déjà été traité dans la section dédiée aux chaînes de caractères, il faut faire très attention avec la fonction sprintf(). Dans la mesure où la fonction n'a aucune idée de la taille de la zone mémoire transmise, il faut s'assurer qu'il n'y aura pas de débordements. Mieux vaut donc utiliser la fonction snprintf(), qui permet de limiter explicitement le nombre de caractère à écrire.

À noter que snprintf() devrait toujours retourner la taille de la chaine à écrire, indépendamment de la limite fixée par le paramètre taille. Le conditionnel reste de mise, car beaucoup d'implémentations de cette fonction se limitent à retourner le nombre de caractères écrit, c'est à dire en s'arrêtant à la limite le cas échéant.

Type de conversion modifier

Mis à part l'« endroit » où écrivent les fonctions, elles fonctionnent exactement de la même manière, nous allons donc décrire leur fonctionnement en prenant l'exemple de printf.

L'argument format est une chaîne de caractères qui détermine ce qui sera affiché par printf et sous quelle forme. Cette chaîne est composée de texte « normal » et de séquences de contrôle permettant d'inclure des variables dans la sortie. Les séquences de contrôle commencent par le caractère « % » suivi d'un caractère parmi :

  • d ou i pour afficher un entier signé au format décimal (int) ;
  • u pour un entier non signé au format décimal ;
  • x ou X pour afficher un entier au format hexadécimal (avec les lettres "abcdef" pour le format 'x' et "ABCDEF" avec le format 'X') ;
  • f pour afficher un réel (double) avec une précision fixe ;
  • e pour afficher un réel (double) en notation scientifique ;
  • g effectue un mixe de 'f' et de 'e' suivant le format le plus approprié ;
  • c pour afficher en tant que caractère ;
  • s pour afficher une chaîne de caractère C standard ;
  • p pour afficher la valeur d'un pointeur, généralement sous forme hexadécimale. Suivant le compilateur, c'est l'équivalent soit à "%08x", ou alors à "0x%08x". ;
  • n ce n'est pas un format d'affichage et l'argument associé doit être de type int * et être une référence valide. La fonction stockera dans l'entier pointé par l'argument le nombre de caractères écrit jusqu'à maintenant ;
  • % pour afficher le caractère '%'.

Contraindre la largeur des champs modifier

Une autre fonctionnalité intéressante du spécificateur de format est que l'on peut spécifier sur combien de caractère les champs seront alignés. Cette option se place entre le '%' et le format de conversion et se compose d'un signe '-' optionnel suivit d'un nombre, éventuellement d'un point et d'un autre nombre ([-]<nombre>[.<nombre>]). Par exemple: %-30.30s.

Le premier nombre indique sur combien de caractère se fera l'alignement. Si la valeur convertie est plus petite, elle sera alignée sur la droite, ou la gauche si un signe moins est présent au début. Si la valeur est plus grande que la largeur spécifiée, le contenu s'étendra au-delà, décalant tout l'alignement. Pour éviter ça, on peut spécifier un deuxième nombre au delà duquel le contenu sera tronqué. Quelques exemples:

printf("%10s",    "Salut");               /* " Salut" */
printf("%-10s",   "Salut");               /* "Salut " */
printf("%10s",    "Salut tout le monde"); /* "Salut tout le monde" */
printf("%10.10s", "Salut tout le monde"); /* "Salut tout" */

Contraindre la largeur des champs numériques modifier

On peut aussi paramétrer la largeur du champ, en spécifiant * à la place. Dans ce cas, en plus de la valeur à afficher (i.e. 1234), il faut donner avant un entier de type int pour dire sur combien de caractères l'alignement se fera (i.e. 10) :

printf("%-*d", 10, 1234); /* "1234 " */
printf("%*d",  10, 1234); /* " 1234" */

À noter que pour le formatage de nombres entiers, la limite « dure » du spécificateur de format est sans effet, pour éviter de fâcheuses erreurs d'interprétation. On peut toutefois utiliser les extensions suivantes :

  • 0 : Si un zéro est présent dans le spécificateur de largeur, le nombre sera aligné avec zéros au lieu de blancs.
  • + : Si un signe plus est présent avec le spécificateur de largeur, le signe du nombre sera affiché tout le temps (0 est considéré comme positif).
  •   (espace) : Si le nombre est positif, un blanc sera mis avant, pour l'aligner avec les nombres négatifs.

Exemples :

printf("%+010d",    543);        /* "+000000543"  */
printf("%-+10d",    543);        /* "+543 "       */
printf("%-+10d",    1234567890); /* "+1234567890" */
printf("%-+10.10d", 1234567890); /* "+1234567890" */
printf("%08x",      543);        /* "0000021f"    */

Contraindre la largeur des champs réels modifier

Pour les réels, la limite « dure » sert en fait à indiquer la précision voulue après la virgule :

printf("%f",   3.1415926535); /* "3.141593"   */
printf("%.8f", 3.1415926535); /* "3.14159265" */

Spécifier la taille de l'objet modifier

Par défaut, les entiers sont présupposés être de type int, les réels de type double et les chaînes de caractères de type char *. Il arrive toutefois que les types soient plus grands (et non plus petits à cause de la promotion des types, c.f. paragraphes opérateurs et fonction à nombre variable d'arguments), le spécificateur de format permet donc d'indiquer la taille de l'objet en ajoutant les attributs suivants avant le caractère de conversion :

  • hh : indique que l'entier est un [un]signed char au lieu d'un [unsigned] int ;
  • h : indique que l'entier est de type [unsigned] short au lieu d'un [unsigned] int ;
  • l : pour les entiers, le type attendu ne sera plus int mais long int et pour les chaînes de caractères, il sera de type wchar_t * (c.f section chaînes de caractères).
  • ll : cet attribut ne concerne que les types entiers, où le type attendu sera long long int.
  • L : pour les types réels, le type attendu sera long double.
  • z pour afficher une variable de type size_t.

Pour résumer les types d'arguments attendus en fonction de l'indicateur de taille et du type de conversion :

Format Attributs de taille Autres attributs
(rarement utilisés)
aucun hh h l (elle) ll (elle-elle) L j z t
n int * signed char * short * long * long long * intmax_t * size_t * ptrdiff_t *
d, i, o, x, X int signed char short long long long intmax_t size_t ptrdiff_t
u unsigned int unsigned char unsigned short unsigned long unsigned long long uintmax_t size_t ptrdiff_t
s char * wchar_t *
c int wint_t
p void *
a, A, e, E, f, F, g, G double long double

hh et ll sont des nouveautés de C99. On notera qu'avec l'attribut hh et les formats n, d, i, o, x ou X, le type est signed char et non char. En effet, comme vu dans le chapitre Types de base, le type char peut être signé ou non, suivant l'implémentation. Ici, on est sûr de manipuler le type caractère signé.

Quelques exemples :

signed char nb;

printf("%d%hhn", 12345, &nb); /* Affichage de "12345" et nb vaudra 5 */
printf("%ls", L"Hello world!"); /* "Hello world!" */

Arguments positionnels modifier

Il s'agit d'une fonctionnalité relativement peu utilisée, mais qui peut s'avérer très utile dans le cadre d'une application internationalisée. Considérez le code suivant (tiré du manuel de gettext) :

printf( gettext("La chaine '%s' a %zu caractères\n"), s, strlen(s) );

gettext est un ensemble de fonctions permettant de manipuler des catalogues de langues. La principale fonction de cette bibliothèque est justement gettext(), qui en fonction d'une chaîne de caractère retourne la chaîne traduite selon la locale en cours (où celle passée en argument si rien n'a été trouvé).

Une traduction en allemand du message précédant, pourrait donner : "%d Zeichen lang ist die Zeichenkette '%s'"

On remarque d'emblée que les spécificateurs de format sont inversés par rapport à la chaîne originale. Or l'ordre des arguments passés à la fonction printf() sera toujours le même. Il est quand même possible de s'en sortir avec les arguments positionnels. Pour cela, il suffit d'ajouter à la suite du caractère % un nombre, suivi d'un signe $. Ce nombre représente le numéro de l'argument à utiliser pour le spécificateur, en commençant à partir de 1. Un petit exemple :

char * s = "Bla bla";
printf("La chaine %2$s a %1$zu caractères\n", strlen(s), s); /* "La chaîne Bla bla a 7 caractères" */

À noter que si un des arguments utilise la référence positionnelle, tous les autres arguments devront faire évidemment de même, sous peine d'avoir un comportement imprévisible.

Écriture par bloc ou par ligne modifier

Il s'agit d'une fonctionnalité relativement pointue de la bibliothèque stdio, mais qui peut expliquer certains comportement en apparence étrange (notamment avec les systèmes POSIX). Les réglages par défaut étant bien faits, il y a peu de chance pour que vous ayez à vous soucier de cet aspect, si ce n'est à titre de curiosité.

En règle générale les flux de sortie ouvert par via la bibliothèque stdio sont gérés par bloc, ce qui veut dire qu'une écriture (via printf(), fprintf() ou fwrite()) ne sera pas systématiquement répercutée dans le fichier associé.

Cela dépend en fait du type d'objet sur lequel les écritures se font :

  • Un terminal : les écritures se feront par ligne, ou si les lignes sont plus grandes qu'une certaine taille (4Ko en général), l'écriture se fera par bloc. Les flux en écriture seront aussi purgés si on tente de lire des données depuis le même terminal.
  • Autre (fichiers, connexion réseau, tubes de communication) : les écritures se feront par bloc, indépendamment des lignes.
  • Flux d'erreur (stderr) : écriture immédiate.

C'est ce qui fait qu'un programme affichant des messages à intervalle régulier (genre une seconde), affichent ces lignes une à une sur un terminal, et par bloc de plusieurs lignes lorsqu'on redirige sa sortie vers un programme de mise en page (comme more), avec une latence qui peut s'avérer gênante. C'est ce qui fait aussi qu'une instruction comme printf("Salut tout le monde"); n'affichera en général rien, car il n'y a pas de retour à la ligne.

En fait ce comportement peut être explicitement réglé, avec cette fonction :

int setvbuf(FILE * restrict flux, char * restrict mem, int mode, size_t taille);

Cette fonction doit être appelée juste après l'ouverture du flux et avant la première écriture. Les arguments ont la signification suivante :

  • flux : Le flux stdio pour lequel vous voulez changer la méthode d'écriture.
  • mem : Vous pouvez transmettre une zone mémoire qui sera utilisée pour stocker les données avant d'être écrites dans le fichier. Vous pouvez aussi passer la valeur NULL, dans ce cas les fonctions stdio, appelleront la fonction malloc() lors de la première écriture.
  • mode : indique comment se feront les écritures :
    • _IONBF : écriture immédiate, pas de stockage temporaire.
    • _IOLBF : écriture par ligne.
    • _IOFBF : par bloc.
  • taille : La taille de la zone mémoire transmise ou à allouer.

La fonction setvbuf() renvoie 0 si elle réussit, et une valeur différente de zéro dans le cas contraire (en général le paramètre mode est invalide).

Cette fonctionnalité peut être intéressante pour les programmes générant des messages sporadiques. Il peut effectivement s'écouler un temps arbitrairement long avant que le bloc mémoire soit plein, si cette commande est redirigée vers un autre programme, ce qui peut s'avérer assez dramatique pour des messages signalant une avarie grave. Dans ce cas, il est préférable de forcer l'écriture par ligne (ou immédiate), plutôt que de faire suivre systématiquement chaque écriture de ligne par un appel à fflush(), avec tous les risques d'oubli que cela comporte.

Quelques remarques pour finir modifier

La famille de fonctions printf() permet donc de couvrir un large éventail de besoins, au prix d'une pléthore d'options pas toujours faciles à retenir.

Il faut aussi faire attention au fait que certaines implémentations de printf() tiennent compte de la localisation pour les conversions des nombres réels (virgule ou point comme séparateur décimal, espace ou point comme séparateurs des milliers, etc.). Ceci peut être gênant lorsqu'on veut retraiter la sortie de la commande. Pour désactiver la localisation, on peut utiliser la fonction setlocale():

#include <locale.h>

/* ... */
setlocale( LC_ALL, "C" );
printf( ... );
setlocale( LC_ALL, "" );

Entrées formatées modifier

La bibliothèque stdio propose quelques fonctions très puissantes pour saisir des données depuis un flux quelconque. Le comportement de certaines fonctions (scanf notamment) peut paraître surprenant de prime abord, mais s'éclaircira à la lumière des explications suivantes.

int scanf(const char * restrict format, ...);
int fscanf(FILE * restrict flux, const char * restrict format, ...);
int sscanf(const char * restrict chaine, const char * restrict format, ...);

Ces trois fonctions permettent de lire des données formatées provenant de :

  • l'entrée standard pour scanf ;
  • un flux pour fscanf ;
  • une chaîne de caractères pour sscanf.

L'argument format ressemble aux règles d'écriture de la famille de fonction printf, cependant les arguments qui suivent ne sont plus des variables d'entrée mais des variables de sortie (ie : l'appel à scanf va modifier leur valeur, il faut donc passer une référence).

Ces fonctions retournent le nombre d'arguments correctement lus depuis le format, qui peut être inférieur ou égal au nombre de spécificateurs de format, et même nul.

Format de conversion modifier

Les fonctions scanf() analysent le spécificateur de format et les données d'entrée, en les comparant caractère à caractère et s'arrêtant lorsqu'il y en a un qui ne correspond pas. À noter que les blancs (espaces, tabulations et retour à la ligne) dans le spécificateur de format ont une signification spéciale : à un blanc de la chaîne format peut correspondre un nombre quelconque de blanc dans les données d'entrée, y compris aucun. D'autres part, il est possible d'insérer des séquences spéciales, commençant par le caractère '%' et à l'image de printf(), pour indiquer qu'on aimerait récupérer la valeur sous la forme décrite par le caractère suivant le '%' :

  • s : extrait la chaîne de caractères, en ignorant les blancs initiaux et ce jusqu'au prochain blanc. L'argument correspondant doit être de type char * et pointer vers un bloc mémoire suffisamment grand pour contenir la chaîne et son caractère terminal.
  • d : extrait un nombre décimal signé de type int, ignorant les espaces se trouvant éventuellement avant le nombre.
  • i : extrait un nombre (de type int) hexadécimal, si la chaîne commence par "0x", octal si la chaîne commence par "0" et décimal sinon. Les éventuels espaces initiaux seront ignorés.
  • f : extrait un nombre réel, en sautant les blancs, de type float.
  • u : lit un nombre décimal non-signé, sans les blancs, de type int.
  • c : lit un caractère (de type char), y compris un blanc.
  • [] : lit une chaîne de caractères qui doit faire partie de l'ensemble entre crochets. Cet ensemble est une énumération de caractère. On peut utiliser le tiret ('-') pour grouper les déclarations (comme "0-9" ou "a-z"). Pour utiliser le caractère spécial ']' dans l'ensemble, il doit être placé en première position et, pour utiliser le tiret comme un caractère normal, il doit être mis à la fin. Pour indiquer que l'on veut tous les caractères sauf ceux de l'ensemble, on peut utiliser le caractère '^' en première position. À noter que scanf terminera toujours la chaîne par 0 et que, contrairement au spécificateur %s, les blancs ne seront pas ignorés.
  • n : Comme pour la fonction printf(), ce spécificateur de format permet de stocker dans l'entier correspondant de type int, le nombre de caractères lus jusqu'à présent.

Contraindre la largeur modifier

Comme pour la fonction printf(), il est possible de contraindre le nombre de caractères à lire, en ajoutant ce nombre juste avant le caractère de conversion. Dans le cas des chaînes, c'est même une obligation, dans la mesure où scanf() ne pourra pas ajuster l'espace à la volée.

Exemple :

/* Lit une chaîne de caractères entre guillemets d'au plus 127 caractères */
char tmp[128];

if (fscanf(fichier, "Config = \"%127[^\"]\"", tmp ) == 1)
{
    printf("L'argument associé au mot clé 'Config' est '%s'\n", tmp);
}

Cet exemple est plus subtil qu'il ne paraît. Il montre comment analyser une structure relativement classique de ce qui pourrait être un fichier de configuration de type "MotClé=Valeur". Ce format spécifie donc qu'on s'attend à trouver le mot clé "Config", en ignorant éventuellement les blancs initiaux, puis le caractère '=', entouré d'un nombre quelconque de blancs, éventuellement aucun. À la suite de cela, on doit avoir un guillemet ('"'), puis au plus 127 caractères autres que que les guillemets, qui seront stockés dans la zone mémoire tmp (qui sera terminée par 0, d'où l'allocation d'un caractère supplémentaire). Le guillemet final est là pour s'assurer, d'une part, que la longueur de la chaîne est bien inférieure à 127 caractère et, d'autre part, que le guillemet n'a pas été oublié dans le fichier.

En cas d'erreur, on peut par exemple ignorer tous les caractères jusqu'à la ligne suivante.

Ajuster le type des arguments modifier

On peut aussi ajuster le type des arguments en fonction des attributs de taille :

Format Attributs de taille
aucun hh h l (elle) ll (elle-elle)
d, i, n int * char * short * long * long long *
u unsigned int * unsigned char * unsigned short * unsigned long * unsigned long long *
s, c, [ ] char *
f float * double * long double *

Ainsi pour lire la valeur d'un entier sur l'entrée standard, on utilisera un code tel que celui ci :

#include <stdio.h>

int main(void)
{
    int i;

    printf("Entrez un entier : ");
    scanf("%d", &i);
    printf("la variable i vaut maintenant %d\n", i);
    return 0;
}

Les appels à printf ne sont pas indispensables à l'exécution du scanf, mais permettent à l'utilisateur de comprendre ce qu'attend le programme (et ce qu'il fait aussi).

Conversions muettes modifier

La fonction scanf reconnaît encore un autre attribut qui permet d'effectuer la conversion, mais sans retourner la valeur dans une variable. Il s'agit du caractère étoile '*', qui remplace l'éventuel attribut de taille.

En théorie, la valeur de retour ne devrait pas tenir compte des conversions muettes.

Exemple:

int i, j;

sscanf("1 2.434e-308 2", "%d %*f %d", &i, &j); /* i vaut 1 et j vaut 2 */

Quelques remarques pour finir modifier

La fonction scanf() n'est pas particulièrement adaptée pour offrir une saisie conviviale à l'utilisateur, même en peaufinant à l'extrême les spécificateurs de format. En général, il faut s'attendre à une gestion très rudimentaire du clavier, avec très peu d'espoir d'avoir ne serait-ce que les touches directionnelles pour insérer du texte à un endroit arbitraire.

Qui plus est, lors de saisie de texte, les terminaux fonctionnent en mode bloc : pour que les données soient transmises à la fonction de lecture, il faut que l'utilisateur confirme sa saisie par entrée. Même les cas les plus simple peuvent poser problèmes. Par exemple, il arrive souvent qu'on ne veuille saisir qu'un caractère pour répondre à une question du genre "Écraser/Annuler/Arrêter ? (o|c|s)", avant d'écraser un fichier. Utiliser une des fonctions de saisie nécessite de saisir d'abord un caractère, ensuite de valider avec la touche entrée. Ce qui peut non seulement être très pénible s'il y a beaucoup de questions, mais aussi risqué si on ne lit les caractères qu'un à un. En effet, dans ce dernier cas, si l'utilisateur entre une chaîne de plusieurs caractères, puis valide sa saisie, les caractères non lus seront disponibles pour des lectures ultérieures, répondant de ce fait automatiquement aux questions du même type.

Il s'agit en fait de deux opérations en apparence simples, mais impossible à réaliser avec la bibliothèque C standard :

  1. Purger les données en attente de lecture, pour éviter les réponses « automatiques ».
  2. Saisir des caractères sans demande de confirmation.

Ces fonctionnalités sont hélas le domaine de la gestion des terminaux POSIX et spécifiées dans la norme du même nom.

On l'aura compris, cette famille de fonction est plus à l'aise pour traiter des fichiers, ou tout objet nécessitant le moins d'interaction possible avec l'utilisateur. Néanmoins dans les cas les plus simples, ce sera toujours un pis-aller.

À noter, que contrairement à la famille de fonction printf(), scanf() n'est pas sensible à la localisation pour la saisie de nombres réels.

Entrées non formatées modifier

Pour saisir des chaînes de caractères indépendamment de leur contenu, on peut utiliser les fonctions suivantes :

char * fgets(char * restrict chaine, int taille_max, FILE * restrict flux);
int fgetc( FILE * restrict flux);
int ungetc( int octet, FILE * flux );

La fonction fgets() permet de saisir une ligne complète dans la zone mémoire spécifiée, en évitant tout débordement. Si la ligne peut être contenue dans le bloc, elle contiendra le caractère de saut de ligne ('\n'), en plus du caractère nul. Dans le cas contraire, la ligne sera tronquée, et la suite de la ligne sera obtenue à l'appel suivant. Si la fonction a pu lire au moins un caractère, elle retournera la chaîne transmise en premier argument, ou NULL s'il n'y a plus rien à lire.

Un exemple de lecture de ligne arbitrairement longue est fournie dans le livre Exercices en langage C (énoncé et solution).

La fonction fgetc() permet de ne saisir qu'un caractère depuis le flux spécifié. À noter que la fonction renvoie bien un entier de type int et non de type char, car en cas d'erreur (y compris la fin de fichier), cette fonction renvoie EOF (défini à -1 en général).

À noter que cette fonction est incapable de traiter des fichiers mixtes (binaire et texte) depuis un descripteur en mode caractère (accès séquentiel). D'une part la fonction ne renvoyant pas le nombre d'octets lus (ce qui aurait facilement réglé le problème) et d'autre part, ftell() ne fonctionnant pas sur de tels descripteurs, il faudra reprogrammer fgets pour gérer ce cas de figure.

Entrées/sorties brutes modifier

Les fonctions suivantes permettent d'écrire ou de lire des quantités arbitraires de données depuis un flux. Il faut faire attention à la portabilité de ces opérations, notamment lorsque le flux est un fichier. Dans la mesure où lire et écrire des structures binaires depuis un fichier nécessite de gérer l'alignement, le bourrage, l'ordre des octets pour les entiers (big endian, little endian) et le format pour les réels, il est souvent infiniment plus simple de passer par un format texte.

Sortie modifier

size_t fwrite(const void * buffer, size_t taille, size_t nombre, FILE * flux);

Entrée modifier

size_t fread(void * buffer, size_t taille, size_t nombre, FILE * flux);

Lit nombre éléments, chacun de taille taille, à partir du flux et stocke le résultat dans le buffer. Renvoie le nombre d'éléments correctement lus.

Gestion des erreurs modifier

Qui dit entrées/sorties dit forcément une pléthore de cas d'erreurs à gérer. C'est souvent à ce niveau que se distinguent les « bonnes » applications des autres : fournir un comportement cohérent face à ces situations exceptionnelles. Dans la description des fonctions précédentes, il est fait mention que, en cas d'erreur, un code spécial est retourné par la fonction. De plus, on dispose de la fonction int ferror( FILE * );, qui permet de savoir si une erreur a été déclenchée sur un fichier lors d'un appel antérieur à une fonction de la bibliothèque standard.

Si ces informations permettent de savoir s'il y a eu une erreur, elles ne suffisent pas à connaître la cause de l'erreur. Pour cela, la bibliothèque stdio repose sur la variable globale errno, dont l'utilisation est décrite dans le chapitre sur la gestion d'erreur.