Entrées/Sorties

Écrit le 11/11/2004 par Wikibooks
Dernière mise à jour : 02/02/2006

Les en-têtes de fonctions d'entrées/sorties se trouvent dans stdio.h, pour les utiliser nous utiliserons donc la directive d'inclusion :

#include <stdio.h>

Manipulation de fichiers

En C, les fichiers ouverts sont représentés par le type FILE *, qui est un type opaque (on ne connait pas la nature réelle du type, mais seulement des fonctions pour le manipuler). Ce type est un flux de données, lire ou écrire des données dans un flux le modifie.
Le fichier stdio.h fournit trois flux que l'on peut utiliser « directement » :

Ouverture

FILE * fopen(const char * chemin, const char * 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é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élerer les opérations sur les fichiers.

Fermeture

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 c'est bien passée (notamment la purge des zones en écriture), ou EOF en cas d'erreur. C.f gestion des erreurs plus savoir comment obtenir plus d'information sur la cause de l'erreur.

Suppression

int remove(const char * path);

Supprime le fichier ou le répertoire pointé par 'path'. La fonction renvoie 0 en cas de réussite et -1 en cas d'erreur, ce qui peut inclure :

Renommage

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'. Si le fichier ou répertoire existe déjà, il sera écrasé, sauf si :

La fonction renvoie 0 si elle réussie, -1 en cas d'erreur.

Déplacement dans le flux

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) :

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 (tube de communication, connexion réseau, etc ...) pour lesquels ces appels échoueront.

Synchronisation

int fflush ( FILE *flux );

Cette fonction purge toutes les zones mémoires en attente d'écriture et renvoie 0 si tout c'est bien passé. 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 au mieux, sera ignorée, au pire provoquera un comportement indéterminé :

/* Cette instruction ne fera rien */
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

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

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

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.

Bien que cela à déjà été traité dans la section dédiée aux chaines 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épendemment 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

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équence 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 :

Contraindre la largeur des champs

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

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

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

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

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

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

