1.6. Les fonctions

Le C++ ne permet de faire que des fonctions, pas de procédures. Une procédure peut être faite en utilisant une fonction ne renvoyant pas de valeur ou en ignorant la valeur retournée.

1.6.1. Définition des fonctions

La définition des fonctions se fait comme suit :

type identificateur(paramètres)
{
    ...   /* Instructions de la fonction. */
}
type est le type de la valeur renvoyée, identificateur est le nom de la fonction, et paramètres est une liste de paramètres. La syntaxe de la liste de paramètres est la suivante :
type variable [= valeur] [, type variable [= valeur] [...]]
type est le type du paramètre variable qui le suit et valeur sa valeur par défaut. La valeur par défaut d'un paramètre est la valeur que ce paramètre prend si aucune valeur ne lui est attribuée lors de l'appel de la fonction.

Note : L'initialisation des paramètres de fonctions n'est possible qu'en C++, le C n'accepte pas cette syntaxe.

La valeur de la fonction à renvoyer est spécifiée en utilisant la commande return, dont la syntaxe est :

return valeur;

Exemple 1-13. Définition de fonction

int somme(int i, int j)
{
    return i+j;
}

Si une fonction ne renvoie pas de valeur, on lui donnera le type void. Si elle n'attend pas de paramètres, sa liste de paramètres sera void ou n'existera pas. Il n'est pas nécessaire de mettre une instruction return à la fin d'une fonction qui ne renvoie pas de valeur.

Exemple 1-14. Définition de procédure

void rien()     /* Fonction n'attendant pas de paramètres */
{               /* et ne renvoyant pas de valeur. */
    return;     /* Cette ligne est facultative. */
}

1.6.2. Appel des fonctions

L'appel d'une fonction se fait en donnant son nom, puis les valeurs de ses paramètres entre parenthèses. Attention ! S'il n'y a pas de paramètres, il faut quand même mettre les parenthèses, sinon la fonction n'est pas appelée.

Exemple 1-15. Appel de fonction

int i=somme(2,3);
rien();

Si la déclaration comprend des valeurs par défaut pour des paramètres (C++ seulement), ces valeurs sont utilisées lorsque ces paramètres ne sont pas fournis lors de l'appel. Si un paramètre est manquant, alors tous les paramètres qui le suivent doivent eux aussi être omis. Il en résulte que seuls les derniers paramètres d'une fonction peuvent avoir des valeurs par défaut. Par exemple :

int test(int i = 0, int j = 2)
{
    return i/j;
}

L'appel de la fonction test(8) est valide. Comme on ne précise pas le dernier paramètre, j est initialisé à 2. Le résultat obtenu est donc 4. De même, l'appel test() est valide : dans ce cas i vaut 0 et j vaut 2. En revanche, il est impossible d'appeler la fonction test en ne précisant que la valeur de j. Enfin, l'expression « int test(int i=0, int j) {...} » serait invalide, car si on ne passait pas deux paramètres, j ne serait pas initialisé.

1.6.3. Déclaration des fonctions

Toute fonction doit être déclarée avant d'être appelée pour la première fois. La définition d'une fonction peut faire office de déclaration.

Il peut se trouver des situations où une fonction doit être appelée dans une autre fonction définie avant elle. Comme cette fonction n'est pas définie au moment de l'appel, elle doit être déclarée. De même, il est courant d'avoir à appeler une fonction définie dans un autre fichier que le fichier d'où se fait l'appel. Encore une fois, il est nécessaire de déclarer ces fonctions.

Le rôle des déclarations est donc de signaler l'existence des fonctions aux compilateurs afin de les utiliser, tout en reportant leur définition plus loin ou dans un autre fichier.

La syntaxe de la déclaration d'une fonction est la suivante :

type identificateur(paramètres);
type est le type de la valeur renvoyée par la fonction, identificateur est son nom et paramètres la liste des types des paramètres que la fonction admet, éventuellement avec leurs valeurs par défaut, et séparés par des virgules.

Exemple 1-16. Déclaration de fonction

int Min(int, int);        /* Déclaration de la fonction minimum */
                          /* définie plus loin. */
