Dans cette série d'articles, nous présentons le langage C. Il ne s'agit pas de réécrire les ouvrages de référence donnés en annexe, mais de donner, au travers du C, une méthode de programmation basée sur la notion d'interface.
Au fur et à mesure de notre progression, nous décrirons les éléments indispensables du langage et conduirons le lecteur à consulter les ouvrages de références. Le but poursuivi est clairement de parvenir à construire des programmes de manière segmentée.
Introduction
Dans les articles précédents, nous avons vu comment écrire nos premiers programmes en C. Notamment, nous savons définir la fonction principale d'un programme, point d'entrée de tous les programmes C.
Pour écrire cette fonction, nous avons entouré les expressions entre des accolades. Les accolades permettent de délimiter un bloc d'instructions. Dans le bloc, les instructions sont séparées par des points-virgules ; elles seront exécutées une à une, en séquence.
Nous savons aussi déclarer des variables, avec les types de base, comme les entiers ou les caractères.
Nous avons vu aussi les instructions d'itération et de test permettant de modifier l'ordre d'exécution des instructions.
Entrons un peu plus dans le fonctionnement de C en ce qui concerne les variables.
Portée des variables
Principalement, nous pouvons déclarer des variables à deux endroits, à l'intérieur d'un bloc délimité par des accolades ou à l'extérieur ;
/* cette variable est accessible partout
* en dessous de sa déclaration.
*/
int variable_globale;
void main (void) {
/* Cette variable n'est acessible que
* dans ce bloc, partout en dessous
* de sa déclaration.
*/
int variable_locale;
...
}
Lorsqu'une variable est déclarée à l'extérieur de tout bloc, on dit que c'est une variable globale. Cela signifie qu'elle est accessible de n'importe quel endroit du programme se situant sous la déclaration (nous verrons plus tard que la visibilité de la variable est encore plus globale, moyennant quelques aménagements).
Lorsqu'une variable est déclarée à l'intérieur d'un bloc, la déclaration ne peut être faite qu'au début ; on dit alors que la variable est locale à ce bloc. Cela signifie que la variable est accessible à toutes les instructions du bloc se situant sous la déclaration.
Une variable locale disparaît lorsque la dernière instruction du bloc auquel elle appartient est terminée. Au contraire, une variable globale ne disparaît jamais.
Une variable globale peut "cacher" des variables déclarées précédemment.
int x; /* variable globale */
void main (void) {
int i, j, k; /* variable locale */
j = k = x = 0;
for (i = 0; i < 3; i = i + 1) {
int x; /* ce x cache le x global */
int j; /* ce j cache le j local */
j = k + i;
x = 3 * j;
k = x;
printf ("i=%d, j=%d, k=%d, x=%d\n"
i, j, k, x);
}
printf ("i=%d, j=%d, k=%d, x=%d\n"
i, j, k, x);
}
- i=0, j=0, x=0, k=0
- i=1, j=1, x=3, k=3
- i=2, j=5, x=15, k=15
- i=2, j=0, x=0, k=15
Les variables locales ne peuvent être déclarées qu'au début des blocs, parmi les autres déclarations :
void main (void) {
int x = 3; /* ok */
int j = x + 4; /* ok */
printf ("%d\n", x); /* instruction */
int u; /* déclaration érronée *
* car située après*
* une instruction*/
}
Si nous essayons de compiler le programme suivant, gcc nous dit :
$ gcc fichier.c
- c.c: In function `main':
- c.c:6: parse error before `int'
Il est possible d'initialiser les variables locales avec des instructions. Les variables globales ne peuvent être initialisées qu'avec des constantes. On a par exemple :
int a = 3 + 3;
char c = ‘e';
int r = rand( ); /* rand est le *
* générateur de nombres *
* aléatoire */
Si on tente de compiler un programme où on retrouve les instructions suivantes, la déclaration de r provoque une erreur car elle n'est pas initialisée par une constante.
Il est possible de déclarer des blocs sans qu'ils soient associés à une instruction for ou while ;
void main (void) { /* bloc de main */
int x; /* déclarations de main */
... /* instructions */
{ /* bloc */
int a, x; /* déclarations du bloc */
... /* instructions */
}
... /* instructions */}
Dans ce fragment de programme, nous déclarons un bloc imbriqué, qui permet de définir des variables.
Les fonctions
Un programme commence toujours par l'exécution de la fonction main. Tiens, fonction ? Comment fait-on pour déclarer d'autres fonctions ?
Une fonction est un bloc d'instructions auquel on donne un type pour la valeur de retour, un nom et des paramètres :
type nom (paramètres) {bloc}
Ainsi, nous écrivons la fonction main :
void main (void) {bloc}
Cette définition indique une fonction de nom main, ne retournant rien, n'ayant pas de paramètres et dont le corps est bloc.
Les paramètres se déclarent avec un type suivi d'un nom, chacune des déclarations étant séparées par une virgule :
int fonction (int a, int b) {bloc}
Cette écriture définit la fonction fonction retournant un entier et attendant deux entiers comme arguments.
Une fonction, en C, n'est pas une fonction mathématique : c'est une séquence d'instructions regroupées dans un bloc auquel on donne un type, un nom et des paramètres.
On ne peut pas définir des fonctions à l'intérieur d'un bloc : toutes les fonctions sont des variables globales (nous verrons cependant comment restreindre la portée des fonctions).
Pour activer le bloc d'instructions d'une fonction, il suffit d'écrire le nom de la fonction, suivi des arguments, entre parenthèses :
/* fonction ne retournant rien et
* prenant deux entiers.
*/
void f (int a, int b) {
printf ("%d + %d = %d\n", a, b, a+b);
}
void main (void) {
/* appel à la fonction f avec comme
* argument, les entiers 1 et 2.
*/
f (1, 2);
}
A l'exécution, les arguments de la fonction sont évalués (ordre applicatif) : ici, 1 et 2. Le résultat de l'évaluation des arguments est placé à un endroit qui, dans le corps de la fonction, permet aux paramètres de prendre la valeur de ces résultats (nous verrons plus tard que cet endroit est une pile). Enfin, les instructions du corps de la fonction sont exécutées.
Lorsqu'une fonction souhaite terminer prématurément, elle appelle l'instruction return avec un argument correspondant au type de la valeur de retour de la fonction. La valeur de retour peut être placée entre parenthèses ou non. Lorsque le type de la valeur de retour de la fonction est void, return ne prend pas d'argument :
/* fonction retournant un entier et
* attendant un entier
*/
int g (int a) {
/* la valeur de retour de return peut
* ne pas être placée entre parenthèses
*/
return (a + 3) / 2;
}
void f (int a) {
printf ("g (%d) = %d\n", g(a));
}
void main (void) {
f (1, 2);
}
L'instruction return peut être utilisée autant de fois que nécessaire, de n'importe quel endroit de la fonction, et pas nécessairement à la dernière position.
Lorsqu'elle est exécutée, l'instruction return provoque la fin de l'exécution de la fonction en cours et met à disposition, le cas échéant, la valeur de retour.
Le compilateur vérifie que le nombre et le type des arguments (en fait le type du résultat de l'évaluation des arguments) correspondent bien au nombre et au type déclarés des paramètres.
Il vérifie aussi que le type de la valeur de retour (en fait, le type du résultat de l'évaluation de la valeur de retour) corresponde au type déclaré de la fonction.
Il y a erreur lorsque le nombre des arguments ne correspond pas au nombre des paramètres. Par contre, il n'y a qu'un avertissement lorsque leurs types ne correspondent pas. Le langage C est assez permissif.
Prototype des fonctions
Forts de nos connaissances, nous décidons maintenant d'utiliser les fonctions pour écrire
/* fonction retournant un réel double
* et attendant un réel double
*/
double f (double a) {
printf ("a = %d\n", a);
if (a <= 0.01) {
return (0.0);
}
else {
return g (a * 2.0);
}
}
double g (double x) {
printf ("x = %d\n", x);
if (x <= 0.01) return (0.0);
else return f (x / 3.0);
}
void main (void) {
f (10);
}
Il s'agit d'un programme ayant trois fonctions, dont la fonction principale. Les deux fonctions f et g sont mutuellement récursives, c'est-à-dire qu'elles s'appellent mutuellement.
Les variables de type double sont des nombres réels au format long, c'est-à-dire qu'ils sont codés sur 64 bits. L'autre type pour les réels est float, codé sur 32 bits.
Si nous compilons ce programme, nous obtenons les messages :
$ gcc x.c
x.c:11: warning: type mismatch with previous external decl
x.c:7: warning: previous external decl of `g'
x.c:11: warning: type mismatch with previous implicit declaration
x.c:7: warning: previous implicit declaration of `g'
x.c:11: warning: `g' was previously implicitly declared to return `int'
La présence des avertissements (warning) n'empêche pas la création du binaire a.out.
Lorsque l'on exécute ce programme, il s'arrête peu après, dû à la convergence des valeurs (le lecteur pourra remplacer le test <= 0.01 par == 0.0 et constater quelque chose d'étrange ; nous en reparlerons plus tard).
Le problème rencontré par le compilateur se situe dans la fonction f : on utilise une fonction g, dont le compilateur ne connaît rien car il ne l'a pas encore rencontrée. Par défaut, le compilateur suppose que les fonctions qu'il ne connaît pas retournent un entier. Là, le compilateur n'émet aucun avertissement.
Puis, en parcourant le fichier, il rencontre la définition de g, déclarée comme retournant un double. Il y a conflit entre ce qu'il avait supposé, g retourne un entier, et ce que l'on déclare, retourne un double.
On pourrait définir la fonction g au-dessus de la fonction f : le problème serait résolu pour g, mais il se poserait pour la fonction f, inconnue dans g.
Pour résoudre ce problème, il faut déclarer f au-dessus de g, définir f, puis définir g. On utilise pour cela le prototype des fonctions : un prototype de fonction consiste dans le type de la valeur de retour, le nom de la fonction, ses paramètres, le tout suivi d'un point-virgule. Le nom des paramètres peut être omis. On déclare g avec :
/* déclaration des fonctions */
(double a); /* format long */
double g (double); /* format court */
/* définition des fonctions */
double f (double a) {
printf ("a = %d\n", a);
if (a <= 0.01) {
return (0.0);
}
else {
return g (a * 2.0);
}
}
double g (double x) {
printf ("x = %d\n", x);
/* syntaxe du if sans accolades */
if (x <= 0.01) return (0.0);
else return f (x / 3.0);
}
/* programme principal */
void main (void) {
f (10);
}
Cette fois-ci, lorsque le compilateur traite ce fichier, il connaît f et g avant de compiler le corps des fonctions. On remarque que le prototype de la fonction main n'est jamais nécessaire car c'est le système qui appelle cette fonction et jamais le programme lui-même.
Nous aurions pu placer le prototype de g à l'intérieur de la fonction f, comme une variable :
double f (double a) {
/* déclaration de g */
double g (double);
printf ("a = %d\n", a);
if (a <= 0.01) {
return (0.0);
}
else {
return g (a * 2.0);
}
}
...
Interface
Les prototypes des fonctions sont un peu la carte de visite d'un programme, c'est-à-dire son interface, qui est l'ensemble des fonctionnalités qu'il propose.
Nous allons nous concentrer sur cette notion d'interface afin de construire des bibliothèques de fonctions constituées de deux parties, l'interface et la réalisation.
Le langage C dispose d'un préprocesseur qui modifie les fichiers sources avant que ceux-ci ne soient effectivement compilés par le compilateur. Le préprocesseur comporte des instructions d'inclusion de fichier, dont on se sert pour écrire les en-têtes (headers) des librairies.
En général, les fichiers sources ont comme extension .c et les fichiers d'en-têtes .h. On appelle souvent les en-têtes des includes, du nom de l'instruction du préprocesseur qui permet de les utiliser.
Les en-têtes regroupent un ensemble de déclarations parmi lesquelles on trouve des déclarations de fonctions.
Dans les systèmes UNIX, les en-têtes standards sont regroupés dans le répertoire /usr/include. Le lecteur pourra y faire un tour et examiner quelques en-têtes. Parmi les lignes, il trouvera sûrement des déclarations de fonctions.
Dans les articles suivants, nous verrons comment construire et utiliser les librairies et comment définir leurs interfaces.
Le prochain article sera consacré aux débogueurs que l'on peut trouver sur les systèmes LINUX, comme gdb, xgdb, ddd. Nous utiliserons aussi le débogueur intégré de emacs, qui constitue en fait un véritable environnement de programmation.
Guilhem
de Wailly, directeur de la société Erian Concept,
partenaire RedHat France, Khéops et Getek: