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] [...]]
où
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 :
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);
où
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 :
elles ne peuvent pas être récursives ;
elles ne sont pas instanciées, donc on ne peut pas faire
de pointeur sur une fonction 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 :
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);
où
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. */
do /* Parcours de la liste. */
{
resultat=resultat+va_arg(varg, double);
compte=compte-1;
} while (compte!=0);
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 do ... while, voir Section 2.4.