/* Fonction principale. */
int main(void)
{
    int i = Min(2,3);     /* Appel à la fonction Min, déjà
                             déclarée. */
    return 0;
}

/* Définition de la fonction min. */
int Min(int i, int j)
{
    if (i<j) return i;
    else return j;
}

Si l'on donne des valeurs par défaut différentes aux paramètres d'une fonction dans plusieurs déclarations différentes, les valeurs par défaut utilisées sont celles de la déclaration visible lors de l'appel de la fonction. Si plusieurs déclarations sont visibles et entrent en conflit au niveau des valeurs par défaut des paramètres de la fonction, le compilateur ne saura pas quelle déclaration utiliser et signalera une erreur à la compilation. Enfin, il est possible de compléter la liste des valeurs par défaut de la déclaration d'une fonction dans sa définition. Dans ce cas, les valeurs par défaut spécifiées dans la définition ne doivent pas entrer en conflit avec celles spécifiées dans la déclaration visible au moment de la définition, faute de quoi le compilateur signalera une erreur.

1.6.4. Surcharge des fonctions

Il est interdit en C de définir plusieurs fonctions qui portent le même nom. En C++, cette interdiction est levée, moyennant quelques précautions. Le compilateur peut différencier deux fonctions en regardant le type des paramètres qu'elle reçoit. La liste de ces types s'appelle la signature de la fonction. En revanche, le type du résultat de la fonction ne permet pas de l'identifier, car le résultat peut ne pas être utilisé ou peut être converti en une valeur d'un autre type avant d'être utilisé après l'appel de cette fonction.

Il est donc possible de faire des fonctions de même nom (on les appelle alors des surcharges) si et seulement si toutes les fonctions portant ce nom peuvent être distinguées par leurs signatures. La surcharge qui sera appelée sera celle dont la signature est la plus proche des valeurs passées en paramètre lors de l'appel.

Exemple 1-17. Surcharge de fonctions

float test(int i, int j)
{
    return (float) i+j;
}

float test(float i, float j)
{
    return i*j;
}

Ces deux fonctions portent le même nom, et le compilateur les acceptera toutes les deux. Lors de l'appel de test(2,3), ce sera la première qui sera appelée, car 2 et 3 sont des entiers. Lors de l'appel de test(2.5,3.2), ce sera la deuxième, parce que 2.5 et 3.2 sont réels. Attention ! Dans un appel tel que test(2.5,3), le flottant 2.5 sera converti en entier et la première fonction sera appelée. Il convient donc de faire très attention aux mécanismes de surcharge du langage, et de vérifier les règles de priorité utilisées par le compilateur.

On veillera à ne pas utiliser des fonctions surchargées dont les paramètres ont des valeurs par défaut, car le compilateur ne pourrait pas faire la distinction entre ces fonctions. D'une manière générale, le compilateur dispose d'un ensemble de règles (dont la présentation dépasse le cadre de ce livre) qui lui permettent de déterminer la meilleure fonction à appeler étant donné un jeu de paramètres. Si, lors de la recherche de la fonction à utiliser, le compilateur trouve des ambiguïtés, il génére une erreur.

1.6.5. Fonctions inline

Le C++ dispose du mot clé inline, qui permet de modifier la méthode d'implémentation des fonctions. Placé devant la déclaration d'une fonction, il propose au compilateur de ne pas instancier cette fonction. Cela signifie que l'on désire que le compilateur remplace l'appel de la fonction par le code correspondant. Si la fonction est grosse ou si elle est appelée souvent, le programme devient plus gros, puisque la fonction est réécrite à chaque fois qu'elle est appelée. En revanche, il devient nettement plus rapide, puisque les mécanismes d'appel de fonctions, de passage des paramètres et de la valeur de retour sont ainsi évités. De plus, le compilateur peut effectuer des optimisations additionnelles qu'il n'aurait pas pu faire si la fonction n'était pas inlinée. En pratique, on réservera cette technique pour les petites fonctions appelées dans du code devant être rapide (à l'intérieur des boucles par exemple), ou pour les fonctions permettant de lire des valeurs dans des variables.

