precedent retour  sommaire du cours C++ suivant

Le Langage C++

cours n° 4

Patrick TRAU 25/10/04

Cours n° 4

9) Fonctions

Quand un programme devient important, il devient également compliqué à gérer, du fait de son grand nombre de lignes et surtout des imbrications multiples des structures de contrôle. C'est pourquoi il est nécessaire de structurer le programme. Le plus efficace est de le découper en différentes parties, chacune effectuant une tâche particulière. Ce sont les différents sous-programmes ou procédures, que l'on appelle « fonctions » en C. Il va donc falloir, avant d'écrire un programme, essayer de le décomposer en sous-tâches, avec pour chacune une définition des données à lui fournir, les traitements à y appliquer, les données résultantes attendues. Prenons pour exemple un technicien sur machine outil. On peut détailler précisément les différentes tâches qu'il peut effectuer (ce sont les fonctions).  Et à un instant donné, on lui donne la liste des tâches à effectuer dans la journée (c'est la fonction main).

9.1) Définition générale

Une fonction est définie par sont entête, suivie d'un bloc d'instructions (voir schéma au cours 1). L'entête est de la forme :

type_retourné nom_fonction(liste_arguments_formels) (surtout pas de ;)

9.2) procédure (fonction ne retournant rien)

Si la fonction ne retourne rien, le type retourné est void. De même si la fonction n'utilise pas d'arguments il faut indiquer void à la place de la liste d'arguments.

regardons cet exemple :

void dire_trois_fois_bonjour(void)
{
 int i;
 for(i=0;i<3;i++) cout<<"bonjour\n";
}

Cette fonction effectue une tâche (dire trois fois bonjour). Pour cela, elle n'a besoin d'aucune information spécifique (on dit qu'elle ne reçoit pas d'arguments), et elle n'a rien non plus à retourner. C'est la raison des deux « void » dans l'entête. Toutes les variables déclarées dans le bloc sont locales (ici, c'est "i"). Ceci signifie qu'elles existent dans la fonction, et uniquement pendant qu'on traite cette fonction. A la sortie (dernière }), la mémoire sera libérée. En clair, la variable i de notre fonction n'est pas la même que celles qui pourraient s'appeler aussi i mais qui seraient déclarées dans d'autres blocs.

L'écriture du texte ci-dessus ne fait que définir la fonction. C'est à dire qu'on a appris au compilateur comment on disait trois fois bonjour, mais on ne lui a pas encore dit quand le faire. Ceci se fait dans le déroulement séquentiel des instructions d'une fonction, par l'appel de la fonction qui se note :

nom_fonction(liste_arguments_réels);

ici : dire_trois_fois_bonjour(); que l'on pourrait par exemple placer dans la fonction « main ».

Pour appeler une fonction, il faut qu'elle ait été définie auparavant, soit parce qu'elle a été écrite avant la procédure qui l'appelle, soit parce qu'on l'a « prototypée », ce qui se fait en indiquant son entête, suivie d'un ; (j'en reparlerai un peu plus loin).

Prenons l'exemple de notre technicien. La tâche "nettoyer la machine" ne nécessite aucune matière première spécifique, c'est pourquoi il n'y a pas de transmission d'arguments. Par contre il a besoin pour cette tâche d'utiliser divers outils, qu'il va trouver sur son poste spécifiquement pour cela (une soufflette par exemple), ce sont les variables locales.

Voyons désormais une fonction recevant des arguments. Par exemple :

void dire_n_fois_bonjour(int N)
{
 int i;
 for(i=0;i<N;i++) cout<<"bonjour\n";
}

Cette fonction a besoin d'une information : le nombre de bonjours désirés (c'est un entier). On l'appelle N dans la fonction, on l'appelle argument formel. Au niveau de l'appel, il faudra donner un entier (l'argument réel) qui peut être une variable (qui n'a pas besoin d'avoir le même nom), mais aussi une constante ou toute expression donnant un entier :

dire_n_fois_bonjour(10);
int j=5;
dire_n_fois_bonjour(j);
dire_n_fois_bonjour(j*2-8);

On peut également transmettre plus d'un argument à une fonction. Mais il faudra qu'à l'appel, on fournisse le même nombre d'arguments réels, dans le même ordre.

Pour notre technicien, la tâche "vidanger la machine" nécessite qu'on fournisse de la matière première : un bidon d'huile. C'est l'argument fourni à la tâche.

9.3) fonction retournant une valeur

Passons maintenant aux fonctions retournant une valeur. Voyons cet exemple :

float produit(float a;float b)
 {
  float z;
  z=a*b;
  return(z);
 }

Cette fonction nécessite qu'on lui fournisse deux flottants. Elle les appelle a et b (mais ils n'ont pas besoin d'avoir le même nom à l'appel). Elle fait un calcul (ici certes trop simple pour justifier l'utilisation d'une fonction), puis quand elle a terminé, elle retourne la valeur de z. Au niveau de l'appel, il faut bien évidemment fournir deux valeurs (arguments réels) à la fonction. Mais surtout il faut utiliser la valeur retournée par la fonction, par une affectation, une opération d'entrée-sortie, ou même en argument d'une autre fonction :

x=produit(z,3*y);
cout<<produit(x,produit(3,2));
for(i=0;sin(z)<produit(x,i);i++);

Pour notre technicien, la tâche "assemblage de la pièce" nécessite des arguments : les différents composants nécessaires. Peut-être aura-t-il besoin également de variables locales (outils spécifiques). Mais à la fin de la tâche nous exigeons qu'il nous donne (en C on dit retourne) la pièce assemblée. Attention, une fonction peut soit ne rien retourner (void), soit retourner une seule valeur. Pour en retourner plusieurs il faut utiliser divers artifices, comme regrouper toutes les valeurs dans un seul "objet". Ou alors on utilise le passage d'arguments par adresse.

