Fonctions et procédures

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

Déclaration standard

type_retour fonction( type1 par1, type2 par2, /* ..., */ typeN parN ) {
    /* Déclarations de variables ... */
    /* Instructions ... */
}

Déclare une fonction renvoyant une valeur de type type_retour et prenant N arguments, par1 de type type1, par2 de type type2, etc.

Une fonction se termine soit lorsque qu'elle atteint l'accolade fermante, soit lorsque qu'elle rencontre le mot clef return. La valeur renvoyée par une fonction est donnée comme paramêtre à return.
Une procédure est une fonction renvoyant void, dans ce cas return est appelé sans paramètre.

Les passages des arguments aux fonctions se font toujours par valeur. Si on veut modifier la valeur d'un argument passé en paramètre à une fonction, en dehors de cette même fonction, il faut utiliser des pointeurs.

À noter que nulle part dans le langage y est spécifié l'ordre de traitement des arguments. Il faut donc faire attention aux opérateurs ayant des effets de bords, notamment lorsqu'on utilise la même variable. Le code suivant est imprévisible et non portable, bien qu'aucun compilateur ne génèrera d'avertissement (et encore moins d'erreur) :

/* Ce code contient une erreur grossière et volontaire */
int fonction( int, int, int );
int a = 0;
int b = fonction( a++, a++, a++ );

Suivant le compilateur employé, tous les cas de figures sont envisageables. La fonction peut effectivement être appelée avec fonction( 0, 0, 0 ); ou fonction( 0, 1, 2 ); ou encore fonction( 2, 1, 0 );. À éviter donc.

Prototype

Le prototype d'une fonction correspond simplement à son entête (tout ce qui précède la première accolade ouvrante). C'est à dire, son nom, son type de retour et les types des différents paramètres. Cela permet au compilateur de vérifier que la fonction est appelée avec le bon nombre de paramètres et surtout avec les bons types :

type_retour nom_fonction( type1, type2, /* ..., */ typeN );

À noter que les noms des paramètres peuvent être omis et que la déclaration doit se terminer par un point-virgule (;), sans quoi vous pourrez vous attendre à une cascade d'erreur.

Un héritage de l'ISO C90, pas toujours facile à assumer, fait que le compilateur peut tout à fait se passer du prototype. Néanmoins, c'est plus qu'une bonne habitude de programmation de s'assurer que chaque fonction utilisée dans un programme ait son prototype déclaré avant. C'est d'autant plus indispensable lorsque les fonctions sont définies et utilisées dans des fichiers différents.

Nombre variable d'arguments

Une fonctionnalité assez utile est d'avoir une fonction avec un nombre variable d'arguments, comme la fameuse fonction printf(). Pour cela, il suffit de déclarer le prototype de la fonction de la manière suivante :

Déclaration

#include <stdarg.h>

void ma_fonction( type1 arg1, type2 arg2, ... )
{
}

Dans l'exemple ci-dessus, les points de suspension ne sont pas un abus d'écriture, mais bel et bien une notation C pour indiquer que la fonction accepte d'autres arguments. L'exemple est limité à deux arguments, mais il est bien sûr possible d'en spécifier autant qu'on veut. C'est dans l'unique but de ne pas rendre ambigu la déclaration, qu'aucun abus d'écriture n'a été employé.

L'inclusion du fichier stdarg.h n'est nécessaire que pour les traiter les arguments à l'intérieur de la fonction. La première remarque que l'on peut faire est qu'une fonction à nombre variable d'arguments contient au moins un paramètre fixe. En effet la déclaration suivante est invalide :

/* Ceci n'est pas valide */
void ma_fonction( ... );

Accès aux arguments

Pour accéder aux arguments situés après le dernier argument fixe, il faut utiliser certaines fonctions (ou plutôt macros) du fichier stdarg.h :

void va_start (va_list ap, last);
type va_arg (va_list ap, type);
void va_end (va_list ap);

va_list est un type opaque dont on n'a pas à se soucier. On commence par l'initialiser avec va_start. Le paramètre last doit correspondre au nom du dernier argument fixe de la fonction, ou alors tout bon compilateur retournera au moins un avertissement.

Vient ensuite la collecte minutieuse des arguments. Il faut bien comprendre qu'à ce stade, le langage n'offre aucun moyen de savoir comment sont structurées les données (c'est à dire leur type). Il faut absolument définir une convention, laissée à l'imagination du programmeur, pour pouvoir extraire les données correctement.

Qui plus est, il faut être extrêmement vigilant lors de la récupération des paramètres, à cause de la promotion des types entiers ou réels. En effet, les entiers sont systématiquement promus en int, sauf si la taille du type est plus grande, auquel cas le type est inchangé. Pour les réels, le type float est promu en double, alors que le type long double est inchangé. C'est pourquoi ce genre d'instruction n'a aucun sens dans une fonction à nombre variable d'arguments :

/* Ce code contient une erreur grossière et volontaire */
char caractere = va_arg( list, char );

Il faut obligatoirement récupérer un entier de type char, comme étant un entier de type int.

Exemple de convention

Un bon exemple de convention est la fonction printf() elle même. Elle utilise un spécificateur de format qui renseigne à la fois le nombre d'arguments qu'on s'attend à trouver mais aussi le type de chacun. D'un autre coté, analyser un spécificateur de format est relativement rébarbatif, et on n'a pas toujours besoin d'une artillerie aussi lourde.

Une autre façon de faire, relativement répandue, est de ne passer que des couples (type, objet), où type correspond à un code représentant un type (une énumération par exemple) et objet le contenu de l'objet lui-même (int, pointeur, double, etc.). On utilise alors un code spécial (généralement 0) pour indiquer la fin des arguments, ou alors un des paramètres pour indiquer combien il y en a. Un petit exemple complet :

#include <stdio.h>
#include <stdarg.h>

enum {
    TYPE_FIN, TYPE_ENTIER, TYPE_REEL, TYPE_CHAINE
};

void affiche( FILE * out, ... )
{
    va_list list;
    int     type;

    va_start( list, out );

    while( (type = va_arg(list, int)) )
    {
       switch( type ) {
       case TYPE_ENTIER: fprintf( out, "%d", va_arg(list, int) ); break;
       case TYPE_REEL:   fprintf( out, "%g", va_arg(list, double) ); break;
       case TYPE_CHAINE: fprintf( out, "%s", va_arg(list, char *) ); break;
       }
    }
    fprintf( out, "\n" );
    va_end( list );
}

int main( int nb, char * argv[] )
{
    affiche( stdout, TYPE_CHAINE, "Le code ascii de 'a' est ", TYPE_ENTIER, 'a', TYPE_FIN );
    affiche( stderr, TYPE_CHAINE, "2 * 3 / 5 = ", TYPE_REEL, 2. * 3 / 5, TYPE_FIN );

    return 0;
}

L'inconvénient de ce genre d'approche est de ne pas oublier le marqueur de fin. Dans les deux cas, il faut être vigilant avec les conversions implicites, notamment dans le second cas. À noter que la conversion (transtypage) explicite des types en une taille inférieure à celle par défaut (int pour les entiers ou double pour les réels) ne permet pas de contourner la promotion implicite. Même écrit de la sorte :

affiche( stderr, TYPE_CHAINE, "2 * 3 / 5 = ", TYPE_REEL, (float) (2. * 3 / 5), TYPE_FIN );

Le résultat transmis au cinquième paramètre sera quand même promu implicitement en type double.

Fonction inline

Il s'agit d'une extension ISO C99, qui à l'origine vient du C++. Ce mot clé doit se placer avant le type de retour de la fonction. Il ne s'agit que d'une indication, le compilateur peut ne pas honorer la demande, notamment si la fonction est récursive. Dans une certaine mesure, les fonctionnalités proposées par ce mot clé sont déjà prises en charge par les instructions du préprocesseur. Beaucoup préfèreront passer par une macro, essentiellement pour des raisons de compatibilité avec d'anciens compilateurs ne supportant pas ce mot clé, et quand bien même l'utilisation de macro est souvent très délicat.

Le mot clé inline permet de s'affranchir des nombreux défauts des macros, et de réellement les utiliser comme une fonction normale, c'est à dire surtout sans effets de bord. À noter qu'il est préférable de classifier les fonctions inline de manière statique. Dans le cas contraire, la fonction sera aussi déclarée comme étant accessible de l'extérieur, et donc définie comme une fonction normale.

En la déclarant static inline, un bon compilateur devrait suprimer toute trace de la fonction et seulement la mettre in extenso aux endroits où elle est utilisée. Ceci permettrait à la limite de déclarer la fonction dans un fichier en-tête, bien qu'il s'agisse d'une pratique assez rare et donc à éviter.

La fonction main

Nous allons revenir ici sur la fonction main, présente dans chaque programme. Cette fonction est le point d'entrée du programme. Elle est exécutée lorsque le programme est appelé, par le système d'exploitation ou par un autre programme. Voici son prototype :

int main( int, char** );

La fonction main retourne une valeur de type entier ; généralement on retourne 0 si le programme s'est déroulé correctement, sinon on retourne un code d'erreur (arbitraire).

Paramètres de ligne de commande

La fonction main prend deux paramètres qui permettent d'accéder aux paramètres passés au programme lors de son appel. Le premier, généralement appelé argc (argument count), est le nombre de paramètres qui ont été passés au programme. Le second, argv (argument vector), est la liste de ces paramètres. Les paramètres sont stockés sous forme de chaîne de caractères, argv est donc un tableau de chaîne de caractères, ou un pointeur sur un pointeur sur char. argc correspond au nombre d'éléments de ce tableau.

Les paramètres passés au programme sont séparés par un ou plusieurs espaces. Le premier élément de argv désigne le nom du programme. Le premier paramètre commence donc à argv[1]. Le dernier paramètre, argv[argc], désigne le pointeur nul.

Exemple

Voici un petit programme très simple qui affiche la liste des paramètres passés au programme lorsqu'il est appelé :

#include <stdio.h>

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

    for( i = 0; i < argc; i++ )
        printf( "paramètre %i : %s\n", i, argv[i] );

    return 0;
}

On effectue une boucle sur argv à l'aide de argc. Enregistrez-le sous le nom params.c puis compilez-le (cc params.c -o params). Vous pouvez ensuite l'appeler ainsi :

./params hello world !                                # sous Unix
params.exe hello world !                              # sous MS-DOS ou Windows

Vous devriez voir en sortie quelque chose comme ceci (paramètre 0 varie selon le système d'exploitation) :

paramètre 0 : ./params
paramètre 1 : hello
paramètre 2 : world
paramètre 3 : !

Ancienne notation

À titre anecdoctique, ceci est la façon historique de déclarer une fonction, avant que le prototypage ne fut officiellement normalisé par l'ISO en 1995. Cette notation est à éviter dans la mesure du possible.

type_retour fonction( par1, par2, ..., parN )
type1 par1;
type2 par2;
...
typeN parN;
{
    /* Déclarations de variables ... */
    /* Instructions ... */
}

Au lieu de déclarer les types à l'intérieur même de la fonction, ils sont simplement décrit après la fonction et avant la première accolade ouvrante. À noter que type_retour pouvait être omis, et dans ce cas valait par défaut int.

On pouvait aussi omettre la moindre déclaration de paramètre et dans ce cas la fonction pouvait être appelé avec n'importe quel type et n'importe quel nombre d'argument, sans que le compilateur n'émette le moindre avertissement. Cette construction étant plutôt dangeureuse, on évitera de s'étaler davantage.

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 15 février 2005 à 05:13. L'article d'origine se trouve à http://fr.wikibooks.org/wiki/Programmat ... ns_et_procédures.