CHAPITRE V
Les Fonctions Et Les Noms Externes
I may not be totally perfect, but parts of me are
excellent.
-- Ashleigh Brilliant
http://www.mescours.ma/C/c5.html
ntounsi@emi.ac.ma
Version: Sept. 2000, Dernière MàJ: Dec. 2021
Rappelons qu'un programme C
est un ensemble de fonctions et qu'en programmation une fonction est un
code (bloc d'instructions) qui a un nom qui permet de l'utiliser comme un
tout à plusieurs endroits. Si en plus on paramètre ce code, on peut
l'utiliser sur différents objets, appelés alors paramètres, et par là même
lui faire retourner une valeur résultat. Rappelons aussi que dans un
programme C une des fonctions doit s'appeler main
.
En C on peut compiler un programme utilisant une fonction même si cette
dernière n'est pas encore écrite. (On dit référence en avant). Cette
possibilité est très intéressante dans la mesure où, au moment de
l'écriture d'un programme, on n'a pas encore envie de réfléchir à comment
programmer une fonction, pour se concentrer uniquement sur l'algorithme en
cours qui va l'utiliser. Il suffit d'avoir une idée de ce qu'elle fait
--le quoi ou sa spécification[1].
D'où les notions de déclaration et de définition de fonction. Définition
qui du reste peut être remplacée par une autre sans remettre en cause le
programme, si la spécification est respectée. En outre si cette définition
se trouve sur un fichier séparé, elle est la seule à être recompilée (en
un fichier .o
, cf Ch-I).
Une déclaration de fonction est la donnée de son entête ou interface --
appelé prototype de fonction par la norme ANSI -- qui est
constituée du nom de la fonction, du type de son résultat (par défaut int
)
et du type de ses paramètres éventuels.
Type Nom ( ListeTypeParametres );
Elle est utile donc car on peut utiliser son nom pour y faire appel avec toute la cohérence nécessaire en ce qui concerne les paramètres.
Exemple:
float cube(float); /* Declaration d'une fonction cube() */ main() { float x; x = cube(3.0); ... }
A la rencontre de cube(3.0)
, le compilateur saura que
l'identificateur cube
est une fonction qui retourne un objet
float
et qui a un argument de type float
. La
présence des parenthèses dans cube(3.0)
suffit en fait pour
indiquer que c'est une fonction. Mais elle sera considérée comme
retournant un entier int
, type par défaut, si la déclaration
est absente ou si le type du résultat n'est pas précisé. Si le type de la
fonction est int
donc, la déclaration n'est pas nécessaire
(cela par conformité avec les premiers compilateurs C qui tolèrent ce
fait). Mais il vaut mieux la mentionner quand même car c'est plus
discipliné, ne serait ce que dans un esprit de clarté.
Le type des paramètres dans la déclaration de fonction n'est pas nécessaire non plus. On aurait pu déclarer (forme classique encore de la déclaration d'une fonction)
float cube();
Mais il vaut mieux encore le mettre --sinon on a des surprise--pour
pouvoir par exemple convertir les paramètres effectifs avant l'appel. Dans
un appel comme cube(3),
3 est converti en float
.
(Voir §1.4. plus bas). Noter cependant qu'il
s'agit bien de types sans variables.
La définition d'une fonction est la donnée du texte de son algorithme, qu'on appelle corps de la fonction. Elle a la forme :
type Nom ( DéclarationParametresFormels )
{ CorpsDeFonction }
Attention: il n'y a pas le terminateur ;
après la
parenthèse fermante de la liste des paramètres. Erreur classique chez les
habitué(e)s du langage Pascal, ou les débutant(e)s..
Exemple:
float cube (float x) { return (x*x*x); }
Dans cette définition on rappelle bien sûr l'entête de la fonction et
on y rajoute le nom des paramètres, x
ici qui joue le rôle
de paramètre formel. Si la définition d'une fonction
précède son utilisation, il n'y a par conséquent pas besoin de
déclaration.
Remarque: On peut écrire
float cube (x) float x;
{...}
par conformité avec l'ancienne forme de C où on énumère les paramètres
formels dans la parenthèse et spécifie leur type après. Là par contre, il
y a le caractère « ;
» car c'est la déclaration des
paramètres formels (Voir plus bas).
L'écriture
NomFonction ( ListeParametresEffectifs );
est un appel de fonction. C'est un nom suivi de parenthèses. Si la fonction est définie avec des paramètres formels, il faut fournir, dans l'ordre respectif, les paramètres effectifs correspondants, si possible avec la concordance des types. N'importe quelle expression peut être paramètre effectif en principe.
Exemple: Pour une déclaration donnée
float f (float, int, char);
nous pouvons avoir les appels suivants:
y = 3 * f(x, i, c);
f(0.5, i+j, "abc"[2]);
x = f(cube(3.5), t[2], c);
Un appel de fonction est une expression (r_value) dont la valeur
est le résultat de la fonction, en l'occurrence l'objet retourné, et le
type celui de la fonction (sauf si celle-ci est de type void
.
Voir plus bas). Un appel de fonction peut donc se trouver dans n'importe
quelle expression. Il peut constituer lui même une instruction, s'il est
suivi de ;
. Le résultat est alors perdu[2],
i.e. non utilisé, comme dans la deuxième expression ci-dessus.
Remarque: A l'appel d'une fonction f(e1, e2, e3,...), l'ordre d'évaluation des paramètres effectifs n'est pas défini: si il y a correspondance 1, 2, 3 avec l'ordre des paramètres formels, il n'est sûr que l'évaluation et la transmission se fera dans cet ordre. Il vaut mieux donc éviter des expressions à effet de bord du type
f(i=3, i, k++, k,...)
Voir exercice V.1.2 plus bas.
Le langage C classique est très libéral sur l'usage des fonctions. Dans
sa définition initiale, C ne permet pas de mentionner (sous peine d'erreur
compilation) le type des paramètres dans la déclaration d'une fonction et
ainsi aucun contrôle n'est (ne peut être) fait lors d'un appel. C'est le
programmeur lui-même qui doit gérer la concordance des paramètres en
nombre, rang et type (au risque d'avoir des surprises, voir exercices
V.1.1). La politique est que ce qui est transmis à une fonction est un double
ou un int
. Lors de l'appel, un argument float
est converti en double
sinon en int
. Cette
liberté, si elle est intéressante surtout pour utiliser une fonction avec
des arguments variant en nombre, peut se révéler dangereuse si pratiquée à
outrance. De plus cette liberté laisse beaucoup de choix au compilateur.
Or une règle d'or en programmation est d'éviter tout ce qui dépend d'un
compilateur donné.
La norme ANSI a repris cela à son compte en introduisant la notion de prototype de fonction. C'est la forme complète de sa déclaration: nom de la fonction et types du résultat et des paramètres. Cette forme a l'avantage de permettre au compilateur de vérifier la cohérence des arguments (en type et en nombre) dès l'appel et d'avertir par un message d'erreur (ou du moins un avertissement «Warning») en cas de non conformité [3]. Néanmoins, des conversions peuvent s'opérer à l'appel, comme dans les expressions, quand c'est possible. Cette notion de prototype permet aussi de déclarer une fonction avec un nombre variable de paramètres (voir plus loin § 1.6.).
Dans la suite, et dans la définition d'une fonction, nous utiliserons parfois le schéma ANSI
f (int x, int y, float z)
{...}
parfois le schéma
f (x, y, z) int x,y; float y;
{...}
Ce dernier ayant l'avantage de factoriser les variables d'un même type (x,y
ici) et d'être compatible avec le langage C classique.
Exercices
V.1.1 Concordance des paramètres.
Soit la fonction:
f (x, y) double x; int y; { printf("x = %lf y = %d\n", x,y); }
qui imprime juste ses paramètres, x double
et y int
.
a) Exécuter les appels suivants et interpréter le résultat:
f(1.0, 2); f(1, 2); f(1.0, 'A'); f(1.0, "toto"); f('A', 2);
b) De même pour les appels suivants (plusieurs arguments à l'appel):
f(1.0, 2, 13, 14); f(1, 2, 13, 14);
Indication: Dans f
, x
occupe deux mots de 4
octets.
c) Refaire (a) et (b) pour f
définie selon le schéma
ANSI: f(double x, int y){...}
(contrôle des arguments) et
regarder le résultat de la compilation.
V.1.2 Ordre d'évaluation des paramètres et effets de bord.
Soit la fonction
f(x, y, z) int x, y, z; { printf("%d %d %d \n", x, y, z); }
Exécuter les appels suivants: (avec int i=1;
)
f(i, i++, i++); f(i++, i, i++); f(i++, i++, i);
Conclure.
En C, comme nous l'avons déjà signalé, il n'y a pas la notion de procédure, au sens classique, avec aucun ou plusieurs paramètres résultats. Les fonctions retournent en principe un résultat, celui de leur calcul, est nécessitent donc un passage des paramètres par valeur.
Pour des «routines» sans paramètres résultats, on peut déclarer une
fonction de type void
i.e. neutre. Par exemple une routine
qui trace une ligne (suite de "_
") pourra être déclarée
void ligne();
est définie comme suit:
void ligne(){
printf("____________________________________\n");
}
Et avec un paramètre donné, longueur de la ligne
void ligne(int n){ int i; for (i=0; i<n; i++) printf("_"); printf("\n"); }
void
sert quand on n'attend pas de résultat d'une fonction.
Si on veut définir une «procédure» avec données et résultats, par exemple
la résolution d'une équation de second degré (décidément on ne se
débarrassera jamais de cet exemple) à coefficients entiers, on déclarera
une fonction equa2
comme suit:
void equa2 (int, int, int, *double, *double);
et on l'utilisera par l'appel
equa2 (a, b, c, &x1, &x2);
en lui passant, pour les paramètres résultats attendus x1
et x2
, les adresses ou références de ces variables. A
travers ces références, c'est les objets désignés par x1
et
x2
qui seront manipulés dans la fonction et qui seront donc
touchés pour recevoir, par effet de bord, les valeurs solutions attendues.
La définition de la fonction sera en conséquence:
void equa2 (a, b, c, s1, s2 ) /* noter l'absence de & */ int a, b, c; double *s1, *s2; /* et la présence de * ici */ { int delta ... ... if (delta > 0) { *s1 = (-b - sqrt((double)delta)) / (2. * a); *s2 = (-b + sqrt((double)delta)) / (2. * a); ... }
Les valeurs de x1
et x2
de l'appelante sont
manipulées par les pointeurs associés s1
et s2
.
Remarque:
Le qualificatif void
n'est pas toujours nécessaire. On peut
l'omettre, la fonction sera alors prise comme rendant un entier qui ne
servira peut être à rien. Mais il est de bon usage de le mettre, surtout
en C ANSI, car cela favorise la documentation pour la compréhension et la
réutilisation du programme. D'autant plus que certains compilateurs,
envoient un message d'avertissement.
Exercices:
V.1.3 Ecrire une fonction qui permute deux variables x et y passées en arguments. Ecrire une autre fonction qui classe trois variables a, b et c passées en arguments, dans l'ordre croissant (si a = 2, b = 1, c = 3 on voudrait a = 1, b = 2, c = 3).
V.1.4 Que peut on dire des appels de fonctions à effet de bord
(e.g. g(&x)
) comme paramètres effectifs (e.g. f(
g(&x), ..., g(&x), ...)
)?
Indication: Prendre g(x)= x*x et pour f une fonction qui imprime ses paramètres et se rappeler l'exercice V.1.2 précédent.
C'est une innovation très intéressante introduite en C ANSI. Même du
point de vue du choix de la syntaxe. On met trois points de suspension ...
avant la parenthèse fermante de la liste des paramètres:
f(x, y, z, ...)
On appelle cela une ellipse. Cette caractéristique est très
utile, quand on voudrait faire un traitement sur un nombre quelconque
d'objets même de type différents. Par exemple, l'opérateur de projection
d'une relation dans le modèle relationnel de E. F. CODD, opère sur
une liste quelconque d'attributs. (C'est la liste SELECT
dans le langage SQL
). Sans aller aussi loin, printf()
ou scanf()
de C, sont des exemples de telles fonctions avec
ellipse: printf( char *, ...)
où char*
est le
paramètre format d'impression/lecture.
Plus concrètement, dans la définition d'une fonction, on a la possibilité de traiter un nombre quelconque de paramètres, dépendant de l'occurrence d'un appel. Un moyen pour faire cela est de disposer dans la fonction d'une liste qui contient la valeur des paramètres effectifs, et d'en connaître le nombre. Un compilateur C standard gère une telle liste et la met à disposition en cours d'exécution (Figure V-1).
déclaration: f(type x, type y, int z,
...);
appel: f(x, y ,3 , a, b, c);
pile d'exécution:
Fig-V-1 Mécanisme de Fonction avec Nombre Variable de
Paramètres.
(Ici 6 paramètres: x, y et z déjà signalés, plus
a, b, c fournis à l'appel.
La valeur 3 pour z est là pour en indiquer le nombre).
Pour bien utiliser ce mécanisme, le programmeur doit aussi choisir un
moyen d'indiquer lors de chaque appel d'une fonction avec ellipse, combien
de paramètres sont actuellement transmis. On pourra décider que le dernier
paramètre connu avant l'ellipse (z
dans la forme f(type
x, type y, int z, ...)
) est ce nombre.
Remarque: Dans printf() c'est le nombre de symboles %
dans la spécification de format qui indique combien de paramètres
effectifs sont transmis.
Voici à présent les outils (type d'objet et fonctions) bibliothèques,
macro-définis, qui permettent à la fonction d'accéder au mécanisme de
liste de paramètres en la parcourant. Ils se trouvent dans le fichier
include <stdarg.h>
Déclaration de la Liste des Paramètres: Type va_list
Il faut d'abord déclarer la structure de l'objet qui contient cette liste (qu'on peut considérer comme une pile par cohérence avec la pile d'exécution). En fait il suffit juste de déclarer un pointeur vers cette liste, laquelle liste existe déjà du fait de l'ellipse.
va_list liste_param;
déclare une variable liste_param
du type va_list
,
et qui est un pointeur qui va servir à parcourir la liste en
pointant à chaque fois vers un nouveau paramètre.
Exemple:
#include <stdarg.h> void f(float x, char y, int nb_param /* z */, ...)
{ va_list liste_param; ... }
Fonctions d'Accès aux Paramètres: Fonctions va_start(),
va_arg(), va_end()
.
Les deux fonctions va_start()
et va_end()
sont juste une initialisation et une finalisation (respectivement) de la
liste des paramètres.
va_start (liste_param, nb_param);
est l'appel à une routine qui initialise l'accès à la liste des
paramètres: liste_param
est le pointeur déclaré
va_list
, et nb_param
est la taille de cette liste
(taille qui est par convention passée explicitement en paramètre à l'appel
comme nous l'avons dit).
Après traitement,
va_end (liste_param);
est l'appel à une routine qui ferme en quelque sorte la liste liste_param
.
Elle est sans effet en général, mais son usage est recommandé avant la fin
de la fonction pour une meilleure documentation.
Exemple: Squelette d'une fonction à nombre variable de paramètres
#include <stdarg.h> void f(float x, char y, int nb_param, ...) { va_list liste_param; .. va_start (liste_param, nb_param); .. va_end (liste_param); }
Pour ce qui concerne l'accès aux paramètres, on a
va_arg (liste_param, type);
qui est l'appel à une fonction qui rend le paramètre suivant de la liste
liste_param
(ou le premier paramètre après appel à
va_start()
). Elle rend donc le paramètre pointé par liste_param
.
L' argument type
est le type supposé de ce paramètre
(int
ou double
en principe, voir plus bas). On
l'utilise donc, à supposer que le prochain paramètre est du type entier,
comme suit:
param = va_arg (liste_param, int);
où param
est une variable int
locale qui
recevra à chaque fois un paramètre. Cela se fera par exemple dans une
boucle avec un appel à va_arg()
par itération.
Exemple: On complète l'exemple précédent par
void f(float x, char y, int nb_param /* z */, ...) { va_list liste_param; int i, param; va_start (liste_param, nb_param); for(i=0; i<nb_param; i++){ param = va_arg(liste_param, int); /* .. traitement de param .. */ } va_end (liste_param); }
Voici un exemple de programme principal avec des appels à cette fonction
f
, dans laquelle on imprime simplement la valeur des
paramètres transmis à chaque fois. On a rajouté les lignes:
printf("\n%d paramètres: \n", 2+nb_param);
printf("x = %f y = %c plus \n", x, y);
avant l'appel va_start
, et la ligne:
printf (" %d \n", param);
dans la boucle for
.
% cat ellipseEssai.c #include <stdarg.h> void f(float x, char y, int nb_param, ...) { /*... voir texte ci dessus */ } main(){ f(0.1, 'a', 4, 1,2,3,4); f(0.2, 'b', 0); f(0.3, 'c', 1,1); } % cc ellipseEssai.c % a.out 6 paramètres: x=0.100000 y=a plus 1 2 3 4 2 paramètres: x=0.200000 y=b plus 3 paramètres: x=0.300000 y=c plus 1
Remarquer l'appel (le deuxième) avec aucun paramètre dans l'ellipse. Noter aussi le résultat des appels suivants:
f(0.4, 'd', 3, 9); /* nb_param=3 > nombre des arguments */ 5 paramètres: x = 0.400000 y = d plus 9 0 <-- on récupère des miettes 0 f(0.5, 'e', 2, 1,2,3,4,5,6); /* nb_param=2 < ce nombre*/ 4 paramètres: x = 0.500000 y = e plus 1 <-- on ne récupère que les premiers 2
Pour une bonne utilisation, le nombre de paramètres doit par conséquent être égal au total des arguments fournis pour l'ellipse.
Variation sur un Thème: Paramètres en nombre et type variables
L'usage des fonctions avec un nombre de paramètres variable est donc
relativement simple. Dans l'exemple précédent, nous avons utilisé des
entiers int
pour ces paramètres. Peut-on utiliser d'autres
types et lesquels?
1) Si on se rappelle que lors d'un appel de fonction, les paramètres de
type char
et short
sont promus int
,
et ceux de type float
promus double
, on
conclut qu'on n'a pas intérêt à utiliser ces types (char, short,
float
) dans la macro va_arg()
, et que celle-ci doit
avoir int
ou double
comme deuxième argument.
2) Par contre si on sait par avance que les paramètres dans l'ellipse
sont tous de même type (soit T
), on peut utiliser va_arg()
par:
T param;
...
param = va_arg (liste_param, T);
Exemple: T
est STRING
. Dans
l'exemple précédent on remplace la déclaration de param
par
typedef char* STRING;
STRING param;
et écrit
param = va_arg(liste_param, STRING);
Avec l'appel
f(0.1, 'a', 2, "toto", "tintin");
on obtient
4 paramètres: x = 0.100000 y = a plus toto tintin
3) Si maintenant le type des paramètres est variable aussi à l'appel, on
doit choisir une politique pour pouvoir l'indiquer lors de l'appel et le
récupérer ensuite dans la fonction. Une solution canonique serait de
transmettre chaque paramètre précédé d'un indicateur entier valant 0 pour
char*
(ou STRING
), 1 pour int
et
2 pour double
etc... comme de toute façon on a droit à
autant d'arguments qu'on veut. On double par conséquent le nombre variable
de paramètres.
Dans la fonction f
on doit alors distinguer les différents
cas comme ainsi par exemple
switch ( va_arg(liste_param, int)) { case 0: param0 = va_arg(liste_param, char*); ... /* c'etait un char* */ case 1: param1 = va_arg(liste_param, int); ... /* c'etait un int */ case 2: param2 = va_arg(liste_param, double); ... /* c'etait un double*/ }
Exercices:
V.1.5 Ecrire une fonction f qui imprime ses arguments, sachant qu'ils sont en nombre et en type quelconque.
V.1.6 Simuler la fonction printf()
avec une fonction
output() dont le premier argument est une chaîne qui contient
autant de caractères 'x'
que d'arguments entiers qui viennent
ensuite (et qui sont à imprimer). Par exemple
output ("abcx efghxx", 1,2,3);
doit imprimer les trois valeurs 1 2 3
fournies (il y a 3 'x'
).
Le passage de tableaux en paramètres se fait toujours par référence. Le
choix n'est pas laissé au programmeur quant au mode de transmission pour
les tableaux, le passage par valeur nécessitant la copie, à chaque fois,
de tout le tableau. Pour passer un tableaux, soit int t[10]
,
en paramètre à une fonction f
, on doit donc toujours écrire
f(t)
Le compilateur envoie l'adresse de t
à la fonction, En
conséquence f(&t)
est une écriture, déjà signalée,
incorrecte. Ne pas confondre, encore, &t
et
&t[i]
.
Remarque: A ce propos d'ailleurs, si on veut passer juste une tranche de tableau, on écrit:
f(&t[i]);
pour la tranche commençant en i. Typiquement, i=0, i.e. tout le tableau. Nous y reviendrons.
Pour les paramètres formels dans la définition de f
, on
peut déclarer un tableau t
comme dans sa déclaration
originale avec dimension, int t[10]
, ou comme
int t[]
pour indiquer que c'est un tableau quelconque à une dimension, (int []
dans le prototype)
int *t
(int*
dans le prototype), ce qui est plus commode et plus général.
int **t
(int**
dans le prototype), si c'est un tableau de pointeurs.
Aussi, toute modification des valeurs d'un tableau dans une fonction, affecte le vrai tableau.
Exemple_1:
L'exemple de la figure V-2, est un programme qui calcule le produit
scalaire de deux vecteurs. La fonction int ProdScal(x,y)
où
x
et y
sont des vecteurs, est réservée à cet
effet. Le calcule consiste en la somme des produits deux à deux des
composants de x
et de y
. Ici le résultat est
11.
int r[2] = {1,2}; int s[2] = {3,4}; int ProdScal(int *, int []);
main(){ int p; p = ProdScal(r, s); printf("Produit Scalaire = %d\n", p); }
int ProdScal(int *x, int y[]){ /* Retourne le produit scalaire z de x et y */ int i, z=0; for( i=0; i<2; i++) z += *x++ * *y++; /* z += *(x+i) * *(y+i) z += x[i] * y[i] marchent aussi */ return z; }
Fig-V-2, Produit Scalaire de Deux Vecteurs
r et s
.
Cet exemple, produit scalaire, illustre entre autre, quatre choses:
r
et s
. x[]
ou *x
, y[]
ou *y
.*x
,
ou comme tableau, avec indexation x[i]
. (Cependant *x++
,
incrémentation du pointeur local, n'est pas identique à *(x+i)
).y
est déclaré y[]
, i.e. tableau,
on peut écrire *y++
(à méditer).Moralité: Un tableau en paramètre et vu comme une chaîne d'objets, et peut se manipuler comme telle.
Autre point à noter
int
m[2][2];
on peut avoir le produit scalaire des deux lignes de m
par:
ProdScal(m[0], m[1]);
car m[0]
, m[1]
, sont des adresses
de lignes. (Exercice: Vérifier qu'on peut remplacer m[0]
par &m[0][0]
cette fois-ci. idem pour m[1]
).
Exemple_2: Passage de matrices en paramètres.
Une matrice en paramètre peut être déclarée et utilisée telle qu'elle, à deux dimension, dans une fonction. A l'appel, on envoie son nom tout simplement. Quelle soit donnée ou résultat. L'exemple de la figure V-3-a, est une somme de deux matrices. (Le produit de deux matrices est laissé comme exercice!).
#define L 3 #define C 2 int r[L][C] = { {1,2}, {3,4}, {5,6}}; int s[L][C] = { {0,1}, {2,3}, {4,5}}; void SommeMat(int x[L][C],int y[L][C],int z[L][C]); main() { int t[L][C]; int i,j; SommeMat(r, s, t); /* t = r + s */ for(i=0; i<L; i++){ for(j=0; j<C; j++) printf("%4d", t[i][j]); printf("\n"); } } void SommeMat(x, y, z) int x[L][C],y[L][C],z[L][C]; /* Somme z de deux matrices x et y */ { int i,j; for (i=0; i<L; i++) for (j=0; j<C; j++) z[i][j] = y[i][j] + x[i][j]; }
Fig-V-3-a, Somme t de Deux Matrices r
et s
.
En C, on peut généraliser et définir la somme de deux matrices comme suit:
void SommeMat(x, y, z) int* x, * y, * z; { int i,j; for (i=0; i<L; i++) for (j=0; j<C; j++) *z++ = *x++ + *y++ ; }Fig-V-3-b, Somme t de Deux Matrices
r
ets
. 2e version
l'appel reste inchangé, et le prototype peut se déclarer void
SommeMat();
(voir plus bas d'autres décalration possibles). Cette
définition illustre une chose importante: un tableau T[L][C]
à deux dimensions en paramètre, est mieux traité dans une fonction comme
tableau T'[]
à une dimension, de taille L*C
,
rangé ligne par ligne. Alors
T[0][0]
devientT'[0]
devient
T[0][1]T'[1]
devient
T[0][2]T'[2]
devient
...
T[1][0]T'[C + 0]
devient
T[1][1]T'[C + 1]
devient
...
T[i][j]T'[i*C + j]
...
le dernier élément T[L-1][C-1]
devient T'[(L-1)*C +
C-1]
soit T'[L*C -1]
. Et en vertu de la moralité
ci-dessus:
T'[m] = *(T' + m)
soit, en remplaçantm
pari*C + j
,
T'[i*C + j] = *(T' + i*C + j)
, notation générale pour accéder au composanti,j
d'une matriceT
en paramètre. C'est aussi le point de vue d'un compilateur.
A propos de cet exemple (Fig-V-3-b), on peut noter que:
z
résultat ici)int x[]
, int y[]
ou
int z[]
dans la définition de la fonction.x[][]
est une erreur de compilation : Null
Dimension
. De même que l'accès par x[i][j]
(on ne
peut deviner la valeur de C
pour le calcul de i*C+j
.
Déclarer x[][C]
plutôt).T[i][j]
par *(T' +
i*C +j)
ou bien par T[i*C+j]
.En effet, on peut remplacer la ligne
*z++ = *x++ + *y++;
par
z[i*C + j] = x[i*C + j] + y[i*C + j] ;
ou par
*(z + i*C +j) = *(x + i*C +j) + *(y + i*C +j) ;
(Exercice: le vérifier). On peut préférer la première écriture. Se rappeler seulement qu'elle n'est pas valable pour un tableau non en paramètre.
Remarques:
Dans la déclaration du prototype d'une fonction (soit f
)
qui doit avoir une matrice (soit T[L][C]
déclarée par
ailleurs) en paramètre, il faut écrire:
(a)
f(int *)
ouf(int [])
ou(b)
f(int **)
ou(c)
f(int [][C])
sinon(d)
f(int [L][C])
avec tout
Dans la définition de la fonction f
, les déclarations des
paramètres doivent alors être identiques (correspondre) au prototype, i.e.
si on a déclaré f(int *)
on doit définir f(int
*x){...}
L'écriture (a) est peut-être à préférer, car elle autorise dans f
toutes les notations d'accès: T'[i*C +j]
, *(T' + i*C
+j)
et *T'++
. L'écriture T'[i][j]
n'est pas possible dans ce cas . A l'appel de f
, il faut
néanmoins écrire f((int*) T)
ou f(&T[0][0])
qui marche dans tous les cas.Ecrire f(T)
est une erreur de
compilation.
L'écriture (b) est possible et marche comme dans le cas (a); (à l'appel
écrire f((int **)T)
).
Par contre, l'écriture (c) ou (d) autorise l'accès par indexation dans f
(notation T'[i][j]
) et l'appel simple par f(T)
.
Elle est à utiliser si pour des raisons de lisibilité on tient à utiliser
la notation matricielle classique dans une fonction. Seulement attention
aux accès de type pointeur (*T'++
etc... ) qui, si combinés
avec l'indexation, peuvent donner des résultats incorrects.
Exercices:
V.1.7 Refaire le programme somme de deux matrices en remplaçant la ligne
*z++ = *x++ + *y++;
avec des notations équivalentes (exercice donné dans le texte).
V.1.8 Ecrire un programme qui fait le produit de deux matrices. Utiliser d'abord une fonction déclarée
void ProdMat();
ensuite la déclarer
void ProdMat(int *, int *, int*);
ou
void ProdMat(int[], int[], int[]);
En C une fonction f peut être appelée par elle même, récursivité directe, ou faire appel à une autre fonction g qui appelle h qui ... appelle f, récursivité indirecte. Les figures V-4 et V-5 sont deux exemples de fonction récursives classiques. Un exemple d'appel est:
printf("%f\n", (float)fibon(20) / fibon(19));
qui donne 1.618304
, le
nombre d'or k, déjà rencontré.
int factorielle (int n) { return ( n <= 1 ? 1 : n * factorielle(n-1) ); } int fibon(int n){ if ( n<2 ) return (1); else return ( fibon(n-1) + fibon(n-2)); }
Figs V-4 et V-5, Fonctions Récursives
Factorielle
et Suite de Fibonacci.
«Procédures» récursives
Une procédure récursive hanoi
(Eh oui, que voulez-vous?)
est donnée dans la figure V-6. C'est un exemple classique dit «la tour de
Hanoi». C'est la donnée de trois piliers numérotés a b
et c, sur l'un d'eux viennent s'empiler n disques (ou
plateaux) de taille décroissante. Le jeu consiste à déplacer ces n
plateaux du pilier a vers le pilier b. On prend les
plateaux l'un après l'autre, on utilise le pilier c comme moyen
temporaire de stockage de plateaux et on évite de poser un grand plateau
sur un plateau plus petit.
void hanoi(int n, int a, int b){ /* resout le problème de la tour de hanoi pour * deplacer n plateaux du piler No a vers No b. * a b differents et egaux a 1, 2 ou 3 */ int c ; /* c No pilier intermediaire */ switch (a+b){ case 3 : c = 3; break; case 4 : c = 2; break; case 5 : c = 1; break; } if (n == 1) move(a,b); /* deplace le plateau du haut de a vers b */ else { hanoi(n-1, a, c); move (a, b); hanoi(n-1, c, b); } }
Fig-V-6, Problème de la Tour de Hanoi.
Nous avons défini une fonction-procédure hanoi(n, a, b);
qui déplace n plateaux de a vers b, le plateau
intermédiaire c étant calculé. La routine move (a,b);
affiche un message "je déplace un plateau de a vers b"
. Voici
le résultat de l'appel hanoi(n,1,3)
, i.e. déplacer n
plateaux du pilier 1 vers le pilier 3 (avec n = 3 ici).
Je déplace un plateau de 1 vers 3
Je déplace un plateau de 1 vers 2
Je déplace un plateau de 3 vers 2
Je déplace un plateau de 1 vers 3
Je déplace un plateau de 2 vers 1
Je déplace un plateau de 2 vers 3
Je déplace un plateau de 1 vers 3
Problème du sac à dos:
Un autre problème de fonction récursive est le problème du sac à dos [5]. Il s'agit de remplir un sac à dos pouvant contenir une masse M donnée avec des pierres de masse données aussi et il faut en mettre un maximum. Voici un exemple de programme C autocommenté pour cela. On y a défini N masses poids[c], 0<= c <N. On a une fonction récursive, sac(m, c) = 1 ou 0, selon qu'un sac de masse m contienne ou pas la masse poids[c]. L'appel initial est donc sac (M, 0). Le reste se deduit de l'observation que
sac(m, c) = 1 si sac(m-poids[c], c+1) =1;
Le programme complet auto commenté:
#define N 10 int poids[N] = { 1, 4, 7, 23, 32, 8, 45, 2, 9, 10}; int masseTot=0; main() /* * Pb du sac a dos. Un sac ne peut contenir qu'une * masse masse. remplir le sac avec les poids fournis * */ { int masse; /* M masse tole're' */ printf("rentrer la masse tole're'e > "); scanf("%d", &masse); printf("\n"); sac(masse, 0); printf("\n\n Au total = %d\n",masseTot); } sac (int M, int C) /* M masse, C candidat */ /* * teste si poids[C], fait partie du sac * de masse M . Si oui elle l'imprime. */ { if (M == 0) /* Il existe solution : Sac vide */ return 1; else if ( M < 0 || C >= N) return 0; /* Cas d'exception */ else /* * Reste le sac avec ou sans la masse poids[C]. * poids[C] convient si il existe un sac de masse * M - poids[C], qui ne contient pas la masse poids[C]. */ if ( sac( M - poids[C], C+1 )) { /* poids[C] convient donc */ printf("La masse %4d convient \n", poids[C]); masseTot += poids[C]; return 1; } else /* * poids[C] ne convient pas, On regarde le poids * candidat suivant. */ return ( sac (M, C+1) ); }
Résultats: Avec le tableau donné en déclaration
rentrer la masse tole're'e > 100 La masse 10 convient La masse 2 convient La masse 45 convient La masse 8 convient La masse 23 convient La masse 7 convient La masse 4 convient La masse 1 convient Au total = 100
Deuxième exemple:
rentrer la masse tole're'e > 67 La masse 32 convient La masse 23 convient La masse 7 convient La masse 4 convient La masse 1 convient Au total = 67
On voit bien une tentative de mettre d'abords les grosses masses. Comme quand on voyage, on tente toujours de mettre d'abord les plus gros bagages.
Remarque: Avec le même tableau, mais poids decroissants,
int poids [N] = {45, 32, 23, 10, 9, 8, 7, 4, 2, 1};
on n'observe pas la même propriété:
rentrer la masse tole're'e > 100 La masse 23 convient La masse 32 convient La masse 45 convient Au total = 100
Deuxième exemple:
rentrer la masse tole're'e > 67 La masse 1 convient La masse 2 convient La masse 9 convient La masse 10 convient La masse 45 convientAu total = 67
Exercice:
V.1.9 Supposer que les piliers de la tour de Hanoi sont de hauteur 5 plateaux, et que ces derniers sont les valeurs 5, 4, 3, 2, 1 dans cet ordre.
a) Ecrire une procédure PrintHanoi()
qui affiche
verticalement les 3 piliers de la tour de Hanoi, en utilisant 3 tableaux.
b) Ecrire la procédure move(a,b)
qui déplace un
plateau, un entier donc d'un tableau à un autre.
c) Utiliser la fonction hanoi
pour simuler le
déplacement des plateaux. A la quatrième étape par exemple, on pourra
avoir sur les piliers a b et c
4 1
5 2 3
-a- -b- -c-
On appelle portée d'une variable la portion de programme où elle est définie et donc utilisable (connue du compilateur comme on dit). La portée est ainsi liée à la phase de compilation d'un programme.
Exemple: Soit le fichier C contenant le texte suivant:
int x,y;
f( int a, int b){
int i,j;
... (1)
{ int k,l;
... (2)
}
... (3)
}
g(){
int a;
char b;
f(a, a);
... (4)
}
x
et y
sont connues partout, i.e. toute la
suite du fichier source à partir de la déclaration.
i
et j
ainsi que a
et b
(paramètres de f
) sont accessible en (1) en (2) et en (3),
c'est à dire dans le bloc de f
.
k
et l
ne sont accessibles que dans le bloc
(2).
Ainsi, une référence à k
(e.g. k = 3;
) en (1)
ou en (3) est une erreur de compilation: "symbole indéfini"
.
Par ailleurs, les variables a
et b
du bloc de
g
désignent des objets différents des a
et b
de f
, et qu'une redéfinition de i
par exemple
dans (2), masquerait le i
du (1).
Les déclarations faites en début d'un bloc, instructions (cas de k,l
)
ou fonction (cas de i,j
), sont locales à ce
bloc (qualificatif auto
par defaut). Il en est de même des
paramètres (cas de a,b
) pour un bloc fonction. Les
déclarations faites en dehors de tout bloc, sont globales à
tous les blocs qui les suivent, même dans d'autres fichiers d'un même
programme --si toutefois elles ne sont pas qualifiées de static
(voir ci-après, auquel cas elles ne sont valables que dans la suite du
même fichier) et elles sont qualifiées de extern
dans les
autres fichiers.
On appelle durée de vie d'une variable la durée
d'existence, à l'exécution, de l'objet qu'elle désigne. On parle alors de
variable statique ou automatique. Statique qualifie l'objet
qui existe durant toute l'exécution d'un programme. Automatique
qualifie l'objet qui n'existe que momentanément, e.g. le temps d'exécution
d'un appel de fonction ou, de façon générale, d'un bloc {...}
,
instruction composée ou fonction. Pour cela l'allocation mémoire des
variables est différente. Virtuellement on peut imaginer la mémoire comme
dans la figure V-7. C'est l'état de la mémoire en cours d'exécution d'un
programme ( état qui, soit dit en passant, constitue avec les registres et
les descripteurs de fichiers l'image d'un processus sous UNIX).
Les variables statiques sont déclarées avec le mot clé static
static type variable;
pour leur allocation dans la zone statique. Elles existent et ont une valeur durant toute l'exécution du programme. Leur zone est de taille fixée à la compilation.
Fig-V-7, Etat de la Mémoire en Cours d'Exécution.
Les variables automatiques, allouées durant l'exécution dans la pile, sont empilées / dépilées continuellement dans la zone dynamique. Ces variables ne conservent donc pas leur valeur, et pour cause, d'une pile à une autre. Celle-ci peut même être vide à un moment donné.
Remarque: Le tas (heap) sert aux données dynamiques repérées par des pointeurs. Ne pas confondre avec les données, dynamiques aussi, de la pile (dynamisme plus «régulier»).
Classes d'allocation par défaut
Les variables globales, déclarées en dehors de toute fonction, sont
systématiquement de classe d'allocation statique et initialisé à 0 par le
compilateur (par défaut). L'attribut static
est donc
implicite et c'est la cas de x
et y
dans
l'exemple ci-dessus. C'est normal car elles sont utilisées dans toutes les
fonctions avec leur bonne valeur. Elles ne doivent donc pas être de classe
automatique et n'ont donc pas besoin d'être déclarées statique. Néanmoins
et pour une meilleur protection, on peut les qualifier par static
pour en limiter la portée au seule fichier où elles sont déclarées.
Les variables locales à un bloc sont de classe d'allocation automatique et non initialisées par défaut. C'est la cas dans le même exemple de toutes les autres variables. Elles sont allouées (empilées) à l'entrée du bloc (ou appel de fonction) et disparaissent (dépilées) à la sortie du bloc (ou retour de fonction). Tout cela en cours d'exécution. Elle n'ont donc pas d'initialisation par défaut (l'overhead en serait grandement affecté).
Toutefois, on peut déclarer des variables statiques à l'intérieur d'un
bloc de fonction (par le mot static
toujours) comme dans
l'exemple
main(){
f();f();f();
}
f(){
static int y = 5;
printf("%d\n",y);
y++;
}
qui imprime 5 6 7
, montrant ainsi que y
est
une variable locale, de portée limitée à f
, mais qui
conserve sa valeur durant toute l'exécution du programme. Ici y
est alloué, en phase de compilation, dans la zone statique en mémoire. La
valeur initiale 5 est affecté par le compilateur (défaut 0) à
l'allocation. Ainsi donc on voit que, d'un appel à l'autre, y
conserve sa valeur. (Enlever static
donnera 5 5 5
).
Initialisations Utilisateurs
Pour les automatiques, seule les variables scalaires peuvent être initialisées par le programme à la déclaration, comme dans
float pi = 3.14159;
Les tableaux par exemple ne peuvent être initialisés que si ils sont
déclarés static
, ou déclarés à l'extérieur de tout bloc,
donc statiques. (Restriction enlevée dans ANSI C).
char *t = "Ma valeur initiale";
static int t[10] = {1,2,3}; /* Le reste du tableau est 0 */
Variable extern
En C l'écriture
extern int x;
dit que x
est une variable globale (donc statique) déclarée
ailleurs, i.e. «plus bas» dans le même fichier ou dans un autre fichier,
par
int x;
Plus précisément, en C int x;
est dite une définition de
variable. Comme pour une fonction. Alors que extern int x;
est une déclaration. Elle ne donne pas lieu à réservation d'espace
mémoire, et déclare juste qu'on va utiliser l'entier x
.
Exemple:
extern int x; (1) f() {...} g() {...}
F1.c
h() {extern int x; (2) ... }
F2.c
extern int x; (3) p() {...} int x; (4) ...
F3.c
La variable x
est définie entière dans F3.c
en (4) et est utilisée, dans les deux fichiers F1.c
en (1)
et F2.c
en (2) par la déclaration extern int x
,
et dans F3.c
en (3) par la même déclaration parce qu'elle
est définie plus bas. Les fonctions des trois fichiers utilisent donc le même
objet statique x
. Remarquer que x
est globale
à f
et g
dans F1.c
, locale à h
dans F2.c
.
Les concepts de global/local et statique/automatique sont donc orthogonaux. Néanmoins global et automatique n'a pas de sens en C (cela en a-t-il?).
Variable Registre
L'écriture
register int x;
définit x
comme devant être alloué dans un registre si
possible. Cela sert pour accélérer certains calculs devant s'effectuer
dans un registre (si la variable est lourdement utilisée). Toutefois, le
compilateur C peut ignorer cette allocation et se réserver lui-même le
droit de le faire selon ses possibilités. Dans ce cas la variable est
considérée automatique. Les variables registres n'acceptent pas
l'opérateur adresse &
(pourquoi à votre avis?).
Qualificatif const
ANSI C a introduit le qualificatif const
pour définir des
objets constants dont la valeur n'est jamais modifiée. Le compilateur
empêche de telles tentatives (du moins si elles sont directes).
const float pi = 3.14159;
const short kelvin = -273;
Une instruction comme
pi = 3.14;
ne peut donc figurer dans le programme car elle modifie une constante. Par contre, on peut modifier un objet constant à travers un pointeur. Une telle modification ne peut de toute façon être détectée qu'à l'exécution: en allouant la constante dans un espace d'adressage en lecture seulement.
Les objets constants servent principalement à se protéger contre des accès indésirables, ou bien pour nommer des constantes pour que leur littéral n'apparaisse qu'à un seul endroit dans le programme. Détournés de ce but ils sont inutiles.
Attention, ne pas confondre
#define pi 3.14159
et
const float pi = 3.14159;
define
est une macro et sert à la précompilation. Dit
autrement, le nom pi
disparaît avant la compilation
du programme. Par contre const
est un qualificatif qui fait
partie d'une déclaration et comme tel il est compilable. Dit autrement, si
dans un programme apparaît l'instruction
x = 2*pi;
avec define
le compilateur lira
x = 2 * 3.14159;
et avec const
il lira
x = 2 * pi;
En C une fonction (f disons) peut constituer une «donnée» et être passée en paramètre à une autre fonction (g disons). Cela se fait par un mécanisme de pointeur: ce qui est passé à g c'est l'adresse du point d'entrée de f. Pointeur vers le début de son code. Ainsi on permet à g de pouvoir appeler f, paramètre formel, pour réaliser à l'exécution des algorithmes différents selon la «valeur» de f, paramètre effectif, fournie à g lors de l'appel. On appelle générique ce genre --d'où le mot-- de fonction g, bien que le terme soit utilisé en programmation (objet surtout) dans un sens plus global.
main(){ int OpBin(); /* déclaration de fonction OpBin, */ int somme(), produit(); /* somme et produit */ int s,p; s = OpBin (3, 4, somme); /* somme est ici paramètre de Opbin*/ printf("%d\n",s); p = OpBin ( 3, 4, produit); /* idem pour produit */ printf("%d\n",p); }
Fig-V-8 Passage de Fonctions en Paramètres.
Exemple: Le programme de la figure V-8 appelle une fonction
générique OpBin (x, y, f)
qui applique un calcul ( e.g.
somme , produit, pgcd, ...) aux arguments x
et y
.
Ce calcul constitue, justement, le troisième paramètre f
qui
est par conséquent une fonction. En d'autres termes, OpBin(x, y, f)
est une fonction qui a pour résultat f(x,y)
où f
est variable. Ce programme imprime: 7 (3+4) et 12 (3*4).
Dans main()
on utilise trois symboles de fonction: OpBin()
,
somme()
et produit()
, déclarée rendant un
entier. La ligne
OpBin (3,4,somme);
est l'appel à la fonction générique OpBin()
qui applique le
paramètre (déclaré fonction) somme
aux entiers 3 et 4. Il en
est de même pour la ligne OpBin(3, 4, produit);
en ce qui
concerne le produit cette fois-ci. Noter qu'il n'est pas fait usage de
l'écriture &somme
ou &produit
dans
l'appel. Le compilateur comprendra, vu que ce sont des fonctions, qu'il
doit passer leur adresse en paramètre. Voilà pour ce qui est appel et
transmission des paramètres effectifs.
Pour les paramètres formels de OpBin()
(voir figure V-9) et
qui sont x y
et f
, on déclare x
et y
comme int
, et f
comme
int (*f)(int,int);
pour indiquer que le symbole f
est un pointeur (*f
)
vers une fonction à deux paramètres (int, int)
rendant un
entier int
. Le signe *
est nécessaire pour
indiquer que f
est pointeur.
Remarquer la présence des parenthèses (*f)
qui sont
nécessaires, car l'écriture int *f()
serait la déclaration
d'une fonction rendant un pointeur sur un entier.
OpBin(x,y,f) int x,y; int (*f)(int, int); /* f est un pointeur vers fonction */ { int z; z = (*f)(x,y); /* appel de f sur les argument x et y */ return z; } somme(a,b) int a,b; { return (a+b); } produit(a,b) int a,b; { return (a*b);}
Fig-V-9, Fonction en Paramètre Formel.
Pour l'appel ou la référence à ce paramètre formel f
, on
écrit (voir même figure)
(*f)(x, y);
i.e. appel de la fonction pointée par f
sur les paramètres
x
et y
. Là aussi on utilise le signe *
et les parenthèses.
Exercice:
V.3.1. Ecrire un programme calculette qui lit
nombre signe nombre
par exemple 12 / 4, et qui sort le résultat correspondant à l'opération (
3 ici). signe
est dans { +, -, *, /, % }. On
utilisera un tableau d'opérateurs, char op[5]
tel que
op[0] = '+', op[1] = '-'
etc... et un tableau de (pointeurs vers)
fonctions rendant un entier, déclaré
int (*f[5])();
(*f[i])(x,y);
est alors l'appel de la ième fonction
du tableau sur les paramètres x
et y
. On
initialisera le tableau par
int (*f[5])() = { somme, diff, prod, div, mod};
Intérêt des fonctions en paramètres
Les fonctions en paramètres, comme signalé auparavant, permettent d'écrire des procédures ( fonctions C) dites génériques, dans le sens où on écrit un code qui peut être utilisable plusieurs fois, non seulement en ce qui concerne l'exécution d'un même algorithme (abstraction procédurale classique), mais ce code peut donner lieu à des calculs différents.
En effet on peut définir une fois pour toute des unités de programmes de fonctionnalités génériques telles qu'on puisse les utiliser dans des applications spécifiques. Considérons l'algorithme de recherche d'un élément a dans un tableau T de taille n et qui retourne un entier égale à l'indice de l'élément, et -1 sinon. Pour cela on doit comparer (fonction comp en paramètre) a avec les éléments de T pris un par un. En voici l'algorithme:
recherche (t, n, a, comp ) retourne entier
début
pour i = 0 tantque i < n faire
si comp (a, t[i])
sortir de boucle ;
si i < n
retourner (i)
sinon
retourner (-1)
fin
Cet algorithme est général et marche pour n'importe quel tableau pourvu qu'on lui fournisse une fonction, comp ici, qui compare deux éléments entre eux et qui doit donc dépendre du type des éléments du tableau. Voici son implantation en C:
recherche(t, a, n, comp) ELEMENT t[], a; int (*comp)(); int n; /* ELEMENT est le type de a et des membres de t, * supposi difini dans l'application */ { int i; for (i=0; i<n; i++) if ( (*comp)(a,t[i]) ) break; if ( i<n) return (i); else
return(-1); }Les figures V-10 et V-11 montrent deux cas d'utilisation, sur un tableau d'entiers. (Fig-V-10) et sur un tableau de caractères (Fig-V-11).
typedef int ELEMENT; /* cette ligne vient avant la suivante */ #include "rechgen.c" int tabent[10] = { 10,20,30,40,50,60,70,80,90,100}; main(){ /* cas de recherche dans un tableau d'entiers */ ELEMENT a; int EqualInt(int, int); /* fonction qui teste l'egalite * de deux entiers */ { int res; a = 20; res = recherche (tabent, a, 10, EqualInt); printf("pour %d on trouve %d \n", a, res); a = 45; res = recherche (tabent, a, 10, EqualInt); printf("pour %d on trouve %d \n", a, res); } } EqualInt(int i, int j) { return (i==j); }
Fig-V-10, Cas_1 Recherche d'Eléments dans un Tableau d'entiers.
On a utilisé typedef
pour définir le type ELEMENT
comme synonyme de int
ou char
selon le cas.
On a aussi supposé la fonction recherche dans un fichier "rechgen.c
".
Le programme de la figure V-10 donne comme résultat attendu
pour 20 on trouve 1 pour 45 on trouve -1
Le programme de la figure V-11 donne comme résultat attendu
pour c on trouve 2 pour d on trouve 3 pour * on trouve 5 pour # on trouve -1
Le lecteur peut trouver que tout cela est compliqué pour une simple recherche dans un tableau, mais là n'est pas la question. Il faut comprendre deux chose:
typedef char ELEMENT; #include "rechgen.c" main(){ /* cas de recherche dans un tableau de caracteres */ char *tabcar = "abcDe*Ghi\0"; ELEMENT a; int EqualChar(char, char); /* Teste si deux caracteres sont egaux */ { int res; a = 'c'; res = recherche (tabcar, a, 10, EqualChar); printf("pour %c on trouve %d \n", a, res); a = 'd'; res = recherche (tabcar, a, 10, EqualChar); printf("pour %c on trouve %d \n", a, res); a = '*'; res = recherche (tabcar, a, 10, EqualChar); printf("pour %c on trouve %d \n", a, res); a = '#'; res = recherche (tabcar, a, 10, EqualChar); printf("pour %c on trouve %d \n", a, res); } } EqualChar (char a, char b){ if( isalpha(a) && isalpha(b) ) return ( a==b || a==(b+32) || b==(a+32) ); /* Majuscule ou Minuscule */ else return (a==b); }
Fig-V-11 Cas_2 Recherche de Caractères
dans une Chaîne.
Pour terminer sur une anecdote, qui n'a rien à voir, noter cette citation d'un chercheur:
Every program is a part of some other program, and rarely fits [4]
Dans un programme on a toujours à lire (ou entrer) des données et à écrire (ou sortir) des résultats. Nous entendons par Entrées/Sorties (E/S) de base, les opérations pour lire les données d'un programme et écrire ses résultats. On dira aussi parfois afficher les résultats. Abstraction faite des opérations générales de traitement des fichiers. (Voir Chapitre VII). Nous allons présenter les opérations d'E/S
getchar()/putchar(), getc()/putc()
pour lire/écrire un caractère
gets()/puts()
pour lire/écrire une chaîne de caractères
scanf()/printf()
pour lire/écrire des données élémentaires quelconques formatées. Opérations qui nous sont familières maintenant.
Ces opérations sont toutes définies dans le fichier include
#include <stdio.h>
parmi d'autres dont nous parlerons de façon générale au
chapitre VII. Ces opérations permettent de lire ou d'afficher des
données sur les périphériques standard d'E/S appelés stdin
et stdout
(Standard Input, Standard Output) et qui
sont assignés par défaut au clavier et à l'écran.
La notion de périphérique standard, lié au système d'exploitation, est logique
et permet à un programme d'être indépendant du dispositif physique d'E/S.
Dans UNIX par exemple, il y a le choix, au moment de l'exécution d'un
programme d'indiquer l'unité physique d'entrée (clavier ou fichier texte)
ou de sortie (écran, imprimante ou fichier texte). De même, ce choix peut
être fait à l'intérieur d'un programme (voir
freopen()
chapitre VII). Par défaut c'est donc le clavier
et l'écran. Nous y reviendrons au chapitre suivant et nous supposerons
tout au long de cette section que les opérations d'E/S concernent le
clavier et l'écran, assignés par défaut à stdin
et stdout
.
Ces eux opérations, getchar() et putchar(), sont les plus élémentaires et les plus simples. Elles lisent ou écrivent un caractère. Elles ont pour interface
int getchar() /* retourne un caractère lu sur stdin */ int putchar (char) /* écrit son argument sur stdout */
Exemple:
Le programme suivant imprime tous ce qu'il lit, i.e. copie le fichier stdin
sur stdout
. La séquence d'entrée doit se terminer par
^D
, indicateur de fin de saisie (transformé en -1).
#include <stdio.h> main(){ int c; while (( c = getchar()) != EOF) putchar(c); }
Ce programme lit un caractère c
qui, tant qu'il est
différent de EOF
, est ensuite écrit. EOF
est
une constante définie dans stdio.h
par
#define EOF -1
La valeur -1 étant celle retournée par getchar()
en fin de
fichier. Remarquer que c
est de type int
au
lieu de char
, ce qui est plus générale (e.g. pour le -1 de EOF
).
putchar() retourne aussi le caractère écrit.
Voici l'exécution du programme:
% cc getchar.c % a.out abcdef <-- saisies (y compris fin de ligne) abcdef <-- imprimés xyz <-- saisies xyz <-- imprimés ^D <-- fin de saisie et donc de fichier entrée %
Ces deux fonctions, getc() et putc() lisent ou écrivent un caractère sur un fichier quelconque.
int getc(flot) /* lit un caractère dans un flot */ FILE *flot; int putc(c,flot) /* écrit un caractère sur un flot */ char c; FILE *flot;
Un fichier est considéré en UNIX comme un flot d'octets
(stream) d'après des considérations en dehors de l'objet de ce
cours (fichier non structuré). Selon Dennis RITCHIE
l'enregistrement est une notion obsolète liée aux vieux matériels et due
aux cartes perforées.Le type FILE
est un type de donnée
associé aux fichiers C. Il est défini dans stdio.h
(voir
encore
Chapitre VII). La variable flot
, utilisée par getc()
et putc()
, est un pointeur vers un descripteur de fichier
UNIX.
En réalité, ce sont getc() et putc() qui lisent ou écrivent un caractère, mais dans un fichier quelconque. getchar() et putchar() sont les macros
#define getchar(
) getc(stdin) #define putchar(x) putc(x, stdout)
qui lisent ou écrivent un caractère sur les fichiers standards d'E/S, stdin
et stdou
t. Ainsi le programme précédent pourra aussi
s'écrire
w
hile ((c= getc(stdin)) != EOF) putc(c, stdout);
Les deux fonctions, gets() et puts(), lisent et écrivent une chaîne de caractères.
cha
r *gets(str) /* lit une ligne dans stdin */ char *str; puts (str) /* écrit une ligne dans stdout */ char *str;
gets() lit une ligne --chaîne terminée par le caractère retour newline
-- et la place dans la zone réceptrice str
en remplaçant newline
par \0
. Elle retourne par aileurs son argument. A l'appel str
doit être char[x]
ou char*
initialisée.
Exemple:
char t[4],u[5], *ligne; gets(u); ligne = malloc(80); gets(ligne); ligne = gets(t);
Si la chaîne lue déborde de la zone réceptrice, des données en mémoire risquent d'être silencieusement écrasées.
Puts(str)
Ecrit une ligne, i.e. copie son argument char*
(jusqu'à \0
remplacé par newline
) sur la sortie standard.
Exemple:
lign
e = "toto"; puts(ligne);
scanf() et printf() sont les opérations que nous avons jusqu'ici utilisées pour rentrer ou sortir des données. Ces données sont des nombres, des chaînes ou des caractères (les types standards).
sca
nf( format , ListePointeurs) /* entrée formatée à partir de stdin */ char* format; printf( format , ListeArguments) /* sortie formatée vers stdout */ char* format;
Ces opérations convertissent leurs données
123e-1
" vers
données en mémoire, e.g. le réel 12.3, en cequi concerne scanf()45
", en ce qui concerne printf().D'où le mot formaté. On se rappellera que ces deux fonctions utilisent une ellipse ... pour leur nombre variable d'arguments.
scanf() lits des caractères sur l'entrée standard stdin
,
les interprète selon le format donné en paramètre et range ensuite le
résultat dans l'argument correspondant, qui est un pointeur vers une
donnée en mémoire. Le format de lecture est donné par la chaîne en
paramètre format
qui contient généralement les
spécifications des conversions pour interpréter les entrées saisies. Cette
chaîne peut se composer de
espace tab
ou newline
)
qui doivent coïncider avec des caractères blancs optionnels dans la
saisie. Autrement dit la lecture commence au prochain caractère non
blanc en entrée. Sauf s'il s'agit de lire un char (spécification %c
)
ou énuméré (spécification %[]
)%
, qui doit coïncider avec le
même caractère en entrée.%
suivi du caractère optionnel *
qui fait sauter
l'assignation du champs suivant en entrée, ou d'un nombre optionnel
spécifiant la taille maximale de la zone d'un champs et d'un caractère
de conversion parmi { d, o, x, c, e, f, %
}.%
%
est attendu mais non assignéd
int*
).h
short*
).o
x
s
char*
. \0
est ajouté au bout.
La chaîne en entrée commence au premier caractère différent d'espace
et se termine par newline
ou espace
.c
char
.
Le saut normal des blancs ne se fait pas dans ce cas. Pour lire le
prochain caractère non blanc il faut faire %1s
ou
faire précéder %c
par un blanc.e
ou f
float
. le format d'entrée est ±entier.entierE±
entier
(E
ou e
).
Un des trois entier
au moins est présent, sinon
on récupère la valeur 0.les formats
o d x
oue f
peuvent être précédés de la lettrel
(%lf
) pour avoir un entier long ou un réel double. Mais%lf
et%f
ne doivent pas correspondre respectivement à des argumentsfloat
etdouble
(inverses). De même pourint
. Le résultat n'est pas garanti.La fonction scanf() retourne le nombre d'arguments remplis avec succès, en particulier 0 si aucun argument n'est rempli. Elle s'arrête si le format est épuisé ou si un mauvais caractère s'est glissé dans l'entrée. Le prochain
scanf()
commence là où le précédent s'est arrêté.EOF
alias-1
aussi est retourné s'il est rencontré (^D
en entrée). Dans ce casscanf()
s'arrête, même s'il reste des arguments à remplir.
Exemples:
int i; float x; char nom[10];
scanf("%d %f %s", &i, &x, nom);
avec la ligne en entrée
12 34.5E-1 Thompson
assignera 12
à i
, 3.45
à x
et Thompson\0
à nom
.
La ligne
12 34.5E-1 Ken Thompson
aurait mis Ken
\0 dans nom
.
scanf("%2d %f %*d %2s", &i, &x, nom);
avec l'entrée
12345 0123 45ici72
mettra 12
dans i
, 345.0
dans
x
, sautera 0123
et placera "45\0
" dans
nom
. La prochaine lecture débutera à i
. En
effet ce qui est saisie et non assigné à des arguments reste pour le
prochain scanf()
.
scanf ("%d%f", &i,&x);
scanf ("%d%f", &j,&y);
avec la ligne
1 2 3 4
assignera 1
à i
, 2.0
à x
,
3
à j
et 4.0
à y
!
printf() affiche sa liste d'arguments en les convertissant selon
le format spécifié. Celui-ci contient deux sortes de formats: des
caractères ordinaires quelconques (sauf %
) qui sont
imprimés, et des spécifications de conversion introduits par %
pour imprimer les arguments fournis. Après %
on peut trouver
dans l'ordre:
-m.n m.n m -m .n
etc ...
m
et n
étant des entiers. m
est la taille minimum de la zone réservée pour afficher une donnée
(justifiée à droite, complétée par des blancs si besoin), n
est alors le nombre de chiffres (la précision) après la virgule pour
les réels, ou bien le nombre maximum de caractères à imprimer pour une
chaîne. le signe -
sert pour placer la justification
vers la gauche. Si m
commence par 0
alors
la zone est complétés par des 0
au lieu des blancs
(Ouf).
l
optionnelle pour les entiers longs ou les
réels doubles.d, u, o, x
-1
(la valeur maximum 11...1
sur
32 bits) donnera -1
en format d
,
4294967295
(232) en format u
, ffffffff
en format x
et 37777777777
en format o
.e, f, g
[+-]ddd.nnnnnnE[+-]xx,
ou en format décimal format
(f)ixe [-]ddd.nnnnnn
. La longueur des n
est donnée par la précision sinon elle est 6 par défaut. Le format g
est pour l' affichage le plus précis dans le minimum d'espace.c
s
\0
(!) ou la limite de la zone réceptrice.%
%
est imprimé par %%
.A la rencontre de \
x
dans le format, le
caractère de contrôle (ou spécial) symbolisé par x
,
est imprimé. Par exemple \n
fait passer à la ligne (cf. writeln
de Pascal).
Exemple:
float x; double y; x = y = acos((double) -1); printf("Simples flottants %f %e %g\n", x, x, x); printf("Flottants doubles %f %e %g\n", y, y, y); printf("Simples flottants+ %18.12f %18.12e %18.12g\n", x, x, x); printf("Flottants doubles+ %18.12f %18.12e %18.12g\n", y, y, y);
donne:
Simples flottants 3.141593 3.141593e+00 3.14159 Flottants doubles 3.141593 3.141593e+00 3.14159 Simples flottants+ 3.141592741013 3.141592741013e+00 3.14159274101 Flottants doubles+ 3.141592653590 3.141592653590e+00 3.14159265359
On voit ainsi que le format g, s'il est moins précis, il occupe moins de place pour un meilleur compromis, et que float est précis jusqu'à 6 chiffres après la virgule. (double offre 16 chiffres significatifs. Il faut utiliser dans ce cas le format %22.15e. Une position pour le point décimal, une pour le signe et 4 positions pour "e+nn". Les 16 positions restantes sont pour les chiffres significatifs, dont 15 après la virgule.)
Pour conclure, nous suggérons au lecteur(trice) d'aller visiter le manuel scanf() et printf() de UNIX par
man scanf ( man printf)
pour plus de détails, et surtout pour les caractéristiques propres à une installation donnée (UNIX à base de SystemV d'ATT ou à base de BSD de Berkeley).
sscanf()
et sprintf()
lisent et écrivent comme
scanf() et printf(), mais sur une variable chaîne de caractère. Celle-ci
est le premier argument de sscanf() ou sprintf().
Exemple:
char *s; int i; floatx; s="12 34"; sscanf(s, "%d %f", &i,&x);
donne:
12 34.000000
Remarque: les deux fonctions fscanf() et fprintf() sont analogues aux précédentes, mais opèrent sur des fichiers quelconques. Le fichier constituant leur premier argument.
FILE* f;
...
fscanf(f, "%d %f", &i,&x);
Ces fonctions sont pratiques dans la mesures ou on peut s'en servir pour manipuler des fichiers textes éditables aussi par éditeur (emacs vi ...)
[2] Certains compilateurs sortent néanmoins un avertissement (Warning) type «résultat de fonction non utilisé».
[3] C++ a renforcé cet aspect.
[4] «Tout programme est une partie d'un autre programme, mais lui sied rarement.»
[5] A. Aho & J.D. Ullman, Foundation of Computer Science, Prentice Hall 1990.