Un long programme est diffcile à appréhender globalement. Il vaut donc mieux le scinder en petits programmes : un programme principal fait appel à des sousprogrammes, qui peuvent euxmêmes faire appel à des sousprogrammes, et ainsi de suite. C'est le principe du raffinement successif. De plus certains sousprogrammes peuvent servir dans d'autres programmes, c'est le principe de la modularité. Ces principes sont mis en oeuvre en langage C grâce aux fonctions.
On peut distinguer, en langage C, les fonctions prédéfinies des bibliothèques (telles que printf() ou scanf()), livrées avec le compilateur et 'intégrées' au programme lors de l'édition des liens, et les fonctions que le programmeur écrit luimême en tant que partie du texte source.
Nous avons déjà vu comment utiliser les premières. Nous allons donc nous intéresser ici aux secondes. Nous verrons aussi, d'ailleurs, la façon de concevoir les fonctions prédéfinies.
| Introduction. | Commençons par donner un exemple simple de définition et d'utilisation d'une fonction. Considérons à nouveau, pour cela, notre exemple de fonction que l'on veut évaluer en un certain nombre de points. Une nouvelle amélioration consiste à dégager la définition de la fonction en utilisant un sous-programme. |
| Programme. | La mise en place suivante est intuitivement compréhensible, nous la détaillerons ensuite :
#include <stdio.h>
#include <math.h>
double F(double x)
{
return ( (sin(x) + log(x)) / (exp(x) + 2) );
}
int main(void)
{
float x, y;
printf("x = ");
scanf("%f",&x);
while (x != 1000)
{
y = F(x);
printf("f(%f) = %f\n", x, y);
printf("x = ");
scanf("%f",&x);
}
}
L'amélioration provient ici plus particulièrement du fait que l'on ne s'occupe pas de la fonction particulière dans le corps du programme, mais uniquement du fait que l'on veut afficher sa valeur en un certain nombre de points, ce qui est l'essence du programme. Il suffit de changer le sous-programme, bien mis en évidence, lorsqu'on veut changer de fonction. |
La définition des fonctions, dans des cas simples, est su#samment claire au vu du programme précédent. Mais l'explicitation suivante donne les limites d'une utilisation intuitive.
Règles de définition |
|
| Nom d'une fonction. | Une fonction a un nom qui est un identificateur (non utilisé pour une autre entité, comme d'habitude). |
Syntaxe de la définition. | Une fonction se définit. La syntaxe de la définition est la suivante : type nom(type_1 variable_1, ..., type_n variable_n)
{corps de la fonction } où la première ligne étant l'entête de la fonction, le reste le corps de la fonction. Le corps de la fonction est constitué exactement comme dans le cas de la fonction principale main(), la seule considérée jusqu'à maintenant. |
| Le type vide. | Il est prévu un type lorsque la fonction ne renvoie pas de valeur, ou lorsqu'elle n'a pas d'argument, le type void. |
Emplacement des définitions |
|
| Définition | Un programme C est une suite de définitions de fonctions, la définition de la fonction main()étant obligatoire : fonction1 fonction2 ......... fonctionn Une définition de fonction ne doit pas contenir de définitions de fonctions. Il faut que les fonctions soient définies avant d'être appelées (pour qu'elles soient connues du compilateur). Nous verrons comment faire lorsque ce n'est pas le cas. Le programme commence par exécuter la fonction main() et ne se sert des autres fonctions que si il est fait appel à elles, soit directement dans la fonction principale, soit indirectement via d'autres fonctions appelées. |
Valeur de retour |
|
| Notion. | Le type précédant le nom de la fonction est celui de la valeur de la fonction, appelée valeur de retour en langage C. |
| Type de la valeur de retour. | La valeur de retour d'une fonction peut être soit d'un type de base (char, short, int, long, float, double, long double), soit un pointeur (notion que nous verrons plus tard) vers n'importe quel type (simple ou complexe) de données. |
| L'instruction de renvoi. | Les valeurs de retour des fonctions sont renvoyées à la fonction appelante grâce à l'instruction return. |
| Emplacement des instructions de retour. | La rencontre (au cours de l'exécution, pas de la définition) d'une instruction return met fin à l'exécution des instructions d'une fonction et rend le contrôle du programme à la fonction appelante. Les parenthèses ne sont pas obligatoires. On peut écrire : return(expression); ou : return expression; Si le type de expression ne coîncide pas avec le type de la valeur retournée tel qu'il est défini dans l'entête de la fonction, alors le compilateur le convertit comme il faut. La spécification de expression est optionnelle. Si elle est omise alors la valeur retournée est indéfinie. |
Paramètres |
|
| Notion. | Les arguments d'une fonction sont appelés ses paramètres en langage C. Le nom de la fonction est suivi de la liste des paramètres et de leurs types. |
| Paramètres formels et paramètres effectifs. | Il faut bien faire la différence entre les paramètres au moment de la définition d'une fonction et les paramètres qui seront donnés lors de l'appel d'une fonction. Les paramètres spécifiés lors de la définition d'une fonction sont qualifiés de paramètres formels. Les paramètres transmis à la fonction lors de son appel de la fonction sont appelés les paramètres effectifs. Un paramètre formel est une variable locale, connue seulement à l'intérieur de la fonction pour laquelle est définie ce paramètre. Les paramètres formels et effectifs doivent correspondre en nombre et en type ; par contre les noms pouvent différer. |
| Introduction. | Donnons quelques exemples variés de définition pour illustrer ce que nous venons de dire. Exemple 1. La fonction f définie par l'expression f(x) = sin(x) x est a priori définie sur R # mais on sait la prolonger par continuité en 0 en lui attribuant la valeur 1. Ceci permet de définir la fonction suivante :
#include <stdio.h>
#include <math.h>
float F(float x)
{ float y;
if (x == 0)
y = 1;
else
y = sin(x)/x;
return y;
}
int main (void)
{
float a,b;
printf("Entrez a : ");
scanf("%f", &a);
b=F(a);
printf("f(%f)=%f\n", a,b);
}
|
| Variable local et boucle | Programmons l'exponentiation, c'est-à-dire au couple (x,n) associe xn :
float PUISS(float x, int n)
{
float y;
int i;
y = 1;
for (i=1; i <= n; i++)
y = y*x;
return y;
}
Cet exemple est intéressant à plusieurs titres :
|
| Fonction sans paramètre |
#include <stdio.h>
void WELCOME(void)
{
printf("Welcome to this fantastic program");
printf("\nwhich proves to you the power");
printf("\nof the modularity. \n");
}
void BIENVENUE(void)
{
printf("Bienvenue dans ce merveilleux");
printf("\nprogramme qui vous montre la");
printf("\npuissance de la modularite. \n");
}
void main(void)
{
char L;
printf("Do you want to continue in");
printf("\nEnglish or in French (E/F) ");
scanf("%c",&L);
if (L == 'E')
WELCOME();
else
BIENVENUE();
}
Les fonctions WELCOME() et BIENVENUE() n'ont ni paramètre ni valeur de retour. Remarquons cependant l'utilisation du type void et le couple de parenthèses pour l'appel de ces fonctions. Le fait qu'il n'y ait pas de type de retour rend l'utilisation de return superflue.
|
| Introduction. | Pour une fonction mathématique f à deux arguments, lorsqu'on évalue cette fonction, en effectuant par exemple y = f(a, b), on s'attend à ce que la valeur de y soit changée mais pas les valeurs de a ou de b. En informatique il en va autrement. On peut, par exemple, désirer écrire un sous-programme dont le but est d'échanger les valeurs de a et de b. Nous savons écrire le corps de ce programme en langage C depuis longtemps. Par exemple si a et b sont des entiers on a :
void swap (int a, int b){
int temporaire;
temporaire = a;
a = b;
b = temporaire;
}
Comment écrire le sous-programme correspondant dans un langage de programmation ? |
| Passage par valeur et passage par référence. | Lorsqu'on ne veut pas changer la valeur d'un paramètre on parle de transmission par valeur (en anglais call by value), c'est-à-dire que le sous-programme ne reçoit qu'une copie de la valeur. Lorsqu'on veut se permettre la possibilité de changer le valeur on parle de transmission par référence (en anglais call by reference). |
| Mise en place. | Beaucoup de langages permettent ces deux types de transmission. Le langage C ne connaît que la transmission par valeur, la transmission par référence est émulée en envoyant la valeur de l'adresse grâce aux pointeurs (comme nous le verrons cidessous). |
| Introduction. | Voyons sur deux exemples le comportement de la transmission par valeur.
#include <stdio.h>
void affiche(int n)
{
printf("%d \n", n);
}
int main(void)
{
int n = 3;
affiche(n);
}
Son exécution fait afficher 3 comme attendu.
#include <stdio.h>
void affiche(int n)
{
n = 4;
printf("%d \n", n);
}
int main(void)
{
int n = 3;
affiche(n);
printf("%d \n", n);
}
Son exécution fait afficher 4 puis 3. La fonction affiche() n'a donc pas changé la valeur de n dans la fonction principale. |
| Introduction. | Une variable globale est une variable déclarée avant toute définition de fonction. L'intérêt est qu'elle est alors connue de toutes les fonctions, on peut donc changer sa valeur dans chacune d'elle. L'inconvénient est qu'on peut aussi changer sa valeur par inadvertance. Il vaut donc mieux utiliser le passage par référence qu'une variable globale. Voyons cependant comment cela fonctionne. |
| Exemple 1. | Considérons le programme suivant :|
| Remarque. | L'utilisation des variables globales dans un programme est permise, mais il vaut mieux éviter de s'en servir. Cela va en effet à l'encontre de la clarté de la modularité. Lorsqu'on considère un sous-programme on devrait voir d'un seul coup d'oeil toutes les variables qui y interviennent. C'est tout l'intérêt des arguments. De plus cela pose des problèmes de sécurité de notre programme, mais nous ne verons pas ce principe pour le moment. |
| Exemple 2. | Remarquons que, dans le cas de la variable globale ci-dessus, la fonction affiche() n'avait pas d'argument. Considérons, par opposition, le programme suivant :
#include <stdio.h>
int n = 3;
void affiche(int n)
{
n = 4;
printf("%d \n", n);
}
int main(void)
{
affiche(n);
printf("%d \n", n);
}
Son exécution fait afficher 4 puis 3. La déclaration de l'argument n dans la fonction affiche() le fait considérer comme une variable locale. La variable n du corps de cette fonction correspond à la dernière variable déclarée, donc à la variable locale, et non à la variable globale ; ceci explique pourquoi la valeur de la variable globale, elle, n'a pas changée. |
| Introduction. | La procédure ellemême peut avoir besoin de variables qui n'ont pas d'intérêt pour le programme en son entier, comme nous l'avons vu à propos de la fonction PUISS. On parle alors de variable locale. |
| Remarque. | Le nom de variable locale se justifie de par la position de sa déclaration. Elle se justifie aussi car une telle variable n'est pas connue du programme dans son entier mais uniquement de la fonction (plus généralement du bloc) dans laquelle elle a été déclarée. Ceci montre l'intérêt d'utiliser le plus possible des variables locales : c'est une protection supplémentaire contre les erreurs de programmation. |
| Nom d'une variable locale. | Nous avons dit précédemment (avant de considérer les fonctions) qu'on ne pouvait pas déclarer le même identificateur comme variable à deux endroits di#érents. Il faut assouplir un petit peu cette règle avec l'utilisation des sousprogrammes : en effet, pour un (gros) programme, des sousprogrammes différents peuvent être conçus par des personnes différentes, qui peuvent donc penser au même nom de variable ; ceci sera détecté lors de la compilation, certes, mais cela risque de ne pas faciliter les choses. On permet donc en général, dans un langage de programmation, la possibilité d'utiliser un même identificateur à la fois comme variable globale et comme variable locale (et même plusieurs fois comme variable locale). |
| Priorité des variables locales sur les variables globales. | Un problème se pose alors : Quelle est la variable considérée lors de son utilisation ? La réponse est simple : celle qui a été déclarée le plus récemment, c'est-à-dire la plus locale à l'intérieur du sous-programme (et, plus généralement, à l'intérieur du bloc). |
Nous avons dit qu'un programme C est une suite de définitions de fonctions. Cette façon de faire risque de donner lieu à des programmes confus lorsqu'on a besoin d'un millier de fonctions, par exemple. Il n'y a en général aucune raison que la 522ième fonction fasse directement appel aux 521 fonctions précédentes. On distingue donc les notions de déclaration et de définition d'une fonction. Nous avons déjà vu la notion de définition ; venons-en à celle de déclaration.
Une déclaration de fonction fournit des indications sur le nom de la fonction, sur le type de la valeur retournée et sur le type des paramètres. Il faut, avec cette nouvelle notion, revenir sur la règle disant qu'une fonction doit être définie
avant d'être utilisée. La règle est en fait la suivante.
La déclaration de fonction en langage C peut se faire suivant deux modes, appelés prototypes.
type nom(type1 variable1, ... , typen variablen);
type nom(type1, ... , typen);Autrement dit, on renonce aux noms des paramètres, puisqu'ils ne sont pas utiles.
#include <stdio.h>
#include <math.h>
int main(void)
{
double f(double x);
float x, y;
printf("x = ");
scanf("%f",&x);
while (x != 1000)
{
y = f(x);
printf("f(%f) = %f\n", x, y);
printf("x = ");
scanf("%f",&x);
}
}
double f(double x)
{
return((sin(x) + log(x))/(exp(x) + 2));
}
#include <stdio.h>
#include <math.h>
double f(double x);
void main(void)
{
float x, y;
printf("x = ");
scanf("%f",&x);
while (x != 1000)
{
y = f(x);
printf("f(%f) = %f\n", x, y);
printf("x = ");
scanf("%f",&x);
}
}
double f(double x)
{
return((sin(x) + log(x))/(exp(x) + 2));
}
Pour mettre un peu d'ordre dans un programme faisant intervenir beaucoup de fonctions, on peut commencer par la définition de la fonction principale, précédée de la déclaration des fonctions dont elle a besoin. On définit ensuite ces fonctions auxiliaires, chacune d'elles précédée des déclarations des fonctions auxiliaires de second niveau dont elles ont besoin, et ainsi de suite.
| Introduction. | La déclaration d'une fonction personnelle ne doit pas nécessairement être incluse explicitement dans le même fichier du programme source que la fonction principale. Elle peut aussi se trouver dans un fichier entête, comme pour les fonctions prédéfinies. Ce dernier est ensuite inséré dans le programme grâce à une commande du préprocesseur. |
| Exemple. | En mettant dans le même répertoire les deux fichiers suivants :
#include <stdio.h>
#include <math.h>
#include "defini.h"
int main(void)
{
float x, y;
printf("x = ");
scanf("%f",&x);
while (x != 1000)
{
y = f(x);
printf("f(%f) = %f\n", x, y);
printf("x = ");
scanf("%f",&x);
}
}
et :
/* defini.h */
double f(double x)
{
return((sin(x) + log(x))/(exp(x) + 2));
}
on obtient un programme correct qui fonctionne. |
| Commentaires. |
|
Nous avons vu l'intérêt d'utiliser des arguments. L'utilisation des arguments transmis par valeur évite bien des erreurs. Il existe cependant des cas oÙ l'on veut réellement changer la valeur de l'argument en appelant une fonction, c'est-à-dire effectuer une transmission par référence. Ce mode de passage des arguments n'a pas été prévu en langage C, ce qui est un défaut. On peut cependant, bien sûr, l'émuler : ceci se fait grâce à la transmission par adresse.
Pour cela nous avons besoin de connaître comment manipuler les adresses des variables grâce à la notion de pointeur.
Les pointeurs jouent très tôt un rôle important en langage C (ce qui est un défaut), parce que l'adressage indirect est important pour le passage par adresse des fonctions. C'est ce premier aspect que nous allons voir ici ; nous reviendrons plus tard sur les pointeurs lors de la définition des structures de données dynamiques.
Cours, éxercices ou graphismes libre de droit. Un mail est souhaitable | Webmestre : Aublet Bastien (bastien.aublet@hotmail.fr)