Le langage C
Nombre d'arguments variables

Par :
Guilhem de Wailly (gdw@erian-concept.com)

Résumé
Dans cette série d'articles, nous présentons le langage C. Il ne s'agit pas de réécrire les ouvrages de référence donnés en annexe, mais de donner, au travers du C, une méthode de programmation basée sur la notion d'interface.
Au fur et à mesure de notre progression, nous décrirons les éléments indispensables du langage et conduirons le lecteur à consulter les ouvrages de références. Le but poursuivi est clairement de parvenir à construire des programmes de manière segmentée.
Ce mois-ci, nous allons examiner un aspect du langage C que nous avons déjà évoqué sans l'avoir expliqué : la définition de fonctions ayant un nombre d'arguments variable. Nous avons toujours utilisé une telle fonction pour l'affichage avec printf.
A titre d'information, cet article est rédigé avec Applix (www.applix.com) et corrigé avec Corrector 101 (www.machinesapiens.com) sous Linux, bien sûr !
Description
Depuis le premier article sur le C, nous utilisons la fonction d'affichage printf. Nous avons constaté qu'elle attend une chaîne de caractère appelée format dans la quelle certaines combinaisons de caractères comme %d sont remplacées par la représentation de l'argument correspondant. Ainsi :
printf ("Entier: %d, Caractère: %c, chaîne: %s\n",
       123, 'r', "de caractères");
affichera :
Entier: 123, Caractère: r, chaîne: de caractères
Le fonctionnement de cette fonction devient vite naturel. On oublie que sa définition définit une fonction ayant un nombre d'arguments variable, dont le premier est la chaîne de format.
Ce type de fonction est possible de à programmer en C car le protocole d'appel des fonctions du langage stipule que l'appelant empile les arguments, appelle la fonction et dépile les arguments. Dans le langage Pascal par exemple, le protocole d'appel des fonctions stipule que l'appelé est responsable de dépiler les arguments ; il est donc impossible de définir des fonctions ayant un nombre d'arguments variable.
Pour définir des fonctions à nombre d'arguments variable, il nous faut un élément de syntaxe supplémentaires et une bibliothèque standard. 
Fonction simple
Dans un premier temps, nous allons définir une fonction f ayant comme premier argument le nombre d'entiers supplémentaire passés. Tout d'abord, au niveau de la signature de la fonction (défini par le type des ses arguments et sa valeur de retour), on indique le fait que la fonction peut prendre un nombre d'arguments variable en utilisant trois petits points dans les paramètres.  
Le prototype de f est donc :
void f (unsigned n, ...);
Lorsque le compilateur rencontre une fonction ayant ce type de signature, il vérifie dans le programme suivant cette déclaration que tout appel à cette fonction a bien en première position un entier, suivit d'un nombre quelconque d'arguments de n'importe quel type.
Ce type de fonction doit obligatoirement comporter un « vrai » argument. C'est une erreur d'écrire int f(...) et le compilateur le signale immédiatement. 
Pour accéder aux arguments supplémentaires, nous devons déclarer une variable spéciale que nous allons déplacer d'argument en argument. Pour cela, nous utilisons la bibliothèque ANSI stdarg.h et les macros :
·	va_list curseur : Déclare la variable curseur comme étant capable de se positionner successivement sur tous les arguments supplémentaires.
·	va_start (curseur, dernier) : Initialise le curseur sur le premier argument supplémentaire situé après le dernier argument déclaré. Pour la fonction f, serait après la variable n.
·	va_arg (curseur, type) : Retourne la valeur de l'argument supplémentaire placé sous le curseur et avance le curseur sur l'argument suivant. La valeur retournée est du type type.
·	va_end (curseur) : Libère les ressources utilisées par le curseur.
Pour certains compilateurs anciens, avant que le C ne soit normalisé par l'ANSI, le fichier à include est vararg.h ; les macros restent, semble-t-il, identiques.
Nous pouvons donc maintenant programmer notre fonction f de la manière suivante :
/* Librairie pour accéder aux
* arguments supplémentaires
*/
#include <stdarg.h>

