10 Fonctions
Définition d'une fonction :
type nom(liste des paramètres) { corps }
-
·
type est le type du résultat renvoyé par la fonction (on utilise
void lorsqu'il s'agit d'une procédure, i.e. lorsqu'il n'y a pas de
valeur de retour).
- ·
La liste des paramètres (appelés paramètres formels) est de la
forme: type1 p1, ... ,typen pn, signifiant que le premier
paramètre s'appelle p1 et est de type type1 etc.
- · Le corps de la fonction décrit les instructions à
effectuer lors de l'appel de cette fonction. Le corps utilise ses propres
variables locales (à définir), les éventuelles variables globales et les
paramètres formels (considérés comme des variables locales).
- ·
Lorsqu'une fonction renvoie un résultat, il doit y avoir (au moins) une
instruction ``return expr;'' où expr est une expression du type de
la fonction. Cette instruction met fin à l'exécution de la fonction et
retourne expr comme résultat. Dans une procédure, il est possible (non
obligatoire) d'utiliser l'instruction return ; (ici sans argument)
pour mettre fin à l'exécution de la procédure.
Exemple :
//=========================
int max(int a,int b)
{ int res=b;
if (a>b) res = a;
return res;
}
//=========================
Appel d'une fonction : nom(liste des arguments)
La liste des arguments (aussi appelés paramètres réels) est de la
forme: expr1, expr2, ...exprn où chaque expri est
compatible avec le type typei du paramètre formel pi.
Exemple :
//=========================
int k=34, t=5, m;
m = max(k,2*t+5);
//=========================
Il est possible de définir des fonctions récursives :
//=========================
int fact(int n)
{ if (n <= 1)
return 1;
else return n*fact(n-1); }
//=========================
Un appel de fonction doit toujours être précédé par la définition
ou une déclaration de la fonction. Une déclaration consiste à donner: son
type, son nom et le type des paramètres (le nom des paramètres n'est pas
nécessaire) suivi d'un point virgule :
Déclaration d'une fonction :
type nom(liste des types des paramètres);
Par exemple : ``int max(int ,int );''.
Une déclaration permet au compilateur de vérifier que les différents appels
sont corrects du point de vue du type des arguments et du type du résultat.
A noter qu'une fonction doit être définie une et une seule fois mais peut
être déclarée autant de fois qu'on le désire.
De plus, il est possible d'utiliser le même nom pour plusieurs fonctions si
la liste des paramètres est différente (nombre ou type
différent). Concernant le passage des paramètres, deux possibilités
existent: le passage par valeur (standard) et le passage par
référence.
10.1 Passage des paramètres par valeur.
C'est le cas standard: les paramètres sont initialisés par une copie des
valeurs des paramètres réels. Modifier la valeur des paramètres formels
dans le corps de la fonction ne change pas la valeur des paramètres
réels. Par exemple, l'exécution du bout de programme à gauche avec la
définition de proc à droite :
8cm
//=========================
int k=10;
proc(k);
cout<<"la valeur de k est "<<k;
//=========================
6cm
//=========================
void proc(int a)
{
a++;
cout<<"la valeur de a est "<<a;
cout << endl;
}
//=========================
entraînera l'affichage de ``la valeur de a est 11'' suivi d'un
retour à la ligne puis de ``la valeur de k est 10''.
10.2 Passage des paramètres par référence.
Il est parfois nécessaire d'autoriser une fonction à modifier la valeur
d'un paramètre réel. Pour cela, il est possible d'utiliser des
références.
Cette notion de référence n'est pas limitée au passage de paramètres. De
manière générale, une référence sur une variable est un synonyme de cette
variable, c'est-à-dire une autre manière de désigner le même emplacement de
la mémoire. On utilise le symbole & pour la déclaration d'une
référence. Dans l'exemple ci-dessous :
int i = 4;
int & j = i;
nous avons déclaré j comme étant un synonyme de i:
modifier j, c'est modifier i. La principale utilisation
des références concernent le passage des paramètres. Dans la liste des
paramètres formels d'une définition de fonction, ``type &
pi'' déclare le paramètre pi comme étant une référence sur le
ieme paramètre réel vi : pi n'est donc plus une variable créée
pour l'exécution de la fonction et initialisée avec vi, mais pi est
un synonyme de vi, modifier pi revient à modifier vi. Par
exemple, l'exécution du bout de programme ci-dessous avec la définition de la
procédure proc à droite :
8cm
//=========================
int k=10;
proc(k);
cout<<"la valeur de k est "<<k;
//=========================
6cm
//=========================
void proc(int & a)
{
a++;
cout<<"la valeur de a est "<<a<<endl;
}
//=========================
entraînera l'affichage de ``la valeur de a est 11'' suivi de ``la valeur de
k est 11''.
Autre exemple :
//=========================
void permut(int & a, int & b)
{
int aux = a;
a = b;
b = aux;
}
//=========================
La fonction permut permet d'échanger la valeur des deux variables
passées en argument. Il faut noter que le passage par référence demande de
passer des variables (au sens large) en paramètres réels : on en peut pas
appeler permut(i,3). On peut noter qu'il existe une fonction
swap qui permute la valeur de deux arguments (de type quelconque)
dans la bibliothèque algorithm (à utiliser avec
#include <algorithm>
).
Remarque 3 *
En général, le passage par référence est plus efficace du point de vue de
la gestion de la mémoire dans la mesure où il évite la recopie des
arguments 4 (car seule
l'adresse mémoire de l'argument est communiquée à la fonction) mais il est
dangereux car des fonctions peuvent modifier les données. Il est donc
quelquefois intéressant de passer des paramètres en tant que référence sur
une constante (notation const type & p) : cela permet
d'éviter le coût de la recopie des arguments et reste sûr car le
compilateur vérifiera que le paramètre p n'est pas modifié dans le corps
de la fonction. Cette technique est très largement utilisée et peut être
vue comme une troisième manière de passer des arguments.
Pour finir, on peut noter qu'il est aussi possible pour une fonction de
renvoyer comme résultat une référence.
10.3 Valeurs par défaut *
On peut attribuer des valeurs par défaut aux paramètres d'une
fonction. Par exemple :
//=========================
int max(int a,int b =0)
{
return (a>b) ? a : b;
}
//=========================
On peut alors appeler max(i) au lieu de max(i,0). Il
faut noter la restriction suivante : si un paramètre a une valeur par
défaut, tous les paramètres suivants en ont une aussi.
10.4 Fonctions en ligne *
Pour de petites fonctions, il peut être plus efficace (rapide) de se
dispenser d'un appel de fonction et d'expanser le corps de la fonction en
lieu et place de chaque appel. C'est possible avec le mot clé
inline. Par exemple, on pourra définir max de la sorte :
//=========================
inline int max(int a,int b =0)
{
return (a>b) ? a : b;
}
//=========================
Attention : ce choix n'est intéressant que pour de petites
fonctions... A utiliser avec parcimonie !
10.5 Structure générale d'un programme C++
Un programme C++ est réparti dans un ou plusieurs fichiers, chacun
contenant des définitions/déclarations de fonctions, des définitions de
types (abordés plus loin) et des définitions de variables globales. Il
existe une (seule) fonction main, elle correspond à la fonction
qui sera exécutée après la compilation. Le profil de main est :
int main(). Il est néanmoins possible de définir des paramètres à
la fonction main afin d'exécuter le programme avec des arguments
(voir un ouvrage de référence pour la syntaxe associée). Le programme
ci-dessous est un programme complet avec deux fonctions :
//=========================
#include <iostream>
void test(int j)
{ cout << j << endl; }
int main()
{
int i =20;
cout << "bonjour" << endl;
test(i);
}
//=========================
Remarque 4 *
Un programme important utilise de nombreuses bibliothèques, fonctions
etc. Pour éviter les problèmes de conflit de noms, on utilise des
espaces de noms
(namespace
) : on associe un nom à un ensemble
de variables, types, fonctions. Pour désigner ces différents objets, il
faut alors utiliser leur nom précédé par leur nom d'espace suivi de
::
. Par exemple, la fonction int fct(){...}
définie dans le
domaine A
aura pour nom complet A::fct()
.
Pour les entrées-sorties avec cout
et cin
, il faut en fait
utiliser std::cout
, std::cin
et std::endl
(i.e. les objets
cout
, cin
et endl
de la bibliothèque standard) pour
être parfaitement correct (le compilateur tolère encore la notation simple
mais...). C'est aussi le cas pour std::string
et std:vector
.
Comme il est pénible de mentionner systématiquement le nom d'un espace, on
peut utiliser la commande using
pour préciser que les objets
non-explicitement définis dans le code viennent de certains espaces de
noms. Par exemple, tous nos programmes devraient (et doivent à partir de
maintenant) comporter l'instruction suivante
``using namespace std;
''.