Introduction aux flux en C

S'il y a une chose très importante que beaucoup méconnaissent au sujet du C, ce sont bien les flux. S'il est tout à fait normal que cette notion soit tout à fait inconnue à un débutant, il lui faudra un jour la découvrir. On notera également que plusieurs grandes écoles d'informatiques qui enseignent pourtant le C comme langage principal oublient totalement d'enseigner les flux à leurs élève. Ces dernières font ça par pédagogie, ce qui est totalement justifié dans leur programme afin de ne pas embrouiller l'élève. Cependant, le fait qu'elles n'en parlent pas du tout durant la scolarité de l'étudiant me semble être grave. Qu'importe, ce post est justement là pour palier à cette lacune.

Rappel des bases sur les entrées/sorties

Comme Ken Thompson l'a magnifiquement exprimé : « Tout est fichier. » C'est ce principe qui fait que, pour écrire sur un terminal, envoyer des données sur le réseau ou autre, il suffit d'écrire dans un fichier. Il est donc tout à fait normal que les écoles que j'évoquais enseignent à leurs élèves à écrire sur la sortie standard avec un simple write. Les élèves de ces écoles reconnaîtrons donc très certainement les trois fonctions suivantes :

#include <unistd.h>

int
my_putchar(char c)
{
  return write(STDOUT_FILENO, &c, 1);
}

int
my_strlen(char *str)
{
  char *end = str;

  while (*end != '\0')
      end++;
  return end - str;
}

int
my_putstr(char *str)
{
  return write(STDOUT_FILENO, str, my_strlen(str));
}

Avant d'aller plus loin, quelques remarques destinées aux élèves de ces écoles :

  • Il existe beaucoup de normes, celle de votre école n'est pas une référence en soit, j'ai volontairement choisi de ne pas la suivre ;
  • STDOUT_FILENO, STDERR_FILENO et STDIN_FILENO sont définies dans unistd.h et représentent respectivement le numéro du descripteur de fichier de la sortie standard, de la sortie d'erreur et de l'entrée standard ;
  • non, utiliser directement les numéros de ces descripteurs de fichiers ou vos propres defines n'est pas une bonne idée ;
  • oui, my_putchar et my_putstr doivent renvoyer un int, enfin un ssize_t en vrai, et dans votre code vous devez vérifier la valeur de retour de ces fonctions ;
  • dans les exemples suivants je ne vérifierais pas les codes de retour, c'est uniquement dans un but pédagogique afin que les exemples soient le plus concis possible, j'insiste sur le fait que lorsque vous écrivez un vrai programme vous devez faire la vérification ;
  • non, my_putstr ne doit pas utiliser my_putchar ;
  • un my_strlen en une ligne avec de la récursivité est intéressant d'un point de vue pédagogique mais une calamité dans un un programme ;
  • un my_strlen qui utilise une différence entre deux pointeurs est bien plus intéressant intellectuellement.

Les appels système

Je vous entends d'ici vous demander « Mais où est le problème aux fonctions précédentes ? ». Et bien le problème vient du fait que l'on contrôle assez mal le nombre d'appels système effectué : on en effectue toujours un par appel à my_putchar ou my_putstr. Les inconscients qui auront implémenté my_putstr avec my_putchar en effectueront n à la place, n étant la longueur de la chaîne de caractère. Le fait de mal contrôler le nombre d'appels système à une conséquence directe sur la performance de votre programme. En effet, généralement, effectuer un appel système déclenche une interruption logicielle, ce qui, à l'échelle d'un programme, prend énormément de temps. Vous devez normalement commencer à comprendre pourquoi utiliser my_putchar dans my_putstr est une très mauvaise idée. Mais bon, en fonction des cas, la performance n'est pas forcément un problème, il ne faut pas regarder uniquement à ça. L'intéret est surtout que les flux rendent possible ce qui était compliqué de mettre correctement en place sans. A ce titre, on citera fgets dont certains ont dû implémenter une version similaire sans flux : le fameux get_next_line. Et j'aime autant vous dire que votre get_next_line a très certainement plein de défauts cachés dont vous n'avez peut être même pas conscience.

Les flux

Comme dans la plupart du temps en programmation, lorsqu'un problème se pose, quelqu'un lui a déjà trouvé une solution. Et la solution à notre problème de maîtrise des appels système se trouve être les flux. Pour rester simple, une flux est un buffer (et donc un bout de la mémoire) associé à un descripteur de fichier dans lequel on va écrire au lieux d'écrire sur le descripteur de fichier. Le contenu de ce buffer est vidé, c'est à dire écrit sur le descripteur de fichier associé selon des règles définies. La bibliothèque standard du C comprend bien entendu tout un lot de fonctions qui travaillent non pas sur un descripteur de fichier mais sur un flux, ces dernières sont reconnaissables au préfixe ou suffixe f. La plus connue est bien entendu printf, d'où le fait que les écoles citées plus haut interdisent l'usage de cette fonction, mais il en existe bien d'autres, telles que fread, fwrite, fopen, fgets et bien d'autres. On notera que, tout comme unistd.h définit STDOUT_FILENO, STDERR_FILENO et STDIN_FILENO, stdio.h définit les flux stdout, stderr et stdin. Notez également qu'un flux est de type FILE *, c'est donc un pointeur.

Vider un buffer

Il existe trois manières pour définir quand un buffer doit être vidé :

  • _IONBF (unbuffered) : aucune utilisation du buffer, tout est écrit à chaque appel ;
  • _IOLBF (line buffered) : le buffer est vidé dès qu'un retour à la ligne \n est détecté ou que le buffer est plein ;
  • _IOFBF (fully buffered) : le buffer est vidé uniquement lorsqu'il est plein.

Par défaut, un buffer utilise le mode _IOLBF (line buffered). Il est possible de modifier le comportement d'un buffer grâce à la fonction setvbuf. Il faut également noter l'existence de la fonction fflush qui permet de vider un buffer quel que soit sa politique de vidange.

Exemples

Afin de ne pas avoir à écrire plein de code différent, nous en utiliseront un seul dans lequel setvbuf est appelée avec des macros définies à la compilation.

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

int
my_putstr(char *str)
{
  return write(STDOUT_FILENO, str, strlen(str));
}

int
main(void)
{
#if defined(BUF_TYPE)
  setvbuf(stdout, NULL, BUF_TYPE, 0);
#endif

  printf("It is practically impossible\nto teach good programming");
  my_putstr("to students that have had a prior\n");
  printf("exposure to BASIC:\n");
  my_putstr("as potential programmers they are\n");
  printf("mentally mutilated beyond hope of regeneration.\n");

  return EXIT_SUCCESS;
}

Exemple 1: mode non bufferisé

-> cc testbuf.c -DBUF_TYPE=_IONBF -o testbuf && ./testbuf
It is practically impossible
to teach good programmingto students that have had a prior
exposure to BASIC:
as potential programmers they are
mentally mutilated beyond hope of regeneration.

Sans utilisation du buffer, tout est écrit à chaque appel et tout appairait donc dans le bon ordre.

Exemple 2: ligne par ligne

-> cc testbuf.c -DBUF_TYPE=_IOLBF -o testbuf && ./testbuf
It is practically impossible
to students that have had a prior
to teach good programmingexposure to BASIC:
as potential programmers they are
mentally mutilated beyond hope of regeneration.

Lors du premier printf, seul "It is practically impossible\n" a été écrit lors de l'appel car il n'y a aps d'autre retour à la ligne. C'est donc ensuite le my_putstr qui a écrit sur la sortie standard. Ensuite, l'appel à printf rajoute des données au buffer contenant un retour à la ligne, le buffer est donc vidé, le "to teach good programming" qui y avait été mis précédemment est donc écrit.

Notez qu'il était également possible de simplement compiler avec cc testbuf.c -o testbuf && ./testbuf car _IOLBF est le mode par défaut.

Exemple 3: entièrement bufferisé

-> cc testbuf.c -DBUF_TYPE=_IOFBF -o testbuf && ./testbuf
to students that have had a prior
as potential programmers they are
It is practically impossible
to teach good programmingexposure to BASIC:
mentally mutilated beyond hope of regeneration.

Toutes les données ajoutées au buffer par printf n'ont été écrites que lorsque le buffer s'est vidé à la terminaison du programme et donc après tous les appels à my_putstr.

Conclusion

Sur un même descripteur de fichier, vous ne devriez jamais mélanger des appels à des fonctions utilisant des flux avec des appels à des fonctions ne les utilisant pas. A moins que vous ne sachiez parfaitement ce que vous faites, vous risqueriez de vous retrouver avec des résultats surprenants. C'est pour cette raison que les écoles dont je parlais interdisent l'utilisation de printf et consort : imaginez la tête d'un débutant quand ses chaînes de caractères sortent dans le mauvais ordre, celui là abandonnerait certainement la programmation. Par contre, une fois que l'on comprend leur fonctionnement, les buffers sont une fonctionnalité formidable dont il serait bête de se passer.

Tags