IV. Fonctions▲
Beaucoup de langages distinguent deux sortes de sous-programmes 24 : les fonctions et les procédures. L'appel d'une fonction est une expression, tandis que l'appel d'une procédure est une instruction. Ou, si on préfère, l'appel d'une fonction renvoie un résultat, alors que l'appel d'une procédure ne renvoie rien.
En C on retrouve ces deux manières d'appeler les sous-programmes, mais du point de la syntaxe le langage ne connait que les fonctions. Autrement dit, un sous-programme est toujours supposé renvoyer une valeur, même lorsque celle-ci n'a pas de sens ou n'a pas été spécifiée. C'est pourquoi on ne parlera ici que de fonctions. C'est dans la syntaxe et la sémantique de la définition et de l'appel des fonctions que résident les principales différences entre le C original et le C ANSI. Nous expliquons principalement le C ANSI, rassemblant dans une section spécifique (cf. section IV.B) la manière de faire du C original.
24La notion de sous-programme (comme les procédures de Pascal, les subroutines de Fortran, etc.) est supposée ici connue du lecteur.
IV-A. Syntaxe ANSI ou « avec prototype »▲
IV-A-1. Définition▲
Une fonction se définit par la construction :
typeopt ident ( déclaration-un-ident , ... déclaration-un-ident )
instruction-bloc
Notez qu'il n'y a pas de point-virgule derrière la « ) » de la première ligne. La présence d'un point-virgule à cet endroit provoquerait des erreurs bien bizarres, car la définition serait prise pour une déclaration (cf. section IV.A.4).
La syntaxe indiquée ici est incomplète ; elle ne convient qu'aux fonctions dont le type est défini simplement, par un identificateur. On verra ultérieurement (cf. section V.D.1) la manière de déclarer des fonctions rendant un résultat d'un type plus complexe.
La première ligne de la définition d'une fonction s'appelle l'entête, ou parfois le prototype, de la fonction. Chaque formule déclaration-un-ident possède la même syntaxe qu'une déclaration de variable 25.
Exemple.
int
extract
(
char
*
dest, char
*
srce, int
combien) {
/* copie dans dest les combien premiers caractères de srce */
/* renvoie le nombre de caractères effectivement copiés */
int
compteur;
for
(
compteur =
0
; compteur <
combien &&
*
srce !=
'
\0
'
; compteur++
)
*
dest++
=
*
srce++
;
*
dest =
'
\0
'
;
return
compteur;
}
Contrairement à d'autres langages, en C on ne peut pas définir une fonction à l'intérieur d'une autre : toutes les fonctions sont au même niveau, c'est-à-dire globales. C'est le cas notamment de main, qui est une fonction comme les autres ayant pour seule particularité un nom convenu.
25Restriction : on n'a pas le droit de « mettre en facteur » un type commun à plusieurs arguments. Ainsi, l'entête de la fonction extract donnée en exemple ne peut pas s'écrire sous la forme char *extract(char *dest, *srce, int n).
IV-A-2. Type de la fonction et des arguments▲
L'entête de la fonction définit le type des objets qu'elle renvoie. Ce peut être :
- tout type numérique ;
- tout type pointeur ;
- tout type struct ou union.
Si le type de la fonction n'est pas indiqué, le compilateur suppose qu'il s'agit d'une fonction à résultat entier. Ainsi, l'exemple précédent aurait aussi pu être écrit de manière équivalente (mais cette pratique n'est pas conseillée) :
extract
(
char
*
dest, char
*
srce, int
combien)
/* etc. */
Fonctions sans résultat. Lorsqu'une fonction ne renvoie pas une valeur, c'est-à-dire lorsqu'elle correspond plus à une procédure qu'à une vraie fonction, il est prudent de la déclarer comme rendant un objet de type voidvoid. Ce type est garanti incompatible avec tous les autres types : une tentative d'utilisation du résultat de la fonction provoquera donc une erreur à la compilation. Le programmeur se trouve ainsi à l'abri d'une utilisation intempestive du résultat de la fonction. Exemple :
void
extract
(
char
*
dest, char
*
srce, int
combien) {
/* copie dans dest les combien premiers caractères de srce */
/* maintenant cette fonction ne renvoie rien */
int
compteur;
for
(
compteur =
0
; compteur <
combien &&
*
srce !=
'
\0
'
; compteur++
)
*
dest++
=
*
srce++
;
*
dest =
'
\0
'
;
}
Fonctions sans arguments. Lorsqu'une fonction n'a pas d'arguments, sa définition prend la forme
typeopt ident (
void
)
instruction-
bloc
On notera que, sauf cas exceptionnel, on ne doit pas écrire une paire de parenthèses vide dans la déclaration ou la définition d'une fonction. En effet, un entête de la forme
type ident
(
)
ne signifie pas que la fonction n'a pas d'arguments, mais (cf. section IV.B.2) que le programmeur ne souhaite pas que ses appels soient contrôlés par le compilateur.
IV-A-3. Appel des fonctions▲
L'appel d'une fonction se fait en écrivant son nom, suivi d'une paire de parenthèses contenant éventuellement une liste d'arguments effectifs.
Notez bien que les parenthèses doivent toujours apparaitre, même si la liste d'arguments est vide : si un nom de fonction apparait dans une expression sans les parenthèses, alors il a la valeur d'une constante adresse (l'adresse de la fonction) et aucun appel de la fonction n'est effectué.
Passage des arguments. En C, le passage des arguments se fait par valeur. Cela signifie que les arguments formels 26 de la fonction représentent d'authentiques variables locales initialisées, lors de l'appel de la fonction, par les valeurs des arguments effectifs 27 correspondants.
Supposons qu'une fonction ait été déclarée ainsi
type fonction (
type1 arg formel1 , ... typek arg formelk )
etc.
alors, lors d'un appel tel que
fonction (
arg effectif 1
, ... arg effectif k )
la transmission des valeurs des arguments se fait comme si on exécutait les affectations :
arg formel1 =
arg effectif 1
...
arg formelk =
arg effectif k
Cela a des conséquences très importantes :
- les erreurs dans le nombre des arguments effectifs sont détectées et signalées,
- si le type d'un argument effectif n'est pas compatible avec celui de l'argument formel correspondant, une erreur est signalée,
- les valeurs des arguments effectifs subissent les conversions nécessaires avant d'être rangées dans les arguments formels correspondants (exactement les mêmes conversions qui sont faites lors des affectations).
Remarque. Le langage ne spécifie pas l'ordre chronologique des évaluations des arguments effectifs d'un appel de fonction. Ces derniers doivent donc être sans effets de bord les uns sur les autres. Par exemple, il est impossible de prévoir quelles sont les valeurs effectivement passées à une fonction lors de l'appel :
x =
une_fonction
(
t[i++
], t[i++
]); /* ERREUR !!! */
Si i0 est la valeur de i juste avant l'exécution de l'instruction ci-dessus, alors cet appel peut aussi bien se traduire par une fonction(t[i0], t[i0 + 1]) que par une fonction(t[i0 + 1], t[i0]) ou même par une fonction(t[i0], t[i0]). Ce n'est sûrement pas indifférent !
Appel d'une fonction inconnue. En C une fonction peut être appelée alors qu'elle n'a pas été définie (sous-entendu : entre le début du fichier et l'endroit ou l'appel figure).
Le compilateur suppose alors
- que la fonction renvoie un int,
- que le nombre et les types des arguments formels de la fonction sont ceux qui correspondent aux arguments effectifs de l'appel (ce qui, en particulier, empêche les contrôles et conversions mentionnés plus haut).
De plus, ces hypothèses sur la fonction constituent pour le compilateur une première déclaration de la fonction. Toute définition ou déclaration ultérieure tant soit peu différente sera qualifiée de « redéclaration illégale » 28.
Lorsque les hypothèses ci-dessus ne sont pas justes, en particulier lorsque la fonction ne renvoie pas un int, il faut :
- soit écrire la définition de la fonction appelée avant celle de la fonction appelante,
- soit écrire, avant l'appel de la fonction, une déclaration externe de cette dernière, comme expliqué à la section 4.1.4.
26 Les arguments formels d'une fonction sont les identificateurs qui apparaissent dans la définition de la fonction, déclarés à l'intérieur de la paire de parenthèses.
27 Les arguments effectifs d'un appel de fonction sont les expressions qui apparaissent dans l'expression d'appel, écrites à l'intérieur de la paire de parenthèses caractéristiques de l'appel.
28C'est une erreur surprenante, car le programmeur, oubliant que l'appel de la fonction a entrainé une déclaration implicite, conçoit sa définition ou déclaration ultérieure comme étant la première déclaration de la fonction.
IV-A-4. Déclaration « externe » d'une fonction▲
Une déclaration externe d'une fonction est une déclaration qui n'est pas en même temps une définition. On « annonce » l'existence de la fonction (définie plus loin, ou dans un autre fichier) tout en précisant le type de son résultat et le nombre et les types de ses arguments, mais on ne donne pas son corps, c'est-à-dire les instructions qui la composent.
Cela se fait en écrivant
- soit un entête identique à celui qui figure dans la définition de la fonction (avec les noms des arguments formels), suivi d'un point-virgule ;
- soit la formule obtenue à partir de l'entête précédent, en y supprimant les noms des arguments formels.
Par exemple, si une fonction a été définie ainsi
void
truc
(
char
dest[80
], char
*
srce, unsigned
long
n, float
x) {
corps de la fonction
}
alors des déclarations externes correctes sont :
extern
void
truc
(
char
dest[80
], char
*
srce, unsigned
long
n, float
x);
ou
ou extern
void
machin
(
char
[80
], char
*
, unsigned
long
, float
);
Ces expressions sont appelées des prototypes de la fonction. Le mot extern est facultatif, mais le point- virgule est essentiel. Dans la première forme, les noms des arguments formels sont des identificateurs sans aucune portée.
IV-B. Syntaxe originale ou « sans prototype »▲
IV-B-1. Déclaration et définition▲
Définition. En syntaxe originale la définition d'une fonction prend la forme
typeopt ident ( ident , ... ident )
déclaration ... déclaration
instruction-bloc
Les parenthèses de l'entête ne contiennent ici que la liste des noms des arguments formels. Les déclarations de ces arguments se trouvent immédiatement après l'entête, avant l'accolade qui commence le corps de la fonction.
Exemple :
int
extract
(
dest, srce, combien)
/* copie dans dest les combien premiers caractères de srce */
/* renvoie le nombre de caractères effectivement copiés */
char
*
dest, char
*
srce;
int
combien;
{
int
compteur;
for
(
compteur =
0
; compteur <
combien &&
*
srce !=
'
\0
'
; compteur++
)
*
dest++
=
*
srce++
;
*
dest =
'
\0
'
;
return
compteur;
}
Déclaration externe. En syntaxe originale, la déclaration externe de la fonction précédente prend une des formes suivantes :
extern
int
extract
(
);
ou
int
extract
(
);
Comme on le voit, ni les types, ni même les noms, des arguments formels n'apparaissent dans une déclaration externe 29.
29Par conséquent, les déclarations externes montrées ici sont sans aucune utilité, puisque int est le type supposé des fonctions non déclarées.
IV-B-2. Appel▲
En syntaxe originale, lors d'un appel de la fonction le compilateur ne tient aucun compte ni du nombre ni des types des arguments formels 30, indiqués lors de la définition de la fonction. Chaque argument est évalué séparément, puis
- les valeurs des arguments effectifs de type char ou short sont converties dans le type int ;
- les valeurs des arguments effectifs de type float sont converties dans le type double ;
- les valeurs des arguments effectifs d'autres types sont laissées telles quelles.
Juste avant le transfert du contrôle à la fonction, ces valeurs sont copiées dans les arguments formels correspondants. Ou plus exactement, dans ce que la fonction appelante croit être les emplacements des arguments formels de la fonction appelée : il n'y a aucune vérification de la concordance, ni en nombre ni en type, des arguments formels et des arguments effectifs. Cela est une source d'erreurs très importante. Par exemple, la fonction carre ayant été ainsi définie
double
carre
(
x)
double
x;
{
return
x *
x;
}
l'appel
x =
carre
(
2
);
est erroné, car la fonction appelante dépose la valeur entière 2 à l'adresse du premier argument formel (qu'elle « croit » entier). Or la fonction appelée croit recevoir le codage d'un nombre flottant double. Le résultat n'a aucun sens. Des appels corrects auraient été
x =
carre
((
double
) 2
);
ou
x =
carre
(
2
.0
);
30,Mais oui, vous avez bien lu ! C'est là la principale caractéristique de la sémantique d'appel des fonctions du C original.
IV-B-3. Coexistence des deux syntaxes▲
À côté de ce que nous avons appelé la syntaxe ANSI, la syntaxe originale pour la définition et la déclaration externe des fonctions fait partie elle aussi du C ANSI ; de nos jours les programmeurs ont donc le choix entre l'une ou l'autre forme. Lorsque la fonction a été définie ou déclarée sous la syntaxe originale, les appels de fonction sont faits sans vérification ni conversion des arguments effectifs. Au contraire, lorsque la fonction a été spécifiée sous la syntaxe ANSI, ces contrôles et conversions sont effectués.
À cause de cette coexistence, si une fonction n'a pas d'argument, la définition de son entête en C ANSI doit s'écrire sous la forme bien peu heureuse :
typeopt ident (
void
)
car une paire de parenthèses sans rien dedans signifierait non pas que la fonction n'a pas d'arguments, mais qu'elle est déclarée sans prototype, c'est-à-dire que ses appels doivent être traités sans contrôle des arguments.
Exemples :
int
getchar
(
void
); /* une fonction sans arguments dont les appels seront contrôlés */
double
moyenne
(
); /* une fonction avec ou sans arguments, aux appels incontrôlés */
IV-C. Arguments des fonctions▲
IV-C-1. Passage des arguments▲
L'idée maitresse est qu'en C le passage des arguments des fonctions se fait toujours par valeur. Après avoir fait les conversions opportunes la valeur de chaque argument effectif est affectée à l'argument formel correspondant. Si l'argument effectif est d'un type simple (nombre, pointeur) ou d'un type struct ou union, sa valeur est recopiée dans l'argument formel correspondant, quelle que soit sa taille, c'est-à-dire quel que soit le nombre d'octets qu'il faut recopier.
IV-C-2. Arguments de type tableau▲
Apparaissant dans la partie exécutable d'un programme, le nom d'un tableau, et plus généralement toute expression déclarée « tableau de T », est considérée comme ayant pour type « adresse d'un T » et pour valeur l'adresse du premier élément du tableau. Cela ne fait pas intervenir la taille effective du tableau 31. Ainsi lorsqu'un argument effectif est un tableau, l'objet effectivement passé à la fonction est uniquement l'adresse du premier élément et il suffit que l'espace réservé dans la fonction pour un tel argument ait la taille, fixe, d'un pointeur.
D'autre part, en C il n'est jamais vérifié que les indices des tableaux appartiennent bien à l'intervalle 0 … N-1 déterminé par le nombre d'éléments indiqué dans la déclaration. Il en découle la propriété suivante :
Dans la déclaration d'un argument formel t de type « tableau de T » :
- l'indication du nombre d'éléments de t est sans utilité 32 ;
- les formules « type t[ ] » et « type *t » sont tout à fait équivalentes ;
- la fonction pourra être appelée indifféremment avec un tableau ou un pointeur pour argument effectif.
Exemple. L'entête « vague » de la fonction suivante
int
strlen
(
chaine de caractères s) {
int
i =
0
;
while
(
s[i] !=
0
)
i++
;
return
i;
}
peut indifféremment être concrétisée de l'une des trois manières suivantes :
int
strlen
(
char
s[80
])
int
strlen
(
char
s[])
int
strlen
(
char
*
s)
Dans les trois cas, si t est un tableau de char et p l'adresse d'un char, les trois appels suivants sont corrects :
l1 =
strlen
(
t);
l2 =
strlen
(
p);
l3 =
strlen
(
"
Bonjour
"
);
À retenir : parce que les arguments effectifs sont passés par valeur, les tableaux sont passés par adresse. Passage par valeur des tableaux. Cependant, puisque les structures sont passées par valeur, elles fournissent un moyen pour obtenir le passage par valeur d'un tableau, bien que les occasions où cela est nécessaire semblent assez rares. Il suffit de déclarer le tableau comme le champ unique d'une structure « enveloppe » ne servant qu'à cela. Exemple :
struct
enveloppe {
int
t[100
];
}
;
void
possible_modification
(
struct
enveloppe x) {
x.t[50
] =
2
;
}
main
(
) {
struct
enveloppe x;
x.t[50
] =
1
;
possible_modification
(
x);
printf
(
"
%d
\n
"
, x.t[50
]);
}
La valeur affichée est 1, ce qui prouve que la fonction appelée n'a modifié qu'une copie locale du tableau enveloppé, c'est-à-dire que celui-ci a bien été passé par valeur (les structures seront vues à la section 5.2.1).
31 Il n'en est pas de même lors de la définition, où la taille du tableau ne peut pas être ignorée (elle est indispensable pour l'allocation de l'espace mémoire).
32 Attention, cette question se complique notablement dans le cas des tableaux multidimensionnels. Cf. 6.2.3
IV-C-3. Arguments par adresse▲
Les arguments qui sont d'un type simple (char, int, pointeur…) ou bien des struc ou des union sont toujours passés par valeur. Lorsque l'argument effectif est une variable, ce mécanisme empêche qu'elle puisse être modifiée par la fonction appelée. Or une telle modification est parfois souhaitable ; elle requiert que la fonction puisse accéder non seulement à la valeur de la variable, mais aussi à son adresse.
C'est ce qui s'appelle le passage par adresse des arguments. Alors que certains langages le prennent en charge, C ne fournit aucun mécanisme spécifique : le passage de l'adresse de l'argument doit être programmé explicitement en utilisant comme argument un pointeur vers la variable.
Par exemple, supposons avoir à écrire une fonction
int
quotient
(
a, b)
censée renvoyer le quotient de la division euclidienne de l'entier a par l'entier b et devant en plus remplacer a par le reste de cette division. On devra la définir :
int
quotient
(
int
*
a, int
b) {
int
r =
*
a /
b;
*
a =
*
a %
b;
return
r;
}
Ci-dessus, b représente un entier ; a l'adresse d'un entier. La notation *a fait référence à une variable (ou, plus généralement, une lvalue) appartenant à la procédure appelante.
L'argument effectif correspondant à un tel argument formel doit être une adresse. Souvent, cela est obtenu par l'utilisation de l'opérateur &. Par exemple, si x, y et z sont des variables entières, un appel correct de la fonction précédente sera
z =
quotient
(&
x, y);
Bien entendu, l'opérateur & ne doit pas être utilisé si l'argument effectif est déjà une adresse. Le schéma suivant résume les principales situations possibles. Soit les fonctions
f
(
int
a) {
...
}
g
(
int
*
b) {
...
}
h
(
int
c, int
*
d) {
...
}
À l'intérieur de la fonction h, des appels corrects de f et g sont :
f
(
c);
f
(*
d);
g
(&
c);
g
(
d);
cette dernière expression correspondant, bien sûr, à g(&*d).
IV-C-4. Arguments en nombre variable▲
Le langage C permet de définir des fonctions dont le nombre d'arguments n'est pas fixé et peut varier d'un appel à un autre. Nous exposons ici la manière dont cette facilité est réalisée dans le C ANSI. Dans le C original elle existe aussi ; elle consiste à utiliser, sans garde-fou, une faiblesse du langage (la non-vérification du nombre d'arguments effectifs) et une caractéristique du système sous-jacent (le sens d'empilement des arguments effectifs). Mais tout cela est suffisamment casse-cou pour que nous préférions n'expliquer que la manière ANSI de faire, un peu plus fiable.
Une fonction avec un nombre variable d'arguments est déclarée en explicitant quelques arguments fixes, au moins un, suivis d'une virgule et de trois points : « … » . Exemple :
int
max
(
short
n, int
x0, ...)
Une telle fonction peut être appelée avec un nombre quelconque d'arguments effectifs, mais il faut qu'il y en ait au moins autant qu'il y a d'arguments formels nommés, ici deux. Exemple d'appel :
m =
max
(
3
, p, q, r);
Pour les arguments nommés, l'affectation des valeurs des arguments effectifs aux arguments formels est faite comme d'ordinaire. Pour les arguments anonymes elle s'effectue comme pour une fonction sans prototype : les char et les short sont convertis en int, les float en double et il n'y a aucune autre conversion.
À l'intérieur de la fonction on accède aux arguments anonymes au moyen des macros va start et va arg définies dans le fichier stdarg.h, comme le montre l'exemple suivant :
#include <stdarg.h>
int
max
(
short
n, int
x1, ...) {
va_list magik;
int
x, i, m;
m =
x1;
va_start
(
magik, x1);
for
(
i =
2
; i <=
n; i++
)
if
((
x =
va_arg
(
magik, int
)) >
m)
m =
x;
return
m;
}
Des appels corrects de cette fonction seraient :
a =
max
(
3
, p, q, r);
b =
max
(
8
, x[0
], x[1
], x[2
], x[3
], x[4
], x[5
], x[6
], x[7
]);
c =
max
(
2
, u, 0
);
Les éléments définis dans le fichier stdarg.h sont :
va list pointeur Déclaration de la variable pointeur, qui sera automatiquement gérée par le dispositif d'accès aux arguments. Le programmeur choisit le nom de cette variable, mais il n'en fait rien d'autre que la mettre comme argument dans les appels des macros va start et va arg.
va start(pointeur, dernier argument) Initialisation de la variable pointeur ; dernier argument doit être le der- nier des arguments explicitement nommés dans l'entête de la fonction.
va arg(pointeur, type) Parcours des arguments anonymes : le premier appel de cette macro donne le premier argument anonyme ; chaque appel suivant donne l'argument suivant le dernier déjà obtenu. Chaque fois, type doit décrire le type de l'argument qui est en train d'être référencé.
Comme l'exemple le montre, le principe des fonctions avec un nombre variable d'arguments est de déclarer effectivement au moins un argument, dont la fonction peut déduire l'adresse des autres. Il faut aussi choisir le moyen d'indiquer à la fonction le nombre de valeurs réellement fournies. Dans l'exemple ci-dessus ce nombre est passé comme premier argument ; une autre technique consiste à utiliser une valeur « intruse » (ex. : le pointeur NULL parmi des pointeurs valides). La fonction printf est un autre exemple de fonction avec un nombre variable d'arguments. Elle est déclarée (dans le fichier stdio.h) :
int
printf
(
char
*
, ...);
Ici encore, le premier argument (le format) renseigne sur le nombre et la nature des autres arguments. La question des fonctions formelles (fonctions arguments d'autres fonctions) et d'autres remarques sur les fonctions et leurs adresses sont traitées à la section 6.3.1.