Vous êtes sur la page 1sur 24

Chapter 2

Conception de programmes
procéduraux

2.1 Motivation

Comme il a été souligné dans le Chapitre 1, un programme procédural se compose d'un


ensemble non vide de procédures. Le découpage d'un programme procédural en procédures
n'est pas fortuit, mais guidé par quelques règles de bon sens. Tout d'abord, il est préférable
que chaque procédure du programme soit dédiée à l'exécution d'une tâche bien déterminée.
En outre, la création d'une procédure peut être motivée par l'identication d'une tâche
récurrente, c'est-à-dire, une tâche qui peut s'exécuter à plusieurs moments du déroulement
du programme.
L'un des avantages de créer des procédures pour les tâches récurrentes est que le code
récurrent pourra être remplacé par un simple appel de procédure (voir le programme 1,
à titre d'exemple). Le recours à une telle pratique évite des programmes biens longs
en termes de nombre de ligne et, surtout, dicile à modier car, sans le découpage en
procédures, on serais amené à répercuter le moindre changement dans tous les endroits
où le code récurrent apparaît.
Une fois dénie, une procédure peut être appelée autant de fois que nécessaire par
une autre procédure ou par elle même. Dans ce dernier cas, on obtient des procédures
dites récursives (voir Section 2.7). En somme, l'utilisation des procédures permet de
structurer le programme et d'améliorer sa lisibilité. Ainsi, le programme devient plus
facile à comprendre et à modier.

2.2 Les fonctions

Les programmes C sont construits au tour de la notion de fonction, au lieu de celle


de procédure. Les procédures sont, en fait, considérées comme des cas particuliers de
fonctions.
Une fonction peut être vue comme un opérateur qui eectue un calcul et qui produit
un résultat. Une fonction réalise, donc, une simple opération dont le résultat peut être,
par la suite, utilisé dans d'autres opérations plus complexes.
Une fonction doit, tout d'abord, être dénie. La dénition d'une fonction est composée
de deux parties, une entête est un corps. En langage C, l'entête d'une fonction est

1
2 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

.
.
. float SaisieReelNonNul()

do {
{ float x;

printf("Donnez un réel non nul"); do


scan("%f",&x1);
{
} printf("Saisir un réel non nul");

while (abs(x1) < eps); scan("%f",&x);

.
.
}
.
while (abs(x) < eps);
do return x;
{ }
printf("Donnez un réel non nul"); .
.
.
scan("%f",&x10);
} x1 = SasieReelNonNul();

while (abs(x10) < eps); .


.
.
. x10 = SaisieReelNonNul();
.
.
Programme 1: À gauche: Un fragment de programme présentant du code récur-
rent. À droite: Création d'une procédure pour le code récurent suivi d'appels à
cette procédure.

type_résultat nom_de_la_fonction (type_1 param_1,. . .,type_n param_n )


{
instructions
}
Programme 2: Forme générale d'une fonction C.
2.3. LES PROCÉDURES 3

composée du type du résultat de la fonction, du nom de la fonction ainsi que d'une liste
de paramètres comprise entre des parenthèses (voir Programme 2 pour la syntaxe des
fonctions C). Le nom de la fonction est un identicateur choisi par le programmeur. La
liste des paramètres dépend des données dont la fonction a besoin pour bien fonctionner.
A l'intérieur du corps de la fonction, on peut trouver des déclarations de variables puis le
corps de la fonction proprement dit. Ce dernier est composé par des instructions eectuant
la tâche spécique que la fonction est sensé réaliser. Parmi ces instructions, il doit avoir
celles dont le rôle est de retourner le résultat de la fonction et dont le type est indiqué
dans l'entête de la fonction. En langage C, le résultat est précisé avec une instruction de
la forme:

return expression ;

Un exemple de la dénition d'une fonction (ProduitScalaire) se trouve dans le pro-


gramme de la Figure 2.1.

Une fois dénie, une fonction peut être appelée autant de fois que nécessaire. L'appel
d'une fonction se fait en évoquant le nom de la fonction suivi d'une liste de paramètres.
Il est important de signaler que l'appel d'une fonction ne constitue pas une instruction
à part entière. En d'autres mots, on ne peut pas trouver dans un programme C, par

exemple, une ligne se limitant à fonc(p1 , p2 , . . .); , où fonc est le nom d'une fonction. La

raison est que, à lui tout seul, un tel appel n'a aucun eet (de bord), c'est-à-dire, qu'il ne
modie pas les valeurs des variables de la routine appelante. Donc, le résultat d'un appel
de fonction doit toujours être récupéré par la routine appelante et inséré soit dans une
aectation, soit dans une expression, soit dans un achage ou autres.

Exemple 1. On voudrait tester la colinéarité de deux vecteurs de R3 dont les coordonnées


sont saisies par l'utilisateur. Pour ce faire, on utilise le plus petit angle formé par ces
deux vecteurs comme mesure de colinéarité. Si cet angle vaut 0 ou π radian alors les
deux vecteurs sont colinéaires sinon il ne le sont pas. Le programme de la Figure 2.1
réalise cette tâche en s'appuyant sur une fonction ProduitScalaire, qui retourne le produit
3
scalaire de deux vecteurs de R dont les coordonnées sont reçus en paramètre.

Selon les bonnes pratiques de la programmation procédurale, il est recommandé d'observer


la règle suivante:

Règle 1. Il est préférable qu'une fonction soit sans eet de bord.

Un eet de bord consiste, principalement, en la modication de valeurs de variables. Une


fonction qui n'a pas d'eet de bord est une fonction qui ne change pas, de manière durable,
les valeurs des variables des autres fonctions et procédures. Par changement durable, nous
entendons un changement qui persiste même après la n du déroulement de la fonction.
À titre d'exemple, la fonction ProduitScalaire du programme de la Figure 2.1 est sans
eet de bord, car elle ne modie pas les valeurs des variables du programme principal.

2.3 Les procédures

On présente souvent les procédures comme étant des fonctions qui ne retournent pas
de résultat. On peut alors se demander a quoi peut bien servir une procédure si elle
4 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

1 #include <s t d i o . h>


2 #include <math . h>
3 #define p i M_PI
4 #define e p s i l o n 1E−6
5
6 double P r o d u i t S c a l a i r e ( double a , double b , double c , double x , double
y , double z )
7 {
8 return a ∗ x + b ∗ y + c ∗ z ;
9 }
10
11 int main ( )
12 {
13 double a , b , c , x , y , z , ps , n1 , n2 , a n g l e ;
14
15 p r i n t f ( " S a i s i r l e s t r o i s composantes du 1 e r v e c t e u r : " ) ;
16 s c a n f ( "%l f %l f %l f " ,&a ,&b,& c ) ;
17 p r i n t f ( " S a i s i r l e s t r o i s composantes du 2eme v e c t e u r : " ) ;
18 s c a n f ( "%l f %l f %l f " ,&x,&y,& z ) ;
19
20 n1 = s q r t ( P r o d u i t S c a l a i r e ( a , b , c , a , b , c ) ) ;
21 n2 = s q r t ( P r o d u i t S c a l a i r e ( x , y , z , x , y , z ) ) ;
22
23 if
( n1 < e p s i l o n | | n2 < e p s i l o n )
24 p r i n t f ( " Les deux v e c t e u r s s o n t c o l i n é a i r e s \n" ) ;
25 else
26 {
27 ps = P r o d u i t S c a l a i r e ( a , b , c , x , y , z ) ;
28 a n g l e = a c o s ( ps / ( n1 ∗ n2 ) ) ;
29
30 if( a n g l e < e p s i l o n | | a n g l e > pi − e p s i l o n )
31 p r i n t f ( " Les deux v e c t e u r s s o n t c o l i n é a i r e s \n" ) ;
32 else
33 p r i n t f ( " Les deux v e c t e u r s ne s o n t pas c o l i n é a i r e s \n" ) ;
34 }
35
36 return 0;
37 }

Figure 2.1: Test de colinéarité de deux vecteurs de R3 .


2.4. PASSAGE DE PARAMÈTRES 5

void nom_procédure(type_1 param_1,. . .,type_n param_n)


{
instructions
}
Programme 3: Forme générale d'une procédure du langage C.

ne retourne pas de résultat. Comme première réponse, on peut dire que les résultats
des calculs eectués par une procédure peuvent bien être achés par la procédure elle
même, sans qu'ils soient communiqués à la routine appelante. C'est le cas de la procédure
VerifDate présentée dans l'Exemple 2 (voir le programme de la Figure 2.2). Cependant,
une procédure est, surtout, utile quand elle a la possibilité de modier les valeurs de ses
propres paramètres d'une manière durable, c'est-à-dire, de telle sorte que les modications
persistent même après la n du déroulement de la procédure.
Comme pour les fonctions, l'utilisation d'une procédure passe par deux étapes: la
dénition et les appels. Une procédure se dénie par une entête et un coprs. En langage
C, l'entête d'une procédure a la même structure que l'entête d'une fonction, sauf que
le type du résultat est le type particulier ensemble vide, void, (voir Programme 3). Le
corps de la procédure est composé d'instructions comme pour les fonctions, sauf qu'une
procédure ne retourne pas de résultat de manière explicite avec l'instruction return.
L'appel d'une procédure constitue, à lui tout seul, une instruction à part entière,
puisque cet appel peut entraîner des modications sur les valeurs des variables de la

routine appelante. Ainsi, dans un programme C, la ligne proc(p_1,p_2,. . .); , où proc


est le nom d'une procédure, constitue une instruction valable.

Exemple 2. Le programme C de la Figure 2.2 permet la saisie d'une date et d'acher


le message d'alerte approprié si l'une des composantes de la date est érronée.

L'appel d'une procédure, (ou d'une fonction d'ailleurs), provoque une rupture avec le
déroulement linéaire des instructions selon l'ordre de leur apparition dans le programme.
Ainsi, le corps de la procédure appelée, qui peut se trouver bien loin de l'appel de la
procédure, s'exécute avant les instructions qui se trouvent immédiatement après l'appel.
Dans le programme de la Figure 2.2, par exemple, le deuxième et troisième appel à la
procédure VerifDate ne sont exécutés que lorsque le premier appel à cette procédure se
termine. Les appels de procédures peuvent donc être considérées comme des structures
de contrôle puisqu'elles permettent de couper avec l'ordre linéaire des exécutions.

2.4 Passage de paramètres

Toute routine, (fonction ou procédure), peut avoir besoin de paramètres pour fonctionner
correctement. Ces paramètres sont déclarés lors de la dénition de la routine, comme
indiqué dans les deux sections précédentes. Au niveau de leur déclaration, c'est-à-dire
dans l'entête de la routine, les paramètres n'ont pas d'existence réelles, dans le sens que,
à ce niveau, ils n'ont pas de valeur précise. On parle alors de paramètres formels. Ces
paramètres sont initialisés, lors de l'appel de la routine, par les valeurs des expressions
utilisées dans l'appel. Ces derniers sont désignés par paramètres eectifs.
6 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

1 #include <s t d i o . h>


2
3 void VerifDate ( int int
data , int
min , charmax , message [ ] )
4 {
5 if ( data < min | | data > max)
6 p r i n t f ( "%s \n" , message ) ;
7 }
8
9 int main ( )
10 {
11 char message [ 6 4 ] ;
12 int j o u r , mois , annee ;
13
14 p r i n t f ( "Donnez l e j o u r , l e mois e t l ' année sous −forme j j mm aaaa :
") ;
15 s c a n f ( "%d %d %d" ,& j o u r ,& mois ,& annee ) ;
16
17 // a f i n d ' a f f i c h e r l e s a c c e n t s c o r r e c t e m e n t , on u t i l i s e

18 // l a f o n c t i o n s p r i n t f p o u r i m p r i m e r l e ' é ' dans l a

19 // c h a i n e m e s s a g e à l ' a i d e d e s o n c o d e ASCII ( 1 3 0 )

20
21 s p r i n t f ( message , " j o u r %c r r o n%c " , 1 3 0 , 1 3 0 ) ;
22 V e r i f D a t e ( j o u r , 1 , 3 1 , message ) ;
23 s p r i n t f ( message , " mois %c r r o n%c " , 1 3 0 , 1 3 0 ) ;
24 V e r i f D a t e ( mois , 1 , 1 2 , message ) ;
25 s p r i n t f ( message , "ann%c e %c r r o n%c e " , 1 3 0 , 1 3 0 , 1 3 0 ) ;
26 V e r i f D a t e ( annee , 1 9 0 0 , 2 0 1 7 , message ) ;
27 return 0;
28 }

Figure 2.2: Programme vérication la validité d'une date.


2.4. PASSAGE DE PARAMÈTRES 7

Routine appelante Routine appelée


param. e1 param. form1

5 copie
5
 7
param. e2 param. form2

7 copie
7
 5

Figure 2.3: Schéma illustrant le passage de paramètres par valeur.

Règle 2. Les paramètres formels d'une routine et les paramètres eectifs correspon-
dants doivent coïncider en nombre et en type.

La liaison entre paramètre eectif et paramètre formel correspondant est déduite de la po-
sition du paramètre eectif, respectivement, formel, dans la liste des paramètres eectifs,
respectivement, formels.

2.4.1 Passage de paramètres par valeur


Avec ce mode de passage de paramètres, le transfert des données via les paramètres est
eectué dans un seul sens: de la routine appelante vers la routine appelée. Pour ce faire,
les zones mémoires qui seront allouées aux paramètres formels doivent être distinctes de
celles qui seront allouées aux paramètres eectifs. Lors de l'appel, une copie des valeurs
des paramètres eectifs est eectuée dans les zones mémoires réservées aux paramètres
formels. Par contre, à la n du déroulement de la routine appelée, il n'y a pas de copies
dans le sens inverse (voir Figure 2.3). Par conséquent, les modications qui pourraient
être eectuées sur les paramètres formels, dans la routine appelée, ne se répercutent pas
sur les paramètres eectifs.

Exemple 3. On se propose d'échanger les valeurs de deux variables a et b saisies par


l'utilisateur. Pour ce faire, on utilise une procédure ayant deux paramètres passés par
valeur (voir la procédure Swap1 du programme de la Figure 2.4). A la n de l'exécution
de la procédure Swap1, qui utilise un passage de paramètres par valeur, on se rend compte
que les variables a et b n'ont subit aucun changement au niveau du programme principal
(voir la trace d'exécution dans la Figure 2.5).

En suivant les bonnes pratiques de la programmation procédurale, une procédure n'a


d'autre moyen de communiquer le résultat de ses calculs, à la routine appelante, que
via ses paramètres. Or, le mode de passage de paramètres par valeur empêche cette
possibilité. Il s'ensuit que le mode de passage de paramètres par valeur n'est pas adéquat
pour les procédures. Par contre, du moment qu'il est préférable qu'une fonction n'ai pas
d'eet de bord (Règle 1), le mode de passage de paramètres par valeur se trouve être le
plus approprié pour les paramètres des fonctions.
8 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

1 #include <s t d i o . h>


2 void Swap1 ( int a , int b)
3 {
4 int tmp ;
5
6 tmp = a ;
7 a = b;
8 b = tmp ;
9 }
10
11 void Swap2 ( int ∗ adra , int ∗ adrb )
12 {
13 int tmp ;
14
15 tmp = ∗ adra ;
16 ∗ adra = ∗ adrb ;
17 ∗ adrb = tmp ;
18 }
19
20
21 int main ( )
22 {
23 int a , b ;
24
25 p r i n t f ( " S a i s i s s e z deux e n t i e r s : " ) ;
26 s c a n f ( "%d %d" ,&a ,&b ) ;
27 Swap1 ( a , b ) ; // P a s s a g e d e p a r a m è t r e s p a r v a l e u r
28 p r i n t f ( "%d %d\n" , a , b ) ;
29 Swap2(&a ,&b ) ; // P a s s a g e d e p a r a m è t r e s p a r a d r e s s e
30 p r i n t f ( "%d %d\n" , a , b ) ;
31
32 return 0;
33 }

Figure 2.4: Programme C illustrant les deux modes de passage de paramètre. Il s'agit
de deux procédures, la première utilise le passage de paramètre par valeur et la seconde
utilise le passage par adresse ou référence.

no. lig. 1 2 3 5 6 7 8 4

a ? 5 5 5 5 7 7 5
b ? 7 7 7 7 7 5 7
tmp - - - ? 5 5 5 -

Figure 2.5: Trace de l'exécution de la procédure Swap1 du programme de la Figure2.4,


avec 5 et 7 comme valeurs saisies pour les entiers a et b. La procédure Swap1 utilise un
passage de paramètres par valeur. Notez que, dans cette trace, les valeurs nales de a et
de b sont inchangées.
2.4. PASSAGE DE PARAMÈTRES 9

Exercice 1. (Nombres amicaux) Deux entiers naturels sont dit amicaux s'il sont
distincts et si chacun des deux entiers est égal à la somme des diviseurs stricts de
l'autre. Proposez un programme C qui détermine si deux entiers naturels, saisis par
l'utilisateur, sont amicaux ou pas. Indication: dénissez une fonction pour eectuer
le calcul qui vous semble le plus récurent.

Exercice 2. (Le carré magique) Un carré magique 3×3 se compose de 9 cases,


disposées en 3 lignes et 3 colonnes, contenant chacune un entier de 1 à 9. Les cases
contiennent des valeurs distinctes telles que la somme des cases se trouvant sur la
même ligne, la même colonne, ou sur l'une des deux diagonales est la même. Écrire
un programme C qui saisie les valeurs des 9 cases d'un carré et qui détermine s'il
est magique ou pas. Indication: dénissez une fonction pour eectuer le calcul qui
vous semble le plus récurent.

2.4.2 Passage de paramètres par adresse (par référence)


Avec ce mode de passage de paramètres, la routine appelante transmet, à la routine
appelée, des données via les paramètres utilisés lors de l'appel. A son tour, la routine
appelée peut transmettre des résultats via ces mêmes paramètres. Pour ce faire, c'est
les adresses des paramètres eectifs utilisés lors de l'appel, qui sont passées à la routine
appelée.
En langage C, l'accès à l'adresse d'une variable se fait à l'aide de l'opérateur &. Ainsi,
&x est l'adresse de la zone mémoire qui sera réservée à la variable x. L'appel de la
procédure Swap2 du programme de la Figure 2.4, (voir Ligne 29), illustre un cas d'appel
de procédure qui utilise des adresses de variable.
Disposant des adresses des variables utilisées comme paramètres eectifs, la routine
appelée va avoir accès à ces variables. Cet accès permet d'eectuer des modications
qui persistent après la n du déroulement de la routine appelée. La déclaration de la
procédure Swap2 du programme de la Figure 2.4, (voir Ligne 11), illustre un cas de
passage de paramètre par adresse.
L'utilisation d'adresse au niveau de l'appel d'une routine, entraîne des modications
aux niveau des types des paramètres formels. En eet, d'une manière générale, si x est une
1
variable de type t alors &x est de type pointeur sur t, qui s'écrit t* selon la syntaxe du
langage C. Réciproquement, pour accéder au contenu d'une variable à partir de l'adresse
de cette dernière, on utilise l'opérateur *. Ainsi, si l'adresse d'une variable x est disponible
dans une variable adrx alors on peut accéder à la zone mémoire réservée à x via *adrx.
Les opérateurs & et * sont, en quelque sorte, réciproques l'un de l'autre, car si le premier
permet d'accéder à l'adresse d'une variable, le second permet d'accéder au contenu d'une
adresse (voir aussi le Tableau 2.1 et la Figure 2.6).
Grâce, au passage de paramètre par adresse, une procédure peut avoir des eets de
bord, puisqu'elle peut modier les variables d'une autre routine.

Exemple 4. On se propose d'échanger les valeurs de deux variables saisies par l'utilisateur.
Pour ce faire, on utilise une procédure, Swap2, ayant deux paramètres passés par adresse,

1 La notion de pointeur sera étudiée en détail dans le Chapitre 3


10 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

x &x *x
t t* ?
t* t** t
Table 2.1: Types des objets obtenues par l'utilisation des opérateurs & et *.

1 #include <s t d i o . h>


2
3 int main ( )
4 {
5 int var , ∗ p t r ;
6
7 var = 2 0 1 7 ;
8 p t r = &var ;
9
10 p r i n t f ( "%d %d %p %p\n" , var , ∗ ptr ,& var , p t r ) ;
11 var −= 2 ;
12 ∗ p t r += 3 ;
13 p r i n t f ( "%d %d %d %p\n" , var , ∗ ptr , ∗ & var ,& ∗ p t r ) ;
14 // Ce d e r n i e r a f f i c h a g e p r o d u i t : 2 0 1 8 2 0 1 8 2 0 1 8 et l ' adresse de la

variable var

15 return 0;
16 }

Figure 2.6: Manipulation d'adresse.


2.5. DES FONCTIONS EN PARAMÈTRE ! 11

Routine appelante Routine appelée


param. e1 param. form1
copie de l'adresse
5
 7 @

param. e2 param. form2


copie de l'addresse
7
 5 @

Figure 2.7: Schéma illustrant le passage de paramètres par adresse. Les changements
opérés par la procédure Swap2 sont directement eectués sur les paramètres eectifs.

no. lig. 1 2 3 5 6 7 8 4

a ? 5 5 5 5 7 7 7
b ? 7 7 7 7 7 5 5
tmp - - - ? 5 5 5 -

Figure 2.8: Trace de l'exécution de la procédure Swap2 du programme de la Figure 2.4,


avec 5 et 7 comme valeurs saisies pour les entiers a et b. On constate que les valeurs des
variables a et b du programme principal ont bien été échangées.

(voir le programme de la Figure 2.4). Avec ce mode de passage de paramètres, les variables
a et b du programme principal ont bien été échangées, comme le montre le Tableau 2.8
qui résume la trace d'exécution de la procédure Swap2.

2.5 Des fonctions en paramètre !

Les fonctions sont des séquences d'instructions qui, lors de l'exécution du programme,
se trouvent stockées en mémoire vive, tout comme les données. Chaque fonction doit
donc avoir un nom qui la distingue des autres fonctions. Ce nom correspondra, lors de
l'exécution, à l'adresse du début de la zone mémoire contenant la fonction. Par ailleurs, il
est possible de dénir des variables de type fonction. Une telle variable peut donc stocker
tantôt le nom d'une fonction tantôt le nom d'une autre. L'utilisation d'une variable de
type fonction comme paramètre d'une tierce fonction permettra à cette dernière d'appeler
diérentes fonctions selon la valeur du paramètre qu'elle reçoit. L'avantage d'un tel
mécanisme est de développer du code générique qui s'exécutera de diérentes manières
selon le besoin.
Avec le langage C, l'entête d'une fonction (foo) qui reçoit, comme paramètre, une
fonction (f), s'écrit comme suit:

type foo(. . .,type_retour f(list_type_param),. . .)


L'appel de la fonction foo se fait alors par

foo(. . .,g,. . .)
où g est la fonction paramètre eectif qui doit avoir la même signature que la fonction
paramètre formel f.
12 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

1 #include <s t d i o . h>


2 #include <math . h>
3 #define p i M_PI
4
5 double I n t e g r a l e R e c t a n g l e ( double f ( double ) , double xMin , double
xMax , int pas )
6 {
7 int i ;
8 double dx=(xMax−xMin ) / pas , som =0.0 , x=xMin+0.5 ∗ dx ;
9
10 for ( i =0; i <pas ; i ++, x+=dx )
11 som += f ( x ) ;
12 return dx ∗ som ;
13 }
14
15 double i n v e r s e ( double x)
16 {
17 return 1/ x ;
18 }
19
20 int main ( )
21 {
22 p r i n t f ( "%l f \n" , I n t e g r a l e R e c t a n g l e ( s q r t , 0 , 1 , 1 0 0 ) ) ;
23 p r i n t f ( "%l f \n" , I n t e g r a l e R e c t a n g l e ( i n v e r s e , 1 , 2 , 1 0 0 ) ) ;
24 p r i n t f ( "%l f \n" , I n t e g r a l e R e c t a n g l e ( s i n , 0 , p i / 2 , 1 0 0 ) ) ;
25 p r i n t f ( "%l f \n" , I n t e g r a l e R e c t a n g l e ( cos , 0 , p i / 2 , 1 0 0 ) ) ;
26 return 0;
27 }

Figure 2.9: Programme d'approximation de l'intégrale de fonctions mathématiques par la


méthode des rectangles.

Nous illustrons le passage des fonctions C comme paramètre dans le contexte de


l'intégration des fonctions mathématiques. Pour ce faire, nous utilisons la méthode des
rectangles pour approximer l'intégrale de fonctions données entre deux bornes données, en
utilisant des rectangle de largeur donnée. Pour ce faire, nous utilisons la fonction générique
IntegraleRectangle() dénie dans le programme de la Figure 2.9. On peut vérier que
les deux fonctions paramètres eectifs des deux appels à IntegraleRectangle(), (voir
lignes 21 et 22), ont la même signature que le paramètre formel de IntegraleRectangle(),
à savoir (double) f(double).

2.6 Variables globales vs variables locales

Avec le langage C, il est possible de déclarer des variables à l'extérieur de toute fonctions,
en général, au début du code juste après les directives. De telles variables sont alors
visibles par toutes les fonctions du programme et on les désigne par variables globales. Ceci
implique qu'il est théoriquement possible de les utiliser dans les diérentes fonctions du
2.6. VARIABLES GLOBALES VS VARIABLES LOCALES 13

1 #include <s t d i o . h>


2
3 int a , b ; // Déclaration de deux variables globales

4
5 void Swap ( int ∗ adra , int ∗ adrb )
6 {
7 // Déclaeation d ' u ne variable locale b

8 // qui masque la variable globale b

9 int b;
10
11 b = ∗ adra ;
12 ∗ adra = ∗ adrb ;
13 ∗ adrb = b ;
14 }
15
16 int main ( )
17 {
18 p r i n t f ( " S a i s i s s e z deux e n t i e r s : " ) ;
19 s c a n f ( "%d %d" ,&a ,&b ) ;
20 Swap(&a ,&b ) ; // P a s s a g e p a r v a l e u r
21 p r i n t f ( "%d %d" , a , b ) ;
22
23 return 0;
24 }

Figure 2.10: Programme illustrant le masquage de variables globales par des variables
locales.

programme. Néanmoins, il est rare de se trouver dans une situation où il est indispensable
d'utiliser des variables globales.

Règle 3. Il est préférable d'éviter, autant que possible, l'utilisation des variables
globales.

Par ailleurs, dans toute routine, (fonction ou procédure), il est possible de déclarer des
variables dites locales à la routine. De telles variables ne peuvent être utilisées que dans
le corps de la routine où elles sont déclarées.
Si par hasard, des variables globales portent les mêmes noms que des variables locales
à une routine R alors les variables globales seront masquées pendant le déroulement de
la routine R et on ne pourra y accéder de nouveau qu'à la n du déroulement de cette
dernière. Dans le programme de la Figure 2.10, par exemple, la variable locale b de la
procédure Swap masque la variable globale de même nom. Ainsi, la valeur de la variable
globale b ne sera pas écrasée par l'aectation de la Ligne 11 et l'échange de valeur entre
les variables a et b peut avoir lieu correctement.
En fait, dès qu'une routine termine de se dérouler, les variables locales à cette rou-
tine n'ont plus d'existence, et ne pourront donc plus masquer d'autres variables, jusqu'au
prochain appel de la routine. Signalons que les variables globales peuvent aussi être
14 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

masquées par des paramètres de routine qui portent les mêmes noms. Néanmoins, il est
recommander de choisir les noms des variables globales et locales ainsi que les noms des
paramètres formels des diérents routines de façon à éviter les masquages. Remarquons
aussi que si l'on s'interdit d'utiliser des variables globales (Règle 3) alors la plupart des
situations de masquage sont évitées.

Soit une routine R d'un programme P, alors il est possible de partitionner l'ensemble
de toutes les variables intervenant dans le programme P en deux sous-ensembles en se
référant à la routine R comme suit:

Dénition 1. Le contexte d'une routine est constitué de l'ensemble des paramètres


de cette routine, de ses variables locales ainsi que des variables globales non
masquées.

À titre d'exemple, le contexte de la fonction Swap du programme de la Figure 2.10 se


compose des paramètres adra et adrb, de la variable locale b et de la variable globale non
masquées a.

Règle 4. Une routine ne peut utiliser que des variables de son propre contexte.

2.7 La récursivité

La récursivité est une technique de programmation simple et élégante qui permet de


résoudre des problèmes informatiques qui sont, parfois, bien complexes.

2.7.1 Routines récursives


Précisons, tout d'abord, la particularité qui fait qu'une routine soit récursive:

Dénition 2. Une routine est dite récursive si elle s'appelle elle même.

L'appel d'une routine récursive est, en général, déclenché par une autre routine. Ce
premier appel déclenche une séquence d'appels récursifs. L'exécution d'une séquence
d'appels récursifs déclenché par un même appel initial suit le principe du  dernier arrivé,
premier servi , c'est-à-dire que les appels récursifs sont terminés dans l'ordre inverse de
leur déclenchement. Ainsi l'appel récursif eectué en premier sera terminé en dernier et
2
inversement. Ceci implique que le contexte d'un appel récursif donné doit être sauveg-
ardé quelque part jusqu'à ce que tous les appels récursifs qui lui ont succédé terminent.
Pour sauvegarder ces contextes d'exécution, on utilise une structure de donnée particulière
qui s'appelle la pile d'exécution. La nécessité de la sauvegarde des contextes d'exécution
est derrière le principal inconvénient de la technique de la récursivité. C'est que cette
dernière fait intervenir un mécanisme d'exécution très consommateur d'espace mémoire,
en particulier, quand il s'agit d'exécuter de longues séquences d'appel récursifs, avec des
contextes d'exécution de grande taille.

2 Rappelons qu'un tel contexte contient des paramètres et des variables locales de cet appel
2.7. LA RÉCURSIVITÉ 15

Context appel n

.
.
.

Context appel 2

Context appel 1

Figure 2.11: Pile des appels récursifs.

Par ailleurs, toute fonction ou procédure récursive doit comporter une instruction (ou
un bloc d'instructions) nommée point terminal. Le point terminal permet d'arrêter la
séquence d'appels récursifs. Donc, l'appel d'une routine récursive qui ne contient pas de
point terminal déclenche une suite d'appels récursifs qui ne termine jamais.

Exemple 5. Il s'agit d'une fonction récursive qui calcule les valeurs d'une suite mathé-
matique connue sous le nom de suite de Syracuse, en référence à la ville américaine qui
porte le même nom. Les termes de cette suite sont dénis par:

 1 si n=1
un = un/2 si n ≡ 0 [2]
u3n+1 sinon

La particularité de la suite de Syracuse c'est que sa convergence vers le nombre 1 n'est


qu'une conjecture. C'est-à-dire qu'il n'est pas prouvé que la suite converge vers 1. Le
programme de la Figure 2.12 est supposée calculer les termes un de la suite de Syracuse
partant d'un entier n donné:

Exemple 6. L'une des énigmes classiques qui se résolvent aisément en utilisant une
récursion est celle connue sous le nom des tours de Hanoi. Il s'agit de faire déplacer
un ensemble de n disques, tous de diamètres diérents, d'un pieu de départ vers un pieu
d'arrivée. La diculté de la tâche provient du fait qu'il est interdit d'empiler un disque
de diamètre plus grand sur un disque de diamètre plus petit. On dispose, néanmoins, d'un
pieu supplémentaire qui va permettre de contourner la diculté (voir la Figure 2.13 pour
une solution de l'énigme pour le cas n=3 disques).
Le programme C de la Figure 2.14 utilise une procédure récursive ( Hanoi), qui construit
et ache la suite des déplacements valides à eectuer pour faire passer la pile de disques
du pieu A au pieu C. Le nombre de disques est donné par le paramètre n. La concision
de cette procédure est remarquable, si on tient compte de la diculté de la résolution de
l'énigme.
16 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

1 #include <s t d i o . h>


2
3 long int S y r a c u s e ( long int n)
4 {
5 p r i n t f ( "%l d " , n ) ;
6
7 if ( n == 1 )
8 return 1 ;
9 else
10 if ( n%2 == 0 )
11 return S y r a c u s e ( n / 2 ) ;
12 else
13 return S y r a c u s e ( 3 ∗ n+1) ;
14 }
15
16 int main ( )
17 {
18 long int n;
19
20 do{
21 p r i n t f ( "Donnez un e n t i e r p o s i t i f : " ) ;
22 s c a n f ( "%l d " ,&n ) ;
23 }while ( n == 0 ) ;
24
25 p r i n t f ( "%l d \n" , S y r a c u s e ( n ) ) ;
26
27 return 0;
28 }

Figure 2.12: Une fonction récursive dont l'exécution pourrait ne pas s'arrêter.

Figure 2.13: Résolution du problème des tours de Hanoi pour n=3 disques.
2.7. LA RÉCURSIVITÉ 17

1 #include <s t d i o . h>


2
3 void Hanoi ( int n , char A, char B, char C)
4 {
5 if ( n > 0 )
6 {
7 Hanoi ( n − 1,A, C, B) ;
8 p r i n t f ( "D%c p l a c e z l e d i s q u e de %c v e r s %c \n" , 1 3 0 ,A, C) ;
9 Hanoi ( n − 1,B, A, C) ;
10 }
11 }
12
13 int main ( )
14 {
15 int n ;
16
17 p r i n t f ( " S a i s i s s e z l e nombre de d i s q u e : " ) ;
18 s c a n f ( "%d" ,&n ) ;
19 Hanoi ( n , ' a ' , ' b ' , ' c ' ) ;
20
21 return 0;
22 }

Figure 2.14: Un programme C qui résout l'énigme des tours de Hanoi.

Exemple 7. (Le tri par fusion) L'une des opérations les plus classiques en programmation
consiste à trier un tableau uni-dimensionnel dans le sens croissant ou décroissant. On
voudrait alors étudier un tri qui soit plus ecace que les tris de complexité quadratique
tel que le tri à bulles, le tri par insertion ou le tri par sélection. Pour ce faire, on choisit
d'étudier le tri par fusion. Ainsi, pour trier un tableau de n éléments, on procède en le
scindant en deux moitiés, de taille quasi-égale qu'on trie de manière indépendante. Une
fois, les deux moitiés du tableau triées, il est possible de les fusionner en un seul tableau
trié en un temps linéaire, O(n). Pour trier les deux moitiés de tableau, on peut appliquer
la même stratégie, de manière récursive, et ainsi de suite, jusqu'à ce que l'on atteigne des
tableaux de taille 1, qui sont triés d'oce.

En résumé, le tri par fusion peut être réalisé par une procédure récursive qui décompose
le problème en deux sous-problèmes de taille quasiment égales puis combine les solutions
des deux sous-problèmes en un temps linéaire, O(n), d'où une complexité en O(n log n).
Le programme C de la Figure 2.15, contient cinq procédures, dont deux qui mettent en
÷uvre le principe du tri par fusion. La première procédure, TriFusion qui est récursive,
procède en décomposant le problème en deux puis en combinant les solutions des deux
sous-problèmes en faisant appel à La deuxième procédure, Fusion. Cette dernière a pour
tâche de fusionner deux tableaux qui sont supposés être triés. Un exemple d'une telle
fusion est le suivant:
Avant fusion
G= 0 3 4 5 7 9 D= 1 2 5 6 7 8
Après fusion
18 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

G= 0 1 2 3 4 5 D= 5 6 7 7 8 9

Exercice 3. On désire calculer la nème puissance d'une matrice binaire particulière:


 n
n 0 1
B =
1 1

• Proposez une procédure C qui reçoit, comme paramètres, les 4 composantes,


a, b, c, d d'une matrice entière 2 × 2, et qui calcule le produit matriciel suivant
dans ces mêmes paramètres:
  
a b 0 1
c d 1 1

• En déduire un programme C qui calcule Bn.

Exercice 4. (La fonction d'Ackermann) L'une des fonctions les plus étudiée en
informatique théorique est celle d'Ackermann. Cette fonction est dénie comme
suit:

Ack(0, m) = m + 1
Ack(n + 1, 0) = Ack(n, 1)
Ack(n + 1, m + 1) = Ack(n, Ack(n + 1, m))

Proposez une fonction C récursive qui calcule la valeur de la fonction d'Ackermann


pour un n et un m donnée.
2.7. LA RÉCURSIVITÉ 19

1 #include <s t d i o . h>


2 #define MAXTAIL 128
3
4 void S a i s i e T a b l e a u ( int tab [ ] , int n)
5 {
6 int i ;
7
8 for ( i =0; i <n ; i ++)
9 {
10 p r i n t f ( "Donnez l '% c l%cment %d : " , 1 3 0 , 1 3 0 , i ) ;
11 s c a n f ( "%d" ,& tab [ i ] ) ;
12 }
13 }
14
15 void A f f i c h e T a b l e a u ( int tab [ ] , int n)
16 {
17 int i ;
18
19 for ( i =0; i <n ; i ++)
20 p r i n t f ( "%d " , tab [ i ] ) ;
21 }
22
23
24 void Fusion ( int tab [ ] , int aux [ ] , int debG , int finG , int debD , int
finD )
25 {
26 int
i , j ,k;
27
28 i = debG ;
29 j = debD ;
30 k = debG ;
31
32 while
( i <= finG && j <= finD )
33 if
( tab [ i ] <= tab [ j ] )
34 aux [ k++] = tab [ i ++];
35 else
36 aux [ k++] = tab [ j ++];
37
38 while
( i <= finG )
39 aux [ k++] = tab [ i ++];
40
41 while
( j <= finD )
42 aux [ k++] = tab [ j ++];
43
44 for
( i=debG ; i<=finD ; i ++)
45 tab [ i ] = aux [ i ] ;

Figure 2.15: Un programme C réalisant le tri par fusion.


20 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

46 }
47
48 void T r i F u s i o n ( int tab [ ] , int aux [ ] , int deb , int fin )
49 {
50 int m;
51
52 if ( deb < f i n )
53 {
54 m = ( deb + f i n ) / 2 ;
55 T r i F u s i o n ( tab , aux , deb ,m) ;
56 T r i F u s i o n ( tab , aux ,m+1, f i n ) ;
57 Fusion ( tab , aux , deb ,m,m+1, f i n ) ;
58 }
59 }
60
61
62 int main ( )
63 {
64 int n , tab [MAXTAIL] , aux [MAXTAIL ] ;
65
66 do{
67 p r i n t f ( "Donnez l e nombre d'% c l%cment %c t r i e r : " , 1 3 0 , 1 3 0 , 1 3 3 ) ;
68 s c a n f ( "%d" ,&n ) ;
69 } while ( n > MAXTAIL) ;
70
71 S a i s i e T a b l e a u ( tab , n ) ;
72 T r i F u s i o n ( tab , aux , 0 , n − 1) ;
73 A f f i c h e T a b l e a u ( tab , n ) ;
74 return 0;
75 }

Figure 2.16: Un programme C réalisant le tri par fusion (suite et n).


2.7. LA RÉCURSIVITÉ 21

Exercice 5. Le tri rapide (quicksort), comme le tri par fusion, s'appuie sur
la stratégie diviser pour régner. Ainsi, pour trier les éléments d'un tableau
tab[deb..fin] dont les indices sont comprit entre un indice de début et un in-
dice de n, le tri rapide procède comme suit: Le tableau T[deb..fin] est scindé
en deux sous-tableaux tab[deb..m-1] et tab[m..fin], où m est un entier comprit
entre deb et fin. Les éléments des deux sous-tableaux sont réarrangés de telle sorte
que:

tous les éléments de tab[deb..m-1] sont inférieur ou égal à tab[m] qui, à son
tour, doit être inférieur ou égal à tous les éléments de tab[m+1..fin].
Le tri des deux sous-tableaux tab[deb..m-1] et tab[m+1..fin] sont, ensuite,
assuré par deux appels récursifs à la même routine de tri rapide. À la diérence
du tri par fusion, le tableau tab[deb..fin] se trouve trié du moment que les deux
sous-tableaux sont triés.

Un point crucial du tri rapide est le choix de l'indice m au niveau duquel le


tableau tab[deb..fin] est scindé en deux. Pour des raisons de simplicité, on
suppose que cet indice correspond à la position nale de l'élément tab[fin], qui
est alors désigné par le pivot, dans le tableau tab[deb..fin].
• Proposez une fonction C qui permet de réarranger un tableau tab[deb..fin]
de sorte que la condition du partitionnement décrite ci-dessus soit vériée, et
qui retourne la position nale de tab[fin] dans tab[deb..fin].
• Proposez une procédure récursive qui met en ÷uvre le principe du tri rapide.

• Complétez le programme C pour qu'il eectue le tri rapide d'un tableau


d'entiers données ayant une taille données.

2.7.2 Récursion terminale et récursivité croisée


Parmi les routines récursives, on peut distinguer celles qui sont à récursion terminale
(voir Chapitre 2 du cours d'algorithmique). Rappelons qu'une routine récursive est dite
à récursion terminale si l'appel récursif est la dernière instruction exécutée par la routine.
Le programme de la Figure 2.12, qui permet l'achage de la suite de Syracuse utilise
une fonction récursive, (Syracuse), qui est à récursion terminale, car les deux appels
récursifs terminent la fonction. Ce n'est pas le cas de la procédure récursive Hanoi, (voir
Figure 2.14), qui n'est pas à récursion terminale, car après le premier appel récursif, la
procédure exécute d'autres instructions.
Les récursions terminale présentent beaucoup d'avantages, parmi lesquels la possibilité
de les dérécursier (voir le Chapitre 2 du cours d'algorithmique).

Exemple 8. En appliquant la technique de dérécursication des fonctions récursives ter-


minale, (voir Chapitre 2 du cours d'algorithmique), à la fonction Syracuse qui est à récur-
sion terminale, on obtient le programme C de la Figure 2.17.
22 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

1 #include <s t d i o . h>


2 #ifndef t r u e
3 #define t r u e 1
4 #endif
5
6 long int S y r a c u s e I t e r ( long int n)
7 {
8 do
9 {
10 p r i n t f ( "%l d " , n ) ;
11
12 if ( n == 1 ) return 1;
13
14 if ( n%2 == 0 )
15 n /= 2 ;
16 else
17 n = 3∗ n + 1 ;
18 } while ( t r u e ) ;
19 }
20
21 int main ( )
22 {
23 long int n;
24
25 do{
26 p r i n t f ( "Donnez un e n t i e r p o s i t i f : " ) ;
27 s c a n f ( "%l d " ,&n ) ;
28 } while ( n == 0 ) ;
29
30 p r i n t f ( "%l d \n" , S y r a c u s e I t e r ( n ) ) ;
31
32 return 0;
33 }

Figure 2.17: Une version itérative de la fonction Syracuse.


2.8. CONCLUSION 23

Exercice 6. Proposez une fonction C à récursion terminale pour le calcul de la


factoriel d'un entier naturel, puis en déduire une version itérative en procédant à
une dérécursication.

Il existe une forme indirecte de récursivité où il ne n'agit pas d'une fonction qui
s'appelle elle même mais plutôt de deux fonctions qui s'appellent mutuellement. Cette
technique, appelée récursivité croisée, est illustrée par le programme de la Figure 2.18, qui
permet de tester la parité d'un entier donné en utilisant deux fonctions, pair et impair,
qui s'appellent mutuellement.

2.8 Conclusion

La conception de programmes procéduraux préconise de résoudre des problèmes infor-


matiques en précisant à la machine comment elle doit procéder. Quand les problèmes à
résoudre commencent à être complexes, la conception de programmes qui apportent la
solution est moins évidente. On pourra alors procéder comme suit:

1. Identier précisément le problème que le programme se doit de résoudre. On doit,


en particulier, identier les données du problème ainsi que les résultats qui doivent
être calculés.

2. Décomposer le problème initial en des sous-problèmes plus simples.

3. Créer la routine appropriée (procédure ou fonction) pour chacun des sous-problèmes.


Pour ce faire, on est amené à déterminer les méthodes de calcul qui permettent de
passer des données à la solution de chaque sous-problème.

4. Écrire le programme principale qui aura pour tâche de récupérer les solutions des
sous-problèmes et de les combiner en une solution pour le problème initial.

5. Tester le programme obtenu.


24 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

1 #include <s t d i o . h>


2 #ifndef t r u e
3 #define t r u e 1
4 #define f a l s e 0
5 #endif
6
7 unsigned short i m p a i r ( unsigned int ) ;
8
9 unsigned short p a i r ( unsigned int n )
10 {
11 if ( n==0)
12 return t r u e ;
13 else
14 return i m p a i r ( n −1) ;
15 }
16
17 unsigned short i m p a i r ( unsigned int n)
18 {
19 if ( n==0)
20 return f a l s e ;
21 else
22 return p a i r ( n −1) ;
23 }
24
25 int main ( )
26 {
27 unsigned int n;
28
29 do{
30 p r i n t f ( "Donnez un e n t i e r p o t i s i f : " ) ;
31 s c a n f ( "%d" ,&n ) ;
32 }while ( n<0) ;
33
34 if ( p a i r ( n ) )
35 p r i n t f ( "L ' e n t i e r %d e s t p a i r \n" , n ) ;
36 else
37 p r i n t f ( "L ' e n t i e r %d e s t i m p a i r \n" , n ) ;
38
39 return 0;
40 }

Figure 2.18: Test de parité par la technique de la récursivité croisée.

Vous aimerez peut-être aussi