Retour à l'index

Programmation modulaire

Un long programme est diffcile à appréhender globalement. Il vaut donc mieux le scinder en petits programmes : un programme principal fait appel à des sous­programmes, qui peuvent eux­mêmes faire appel à des sous­programmes, et ainsi de suite. C'est le principe du raffinement successif. De plus certains sous­programmes 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 lui­mê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.

Un exemple

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.

Définition d'une 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.

Syntaxe


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'en­tê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éfi­nition) 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'en­tê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.

Exemples de définition

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 :
  • la fonction a deux paramètres et, de plus, de types différents ;
  • le corps comporte des déclarations de variables, dites variables locales ;
  • on utilise une boucle pour définir la fonction.
On remarquera que nous sommes obligés d'introduire la variable locale 'y' car PUISS ne pourrait pas être utilisé à droite d'une affectation.
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.

Mode de transmission des paramètres

Notion

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 pa­ramè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 ci­dessous).

Passage par valeur

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.

Variable globale

Considérons le programme suivant :
		#include <stdio.h>  
		  
		int n = 3;   
		  
		void affiche(void)   
		{   
		n = 4;   
		printf("%d \n", n);   
		}   
		  
		int main(void)   
		{   
		affiche();   
		printf("%d \n", n);   
		}   
		

L'exécution de ce programme fait afficher 4 puis 4. La fonction affiche() a donc 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.
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.

Variable locale

Introduction. La procédure elle­mê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 sous­programmes : en effet, pour un (gros) programme, des sous­programmes 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).

Déclaration des fonctions

Notion

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 522­iè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.


Syntaxe de la déclaration de fonction en C

La déclaration de fonction en langage C peut se faire suivant deux modes, appelés prototypes.

Prototype complet. La syntaxe de la déclaration d'une fonction sous forme de prototype com­plet est la suivante :
type nom(type1 variable1, ... , typen variablen); 

où type, type1, ... ,typen sont des types et où nom et variable1, ... , variablen sont des identificateurs (le nom de la fonction et les noms des paramètres).
Prototype simplifié.­ La syntaxe de la déclaration d'une fonction sous forme de prototype sim­plifié est la suivante :
type nom(type1, ... , typen); 
Autrement dit, on renonce aux noms des paramètres, puisqu'ils ne sont pas utiles.
Situation de la déclaration. La déclaration locale d'une fonction se fait au début du bloc de la définition d'une fonction, avant les déclarations de variables. La fonction ainsi déclarée n'est connue que de la fonction dans laquelle elle est déclarée. On peut aussi utiliser une déclaration globale d'une fonction avant toute définition de fonction.
Exemple de déclaration locale. Le programme suivant illustre la notion de déclaration locale de fonction :

		#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));   
		}  
		

Exemple de déclaration globale. Le programme suivant illustre la notion de déclaration globale de fonction :
		#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));   
		}   
				

Pragmatique

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.

Déclaration de fonctions dans des fichiers en­tête

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 en­tê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.
  • Le fichier en­tête definiti.h contient ici non pas la déclaration d'une fonc­tion mais sa définition. On loge en principe le code des fonctions directement dans le programme ou dans des bibliothèques. Les définitions des fonctions livrées avec le compilateur se trouvent (sous forme de code objet) dans de telles bibliothèques ayant l'extension .lib (pour LIBrary).
  • Le fichier en­tête est écrit entre guillemets verticaux "..." et non entre les symboles < et > car il ne se trouve pas dans le répertoire standard des en­têtes mais dans le répertoire contenant le programme source.

Les pointeurs et le passage par adresse

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)