Chapitre 6
Les fonctions en C

6.0. Notion préliminaire de sous-programme
Sous-programme  :
•	moyen de nommer une action complexe.
•	ensemble d'instructions, calculant un certain nombre de résultats en fonction d'un certain nombre de données.

R
Résultats

SP
Calculs

D
Données

Un sous programme peut agir pour le compte d'un autre programme ou sous-programme :

appel d'un sous-programme apr un programme ou un sous programme afin d'effectuer une certaine action.

Concepts de programme appelant et de programme appelé.

A la fin de l'éxécution du programme appelé S, il y retour vers le programme appelé P, P poursuivant son exécution sur l'instruction suivant l'appel de S.

P

•	action1
•	action2
•	…
•	appel de S

•	actionm
……………..

S
•	action1
•	…
•	…..
•	
•	actionm

Définition
On appelle arguments (ou paramètres) l'ensemble des données et des résultats (ou données modifiés) du sous-programme. 

Tout sous programme peut aussi manipuler des variables
intermédiaires (variables locale)  nécessaire à son calcul.

DECLARATION ET DEFINITION D'UN SOUS-PROGRAMME

La déclaration (Prototype) d'un sous programme.
contient :
•	le nom du sous programme
•	la liste des arguments (donnés, résultats) qui lui doivent être fournis et le type de chacun d'eux. Le nom des arguments est représenté par des nom quelconques dits arguments formels (ou paramètres formels) qui jouent le rôle de "variables muettes".

La définition (implémentation) contient le corps du sous-programme, c'est à dire le détail de l'ensemble des actions
que doit exécuter le sous-programme.

Le sous-programme étant déclaré, tout programme peut l'appeler.

APPEL D'UN SOUS-PROGRAMME
L'appel d'un sous-programme se fait par un programme (ou un sous-programme) en indiquant le nom de ce sous-programme et la liste des objets constituant les données dont la valeur est fournie au sous programme.
Ces objets transmis qu sous-programme appelé sont dits arguments réels (ou effectifs).

6.1. Déclaration (prototype) d'une fonction en C.

Les sous-programmes en C sont des fonctions, c'est à dire peuvent retourner à l'appelant (programme ou sous programme) une valeur calculée.

Syntaxe générale de la déclaration d'une fonction en C

type_valeur_retournée Nom_de_la_fonction(liste_des_arguments);

type_valeur_retournée est le type de donnée que la fonction renvoie à l'appelant. On dit aussi que c'est le type de la fonction.

Nom_de_la_fonction est le nom de la fonction. C'est pour le langage C, un identificateur (mot).

liste_des_arguments est la liste des types d'arguments, avec optionnellement le nom de des arguments formels correspondants à ces types.

Exemples de déclarations

Exemple 1
void AfficheMessage();

La fonction AfficherMessage est de type void, c'est à dire ne retourne aucune valeur.
Les deux parenthèses () indiquent que le nom AfficheMessage est associé à une fonction. 
Il n’y a rien entre ces parenthèses : la fonction ne possède aucun argument formel.

Exemple 2
float puissance(float,unsigned);
La fonction puissance est de type float (elle retourne une valeur de type float) et possède deux arguments de type respectifs float et unsigned (entier non signé).

Exemple 3
Il est possible aussi de donner le nom des arguments formels (et non seulement leur type).

int  Position(char texte[ ], char lettre);
La fonction Position est de type int, et possède les arguments formels suivants:
texte est du type char[ ] (chaîne de caractères) 
lettre est du type char ( caractère)

6.2. Définition ( implémentation) d'une fonction C.

La définition d'une fonction comprend une déclaration de fonction complétée par le corps  de cette fonction qui est un bloc fournissant le détail des actions effectuées par le sous-programme.

Valeur retournée

Cas général.