/* Définition de la fonction */
void f (unsigned n, ...) {
 /* Déclaration du curseur */
 va_arg curseur;

 /* Initialisation du curseur */
 va_start (curseur, n);
 printf ("\nIl y a %d argument%c\n", 
         n,
         n > 1 ? 's' : ' '); /* expliqué après */
 while (n--) {
   /* Récupération des arguments 
    * comme des entiers */
   int entier = va_arg (curseur, int);

   printf ("On a: %d\n", entier);
 }
 /* Libération du curseur */
 va_end (curseur);
}
Le curseur est déclaré, puis initialisé après l'argument n qui est le dernier argument déclaré. Ensuite, pour tous les arguments supplémentaires dont l'appelant a placé le nombre dans n, l'itération lit sa valeur comme un entier et l'affiche. Enfin, le curseur est libéré.
Nous avons utilisé une syntaxe particulière pour mettre le mot argument au singulier ou au pluriel, en fonction du nombre n. Cette alternative (test) a comme syntaxe condition ? alors : sinon, où condition, alors, sinon sont des expressions C. La valeur de retour de cette alternative est l'évaluation de la clause alors ou de la clause sinon, en fonction du résultat de l'évaluation de la condition (On rappelle qu'en C, faux vaut 0, et vrai toutes les autres valeurs). Cette syntaxe est très pratique car d'une part elle est plus économique en terme de touches tapées que le classique if, et en plus, l'expression possède une valeur (ici le caractère 's' ou le caractère ' '), à la différence de l'expression if qui n'en a pas.
Pour appeler cette fonction, nous pourrions avoir :
void main (void) {
  f (0);
  f (1, 123);
  f (2, 456, 789);
}
Ce programme provoque l'affichage suivant :
Il y a 0 argument

Il y a 1 argument
	On a: 123

Il y a 2 arguments
	On a: 456
	On a: 789
Les arguments supplémentaires ne sont pas contrôlés par le compilateur, ni dans leur type, ni dans leur nombre. La fonction est donc potentiellement dangereuse et il faudra la réaliser avec soin. 
Par exemple, les appels suivants ne provoquent pas d'erreur :
void main (void) {
  /* ``mauvais'' types */
  f (2, 'a', "une chaîne de caractères");

  /* ``trop'' d'arguments */
  f (2, 1, 2, 3, 4, 5, 6);

  /* ``trop peu'' d'arguments */
  f (2);

  /* non, là, c'est exagéré ! */
  f (1000);
}
Lorsqu'il est exécuté, ce programme provoque l'affichage :
Il y a 2 arguments
	On a: 64
	On a: 57865786

Il y a 2 arguments
	On a: 5235455
	On a: 57865786

Il y a 2 arguments
	On a: 1
	On a: 2

Il y a 1000 arguments
	On a: 345345
	On a: 34352345345
	On a: 7567345345
	...
Le premier cas montre que le contrôle des types n'est pas effectué. Comme nous lisons les arguments comme des entiers avec va_arg (curseur, int), nous obtenons la valeur entière correspondante. Dans le cas du caractère 'a', l'entier 64 qui est lu correspond au code ASCII de la lettre a sur un PC sous Linux. Sur d'autres systèmes, d'autres codes pour les caractères peuvent être utilisés, et la valeur entière correspondant à la lettre a pourra être différente. 
La seconde valeur est la valeur de l'adresse de la chaîne de caractères constante "une chaîne de caractères". Cette valeur sera sans doute différente à chaque appel car elle dépend de l'emplacement du programme au chargement.
Les cas suivants montrent que le nombre des arguments n'est pas vérifié. En effet, la valeur de l'argument n est de la responsabilité de l'appelant. Le second cas montre que si trop d'arguments sont passés, cela n'affecte pas leur ordre, et le premier argument lu est toujours le premier argument passé. 
Le troisième cas montre que si trop peu d'arguments sont passés, nous lisons des valeurs aléatoires qui sont en fait les valeurs placées sur la pile matérielle de la machine.
Passons maintenant à une fonction un peu plus compliquée.
Afficher
Nous allons maintenant programmer notre propre version simplifiée de la fonction standard printf que nous nommerons afficher.
Nous allons utiliser une chaîne de caractères de format qui sera affichée telle quelle. Les combinaisons de caractères suivantes seront interprétées et remplacées par la valeur de l'argument correspondant :
·	~c : Introduit un argument de type caractère.
·	~d : Introduit un argument de type entier.
·	~s : Introduit une chaîne de caractères.
·	~f : Introduit un nombre réel.
·	~n : Ecrit un saut à la ligne.
·	~t : Ecrit une tabulation.
·	~~ : Ecrit le caractère tilde.
Pour simplifier le programme, nous utiliserons de manière interne la fonction printf pour afficher individuellement les arguments de type entier et réel. Sans cela, il nous faudrait écrire des fonctions d'affichage pour chacun de ces types, ce qui n'est pas très difficile mais assez long.
La fonction retourne le nombre d'arguments traités.
#include <stdio.h>
#include <stdarg.h>

int afficher (char * format, ...) {
 /* Variables de lecture des arguments */
  char    c;
 int     n;
 double  f;
 char  * s;
 /* Curseur de lecture */
  va_list curseur;
  /* Nombre d'arguments lus */
  int     retour = 0;

  /* Contrôle du format */
  if (! format) return 0;

  /* Initialisation du curseur */
  va_start (curseur, format);

 /* Itération */
  while (1) {
   /* Cas selon le caractères courant */
   switch (* format) {
  case 0:    /* fin de chaîne */
             break;
  case '~':  /* formatage */
    /* Avancer au caractère suivant */
    format ++;
   switch (* format) {
   case 0:   /* fin de chaîne */
             break;
   case '~': /* tilde */
             putchar ('~');
             break;
   case 'n': /* saut de ligne */
             putchar ('\n');
             break;
   case 't': /* tabulation */
             putchar ('\t');
             break;
   case 'c': /* caractère */
             c = va_arg (ap, char);
             putchar (c);
             retour++;
             break;
   case 'd': /* entier */
             n = va_arg (ap, int);
             printf ("%d", n);
             retour++;
             break;
   case 'f': /* réel */
             f = va_arg (ap, double);
             printf ("%f", f);
             retour++;
             break;
   case 's': /* chaîne de caractères */
             s = va_arg (ap, char *);
             puts (s);
             retour++;
             break;
   default:  /* cas non traité */
             putchar ('~');
             putchar (* format);
             break;
   }
   break;
  default:  /* autres caractères */
            putchar (* format);
            break;
  } /* fin du switch sur le caractère ~ */
   /* Avancer au caractère suivant */
   format++;
  } /* fin du switch */
 } /* fin du while */

  /* Libération du curseur */
  va_end (curseur);

  /* retourne le nombre d'arguments lus */
  return retour;
}
Pour utiliser afficher, nous pouvons écrire :
void main (void) {
afficher ("Entier: ~d, Caractère: ~c, chaîne: ~s~n",
          123, 'r', "de caractères");
}
ce qui affichera :
Entier: 123, Caractère: r, chaîne: de caractères
Comme pour la fonction printf, il serait possible d'étendre les possibilités de notre fonction, en ajoutant par exemple des options de formatage ou d'alignement, de nouveaux types de données, etc. On pourrait aussi ajouter la possibilité d'écrire dans un fichier ou dans un tampon en mémoire plutôt que sur le port de sortie standard. Notamment, la possibilité d'écrire dans des tampons en mémoire est extrêmement intéressante. Pour se rendre compte des possibilités d'extension, le lecteur pourra se référer au pages de manuels de la fonction printf.
Indirrection
La fonction d'affichage que nous avons définie traite elle-même les arguments supplémentaires. Cependant, dans certains cas, il est utile de séparer une telle fonction en deux fonctions, la première initialisant le curseur, et la seconde utilisant le curseur initialisé par la première. Si nous souhaitions redéfinir la fonction afficher de cette manière, voici comment il faudrait procéder.
Tout d'abord, nous redéfinissons la fonction afficher de manière à ce qu'elle ne traite plus elle-même les arguments, mais confie ce traitement à une fonction vafficher définie plus bas :
int afficher (char * format, ...) {
 /* Curseur de lecture */
  va_list curseur;
  /* Nombre d'arguments lus */
  int     retour = 0;

  /* Contrôle du format */
  if (! format) return 0;

  /* Initialisation du curseur */
  va_start (curseur, format);

 /* Affichage */
  retour = vafficher (format, curseur);

  /* Libération du curseur */
  va_end (curseur);

  /* retourne le nombre d'arguments lus */
  return retour;
}
Ensuite, nous définissons la fonction vafficher avec en argument la chaîne de format et un curseur. Cette fonction traite les arguments supplémentaires :
int vafficher (char * format, va_list curseur) {
 /* Variables de lecture des arguments */
  char    c;
 int     n;
 double  f;
 char  * s;
  /* Nombre d'arguments lus */
  int     retour = 0;

  /* Contrôle du format */
  if (! format) return 0;

 /* Itération */
 while (1) {
   /* code identique */
 } /* fin du while */

  /* Libération du curseur */
  va_end (curseur);

  /* retourne le nombre d'arguments lus */
  return retour;
}
De cette manière, l'utilisateur peut lui aussi définir des fonctions d'affichage ajoutant par exemple un en-tête au message. Par exemple, si nous voulions définir une fonction appelée en cas d'erreur et utilisant une chaîne de format identique aux fonctions afficher et vafficher, nous écrivons :
/* prototype de la fonction abort */
#include <stdlib.h>

