Programmer

publicité
���������������
�
����������
��
�������
���������������������������
5e édition
2009
10 tirage 2004
e
© Groupe Eyrolles, 1992-2009,
ISBN : 978-2-212-12546-7
Delannoy Livre.book Page 93 Mercredi, 6. mai 2009 4:26 16
Chapitre 6
La programmation modulaire
et les fonctions
Comme tous les langages, C permet de découper un programme en plusieurs parties nommées
souvent « modules ». Cette programmation dite modulaire se justifie pour de multiples raisons :
●
Un programme écrit d’un seul tenant devient difficile à comprendre dès qu’il dépasse une
ou deux pages de texte. Une écriture modulaire permet de le scinder en plusieurs parties et
de regrouper dans le programme principal les instructions en décrivant les enchaînements.
Chacune de ces parties peut d’ailleurs, si nécessaire, être décomposée à son tour en modules plus élémentaires ; ce processus de décomposition pouvant être répété autant de fois
que nécessaire, comme le préconisent les méthodes de programmation structurée.
●
La programmation modulaire permet d’éviter des séquences d’instructions répétitives, et
cela d’autant plus que la notion d’argument permet de paramétrer certains modules.
●
La programmation modulaire permet le partage d’outils communs qu’il suffit d’avoir écrits
et mis au point une seule fois. Cet aspect sera d’autant plus marqué que C autorise effectivement la compilation séparée de tels modules.
© Éditions Eyrolles
93
Delannoy Livre.book Page 94 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
1 La fonction : la seule sorte de module existant en C
Dans certains langages, on trouve deux sortes de modules, à savoir :
●
Les fonctions, assez proches de la notion mathématique correspondante. Notamment, une
fonction dispose d’arguments (en C, comme dans la plupart des autres langages, une fonction peut ne comporter aucun argument) qui correspondent à des informations qui lui sont
transmises et elle fournit un unique résultat scalaire (simple) ; désigné par le nom même de
la fonction, ce dernier peut apparaître dans une expression. On dit d’ailleurs que la fonction possède une valeur et qu’un appel de fonction est assimilable à une expression.
●
Les procédures (terme Pascal) ou sous-programmes (terme Fortran ou Basic) qui élargissent
la notion de fonction. La procédure ne possède plus de valeur à proprement parler et son
appel ne peut plus apparaître au sein d’une expression. Par contre, elle dispose toujours
d’arguments. Parmi ces derniers, certains peuvent, comme pour la fonction, correspondre
à des informations qui lui sont transmises. Mais d’autres, contrairement à ce qui se passe
pour la fonction, peuvent correspondre à des informations qu’elle produit en retour de son
appel. De plus, une procédure peut réaliser une action, par exemple afficher un message
(en fait, dans la plupart des langages, la fonction peut quand même réaliser une action,
bien que ce ne soit pas là sa vocation).
En C, il n’existe qu’une seule sorte de module, nommé fonction (il en ira de même en C++ et
en Java, langage dont la syntaxe est proche de celle de C). Ce terme, quelque peu abusif, pourrait laisser croire que les modules du C sont moins généraux que ceux des autres langages. Or
il n’en est rien, bien au contraire ! Certes, la fonction pourra y être utilisée comme dans
d’autres langages, c’est-à-dire recevoir des arguments et fournir un résultat scalaire qu’on
utilisera dans une expression, comme, par exemple, dans :
y = sqrt(x)+3 ;
Mais, en C, la fonction pourra prendre des aspects différents, pouvant complètement dénaturer
l’idée qu’on se fait d’une fonction. Par exemple :
●
La valeur d’une fonction pourra très bien ne pas être utilisée ; c’est ce qui se passe fréquemment lorsque vous utilisez printf ou scanf. Bien entendu, cela n’a d’intérêt que
parce que de telles fonctions réalisent une action (ce qui, dans d’autres langages, serait
réservée aux sous-programmes ou procédures).
●
Une fonction pourra ne fournir aucune valeur.
●
Une fonction pourra fournir un résultat non scalaire (nous n’en parlerons toutefois que
dans le chapitre consacré aux structures).
●
Une fonction pourra modifier les valeurs de certains de ses arguments (il vous faudra toutefois attendre d’avoir étudié les pointeurs pour voir par quel mécanisme elle y parviendra).
Ainsi, donc, malgré son nom, en C, la fonction pourra jouer un rôle aussi général que la procédure ou le sous-programme des autres langages.
94
© Éditions Eyrolles
Delannoy Livre.book Page 95 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
Par ailleurs, nous verrons qu’en C plusieurs fonctions peuvent partager des informations,
autrement que par passage d’arguments. Nous retrouverons la notion classique de « variables
globales » (en Basic, toutes les variables sont globales, de sorte qu’on ne le dit pas - en Fortran, ces variables globales sont rangées dans des « COMMON »).
Enfin, l’un des atouts du langage C réside dans la possibilité de compilation séparée. Celleci permet de découper le programme source en plusieurs parties, chacune de ces parties pouvant comporter une ou plusieurs fonctions. Certains auteurs emploient parfois le mot
« module » pour désigner chacune de ces parties (stockées dans un fichier) ; dans ce cas, ce
terme de module devient synonyme de fichier source. Cela facilite considérablement le développement et la mise au point de grosses applications. Cette possibilité crée naturellement
quelques contraintes supplémentaires, notamment au niveau des variables globales que l’on
souhaite partager entre différentes parties du programme source (c’est d’ailleurs ce qui justifiera
l’existence de la déclaration extern).
Pour garder une certaine progressivité dans notre exposé, nous supposerons tout d’abord que
nous avons affaire à un programme source d’un seul tenant (ce qui ne nécessite donc pas de
compilation séparée). Nous présenterons ainsi la structure générale d’une fonction, les notions
d’arguments, de variables globales et locales. Ce n’est qu’alors que nous introduirons les
possibilités de compilation séparée en montrant quelles sont ses incidences sur les points
précédents ; cela nous amènera à parler des différentes « classes d’allocation » des variables.
2 Exemple de définition et d’utilisation d’une fonction en C
Nous vous proposons d’examiner tout d’abord un exemple simple de fonction correspondant à
l’idée usuelle que l’on se fait d’une fonction, c’est-à-dire recevant des arguments et fournissant
une valeur.
Exemple de définition et d’utilisation d’une fonction
#include <stdio.h>
/***** le programme principal (fonction main) *****/
main()
{
float fexple (float, int, int) ; /* déclaration de fonction fexple */
float x = 1.5 ;
float y, z ;
int n = 3, p = 5, q = 10 ;
/* appel de fexple avec les arguments x, n et p */
y = fexple (x, n, p) ;
printf ("valeur de y : %e\n", y) ;
© Éditions Eyrolles
95
Delannoy Livre.book Page 96 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
Exemple de définition et d’utilisation d’une fonction (suite)
/* appel de fexple avec les arguments x+0.5, q et n-1 */
z = fexple (x+0.5, q, n-1) ;
printf ("valeur de z : %e\n", z) ;
}
/*************** la fonction fexple ****************/
float fexple (float x, int b, int c)
{ float val ;
/* déclaration d’une variable "locale" à fexple
val = x * x + b * x + c ;
return val ;
}
Nous y trouvons tout d’abord, de façon désormais classique, un programme principal formé
d’un bloc. Mais, cette fois, à sa suite, apparaît la définition d’une fonction. Celle-ci possède
une structure voisine de la fonction main, à savoir un en-tête et un corps délimité par des
accolades ({ et }). Mais l’en-tête est plus élaboré que celui de la fonction main puisque, outre
le nom de la fonction (fexple), on y trouve une liste d’arguments (nom + type), ainsi que le
type de la valeur qui sera fournie par la fonction (on la nomme indifféremment « résultat »,
« valeur de la fonction », « valeur de retour »...) :
float
|
type de la
"valeur
de retour"
fexple
|
nom de la
fonction
(float x,
|
premier
argument
(type float)
int b,
|
deuxième
argument
(type int)
int c)
|
troisième
argument
(type int)
Les noms des arguments n’ont d’importance qu’au sein du corps de la fonction. Ils servent à
décrire le travail que devra effectuer la fonction quand on l’appellera en lui fournissant trois
valeurs.
Si on s’intéresse au corps de la fonction, on y rencontre tout d’abord une déclaration :
float val ;
Celle-ci précise que, pour effectuer son travail, notre fonction a besoin d’une variable de type
float nommée val. On dit que val est une variable locale à la fonction fexple, de même
que les variables telles que n, p, y... sont des variables locales à la fonction main (mais
comme jusqu’ici nous avions affaire à un programme constitué d’une seule fonction, cette
distinction n’était pas utile). Un peu plus loin, nous examinerons plus en détail cette notion de
variable locale et celle de portée qui s’y attache.
L’instruction suivante de notre fonction fexple est une affectation classique (faisant toutefois
intervenir les valeurs des arguments x, n et p).
96
© Éditions Eyrolles
Delannoy Livre.book Page 97 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
Enfin, l’instruction return val précise la valeur que fournira la fonction à la fin de son travail.
En définitive, on peut dire que fexple est une fonction telle que fexple (x, b, c) fournisse la valeur de l’expression x2 + bx + c. Notez bien l’aspect arbitraire du nom des
arguments ; on obtiendrait la même définition de fonction avec, par exemple :
float fexple (float z, int coef, int n)
{
float val ; /* déclaration d’une variable "locale" à fexple */
val = z * z + coef * z + n ;
return val ;
}
Examinons maintenant la fonction main. Vous constatez qu’on y trouve une déclaration :
float fexple (float, int, int) ;
Elle sert à prévenir le compilateur que fexple est une fonction et elle lui précise le type de
ses arguments ainsi que celui de sa valeur de retour. Nous reviendrons plus loin en détail sur le
rôle d’une telle déclaration.
Quant à l’utilisation de notre fonction fexple au sein de la fonction main, elle est classique
et comparable à celle d’une fonction prédéfinie telle que scanf ou sqrt. Ici, nous nous sommes contenté d’appeler notre fonction à deux reprises avec des arguments différents.
3 Quelques règles
3.1 Arguments muets et arguments effectifs
Les noms des arguments figurant dans l’en-tête de la fonction se nomment des « arguments
muets », ou encore « arguments formels » ou « paramètres formels » (de l’anglais : formal
parameter). Leur rôle est de permettre, au sein du corps de la fonction, de décrire ce qu’elle
doit faire.
Les arguments fournis lors de l’utilisation (l’appel) de la fonction se nomment des « arguments
effectifs » (ou encore « paramètres effectifs »). Comme le laisse deviner l’exemple précédent,
on peut utiliser n’importe quelle expression comme argument effectif ; au bout du compte,
c’est la valeur de cette expression qui sera transmise à la fonction lors de son appel. Notez
qu’une telle « liberté » n’aurait aucun sens dans le cas des paramètres formels : il serait impossible d’écrire un en-tête de fexple sous la forme float fexple (float a+b, ...) pas
plus qu’en mathématiques vous ne définiriez une fonction f par f (x + y) = 5 !
© Éditions Eyrolles
97
Delannoy Livre.book Page 98 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
3.2 L’instruction return
Voici quelques règles générales concernant cette instruction.
●
L’instruction return peut mentionner n’importe quelle expression. Ainsi, nous aurions pu
définir la fonction fexple précédente d’une manière plus simple :
float fexple (float x, int b, int c)
{
return (x * x + b * x + c) ;
}
●
L’instruction return peut apparaître à plusieurs reprises dans une fonction, comme dans
cet autre exemple :
double absom (double u, double v)
{
double s ;
s = a + b ;
if (s>0)
return (s) ;
else
return (-s)
}
Notez bien que non seulement l’instruction return définit la valeur du résultat, mais, en
même temps, elle interrompt l’exécution de la fonction en revenant dans la fonction qui
l’a appelée (n’oubliez pas qu’en C tous les modules sont des fonctions, y compris le
programme principal). Nous verrons qu’une fonction peut ne fournir aucune valeur : elle
peut alors disposer de une ou plusieurs instructions return sans expression, interrompant simplement l’éxecution de la fonction ; mais elle peut aussi dans ce cas ne comporter
aucune instruction return, le retour étant alors mis en place automatiquement par le
compilateur à la fin de la fonction.
●
Si le type de l’expression figurant dans return est différent du type du résultat tel qu’il a
été déclaré dans l’en-tête, le compilateur mettra automatiquement en place des instructions
de conversion.
Il est toujours possible de ne pas utiliser le résultat d’une fonction, même si elle en produit un. C’est d’ailleurs ce que nous avons fait fréquemment avec printf ou scanf.
Bien entendu, cela n’a d’intérêt que si la fonction fait autre chose que de calculer un
résultat. En revanche, il est interdit d’utiliser la valeur d’une fonction ne fournissant pas
de résultat (si certains compilateurs l’acceptent, vous obtiendrez, lors de l’exécution, une
valeur aléatoire !).
98
© Éditions Eyrolles
Delannoy Livre.book Page 99 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
3.3 Cas des fonctions sans valeur de retour
ou sans arguments
Quand une fonction ne renvoie pas de résultat, on le précise, à la fois dans l’en-tête et dans sa
déclaration, à l’aide du mot-clé void. Par exemple, voici l’en-tête d’une fonction recevant un
argument de type int et ne fournissant aucune valeur :
void sansval (int n)
et voici quelle serait sa déclaration :
void sansval (int) ;
Naturellement, la définition d’une telle fonction ne doit, en principe, contenir aucune instruction return. Certains compilateurs ne détecteront toutefois pas l’erreur.
Quand une fonction ne reçoit aucun argument, on place le mot-clé void (le même que précédemment, mais avec une signification différente !) à la place de la liste d’arguments (attention, en C++, la règle sera différente : on se contentera de ne rien mentionner dans la liste
d’arguments). Voici l’en-tête d’une fonction ne recevant aucun argument et renvoyant une
valeur de type float (il pourrait s’agir, par exemple, d’une fonction fournissant un nombre
aléatoire !) :
float tirage (void)
Sa déclaration serait très voisine (elle ne diffère que par la présence du point-virgule !) :
float tirage (void) ;
Enfin, rien n’empêche de réaliser une fonction ne possédant ni arguments ni valeur de retour.
Dans ce cas, son en-tête sera de la forme :
void message (void)
et sa déclaration sera :
void message (void) ;
En toute rigueur, la fonction main est une fonction sans argument et sans valeur de retour. Elle
devrait donc avoir pour en-tête « void main (void) ». Certains compilateurs fournissent
d’ailleurs un message d’avertissement (« warning ») lorsque vous vous contentez de l’en-tête
usuel main.
© Éditions Eyrolles
99
Delannoy Livre.book Page 100 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
Voici un exemple illustrant deux des situations évoquées. Nous y définissons une fonction
affiche_carres qui affiche les carrés des nombres entiers compris entre deux limites fournies en arguments et une fonction erreur qui se contente d’afficher un message d’erreur (il
s’agit de notre premier exemple de programme source contenant plus de deux fonctions).
#include <stdio.h>
main()
{ void affiche_carres (int, int) ; /* prototype de affiche_carres */
void erreur (void) ;
/* prototype de erreur */
int debut = 5, fin = 10 ;
.....
affiche_carres (debut, fin) ;
.....
if (...) erreur () ;
}
void affiche_carres (int d, int f)
{ int i ;
for (i=d ; i<=f ; i++)
printf ("%d a pour carré %d\n", i, i*i) ;
}
void erreur (void)
{ printf ("*** erreur ***\n") ; }
3.4 Les anciennes formes de l’en-tête des fonctions
Dans la première version du langage C, telle qu’elle a été définie par Kernighan et Ritchie,
avant la normalisation par le comité ANSI, l’en-tête d’une fonction s’écrivait différemment de
ce que nous avons vu ici. Par exemple, l’en-tête de notre fonction fexple aurait été :
float fexple (x, b, c)
float x ;
int b, c ;
La norme ANSI autorise les deux formes, lesquelles sont actuellement acceptées par la plupart
des compilateurs. Toutefois, seule la forme moderne, c’est-à-dire celle que nous avons présentée
précédemment, sera autorisée par C++.
L ’habitude veut que les en-têtes écrits sous l’ancienne forme le soient sur plusieurs lignes
comme dans notre exemple. Mais rien ne nous empêcherait de l’écrire sous cette forme :
float fexple (x, b, c) float x ; int b, c ;
100
© Éditions Eyrolles
Delannoy Livre.book Page 101 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
4 Les fonctions et leurs déclarations
4.1 Les différentes façons de déclarer (ou de ne pas déclarer) une fonction
Dans notre exemple du paragraphe 2, nous avions fourni la définition de la fonction fexple
après celle de la fonction main. Mais nous aurions pu tout aussi bien faire l’inverse :
float fexple (float x, int b, int c)
{
....
}
main()
{
float fexple (float, int, int) ; /* déclaration de la fonc. fexple */
.....
y = fexple (x, n, p) ;
.....
}
En toute rigueur, dans ce cas, la déclaration de la fonction fexple (ici, dans main) est facultative, car, lorsqu’il traduit la fonction main, le compilateur connaît déjà la fonction fexple.
Néanmoins, nous vous déconseillons d’omettre la déclaration de fexple dans ce cas ; en
effet, il est tout à fait possible qu’ultérieurement vous soyez amené à modifier votre programme
source ou même à l’éclater en plusieurs fichiers source comme l’autorisent les possibilités de
compilation séparée du langage C.
Par ailleurs, le langage C (mais pas le C++) vous permet d’effectuer des déclarations partielles
en ne mentionnant pas le type des arguments ; ainsi, dans notre exemple du paragraphe 2, nous
pourrions déclarer fexple de cette façon dans la fonction main :
float fexple () ;
Qui plus est, C vous autorise à ne pas déclarer du tout une fonction qui renvoie une valeur de
type int (là encore, ce sera interdit en C++ ainsi qu’en C99).
Nous ne saurions trop vous conseiller d’éviter de telles possibilités. Toutefois, sachez que vous
risquez d’employer la dernière sans y prendre garde. En effet, toute fonction que vous utiliserez
sans l’avoir déclarée sera considérée par le compilateur comme ayant des arguments quelconques et fournissant un résultat de type int. Les conséquences en seront différentes suivant
que ladite fonction est ou non fournie dans le même fichier source. Dans le premier cas, on
obtiendra bien une erreur de compilation ; dans le second, en revanche, les conséquences
n’apparaîtront (de manière plus ou moins voilée) que lors de l’exécution !
© Éditions Eyrolles
101
Delannoy Livre.book Page 102 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
La déclaration complète d’une fonction porte le nom de prototype. Il est possible, dans un
prototype, de faire figurer des noms d’arguments, lesquels sont alors totalement arbitraires ;
cette possibilité a pour seul intérêt de pouvoir écrire des prototypes qui sont identiques à l’entête de la fonction (au point-virgule près), ce qui peut en faciliter la création automatique.
Dans notre exemple du paragraphe 2, notre fonction fexple aurait pu être déclarée ainsi :
float fexple (float x, int b, int c) ;
4.2 Où placer la déclaration d’une fonction
La tendance la plus naturelle consiste à placer la déclaration d’une fonction à l’intérieur des
déclarations de toute fonction l’utilisant ; c’est ce que nous avons fait jusqu’ici. Et, de surcroît,
dans tous nos exemples précédents, la fonction utilisatrice était la fonction main elle-même !
Dans ces conditions, nous avions affaire à une déclaration locale dont la portée était limitée à
la fonction où elle apparaissait.
Mais il est également possible d’utiliser des déclarations globales, en les faisant apparaître
avant la définition de la première fonction. Par exemple, avec :
float fexple (float, int, int) ;
main()
{ .....
}
void f1 (...)
{ .....
}
la déclaration de fexple est connue à la fois de main et de f1.
4.3 À quoi sert la déclaration d’une fonction
Nous avons vu que la déclaration d’une fonction est plus ou moins obligatoire et qu’elle peut
être plus ou moins détaillée. Malgré tout, nous vous avons recommandé d’employer toujours
la forme la plus complète possible qu’on nomme prototype. Dans ce cas, un tel prototype peut
être utilisé par le compilateur, et cela de deux façons complètement différentes.
a) Si la définition de la fonction se trouve dans le même fichier source (que ce soit avant ou
après la déclaration), il s’assure que les arguments muets ont bien le type défini dans le prototype. Dans le cas contraire, il signale une erreur.
b) Lorsqu’il rencontre un appel de la fonction, il met en place d’éventuelles conversions des
valeurs des arguments effectifs dans le type indiqué dans le prototype. Par exemple, avec notre
fonction fexple du paragraphe 2, un appel tel que :
fexple (n+1, 2*x, p)
102
© Éditions Eyrolles
Delannoy Livre.book Page 103 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
sera traduit par :
●
l’évaluation de la valeur de l’expression n+1 (en int) et sa conversion en float,
●
l’évaluation de la valeur de l’expression 2*x (en float) et sa conversion en int ; il y a
donc dans ce dernier cas une conversion dégradante.
R appelons que, lorsque le compilateur ne connaît pas le type des arguments d’une fonction, il
utilise des règles de conversions systématiques : char et short -> int et float -> double.
La fonction printf est précisément dans ce cas.
C ompte
tenu de la remarque précédente, seule une fonction déclarée avec un prototype
pourra recevoir un argument de type float, char ou short.
5 Retour sur les fichiers en-tête
Nous avons déjà dit qu’il existe un certain nombre de fichiers d’extension .h, correspondant
chacun à une classe de fonctions. On y trouve, entre autres choses, les prototypes de ces
fonctions.
Ce point se révèle fort utile :
●
d’une part pour effectuer des contrôles sur le nombre et le type des arguments mentionnés
dans les appels de ces fonctions,
●
d’autre part pour forcer d’éventuelles conversions auxquelles on risque de ne pas penser.
À titre d’illustration de ce dernier aspect, supposez que vous ayez écrit ces instructions :
float x, y ;
.....
y = sqrt (x) ;
.....
sans les faire précéder d’une quelconque directive #include.
Elles produiraient alors des résultats faux. En effet, il se trouve que la fonction sqrt s’attend
à recevoir un argument de type double (ce qui sera le cas ici, compte tenu des conversions
implicites), et elle fournit un résultat de type double. Or, lors de la traduction de votre
programme, le compilateur ne le sait pas. Il attribue donc d’office à sqrt le type int et il met
en place une conversion de la valeur de retour (laquelle sera en fait de type double) en int.
On se trouve en présence des conséquences habituelles d’une mauvaise interprétation de type.
Un premier remède consiste à placer dans votre module la déclaration :
double sqrt(double) ;
mais encore faut-il que vous connaissiez de façon certaine le type de cette fonction.
© Éditions Eyrolles
103
Delannoy Livre.book Page 104 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
Une meilleure solution consiste à placer, en début de votre programme, la directive :
#include <math.h>
laquelle incorporera automatiquement le prototype approprié (entre autres choses).
6 En C, les arguments sont transmis par valeur
Nous avons déjà eu l’occasion de dire qu’en C les arguments d’une fonction étaient transmis par
valeur. Cependant, dans les exemples que nous avons rencontrés dans ce chapitre, les conséquences
et les limitations de ce mode de transmission n’apparaissaient guère. Or voyez cet exemple :
Conséquences de la transmission par valeur des arguments
#include <stdio.h>
main()
{ void echange (int a, int
int n=10, p=20 ;
printf ("avant appel
:
echange (n, p) ;
printf ("après appel
:
}
void echange (int a, int b)
{
int c ;
printf ("début echange :
c = a ;
a = b ;
b = c ;
printf ("fin echange
:
}
avant appel
début echange
fin echange
après appel
:
:
:
:
10
10
20
10
b) ;
%d %d\n", n, p) ;
%d %d", n, p)
%d %d\n", a, b) ;
%d %d\n", a, b) ;
20
20
10
20
La fonction echange reçoit deux valeurs correspondant à ses deux arguments muets a et b.
Elle effectue un échange de ces deux valeurs. Mais, lorsque l’on est revenu dans le programme
principal, aucune trace de cet échange ne subsiste sur les arguments effectifs n et p.
En effet, lors de l’appel de echange, il y a eu transmission de la valeur des expressions n et
p. On peut dire que ces valeurs ont été recopiées localement dans la fonction echange dans
104
© Éditions Eyrolles
Delannoy Livre.book Page 105 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
des emplacements nommés a et b. C’est effectivement sur ces copies qu’a travaillé la fonction
echange, de sorte que les valeurs des variables n et p n’ont, quant à elles, pas été modifiées.
C’est ce qui explique le résultat constaté.
Ce mode de transmission semble donc interdire a priori qu’une fonction produise une ou
plusieurs valeurs en retour, autres que celle de la fonction elle-même.
Or, il ne faut pas oublier qu’en C tous les modules doivent être écrits sous forme de fonction.
Autrement dit, ce simple problème d’échange des valeurs de deux variables doit pouvoir se
résoudre à l’aide d’une fonction.
Nous verrons que ce problème possède plusieurs solutions, à savoir :
●
Transmettre en argument la valeur de l’adresse d’une variable. La fonction pourra éventuellement agir sur le contenu de cette adresse. C’est précisément ce que nous faisons
lorsque nous utilisons la fonction scanf. Nous examinerons cette technique en détail dans
le chapitre consacré aux pointeurs.
●
Utiliser des variables globales, comme nous le verrons dans le prochain paragraphe ; cette
deuxième solution devra toutefois être réservée à des cas exceptionnels, compte tenu des
risques qu’elle présente (effets de bords).
C ’est bien parce que la transmission des arguments se fait « par valeur » que les arguments
effectifs peuvent prendre la forme d’une expression quelconque. Dans les langages où le seul
mode de transmission est celui « par adresse », les arguments effectifs ne peuvent être que
l’équivalent d’une lvalue.
L a norme n’impose aucun ordre pour l’évaluation des différents arguments d’une fonction lors
de son appel. En général, ceci est de peu d’importance, excepté dans une situation (fortement
déconseillée !) telle que :
int i = 10 ;
...
f (i++, i) ;
/* on peut calculer i++ avant i --> f (10, 11) */
/*
ou après i --> f (10, 10) */
7 Les variables globales
Nous avons vu comment échanger des informations entre différentes fonctions grâce à la
transmission d’arguments et à la récupération d’une valeur de retour.
En fait, en C, plusieurs fonctions (dont, bien entendu le programme principal main) peuvent
partager des variables communes qu’on qualifie alors de globales.
L a norme ANSI ne parle pas de variables globales, mais de variables externes. Le terme « global »
illustre plutôt le partage entre plusieurs fonctions tandis que le terme « externe » illustre plutôt le partage entre plusieurs fichiers source. En C, une variable globale est partagée par plusieurs fonctions ;
elle peut être (mais elle n’est pas obligatoirement) partagée entre plusieurs fichiers source.
© Éditions Eyrolles
105
Delannoy Livre.book Page 106 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
7.1 Exemple d’utilisation de variables globales
Voyez cet exemple de programme.
Exemple d’utilisation d’une variable globale
#include <stdio.h>
int i ;
main()
{ void optimist (void) ;
for (i=1 ; i<=5 ; i++)
optimist() ;
}
void optimist(void)
{
printf ("il fait beau %d fois\n", i) ;
}
il
il
il
il
il
fait
fait
fait
fait
fait
beau
beau
beau
beau
beau
1
2
3
4
5
fois
fois
fois
fois
fois
La variable i a été déclarée en dehors de la fonction main. Elle est alors connue de toutes les
fonctions qui seront compilées par la suite au sein du même programme source. Ainsi, ici, le programme principal affecte à i des valeurs qui se trouvent utilisées par la fonction optimist.
Notez qu’ici la fonction optmist se contente d’utiliser la valeur de i mais rien ne l’empêche
de la modifier. C’est précisément ce genre de remarque qui doit vous inciter à n’utiliser les
variables globales que dans des cas limités. En effet, toute variable globale peut être modifiée
insidieusement par n’importe quelle fonction. Lorsque vous aurez à écrire des fonctions susceptibles de modifier la valeur de certaines variables, il sera beaucoup plus judicieux de prévoir
d’en transmettre l’adresse en argument (comme vous apprendrez à le faire dans le prochain
chapitre). En effet, dans ce cas, l’appel de la fonction montrera explicitement quelle est la
variable qui risque d’être modifiée et, de plus, ce sera la seule qui pourra l’être.
7.2 La portée des variables globales
Les variables globales ne sont connues du compilateur que dans la partie du programme source
suivant leur déclaration. On dit que leur portée (ou encore leur espace de validité) est limitée à
la partie du programme source qui suit leur déclaration (n’oubliez pas que, pour l’instant, nous
nous limitons au cas où l’ensemble du programme est compilé en une seule fois).
106
© Éditions Eyrolles
Delannoy Livre.book Page 107 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
Ainsi, voyez, par exemple, ces instructions :
main()
{
....
}
int n ;
float x ;
fct1 (...)
{
....
}
fct2 (...)
{
....
}
Les variables n et x sont accessibles aux fonctions fct1 et fct2, mais pas au programme
principal. En pratique, bien qu’il soit possible effectivement de déclarer des variables globales
à n’importe quel endroit du programme source qui soit extérieur aux fonctions, on procédera
rarement ainsi. En effet, pour d’évidentes raisons de lisibilité, on préférera regrouper les déclarations de toutes les variables globales au début du programme source.
7.3 La classe d’allocation des variables globales
D’une manière générale, les variables globales existent pendant toute l’exécution du programme
dans lequel elles apparaissent. Leurs emplacements en mémoire sont parfaitement définis lors
de l’édition de liens. On traduit cela en disant qu’elles font partie de la classe d’allocation
statique.
De plus, ces variables se voient initialisées à zéro, avant le début de l’exécution du programme,
sauf, bien sûr, si vous leur attribuez explicitement une valeur initiale au moment de leur
déclaration.
8 Les variables locales
À l’exception de l’exemple du paragraphe précédent, les variables que nous avions rencontrées
jusqu’ici n’étaient pas des variables globales. Plus précisément, elles étaient définies au sein
d’une fonction (qui pouvait être main). De telles variables sont dites locales à la fonction dans
laquelle elles sont déclarées.
© Éditions Eyrolles
107
Delannoy Livre.book Page 108 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
8.1 La portée des variables locales
Les variables locales ne sont connues qu’à l’intérieur de la fonction où elles sont déclarées.
Leur portée est donc limitée à cette fonction.
Les variables locales n’ont aucun lien avec des variables globales de même nom ou avec
d’autres variables locales à d’autres fonctions.
Voyez cet exemple :
int n ;
main()
{
int p ;
....
}
fct1 ()
{
int p ;
int n ;
}
La variable p de main n’a aucun rapport avec la variable p de fct1. De même, la variable n
de fct1 n’a aucun rapport avec la variable globale n. Notez qu’il est alors impossible, dans la
fonction fct1, d’utiliser cette variable globale n.
8.2 Les variables locales automatiques
Par défaut, les variables locales ont une durée de vie limitée à celle d’une exécution de la
fonction dans laquelle elles figurent.
Plus précisément, leurs emplacements ne sont pas définis de manière permanente comme ceux
des variables globales. Un nouvel espace mémoire leur est alloué à chaque entrée dans la fonction et libéré à chaque sortie. Il sera donc généralement différent d’un appel au suivant.
On traduit cela en disant que la classe d’allocation de ces variables est automatique. Nous
aurons l’occasion de revenir plus en détail sur cette gestion dynamique de la mémoire. Pour
l’instant, il est important de noter que la conséquence immédiate de ce mode d’allocation est
que les valeurs des variables locales ne sont pas conservées d’un appel au suivant (on dit aussi
qu’elles ne sont pas « rémanentes »). Nous reviendrons un peu plus loin (paragraphe 11.2) sur
les éventuelles initialisations de telles variables.
D’autre part, les valeurs transmises en arguments à une fonction sont traitées de la même
manière que les variables locales. Leur durée de vie correspond également à celle de la fonction.
108
© Éditions Eyrolles
Delannoy Livre.book Page 109 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
8.3 Les variables locales statiques
Il est toutefois possible de demander d’attribuer un emplacement permanent à une variable
locale et qu’ainsi sa valeur se conserve d’un appel au suivant. Il suffit pour cela de la déclarer
à l’aide du mot-clé static (le mot static employé sans indication de type est équivalent à
static int).
En voici un exemple :
Exemple d’utilisation de variable locale statique
#include <stdio.h>
main()
{ void fct(void) ;
int n ;
for ( n=1 ; n<=5 ; n++)
fct() ;
}
void fct(void)
{ static int i ;
i++ ;
printf ("appel numéro : %d\n", i) ;
}
appel
appel
appel
appel
appel
numéro
numéro
numéro
numéro
numéro
:
:
:
:
:
1
2
3
4
5
La variable locale i a été déclarée de classe « statique ». On constate bien que sa valeur progresse de un à chaque appel. De plus, on note qu’au premier appel sa valeur est nulle. En effet,
comme pour les variables globales (lesquelles sont aussi de classe statique) : les variables
locales de classe statique sont, par défaut, initialisées à zéro.
Prenez garde à ne pas confondre une variable locale de classe statique avec une variable
globale. En effet, la portée d’une telle variable reste toujours limitée à la fonction dans laquelle
elle est définie. Ainsi, dans notre exemple, nous pourrions définir une variable globale nommée i qui n’aurait alors aucun rapport avec la variable i de fct.
© Éditions Eyrolles
109
Delannoy Livre.book Page 110 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
8.4 Le cas des fonctions récursives
Le langage C autorise la récursivité des appels de fonctions. Celle-ci peut prendre deux aspects :
●
récursivité directe : une fonction comporte, dans sa définition, au moins un appel à elle-même,
●
récursivité croisée : l’appel d’une fonction entraîne celui d’une autre fonction qui, à son tour,
appelle la fonction initiale (le cycle pouvant d’ailleurs faire intervenir plus de deux fonctions).
Voici un exemple fort classique (d’ailleurs inefficace sur le plan du temps d’exécution) d’une
fonction calculant une factorielle de manière récursive :
Fonction récursive de calcul de factorielle
long fac (int n)
{
if (n>1) return (fac(n-1)*n) ;
else return(1) ;
}
Il faut bien voir qu’alors chaque appel de fac entraîne une allocation d’espace pour les variables locales et pour son argument n (apparemment, fct ne comporte aucune variable locale ;
en réalité, il lui faut prévoir un emplacement destiné à recevoir sa valeur de retour). Or chaque
nouvel appel de fac,à l’intérieur de fac, provoque une telle allocation, sans que les emplacements précédents soient libérés.
Il y a donc un empilement des espaces alloués aux variables locales, parallèlement à un empilement des appels de la fonction. Ce n’est que lors de l’exécution de la première instruction
return que l’on commencera à « dépiler » les appels et les emplacements et donc à libérer de
l’espace mémoire.
9 La compilation séparée et ses conséquences
Si le langage C est effectivement un langage que l’on peut qualifier d’opérationnel, c’est en
partie grâce à ses possibilités dites de compilation séparée. En C, en effet, il est possible de
compiler séparément plusieurs programmes (fichiers) source et de rassembler les modules
objet correspondants au moment de l’édition de liens. D’ailleurs, dans certains environnements de programmation, la notion de projet permet de gérer la multiplicité des fichiers
(source et modules objet) pouvant intervenir dans la création d’un programme exécutable.
Cette notion de projet fait intervenir précisément les fichiers à considérer ; généralement, il est
possible de demander de créer le programme exécutable, en ne recompilant que les sources
ayant subi une modification depuis leur dernière compilation.
Indépendamment de ces aspects techniques liés à l’environnement de programmation considéré,
les possibilités de compilation séparée ont une incidence importante au niveau de la portée des
variables globales. C’est cet aspect que nous nous proposons d’étudier maintenant. Dans le
110
© Éditions Eyrolles
Delannoy Livre.book Page 111 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
paragraphe suivant, nous serons alors en mesure de faire le point sur les différentes classes
d’allocation des variables.
Notez que, à partir du moment où l’on parle de compilation séparée, il existe au moins (ou il
a existé) deux programmes source ; dans la suite, nous supposerons qu’ils figurent dans des
fichiers, de sorte que nous parlerons toujours de fichier source.
Pour l’instant, voyons l’incidence de cette compilation séparée sur la portée des variables globales.
9.1 La portée d’une variable globale - la déclaration extern
A priori, la portée d’une variable globale semble limitée au fichier source dans lequel elle a été
définie. Ainsi, supposez que l’on compile séparément ces deux fichiers source :
source 1
int x ;
main()
{
.....
}
fct1()
{
.....
}
source 2
fct2()
{
.....
}
fct3()
{
.....
}
Il ne semble pas possible, dans les fonctions fct2 et fct3 de faire référence à la variable
globale x déclarée dans le premier fichier source (alors qu’aucun problème ne se poserait si
l’on réunissait ces deux fichiers source en un seul, du moins si l’on prenait soin de placer
les instructions du second fichier à la suite de celles du premier).
En fait, le langage C prévoit une déclaration permettant de spécifier qu’une variable globale a
déjà été définie dans un autre fichier source. Celle-ci se fait à l’aide du mot-clé extern. Ainsi,
en faisant précéder notre second fichier source de la déclaration :
extern int x ;
il devient possible de mentionner la variable globale x (déclarée dans le premier fichier
source) dans les fonctions fct2 et fct3.
C ette déclaration extern n’effectue pas de réservation d’emplacement de variable. Elle ne
fait que préciser que la variable globale x est définie par ailleurs et elle en précise le type.
Nous n’avons considéré ici que la façon la plus usuelle de gérer des variables globales (celle-ci
est utilisable avec tous les compilateurs, qu’ils soient d’avant ou d’après la norme). La norme
prévoit d’autres possibilités, au demeurant fort peu répandues et, de surcroît, peu logiques
(doubles déclarations, mot extern utilisé même pour la réservation d’un emplacement, définitions potentielles).
© Éditions Eyrolles
111
Delannoy Livre.book Page 112 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
9.2 Les variables globales et l’édition de liens
Supposons que nous ayons compilé les deux fichiers source précédents et voyons d’un peu
plus près comment l’éditeur de liens est en mesure de rassembler correctement les deux modules
objet ainsi obtenus. En particulier, examinons comment il peut faire correspondre au symbole x
du second fichier source l’adresse effective de la variable x définie dans le premier.
D’une part, après compilation du premier fichier source, on trouve, dans le module objet correspondant, une indication associant le symbole x et son adresse dans le module objet. Autrement
dit, contrairement à ce qui se produit pour les variables locales, pour lesquelles ne subsiste
aucune trace du nom après compilation, le nom des variables globales continue à exister au
niveau des modules objet. On retrouve là un mécanisme analogue à ce qui se passe pour les
noms de fonctions, lesquels doivent bien subsister pour que l’éditeur de liens soit en mesure de
retrouver les modules objet correspondants.
D’autre part, après compilation du second fichier source, on trouve dans le module objet
correspondant, une indication mentionnant qu’une certaine variable de nom x provient de
l’extérieur et qu’il faudra en fournir l’adresse effective.
Ce sera effectivement le rôle de l’éditeur de liens que de retrouver dans le premier module
objet l’adresse effective de la variable x et de la reporter dans le second module objet.
Ce mécanisme montre que s’il est possible, par mégarde, de réserver des variables globales
de même nom dans deux fichiers source différents, il sera, par contre, en général, impossible
d’effectuer correctement l’édition de liens des modules objet correspondants (certains éditeurs
de liens peuvent ne pas détecter cette anomalie). En effet, dans un tel cas, l’éditeur de liens
se trouvera en présence de deux adresses différentes pour un même identificateur, ce qui est
illogique.
9.3 Les variables globales cachées - la déclaration static
Il est possible de « cacher » une variable globale, c’est-à-dire de la rendre inaccessible à
l’extérieur du fichier source où elle a été définie (on dit aussi « rendre confidentielle » au lieu
de « cacher » ; on parle alors de « variables globales confidentielles »). Il suffit pour cela d’utiliser
la déclaration static comme dans cet exemple :
static int a ;
main()
{
.....
}
fct()
{
.....
}
112
© Éditions Eyrolles
Delannoy Livre.book Page 113 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
Sans la déclaration static, a serait une variable globale ordinaire. Par contre, cette
déclaration demande qu’aucune trace de a ne subsiste dans le module objet résultant de ce
fichier source. Il sera donc impossible d’y faire référence depuis une autre source par extern.
Mieux, si une autre variable globale apparaît dans un autre fichier source, elle sera acceptée à
l’édition de liens puisqu’elle ne pourra pas interférer avec celle du premier source.
Cette possibilité de cacher des variables globales peut s’avérer précieuse lorsque vous êtes
amené à développer un ensemble de fonctions d’intérêt général qui doivent partager des variables globales. En effet, elle permet à l’utilisateur éventuel de ces fonctions de ne pas avoir à se
soucier des noms de ces variables globales puisqu’il ne risque pas alors d’y accéder par
mégarde. Cela généralise en quelque sorte à tout un fichier source la notion de variable locale
telle qu’elle était définie pour les fonctions. Ce sont d’ailleurs de telles possibilités qui permettent
de développer des logiciels en C, en utilisant une philosophie orientée objet.
10 Les différents types de variables, leur portée et leur classe
d’allocation
Nous avons déjà vu différentes choses concernant les classes d’allocation des variables et leur
portée. Ici, nous nous proposons de faire le point après avoir introduit quelques informations
supplémentaires.
10.1 La portée des variables
On peut classer les variables en quatre catégories en fonction de leur portée (ou espace de
validité). Nous avons déjà rencontré les trois premières que sont : les variables globales, les
variables globales cachées et les variables locales à une fonction. En outre, il est possible de
définir des variables locales à un bloc. Elles se déclarent en début d’un bloc de la même façon
qu’en début d’une fonction. Dans ce cas, la portée de telles variables est limitée au bloc en
question. Leur usage est, en pratique, peu répandu.
10.2 Les classes d’allocation des variables
Il est également possible de classer les variables en trois catégories en fonction de leur classe
d’allocation. Là encore, nous avons déjà rencontré les deux premières, à savoir :
●
© Éditions Eyrolles
la classe statique : elle renferme non seulement les variables globales (quelles qu’elles
soient), mais aussi les variables locales faisant l’objet d’une déclaration static. Les
emplacements mémoire correspondants sont alloués une fois pour toutes au moment de
l’édition de liens,
113
Delannoy Livre.book Page 114 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
la classe automatique : par défaut, les variables locales entrent dans cette catégorie. Les
emplacements mémoire correspondants sont alloués à chaque entrée dans la fonction où
sont définies ces variables et ils sont libérés à chaque sortie.
En toute rigueur, il existe une classe un peu particulière, à savoir la classe registre : toute
variable entrant a priori dans la classe automatique peut être déclarée explicitement avec le
qualificatif register. Celui-ci demande au compilateur d’utiliser, dans la mesure du possible,
un registre pour y ranger la variable ; cela peut amener quelques gains de temps d’exécution.
Bien entendu, cette possibilité ne peut s’appliquer qu’aux variables scalaires.
●
L e cas des fonctions. La fonction est considérée par le langage C comme un objet global. C’est
ce qui permet d’ailleurs à l’éditeur de liens d’effectuer correctement son travail. Il faut noter toutefois qu’il n’est pas nécessaire d’utiliser une déclaration extern pour les fonctions définies dans un
fichier source différent de celui où elles sont appelées (mais le faire ne constitue pas une erreur).
En tant qu’objet global, la fonction peut voir sa portée limitée au fichier source dans lequel elle
est définie à l’aide d’une déclaration static comme dans :
static int fct (...)
10.3 Tableau récapitulatif
Voici un tableau récapitulant la portée et la classe d’allocation des différentes variables suivant
la nature de leur déclaration (la colonne « Type » donne le nom qu’on attribue usuellement à
de telles variables).
Type, portée et classe d’allocation des variables
Type de variable
Déclaration
Portée
Globale
en dehors de toute
fonction
• la partie du fichier source
suivant sa déclaration,
• n’importe quel fichier
source, avec extern.
Globale cachée
en dehors de toute
fonction, avec l’attribut
uniquement la partie du
fichier source suivant sa
déclaration
static
Locale
« rémanente »
au début d’une fonction,
avec l’attribut static
la fonction
Locale à une fonction
au début d’une fonction
la fonction
Locale à un bloc
au début d’un bloc
le bloc
Classe d’allocation
Statique
Automatique
114
© Éditions Eyrolles
Delannoy Livre.book Page 115 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
11 Initialisation des variables
Nous avons vu qu’il était possible d’initialiser explicitement une variable lors de sa déclaration. Ici, nous allons faire le point sur ces possibilités, lesquelles dépendent en fait de la
classe d’allocation de la variable concernée.
11.1 Les variables de classe statique
Ces variables sont permanentes. Elles sont initialisées une seule fois avant le début de l’exécution du programme.
Elles peuvent être initialisées explicitement lors de leur déclaration. Nous verrons que cela
s’applique également aux tableaux ou aux structures. Bien entendu, les valeurs servant à ces
initialisations ne pourront être que des constantes ou des expressions constantes (c’est-à-dire
calculables par le compilateur). N’oubliez pas que les constantes symboliques définies par
const ne sont pas considérées comme des expressions constantes.
En l’absence d’initialisation explicite, ces variables seront initialisées à zéro.
11.2 Les variables de classe automatique
Ces variables ne sont pas initialisées par défaut. En revanche, comme les variables de classe
statique, elles peuvent être initialisées explicitement lors de leur déclaration.
Dans ce cas, lorsqu’il s’agit de variables scalaires (ce qui est le cas de toutes celles rencontrées
jusqu’ici, mais ne sera pas le cas des tableaux ou des structures), la norme vous autorise à
utiliser comme valeur initiale non seulement une valeur constante, mais également n’importe
quelle expression (dans la mesure où sa valeur est définie au moment de l’entrée dans la fonction
correspondante, laquelle, ne l’oubliez pas, peut être la fonction main).
En voici un cas d’école :
Initialisation de variables de classe automatique
#include <stdio.h>
int n ;
main()
{ void fct (int r) ;
int p ;
for (p=1 ; p<=5 ; p++)
{ n = 2*p ;
fct(p) ;
}
}
void fct(int r)
{
int q=n, s=r*n ;
printf ("%d %d %d\n", r,q,s) ;
}
© Éditions Eyrolles
115
Delannoy Livre.book Page 116 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
N’oubliez pas que ces variables automatiques se trouvent alors initialisées à chaque appel de
la fonction dans laquelle elles sont définies.
12 Les arguments variables en nombre
Dans tous nos précédents exemples, le nombre d’arguments fournis au cours de l’appel d’une
fonction était prévu lors de l’écriture de cette fonction.
Or, dans certaines circonstances, on peut souhaiter réaliser une fonction capable de recevoir un
nombre d’arguments susceptible de varier d’un appel à un autre. C’est d’ailleurs ce que nous
faisons (sans même y penser) lorsque nous utilisons des fonctions comme printf ou scanf
(en dehors du premier argument, qui représente le format, les autres sont en nombre quelconque)
Le langage C permet de résoudre ce problème à l’aide des fonctions particulières va_start
et va_arg. La seule contrainte à respecter est que la fonction doit posséder certains arguments fixes (c’est-à-dire toujours présents), leur nombre ne pouvant être inférieur à un. En
effet, comme nous allons le voir, c’est le dernier argument fixe qui permet, en quelque sorte,
d’initialiser le parcours de la liste d’arguments.
12.1 Premier exemple
Voici un premier exemple de fonction à arguments variables : les deux premiers arguments
sont fixes, l’un étant de type int, l’autre de type char. Les arguments suivants, de type int,
sont en nombre quelconque et l’on a supposé que le dernier d’entre eux était -1. Cette
dernière valeur sert donc, en quelque sorte, de sentinelle. Par souci de simplification, nous
nous sommes contentés, dans cette fonction, de lister les valeurs de ces différents arguments
(fixes ou variables), à l’exception du dernier.
Arguments en nombre variable délimités par une sentinelle
#include <stdio.h>
#include <stdarg.h>
void essai (int par1, char par2, ...)
{
va_list adpar ;
int parv ;
printf ("premier paramètre : %d\n", par1) ;
printf ("second paramètre : %c\n", par2) ;
va_start (adpar, par2) ;
while ( (parv = va_arg (adpar, int) ) != -1)
printf ("argument variable : %d\n", parv) ;
}
116
© Éditions Eyrolles
Delannoy Livre.book Page 117 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
Arguments en nombre variable délimités par une sentinelle (suite)
main()
{
printf ("premier essai\n") ;
essai (125, 'a', 15, 30, 40, -1) ;
printf ("\ndeuxième essai\n") ;
essai (6264, 'S', -1) ;
}
premier essai
premier paramètre
second paramètre
argument variable
argument variable
argument variable
:
:
:
:
:
125
a
15
30
40
deuxième essai
premier paramètre : 6264
second paramètre : S
Vous constatez la présence, dans l’en-tête, des deux noms des paramètres fixes par1 et par2,
déclarés de manière classique ; les trois points servent à spécifier au compilateur l’existence
de paramètres en nombre variable.
La déclaration :
va_list adpar
précise que adpar est un identificateur de liste variable. C’est lui qui nous servira à récupérer,
les uns après les autres, les différents arguments variables.
Comme à l’accoutumée, une telle déclaration n’attribue aucune valeur à adpar. C’est effectivement la fonction va_start qui va permettre de l’initialiser à l’adresse du paramètre variable.
Notez bien que cette dernière est déterminée par va_start à partir de la connaissance du
nom du dernier paramètre fixe.
Le rôle de la fonction va_arg est double :
●
d’une part, elle fournit comme résultat la valeur trouvée à l’adresse courante fournie par
adpar (son premier argument), suivant le type indiqué par son second argument (ici int).
●
d’autre part, elle incrémente l’adresse contenue dans adpar, de manière que celle-ci
pointe alors sur l’argument variable suivant.
Ici, une instruction while nous permet de récupérer les différents arguments variables, sachant
que le dernier a pour valeur -1.
© Éditions Eyrolles
117
Delannoy Livre.book Page 118 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
Enfin, la norme ANSI prévoit que la macro va_end doit être appelée avant de sortir de la
fonction concernée. Si vous manquez à cette règle, vous courrez le risque de voir un prochain
appel à la fonction conduire à un mauvais fonctionnement du programme.
L es arguments variables peuvent être de types différents, à condition toutefois que la fonction
soit en mesure de les connaître, d’une façon ou d’une autre.
L es macros va_start et va_end, ainsi que la description du type va_list, figurent dans le fichier
en-tête stdarg.h (d’où la directive #include correspondante). Cette description utilise une
méthode de définition de type (instruction typedef) qui ne sera exposée que dans le chapitre
relatif aux structures. Notez bien qu’ici vous n’avez pas besoin de savoir ce qui se cache derrière
va_list pour l’utiliser correctement.
12.2 Second exemple
La gestion de la fin de la liste des arguments variables est laissée au bon soin de l’utilisateur ; en
effet, il n’existe aucune fonction permettant de connaître le nombre effectif de ces arguments.
Cette gestion peut se faire :
●
par sentinelle, comme dans notre précédent exemple,
●
par transmission, en argument fixe, du nombre d’arguments variables.
Voici un exemple de fonction utilisant cette seconde technique. Nous n’y avons pas prévu
d’autres arguments fixes que celui spécifiant le nombre d’arguments variables.
Arguments variables dont le nombre est fourni en argument fixe
#include <stdio.h>
#include <stdarg.h>
void essai (int nbpar, ...)
{ va_list adpar ;
int parv, i ;
printf ("nombre de valeurs : %d\n", nbpar) ;
va_start (adpar, nbpar) ;
for (i=1 ; i<=nbpar ; i++)
{ parv = va_arg (adpar, int) ;
printf ("argument variable : %d\n", parv) ;
}
}
118
© Éditions Eyrolles
Delannoy Livre.book Page 119 Mercredi, 6. mai 2009 4:26 16
chapitre n° 6
La programmation modulaire et les fonctions
Arguments variables dont le nombre est fourni en argument fixe (suite)
main()
{ printf ("premier essai\n") ;
essai (3, 15, 30, 40) ;
printf ("\ndeuxième essai\n") ;
essai (0) ;
}
premier essai
nombre de valeurs
argument variable
argument variable
argument variable
:
:
:
:
3
15
30
40
deuxième essai
nombre de valeurs : 0
© Éditions Eyrolles
119
Delannoy Livre.book Page 120 Mercredi, 6. mai 2009 4:26 16
Programmer en langage C
Exercices
Tous ces exercices sont corrigés en fin de volume.
1) Écrire :
●
une fonction, nommée f1, se contentant d’afficher "bonjour" (elle ne possèdera aucun
argument ni valeur de retour),
●
une fonction, nommée f2, qui affiche "bonjour" un nombre de fois égal à la valeur reçue
en argument (int) et qui ne renvoie aucune valeur,
●
une fonction, nommée f3, qui fait la même chose que f2, mais qui, de plus, renvoie la valeur
(int) 0.
Écrire un petit programme appelant successivement chacune de ces trois fonctions, après les
avoir convenablement déclarées sous forme d’un prototype.
2) Qu’affiche le programme suivant ?
int n=5 ;
main()
{
void fct (int p) ;
int n=3 ;
fct(n) ;
}
void fct(int p)
{
printf("%d %d", n, p) ;
}
3) Écrire une fonction qui se contente de comptabiliser le nombre de fois où elle a été appelée
en affichant seulement un message de temps en temps, à savoir :
●
au premier appel : *** appel 1 fois ***
●
au dixième appel : *** appel 10 fois ***
●
au centième appel : *** appel 100 fois ***
●
et ainsi de suite pour le millième, le dix millième appel...
●
On supposera que le nombre maximal d’appels ne peut dépasser la capacité d’un long.
4) Écrire une fonction récursive calculant la valeur de la « fonction d’Ackermann » A définie
pour m>0 et n>0 par :
A(m,n) = A(m-1,A(m,n-1))
pour m>0 et n>0
A(0,n) = n+1
pour n>0
A(m,0) = A(m-1,1)
pour m>0
120
© Éditions Eyrolles
Téléchargement