9.4) passage d'arguments (par valeur, par référence,...)

Pour l'instant , nous avons utilisé un passage d'arguments par valeur (comportement par défaut en C).

Imaginons la fonction :

void échange(int i;int j)
 {
  int tampon;
  tampon=i;
  i=j;
  j=tampon;
 }

Lors d'un appel à cette fonction par échange(x,y), les variables locales i,j,tampon sont créées localement. i vaut la valeur de x, j celle de y. Les contenus de i et j sont échangés puis la pile (endroit qui contient toutes les variables locales) est libérée, sans modifier x et y. Pour résoudre ce problème, il faut utiliser un passage d'arguments par référence. On définira la fonction ainsi :

void échange(int &i;int &j) //le reste de la fonction (le bloc) est inchangé

On appelle la fonction par échange(x,y); Les deux arguments formels de la fonction (i et j) sont des références, c'est à dire qu'au lieu d'avoir transmis à la fonction la valeur d'x et y (combien ils valent), on lui a transmis leur adresse (où ils sont). La fonction peut maintenant les modifier, puisqu'elle sait où sont stockés en mémoire les arguments formels.

On utilise le passage d'arguments par référence quand on désire qu'un argument  réel puisse être modifié par la fonction. Mais on peut aussi les utiliser lorsque l'on désirerait retourner plusieurs valeurs : il suffit de donner à la fonction les adresses de différentes variables dans lesquelles la fonction écrira les résultats qu'elle aimerait nous retourner.

On s'en sert également si l'on désire éviter une copie d'un objet de grande taille de l'argument réel vers l'argument formel, même si on ne désire pas modifier l'argument. Pour cela, on peut ajouter le mot clef "const" devant le type, dans l'entête, pour plus de sécurité.

Comme en C ANSI le passage par référence n'était pas possible, on utilisait un passage par adresse (pointeurs). Cela reste possible, mais peut-être plus complexe. Je n'en dirai donc pas plus ici.

9.5) appel de fonctions, prototypes

Les arguments (formels) sont des variables locales à la fonction. Les valeurs fournies à l'appel de la fonction (arguments réels) y sont recopiés à l'entrée dans la fonction. Les instructions de la fonction s'exécutent du début du bloc ({) jusqu'à return(valeur) ou la sortie du bloc (}). La valeur retournée par la fonction est indiquée en argument de return. Toutes les variables locales (arguments formels et variables déclarées dans le bloc) sont libérées au retour de la fonction.

Si l'on désire appeler une fonction, il faut l'avoir définie avant. Soit on les définit dans le bon ordre (donc en particulier on définit "main" en dernier), soit on utilise un prototype : c'est une déclaration (en général globale, souvent placée avant la première définition de fonction). On y indique les mêmes informations que dans l'entête : le type retourné, le nom de la fonction, et le type des arguments. Seul le nom des arguments peut être omis. Un prototype est toujours suivi d'un « ; ».

Le prototype permet aussi de définir des valeurs d'arguments par défaut. Les arguments réels peuvent être omis en commençant par le dernier (impossible d'omettre les premiers si l'on veut préciser un suivant).

void maFonction(int argument1=18,int argument2=3);
maFonction(1,2); //c'est moi qui fixe tout
maFonction(10);  //le deuxième argument vaudra 3
maFonction();    //les deux sont fixés par défaut

9.6) Exemple

Nous pouvons maintenant écrire tout un programme, décomposé en fonctions. Je vous rappelle que la fonction main constitue le point d'entrée du programme, c'est à dire la fonction appelée en premier. Dans cet exemple, j'ai également placé une variable globale (bien que ce soit une très mauvaise pratique).

#include <iostream.h>
void affiche_calcul(float,float); /* prototype */
float produit(float,float);
void message(void);
int varglob; //ceci est une variable globale accessible partout et tout le temps
int main(void)
 {
  float a,b; /* déclaration locale */
  message();
  varglob=0;
  cout<<("veuillez entrer 2 valeurs : ");
  cin>>a;
  cin>>b;
  affiche_calcul(a,b);
  cout<<"nombre d'appels à produit : "<<<"\n";
 }
float produit(float r, float s)
 {
  varglob++;
  return(r*s);
 }
void affiche_calcul(float x,float y)
 {
  float varloc;
  varloc=produit(x,y);
  varloc=produit(varloc,varloc);
  cout<<"le carré du produit est "<<<"\n";
 }
void message(void)
 {
  const char ligne[]="*******************************\n";
  cout <<<"bonjour\n"
 } 

9.7) Récursivité, gestion de la pile

Nous n'avons pas traité la récursivité en cours. Bien que ce soit une notion tres importante, il faut déjà une certaine expérience pour la comprendre, ce qui n'est pas encore le cas à ce niveau du cours (peut-être après quelques TP ?)

Une fonction peut s'appeler elle-même :

int factorielle(int i)
 {
  if (i>1) return(i*factorielle(i-1));
  else return(1);
 }
analysons l'état la pile lors d'un appel à factorielle(3) :

evolution de la pile

Attention, la récursivité est gourmande en temps et mémoire, il ne faut l'utiliser que si l'on ne sait pas facilement faire autrement :

int factorielle(int i)
 {
  int result;
  for(result=1;i>1;i--) result*=i;
  return(result);
 }

suivant retour  sommaire du cours C++ precedent Patrick TRAU, ULP - IPST octobre 04