Cependant, il faut se méfier. Le mot clé inline est un indice indiquant au compilateur de faire des fonctions inline. Il n'y est pas obligé. La fonction peut donc très bien être implémentée classiquement. Pire, elle peut être implémentée des deux manières, selon les mécanismes d'optimisation du compilateur. De même, le compilateur peut également inliner les fonctions normales afin d'optimiser les performances du programme.

De plus, il faut connaître les restrictions des fonctions inline :

Si l'une de ces deux conditions n'est pas vérifiée pour une fonction, le compilateur l'implémentera classiquement (elle ne sera donc pas inline).

Enfin, du fait que les fonctions inline sont insérées telles quelles aux endroits où elles sont appelées, il est nécessaire qu'elles soient complètement définies avant leur appel. Cela signifie que, contrairement aux fonctions classiques, il n'est pas possible de se contenter de les déclarer pour les appeler, et de fournir leur définition dans un fichier séparé. Dans ce cas en effet, le compilateur générerait des références externes sur ces fonctions, et n'insérerait pas leur code. Ces références ne seraient pas résolues à l'édition de lien, car il ne génère également pas les fonctions inline, puisqu'elles sont supposées être insérées sur place lorsqu'on les utilise. Les notions de compilation dans des fichiers séparés et d'édition de liens seront présentées en détail dans le Chapitre 6.

Exemple 1-18. Fonction inline

inline int Max(int i, int j)
{
    if (i>j)
        return i;
    else
        return j;
}

Pour ce type de fonction, il est tout à fait justifié d'utiliser le mot clé inline.

1.6.6. Fonctions statiques

Par défaut, lorsqu'une fonction est définie dans un fichier C/C++, elle peut être utilisée dans tout autre fichier pourvu qu'elle soit déclarée avant son utilisation. Dans ce cas, la fonction est dite externe. Il peut cependant être intéressant de définir des fonctions locales à un fichier, soit afin de résoudre des conflits de noms (entre deux fonctions de même nom et de même signature mais dans deux fichiers différents), soit parce que la fonction est uniquement d'intérêt local. Le C et le C++ fournissent donc le mot clé static qui, une fois placé devant la définition et les éventuelles déclarations d'une fonction, la rend unique et utilisable uniquement dans ce fichier. À part ce détail, les fonctions statiques s'utilisent exactement comme des fonctions classiques.

Exemple 1-19. Fonction statique

// Déclaration de fonction statique :
static int locale1(void);

/* Définition de fonction statique : */
static int locale2(int i, float j)
{
    return i*i+j;
}

Les techniques permettant de découper un programme en plusieurs fichiers sources et de générer les fichiers binaires à partir de ces fichiers seront décrites dans le chapitre traitant de la modularité des programmes.

1.6.7. Fonctions prenant un nombre variable de paramètres

En général, les fonctions ont un nombre constant de paramètres. Pour les fonctions qui ont des paramètres par défaut en C++, le nombre de paramètres peut apparaître variable à l'appel de la fonction, mais en réalité, la fonction utilise toujours le même nombre de paramètres.

Le C et le C++ disposent toutefois d'un mécanisme qui permet au programmeur de réaliser des fonctions dont le nombre et le type des paramètres sont variables. Nous verrons plus loin que les fonctions d'entrée / sortie du C sont des fonctions dont la liste des arguments n'est pas fixée, cela afin de pouvoir réaliser un nombre arbitraire d'entrées / sorties, et ce sur n'importe quel type prédéfini.

En général, les fonctions dont la liste des paramètres est arbitrairement longue disposent d'un critère pour savoir quel est le dernier paramètre. Ce critère peut être le nombre de paramètres, qui peut être fourni en premier paramètre à la fonction, ou une valeur de paramètre particulière qui détermine la fin de la liste par exemple. On peut aussi définir les paramètres qui suivent le premier paramètre à l'aide d'une chaîne de caractères.

Pour indiquer au compilateur qu'une fonction peut accepter une liste de paramètres variable, il faut simplement utiliser des points de suspensions dans la liste des paramètres :