Par défaut, les entiers sont présuposés être de type int, les réels de type double et les chaines de caractères de type char *. Il arrive toutefois que les types soient plus grands (et non plus petit à cause de la promotion des types, C.f. section 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 :

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

Format Attributs de taille
aucun hh h l (elle) ll (elle-elle) ou L
n int * char * short * long * long long *
d, i, o, x int     long long long
u unsigned int     unsigned long unsigned long long
s char *     wchar_t *  
p void *        
f, e, g float     double long double

Quelques exemples (sur une architecture 32bits) :

char nb;

printf("%Lu", -1LL);                 /* "18446744073709551615" */
printf("%d%hhn", 12345, &nb);        /* "12345" et nb vaudra 5 */
printf("%ls", L"Hello world!");      /* "Hello world!" */

Arguments positionnels

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 internationnalisée. Considérez le code suivant (tiré du manuel de gettext) :

printf( gettext("La chaine `%s' a %d 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 chaine de caractère retourne la chaine 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 chaine 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, suivit 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$d caractères\n", strlen(s), s); /* "La chaine 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

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 :

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 plusiseurs 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 * flux, char * mem, int mode, int 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 :

La fonction setvbuf() renvoie 0 si elle réussi, 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

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 certaine implémentation de printf() tienne compte de la localisation en cours pour les conversions des 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()&nbsp;:

#include <locale.h>

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

Entrées formatées

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 paraitre surprenant de prime abord, mais s'éclaircira à la lumière des explications suivantes.

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

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

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 inférieur ou égal au nombre de spécificateurs de format, et même nul.

Format de conversion

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 chaine 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 '%' :

Contraindre la largeur

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

Exemple :

/* Lit une chaine de caractère entre guillement 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 parait. 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'attends à trouver le mot clé "Config", en ignorant éventuellement les blancs initiaux, puis le caratère '=', entouré d'un nombre quelconque de blanc, eventuellement aucun. À la suite de cela, on doit avoir un guillement ('"'), puis au plus 127 caractères différents du guillement, qui seront stockés dans la zone mémoire tmp. Le guillement final est pour s'assurer d'une part que la chaine est bien inférieure à 127 caractère, et d'autre par que le guillement 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

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

Format Attributs de taille
aucun h l (elle) ll (elle-elle)
d, i, n int * short * long * long long *
u unsigned int * 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 :

int main(int argc, char **argv)
{
    int i;

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

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

La fonction scanf reconnait 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, une conversion '%n' 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

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 chaine 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

Pour saisir des chaines de caractères indépendemment de leur contenu, on peut utiliser les fonctions suivantes :

char * fgets( char * chaine, int taille_max, FILE * flux );
int    fgetc( FILE * 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 chaine transmise en premier argument, ou NULL s'il n'y a plus rien à lire. Voici une manière de lire une ligne arbitrairement longue :

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

char * saisie_chaine( FILE * flux )
{
    int    taille = 256;
    int    offset = 0;
    char * chaine = malloc( taille );

    while( chaine && fgets( chaine + offset, taille - offset, flux ) )
    {
        offset = taille - 1;
        if( strrchr( chaine, '\n' ) == 0 )
        {
            char * nouvelle = realloc( chaine, taille += 256 );

            if( nouvelle == NULL ) free( chaine );

            chaine = nouvelle;
        }
        else break;
    }
    if( offset == 0 && chaine )
    {
        free( chaine );
        return NULL;
    }
    return chaine;
}

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).

Entrées/sorties brutes

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

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

Entrée

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

Qui dit entrées/sorties, dit forcément une pléthore de cas d'erreurs à gérer. C'est souvent à ce niveau que se distingue les « bonnes » applications des autres : fournir un comportement cohérent face à ces situations exceptionnelles. Il peut s'agir d'un travail de longue haleine, dépendamment du degré de convivialité recherché, mais laisser de coté des cas que l'on penseraient exceptionnels est le meilleur moyen de découvrir l'impitoyable loi de Murphy.

Dans la description des fonctions précédentes, il est fait mention qu'en cas d'erreur, un code spécial est retourné par la fonction. C'est un peu court pour présenter à l'utilisateur un message pertinent sur la cause réelle de cette erreur. En fait la bibliothèque stdio repose, comme beaucoup d'autres, sur la variable globale errno, déclarée dans le fichier en-tête errno.h.

Hélas, le contenu de cette variable est extrêmement pénible à gérer. Pour être portable, on ne peut être sûr de son contenu que lorsqu'une fonction signale une erreur. Ce qui veut dire que le code de retour de chaque fonction susceptible de faire une entrée/sortie doit être testé. Qui plus est, en cas d'erreur, il est sage de présupposer la durée de vie la plus courte possible du contenu de errno et de le copier le plus rapidement possible dans un endroit sûr. Bien que le standard C impose ne modifier errno qu'en cas d'erreur, d'autres fonctions ou appels systèmes ne suivent pas nécessairement cette règle et positionnent systématiquement errno à 0 et éventuellement à une valeur significative en cas d'erreur.

Tester le code de retour de chaque appel peut devenir particulièrement pénible, c'est d'autant plus navrant que la bibliothèque stdio propose la fonction int ferror( FILE * );, qui permet de savoir si une erreur a été déclenchée lors d'un appel antérieur... mais hélas sans retourner la cause de l'erreur de l'appel fautif ! Cela aurait permis de ne faire qu'un minimum de tests à des endroits critiques, plutôt qu'à chaque appel pour récupérer la cause. C'est encore une fois, un problème tellement classique, que le premier réflexe dans une application digne de ce nom est de proposer une surcouche à ces fonctions (de sorties principalement).

Cet article provient de Wikibooks et est sous licence GNU Free Documentation License. Il a été écrit par plusieurs personnes et est constamment mis à jour. Cet article est la version du 6 mai 2005 à 04:31. L'article d'origine se trouve à http://fr.wikibooks.org/wiki/Programmation_C_Entrées/sorties.