Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Prog Proc Chap 2

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 21

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 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.6).
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

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
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.

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_du_résultat nom_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

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 sous-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
scalaire de deux vecteurs 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
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
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.

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.

Règle 2. Les paramètres formels d'une routine et les paramètres eectifs correspondants
doivent coïncider en nombre et en type.
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.

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.

Exercice 1. (Nombres amicaux) Deux entiers naturels sont dit amicaux s'il sont dis-
tincts 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.
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

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

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 (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,
(voir le programme de la Figure 2.4). Avec ce mode de passage de paramètres, les variables

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


10 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

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.

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.

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. VARIABLES GLOBALES VS VARIABLES LOCALES 11

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.

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.

2.5 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
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.9, 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
12 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

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 ' une 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.9: Programme illustrant le masquage de variables globales par des variables
locales.
2.6. LA RÉCURSIVITÉ 13

prochain appel de la routine. Signalons que les variables globales peuvent aussi être
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.9 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.6 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.6.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.
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

2 Rappelons qu'un tel contexte contient des paramètres et des variables locales de cet appel
14 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.11: Une fonction récursive dont l'exécution pourrait ne pas s'arrêter.

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.11 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
2.6. LA RÉCURSIVITÉ 15

Figure 2.12: Résolution du problème des tours de Hanoi pour n=3 disques.

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.12 pour
une solution de l'énigme pour le cas n=3 disques).
Le programme C de la Figure 2.13 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.

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.14, 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
16 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX

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.13: Un programme C qui résout l'énigme des tours de Hanoi.

G= 0 3 4 5 7 9 D= 1 2 5 6 7 8
Après fusion
G= 0 1 2 3 4 5 D= 5 6 7 7 8 9

Exercice 4. (La fonction d'Ackermann) L'une des fonctions les plus étudiée en informa-
tique 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.6. LA RÉCURSIVITÉ 17

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.14: Un programme C réalisant le tri par fusion.


18 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.15: Un programme C réalisant le tri par fusion (suite et n).


2.6. LA RÉCURSIVITÉ 19

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 indice 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.6.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.11, 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.13), 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.16.

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écursi-
cation.
20 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.16: Une version itérative de la fonction Syracuse.


2.7. CONCLUSION 21

Exercice 7. Codez, dans le langage C, le test de parité qui utilise la récursivité croisée
(voir le Chapitre 2 du cours d'algorithmique).

2.7 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.

Vous aimerez peut-être aussi