Si le type de la fonction est T, T étant différent de void, la fonction doit comporter une instruction finale de la forme :
return expression;
où expression calcul une valeur de type T (ou compatible avec le type T).
return expression; peut être située dans n'importe quelle partie du corps de la fonction qui doit être la terminaison de la fonction.

Cas des fonctions de type void.
Dans le cas où la fonction est de type void, celle-ci doit se terminer par l'instruction de la forme :
return;

return; peut être située dans n'importe quelle partie du corps de la fonction qui doit être la terminaison de la fonction.

Remarque
Pour les fonctions du type void, en cas d'omission de l'instruction return; ,  celle-ci est prise par défaut par le compilateur C à la fin du bloc fonctionnelle.

Forme générale de la définition d'une fonction C

type_valeur_retournée Nom_de_la_fonction(liste_des_arguments)

{ // début du bloc fonctionnel

expression1;
………………
……………….
expressionn;
return expression_finale;
/* attention :doit etre du type de la fonction et,
si ce type est void, il n'y que l'instruction return;
*/
} // fin du bloc fonctionnel

ATTENTION :
Avant d'appeler une fonction, la déclaration définition de cette fonction doit être préalablement donnée.

En effet le compilateur doit pouvoir connaître le prototype de la fonction avant de l'utiliser par un appel.
(Remarquons que la définition d'une fonction contient le prototype de cette fonction.)

Exemple

#include <stdio.h>
void main() {
/* AfficherMessage est déclarée comme une fonction de  type void sans aucun argument   
*/

void AfficherMessage();

AfficherMessage();   //l'appel se fait ici …
printf("Fin de programme.\n");
   }

// Implémentation (définition) de AfficherMessage
//=======================================

void AfficherMessage() { 
  printf( "Je m'appelle Jean.\n");
return;
   }

Le compilateur C permettant de créer l'application met en œuvre des phases :

•	Phase 1 : La compilation du code C en code binaire
•	Phase 2 : L'édition de lien

Pour que la Phase 1 se déroule sans erreur, tout identificateur doit être parfaitement déclaré avant d'entre évoqué, et en particulier toute fonction doit être déclarée avant d'être appelée.

La phase 2 ne pourra se faire correctement que si l'implémentation (le corps) de toutes les fonctions appelées dans le programme ont été élaborées.
6.3. Passage de paramètres.
#include <stdio.h>

/* fonction calculant l'aire d'un triangle
connaissant la base et la hauteur
*/
float AireTriangle(float,float);

// Programme principal
void main() {

float b, // base du triangle
	  h ; // hauteur du triangle
do {
printf("Entrer la base : ");
scanf("%f",&b);

printf("Entrer la hauteur : ");
scanf("%f",&h);

printf("L'aire du triangle vaut : %f\n",AireTriangle(b,h));

}  while(b&&h); // s'arrete si l'une des deux valeurs est 0
printf("Fin de programme.\n");
   }

// fin du programme prinipal
//===========================

// Implémentation (définition) de AireTriangle
//============================================

float AireTriangle(float base,float hauteur) {
return (base*hauteur)/(float)2.0 ;
 }

Les arguments permettent de passer des valeurs à la fonction; l fonction peut retourner des valeurs à l'appelant.
Il existe deux façons possibles d'effectuer ces passages de données.

6.4.1. Passage par valeurs  (recopie): c'est le mode normal de transmission des arguments réels: ne modifie pas la valeur du paramètre transmis.
A l'appel, la valeur des données est recopié pour être transmise à la fonction.

Exemple
#include <stdio.h>
//=============================
int Doubler(int x) {
x*=2;
return x;
}
//=============================
void main(){
int v=3;
printf("Resultat de l'appel : %i\n", Doubler(v));
printf("La variable x vaut : %i\n",v);
 }

Resultat de l'appel : 6
La variable x vaut : 3

La valeur de v, c'est à dire 3, est recopié sur l'argument formel x de la fonction double. La valeur calculée x=2*x est
retournée, mais la valeur de la variable v reste  inchangée.
C'est un moyen de "protéger" la valeur des objets passés aux sous-programmes.

6.4. passage par adresse ( ou par variable).

Notion d'adresse d'une variable et de pointeurs.
Chaque variable, quelque soit son type, possède une adresse mémoire.

Le langage C permet d'obtenir la valeur de l'adresse d'une variable au moyen de l'opérateur &.

Supposons que x soit une variable d'un certain type T.
L'adresse de x vaut  &x.

Les variables contenant l'adresse mémoire d'autres variables sont dits des pointeurs.

Les variables pointeurs d'un type de données T forment un type noté T* qui sera étudié de manière détaillé dans un chapitre consacrée à leur arithmétique.

Si p est un pointeur, la valeur de la donnée désignée par p vaut *p.

Exemple 

#include <stdio.h>
//=============================
void main(){
int x=5;
int* p;
// p peut  contenir l'adresse de toute variable de type int 

p=&x; // p contient l'adresse de x
printf("La variable x vaut : %i\n",*p); // *p vaut x

 }

Soient :
T x, * p;
x est une variable d'un type quelconque T et p une pointeur sur des variables de type T.
Soit :
p=&x ;
p "réfère" x 
*p "défère" x : *p possède la même valeur que x.
Toute modification de *p est répercutée sur x.

Exemple

#include <stdio.h>
//==================

void main(){
int x=5;
int* p=&x; // p désigne la variable x
*p=10 + (*p); // le contenu désigné par x est modifié ici
printf("La variable x vaut : %i\n",x); // la valeur de x est 15
 }

C'est en utilisant la notion de passage par adresse que la deuxième méthode de passage d'arguments va se réaliser.

Afin de passer des données modifiables à une fonction, il faut passer leur adresse et non une copie de leur valeur.

Exemple 
 Supossons qu'une fonction de type T1 doit modifier une donnée de type T2 passée en argument. Son prototype
doit être de la forme :
T1 f(T2 *p);

Le corps de la fonction f doit ressembler à :

T1 f(T2 *p) {
….
actions(*p); //Calculs sur *p

return expression_de_type_T1;

}

Donnons un exemple complet.

#include <stdio.h>
//=============================
void Modulo3 (int * p) { 
// les données référencée par p seront modifiées

(*p)=(*p)%3 ;

}

//================
void main(){
int x=5;
Modulo3(&x);
 // passage de l'adresse de x sur l'argument p

printf("La variable x vaut : %i\n",x);
 // la valeur de x est devenue 15

return;
 }

Autre exemple : l’échange de deux valeurs

#include <stdio.h>

void Echanger(int * a, int * b) {
int temp=*a;
*a=*b;
*b=temp;
return;
  }

void main() {
int x=3,y=-10;
Echanger(&x,&y);
printf("x=%i y=%i\n",x,y);
return;
}

Remarques.
Dans certaines circonstances le passage par valeur est exclure (par exemple, cas des grands tableaux) et il est faut utiliser le passage par adresse.
Cependant, dans un certain nombre de cas, il ne faut pas changer  (accidentellement) la valeur de la variable qui a été passée en référence.

Dans ce cas , il faut utiliser :

const type_du_paramètre *  nom_du_paramètre.

On dit que l'on a une référence verrouillée.
Exemple d'une erreur d'utilisation.
void Modifier ( const int *  p) {
 *p=*p+2;
 // Erreur de compilation :
// la valeur de *p ne peut être modifiée
return ; 
    }

6.5. Références et valeurs d'une fonction.

Les fonctions peuvent renvoyer une valeur de type quelconque.

Attention : il ne faut jamais renvoyer l'adresse d'un paramètre locale utilisé dans une d'une fonction.
Un paramètre local est une variable temporaire dont la durée de vie est insuffisante pour être exploitable à l'extérieure de la fonction.

Exemple à ne pas suivre !

int * mauvais_exemple ()
{ int v=7;
     return &v;  // Erreur grave
}

Cependant une fonction peut retourner la valeur de l'adresse d'une variable non locale à la focntion, passée par exemple en argument.

#include <stdio.h>

int * PuissanceDeux (int *);
//=====================
void main() {
int x=20;
 printf("%i\n",*PuissanceDeux(&x)); // affiche 400
}

//=======================
int * PuissanceDeux(int *p)
{
      *p=(*p) * (*p);
    return p; 
}

6.6. Conception de fonctions

Quelques questions préliminaires à se poser :
1.	A quoi sert le fonction?
2.	
3.	Quelle est l'interface de la fonction? (prototype)

4.	Comment la fonction remplit-elle son rôle ?
Quelle est son algorithme ?

5.	Quel nom lui donner?

Exemple.

Objet : développer une version de la fonction racine carrée .

1. Cette fonction sert à calculer la racine carrée d'un nombre.
2. La fonction manipule des nombres réels : elle est de type double ou float.
On choisit le type double. Son paramètre sera de type double.
3. Utilisation de l'algorithme de Newton-Raphson:

On définit ainsi une suite xn d'approximation de la racine carrée de a.

4. On nomme cette fonction sqrt.

Ainsi la déclaration (prototype) de cette fonction est :
		
double Sqrt(double); 

Nous devons écrire le Manuel de l'utilisateur:

Manuel Uilisateur
double Sqrt(double);
Cette fonction extrait une racine carrée avec une précison de , en utilisant la méthode d'approximation de Newton-Raphson.
Cette fonction ne doit jamais être appelée avec un paramètre négatif.

Nous devons écrire un Plan de Test afin de pouvoir valider la fonction sqrt :

Pour l'exactitude des calculs , on test les valeurs 1,2,3,4,9,10 et 100.

Tester la valeur 0

Réaliser quelques test avec des valeurs négatives.

// Plan de Test 

#include <stdio.h>
double Sqrt(double);
void main() {
double x;
while( printf("Valeur de x? (^Z pour finir) ") ,
	   scanf("%lf",&x),
	  (!feof(stdin) ) )
//On demande un affichage avec 12 chiffres
// après la virgule
	   printf("sqrt(%lf)=%.12lf\n", x, Sqrt(x) );
 }

Remarque
Le programme de test introduit une caractéristique que nous ne connaissons pas encore :
			(!feof(stdin) )
qui indique si l'on atteint la fin du fichier de saisie (clavier)  par la frappe de ^Z.

Travail de l'algorithme de la fonction Sqrt.

C'est la relation de récurrence qui définit une suite d'approximations de la racine carrée. 

Dans une itération nous calculerons l'approximation suivante, jusqu'à l'obtention d'une valeur suffisamment proche de la racine carrée. 

Cette différence sera acceptable si la différence avec la vrai racine carrée est inférieure à un seuil fixé.

Dans ce cas, on considérera que la valeur de la racine carrée est atteinte et que la fonction peut renvoyer cette approximation.

Il s'agit maintenant d'écrire le manuel de conception.

Dans le diagramme du manuel de conception, les actions ne sont pas des instructions C mais de brèves descriptions de ce qu'il faut réaliser à chaque étape.

Manuel de conception de :
double Sqrt(double);
Fonction qui calcule une racine carrée avec une précision de :.
Elle utilise la méthode d'approximation de Newton-Raphson .
La relation de récurrence est :

Le diagramme structurel de l'algorithme est le suivant :

double Sqrt(double)

Initialisation de l'approximation

approximation satisfaisante ?

OUI

NON

retourner la valeur de l'approximation

Améliorer l'approximation

Mise en oeuvre de la fonction Sqrt. 

// Mise en oeuvre de la fonction Sqrt
#include <math.h>
/ /necessaire pour fabs, valeur absolue d'un double 
double Sqrt(double x)
// Sqrt -- extrait la racine carrée d'un double et renvoie un double
// Methode de Newton-Raphson
// Ne jamais appeler avec un argument négatif

double Sqrt(double x)
{
	double approx=1.0;
//fabs : valeur absolue d'un double 
	while(fabs(approx*approx-x)>0.0001) 
	 {
	   approx=(approx + (x/approx))/2;
	 }
	 return approx;
   }
Valeur de x? (^Z pour finir) 0

Différentes valeurs sont testées

sqrt(0.000000)=0.007812500000
Valeur de x? (^Z pour finir) 1
sqrt(1.000000)=1.000000000000
Valeur de x? (^Z pour finir) 2
sqrt(2.000000)=1.414215686275
Valeur de x? (^Z pour finir) 3
sqrt(3.000000)=1.732050810015
Valeur de x? (^Z pour finir) 4
sqrt(4.000000)=2.000000092922
Valeur de x? (^Z pour finir) 9
sqrt(9.000000)=3.000000001397
Valeur de x? (^Z pour finir) 10
sqrt(10.000000)=3.162277665176
Valeur de x? (^Z pour finir) 100
sqrt(100.000000)=10.000000000140
Valeur de x? (^Z pour finir)

6.7. Portée, visibilité et durée de vie
(scope, visibility, lifetime)

6.7.1. Eléments locaux et  globaux.
 Nous avons rencontré des variables définies à l'intérieur d'une fonction: ce sont des variables locales.

Les arguments formels d'une fonction sont aussi considérés comme des paramètres locaux à la fonction.

Une variable peut être définie à l'extérieur de toute fonction Elle est dite alors globale.

Les variables globales doivent être utilisées avec circonspection pour des raisons de portée, de visibilité et de durée de vie.

Attention : en lagage C une fonction ne peut pas être définie à l'intérieure d'une autre fonction.

En C toutes les fonctions sont globales.

6.7.2. Portée.

C'est la zone de texte où un identificateur est reconnu.
Pour un identificateur local, un nom peut être utilisé depuis sa déclaration et jusqu'à la fin du bloc le contenant.

Une variable globale est connue dans tout le programme (d'où un danger possible...).

6.7.3. Visibilité.

Une variable défini dans un bloc B masque celles de mêmes noms (si elles existent) définies dans tout autre bloc contenant B. 
En langage C, il n'est pas possible faire coexister dans un même bloc deux variables de même nom.

//                   Exemple de règles de visibilité

 #include <stdio.h>

void afficher(int y)
{
  printf( "valeur du paramètre : %i\n",y); 
}

//****************************

int  x=99; // globale : niveau 0

void main()  {

	int x=9;
	afficher(x); // locale : niveau 1
    
	{
		int x=-1; // local : niveau 2
	        afficher(x); 

		{
			int x=15; // locale niveau 3
			afficher(x);
		}
	}
}

 6.7.4. Durée de vie.

Il s'agit de préciser les conditions de création et de destruction d'une variable.

Pour un programme, une variable globale qui a été déclarée existe tant que le programme est exécuté.
Elle est créée avant même que l'exécution ne commence et est détruite juste après que l'exécution ne se termine.

Les variables locales sont créées lors de l'exécution de leur définition et sont détruites à la fin du bloc qui contient leur définition.

Elles sont dites variables automatiques car il y a création et destruction automatiques de ces variables lors de l'execution du programme.

Il existe des variables de portés limitées, mais d'une durée de vie égale au programme : ce sont les variables statiques.

Mot clé :             static 

Les variables locales statiques ont une durée de vie égale à celle du programme et, conservent donc leur valeurs comme des variables globale.

Le qualificatif static confère aux variables locales une classe de mémorisation rendant permanentes les valeurs contenues dans ces variables.

static ne modifie ni la portée, ni la visibilité d'une variable, mais uniquement sa durée de vie. La variable est créee et initialisée avant que le programme ne commence et elle est détruite après que qu'il ne soit terminé, exactement comme une variable globale.

Exemple

On veut savoir quel est le nombre de fois où une fonction est appelé lors de l'exécution d'un programme.
Pour ce faire on utilise une variable locale static qui va mémorisé le nombre d'appels effectués sur cette fonction.

#include <stdio.h>

void test() {

static appel =0; // à l'origine

printf("Appel %i\n",++appel);

}

void main()
{
	int i;
 	for ( i=0;i<5;i++) test();
}

Les variables locales statics sont dites aussi variables rémanentes.

6.8 Introduction à la récursivité.

Il s'agit de la possibilité d'une fonction de s'appeler elle-même directement (récursivité directe) ou indirectement au travers d'autres fonctions (récursivité indirecte).

Notion de pile

La notion de récursivité est étroitement liée au concept de pile.
La structure de pile correspond à la notion usuelle de pile d'assiettes, pile de dossiers, ect.
Dans une pile on retire toujours d'abord le dernier élément déposé, l'ordre de sortie étant l'inverse de l'ordre d'entrée.
La gestion d'une pile est dite LIFO (last in, first out), c'est à dire premier entré, premier sorti.
Les données sont empilées ou dépilée par l'intermédiaires d'une tête d'accès dite sommet de pile.

Ajout

Retrait

Sommet

Pile d'une fonction récursive.

Un programme récursif s'appelle lui-même.
On recommence l'exécution de la fonction avec de nouvelles données avant d'avoir fini l'exécution avec les données de l'appel précédent.

Une fonction récursive doit donc sauvegarder à chaque appel de la fonctions, des résultats intermédiaires et des adresses de retour.
Ces sauvegardes se font automatiquement sur une pile.
A chaque entrée dans la fonction en empile les arguments et les adresses de retour. A chaque sortie de la pile, on dépile les
données et on se branche à l'adresse de retour.

Exemple classique : la factorielle

// factorielle

long factorielle(const long nombre) {

return (nombre<2)? 1 : nombre*factorielle(nombre-1);
 
}

Nous avons admis que n! vaut 1 pour des valeurs 1,0 et même pour des valeurs négatives! ce qui rend plus "robuste" cette fonction aux erreurs de calculs....

La condition d'arrêt de cette fonction est donnée par la condition
 (nombre<2)? 

Illustrons le mécanisme d'un appel sur cette fonction récursive.
Pour ce faire, supposons que l'on a l'appel suivant :

long unsigned val;
val=factorielle(5);

Nous allons simuler les appels récursifs au moyen de la pile associée à la fonction factorielle. Nous n'indiquerons que les valeurs des arguments empilés ou dépilés

nombre	factorielle(nombre)	empiler	dépiler	
5	factorielle(5)	5*factorielle(4)	5*24= 120	
4	factorielle(4)	4*factorielle(3)	4*6=24	
3	factorielle(3)	3*factorielle(2)	3*2 = 6	
2	factorielle(2)	2*factorielle(1)	2*1 = 2	
1	factorielle(1)	1	 1	

Les valeurs intermédiaires 5*factorielle(4),…,2*factorielle(1) sont empilées. On commence à dépiler pour la valeur nombre=1, dont la factorielle vaut 1; à chaque retour, il est possible de terminer les calculs entrepris :
factorielle(2) vaut 2,..,factorielle(5) vaut 120.
Toutes les valeurs ont été dépilées et val=120.

6.9. Exercices

6.9.1. Ecrire la fonction récursive f définie par :
		1 			si n=1
f(n)=		f( n/2 ) 		si n est paire
		f( (3n+1)/2 ) 	si n est impaire

n est un entier 1.

Tester cette fonction pour différentes valeurs de n.

6.9.2. Ecrire la fonction récursive Puissance définie par:
				 1 				     si m=0		
Puissance(n,m) = 
				n*Puissance(n,m-1) 	sinon	
n > 0 et m 0 étant des entiers.