Académique Documents
Professionnel Documents
Culture Documents
Conception de programmes
procéduraux
2.1 Motivation
1
2 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX
.
.
. float SaisieReelNonNul()
do {
{ float x;
.
.
}
.
while (abs(x) < eps);
do return x;
{ }
printf("Donnez un réel non nul"); .
.
.
scan("%f",&x10);
} x1 = SasieReelNonNul();
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 ;
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.
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
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
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.
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
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 }
5 copie
5
7
param. e2 param. form2
7 copie
7
5
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.
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 -
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.
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,
x &x *x
t t* ?
t* t** t
Table 2.1: Types des objets obtenues par l'utilisation des opérateurs & et *.
variable var
15 return 0;
16 }
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 -
(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.
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:
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
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
4
5 void Swap ( int ∗ adra , int ∗ adrb )
6 {
7 // Déclaeation d ' u ne variable locale 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:
Règle 4. Une routine ne peut utiliser que des variables de son propre contexte.
2.7 La récursivité
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
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
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
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
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 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))
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 }
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.
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
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.