type identificateur(paramètres, ...)
dans les déclarations et la définition de la fonction. Dans tous les cas, il est nécessaire que la fonction ait au moins un paramètre classique. Les paramètres classiques doivent impérativement être avant les points de suspensions.

La difficulté apparaît en fait dans la manière de récupérer les paramètres de la liste de paramètres dans la définition de la fonction. Les mécanismes de passage des paramètres étant très dépendants de la machine (et du compilateur), un jeu de macros a été défini dans le fichier d'en-tête stdarg.h pour faciliter l'accès aux paramètres de la liste. Pour en savoir plus sur les macros et les fichiers d'en-tête, consulter le Chapitre 5. Pour l'instant, sachez seulement qu'il faut ajouter la ligne suivante :

#include <stdarg.h>

au début de votre programme. Cela permet d'utiliser le type va_list et les expressions va_start, va_arg et va_end pour récupérer les arguments de la liste de paramètres variable, un à un.

Le principe est simple. Dans la fonction, vous devez déclarer une variable de type va_list. Puis, vous devez initialiser cette variable avec la syntaxe suivante :

va_start(variable, paramètre);
variable est le nom de la variable de type va_list que vous venez de créer, et paramètre est le dernier paramètre classique de la fonction. Dès que variable est initialisée, vous pouvez récupérer un à un les paramètres à l'aide de l'expression suivante :
va_arg(variable, type)
qui renvoie le paramètre en cours avec le type type et met à jour variable pour passer au paramètre suivant. Vous pouvez utiliser cette expression autant de fois que vous le désirez, elle retourne à chaque fois un nouveau paramètre. Lorsque le nombre de paramètres correct a été récupéré, vous devez détruire la variable variable à l'aide de la syntaxe suivante :
va_end(variable);

Il est possible de recommencer ces étapes autant de fois que l'on veut, la seule chose qui compte est de bien faire l'initialisation avec va_start et de bien terminer la procédure avec va_end à chaque fois.

Note : Il existe une restriction sur les types des paramètres des listes variables d'arguments. Lors de l'appel des fonctions, un certain nombre de traitements a lieu sur les paramètres. En particulier, des promotions implicites ont lieu, ce qui se traduit par le fait que les paramètres réellement passés aux fonctions ne sont pas du type déclaré. Le compilateur continue de faire les vérifications de type, mais en interne, un type plus grand peut être utilisé pour passer les valeurs des paramètres. En particulier, les types char et short ne sont pas utilisés : les paramètres sont toujours promus aux type int ou long int. Cela implique que les seuls types que vous pouvez utiliser sont les types cibles des promotions et les types qui ne sont pas sujets aux promotions (pointeurs, structures et unions). Les types cibles dans les promotions sont déterminés comme suit :

  • les types char, signed char, unsigned char, short int ou unsigned short int sont promus en int si ce type est capable d'accepter toutes leurs valeurs. Si int est insuffisant, unsigned int est utilisé ;

  • les types des énumérations (voir plus loin pour la définition des énumérations) et wchar_t sont promus en int, unsigned int, long ou unsigned long selon leurs capacités. Le premier type capable de conserver la plage de valeur du type à promouvoir est utilisé ;

  • les valeurs des champs de bits sont converties en int ou unsigned int selon la taille du champ de bit (voir plus loin pour la définition des champs de bits) ;

  • les valeurs de type float sont converties en double.

Exemple 1-20. Fonction à nombre de paramètres variable

#include <stdarg.h>

/* Fonction effectuant la somme de "compte" paramètres : */
double somme(int compte, ...)
{
    double resultat=0;      /* Variable stockant la somme. */
    va_list varg;           /* Variable identifiant le prochain
                               paramètre. */
    va_start(varg, compte); /* Initialisation de la liste. */
    while (compte!=0)       /* Parcours de la liste. */
    {
        resultat=resultat+va_arg(varg, double);
        compte=compte-1;
    }
    va_end(varg);           /* Terminaison. */
    return resultat;
}

La fonction somme effectue la somme de compte flottants (float ou double) et la renvoie dans un double. Pour plus de détails sur la structure de contrôle while, voir Section 2.3.