void erreur (char * fichier,
             int    ligne,
             int    fatale,
             char * format, ...) {
 /* Curseur de lecture */
  va_list curseur;

  /* Contrôle du format */
  if (! format) return 0;

  /* Initialisation du curseur */
  va_start (curseur, format);

 /* Affichage */
  afficher ("Erreur~s, fichier ~s, ligne ~d: ",
            fatale ? " fatale" : "",
            fichier,
            ligne);
  vafficher (format, curseur);
  afficher ("~n");

  /* Libération du curseur */
  va_end (curseur);

  if (fatale) abort();
}
Cette fonction d'erreur prend en argument le fichier et la ligne dans le code où l'erreur s'est produite, suivit de l'argument fatale indiquant si le programme doit être interrompu, suivit d'une chaîne de format et enfin des arguments supplémentaires. Le reste de la fonction est lisible. Si l'erreur est fatale, nous appelons la fonction standard abort qui interrompt le programme en causant une exception.
Nous utiliserons le meme type de fonction d'erreur dans un prochain article, lorsque nous présenterons une implémentation de la macro assert().
Fonction standard d'entrée / sortie
Les fonctions de sortie de haut niveau (famille « printf ») utilisent un nombre d'arguments variable. L'utilisateur pourra consulter les pages de manuel de son système des fonctions :
·	printf (char * format, ...) : Affichage sur la sortie standard.
·	vprintf (char * format, va_list ap) : Affichage sur la sortie standard, avec curseur.
·	sprintf (char * buffer, char * format, ...) : « Affichage» dans une chaîne de caractères, sans contrôle de taille.
·	vsprintf (char * buffer, char * format, va_list ap) : « Affichage » dans une chaîne de caractères, sans contrôle de taille, avec curseur.
·	snprintf (char * buffer, int taille, char * format, ...) : « Affichage » dans une chaîne de caractères, avec contrôle de taille.
·	vsnprintf (char * buffer, int taille, char * format, va_list ap) : « Affichage » dans une chaîne de caractères, avec contrôle de taille et curseur.
·	fprintf (FILE * fichier, char * format, ...) : Affichage dans un fichier.
·	vfprintf (FILE * fichier, char * format, va_list ap) : Affichage dans un fichier, avec curseur.
Les fonctions de la famille « sprintf » écrivent leur résultat dans un tampon. Elles ne contrôlent pas la taille du tampon, ce qui peut compromettre le programme si trop de caractères y sont écrits. On préférera donc utiliser les fonctions de la famille « snprintf » ou la taille maximale du tampon réceptacle dont être donnée. Le nombre de caractères écrits dans le tampon dépassera pas cette taille limite.
Nous rappelons que l'essentiel de la documentation des fonctions standard du C sont accessibles depuis l'éditeur de texte emacs, en tapant ESC-X info ENTER, puis en cherchant le mot libc avec CTRL+S libc. Aller au début du mot, et taper ENTER. Pour remonter, taper CTRL+U. Pour quitter emacs taper CTRL+X CTRL+C.
L'auteur
Guilhem de Wailly, directeur de la société Erian Concept, partenaire NetUltra  et Getek.
	Votre interlocuteur Linux !
Support, formations, configurations, administration, développements Linux.
 http://www.erian-concept.com	
Références
Langage C - norme ANSI 
Ph. DRIX
Masson
Programmer en C++ 
S.C. DEWHURST et K.T. STARK
Masson
Le Langage C
B.W. KERNIGHAN et D.M. RITCHIE 
Masson
Kdevel
http://samuel.cs.uni-potsdam.de/~smeier/kdevelop_new/index.html