CHAPITRE V

Les Fonctions Et Les Noms Externes

I may not be totally perfect, but parts of me are excellent.
-- Ashleigh Brilliant


Najib TOUNSI,   Original: http://www.mescours.ma/C/c5.html

ntounsi@emi.ac.ma
Version: Sept. 2000, Dernière MàJ: Dec. 2021



< Précédent Sommaire Suivant >

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.

1. Les Fonctions

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

1.1. Déclaration de Fonction

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.

1.2. Définition de Fonction

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

1.3. Appel de Fonction

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.

1.4. C ANSI vs C Classique

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.

1.5. Mode de Passage des Paramètres

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.

1.6. Fonctions à Nombre Variable de Paramètres

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 *, ...)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:

Pile d'Execution

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);

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

1.7. Passage de Tableaux en Paramètres

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)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:

Moralité: Un tableau en paramètre et vu comme une chaîne d'objets, et peut se manipuler comme telle.

Autre point à noter

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] devient T'[0]
T[0][1]
devient T'[1]
T[0][2]
devient T'[2]
...
T[1][0]
devient T'[C + 0]
T[1][1]
devient T'[C + 1]
...
T[i][j]
devient 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çant m par i*C + j,

T'[i*C + j] = *(T' + i*C + j), notation générale pour accéder au composant i,j d'une matrice T 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:

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 *) ou f(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[]);

1.8. La Récursivité

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&egrave;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 convient
 Au 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-

2. Statut des Variables et Classes d'Allocation

2.1. Portée vs Durée de Vie

2.1.1. Portée

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.

2.1.2. Durée de Vie

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.

Memoire d'Execution

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 */

2.2. Variables Externes et Variables Registres

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()  
   {...}
Fichier F1.c
h() 
  {extern int x;  (2) 
   ... 
  
  }
Fichier F2.c
extern int x;  (3) 
p() 
    {...} 
int x;        (4) 
    ...
Fichier 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;

3. Les Fonctions en Paramètres

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)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]

4. Les Fonctions d'Entrées/Sorties de Base

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.

4.1. Getchar() et Putchar()

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 
%

4.2. Getc() et Putc()

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 stdout. Ainsi le programme précédent pourra aussi s'écrire

while ((c= getc(stdin)) != EOF) 
        putc(c, stdout);

4.3. Gets() et Puts()

Les deux fonctions, gets() et puts(), lisent et écrivent une chaîne de caractères.

char *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:

ligne = "toto"; 
puts(ligne);

4.4. scanf() et printf()

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

scanf( 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

  1. de caractères imprimables, e.g. la séquence "123e-1" vers données en mémoire, e.g. le réel 12.3, en cequi concerne scanf()
  2. et inversement, de donnée mémoire, e.g. 45, vers caractères imprimables, e.g. "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.

4.4.1. scanf()


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

les formats o d x ou e f peuvent être précédés de la lettre l (%lf) pour avoir un entier long ou un réel double. Mais %lf et %f ne doivent pas correspondre respectivement à des arguments float et double (inverses). De même pour int. 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 cas scanf() 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!

4.4.2. printf()

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:

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

4.5. sscanf() et sprintf()

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



[1] Spécification qui inclut en réalité la description de ce que fait la fonction, e.g. sous forme de commentaire..

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



< Précédent Sommaire Suivant >


Copyleft © Najib TOUNSI
ntounsi@emi.ac.ma
Version : Sept 2000 Dernière MàJ: Dec 2021