Section courante

A propos

Section administrative du site

Les premiers pas

La seule façon d'apprendre un nouveau langage de programmation est d'écrire des programmes dans ce langage. Le premier programme à écrire est le même pour tous les langages de programmation :

Bonjour le monde !

C'est un obstacle de taille; pour le franchir, vous devez être capable de créer le texte du programme quelque part, de le compiler avec succès, de le charger, de l'exécuter et de découvrir où se trouve votre sortie. Une fois ces détails mécaniques maîtrisés, tout le reste est relativement facile.

En C, le programme pour afficher «Bonjour le monde !» est :

  1. #include <stdio.h>
  2.  
  3. main() {
  4.  printf("Bonjour le monde !\n");
  5. }

La manière d'exécuter ce programme dépend du système que vous utilisez. À titre d'exemple, sur le système d'exploitation UNIX, vous devez créer le programme dans un fichier dont le nom se termine par «.c», comme bonjour.c, puis le compiler avec la commande :

cc bonjour.c

Si vous n'avez rien raté, comme omettre un caractère ou faire une faute d'orthographe, la compilation se déroulera silencieusement et créera un fichier exécutable appelé a.out. Si vous exécutez a.out en tapant la commande :

a.out

il affichera :

Bonjour le monde !

Sur d'autres systèmes, les règles seront différentes; vérifiez auprès d'un expert local.

Maintenant, voici quelques explications sur le programme lui-même. Un programme C, quelle que soit sa taille, est composé de fonctions et de variables. Une fonction contient des instructions spécifiant les opérations de calcul à effectuer, et les variables entreposent les valeurs utilisées pendant le calcul. Les fonctions C sont comme les sous-routines et les fonctions de Fortran ou les procédures et fonctions de Pascal. Dans l'exemple, il y une fonction nommée main. Normalement, vous êtes libre de donner aux fonctions les noms que vous voulez, mais «main» est spécial : votre programme commence à s'exécuter au début de main. Cela signifie que chaque programme doit avoir un main quelque part.

main appellera généralement d'autres fonctions pour l'aider à effectuer son travail, certaines que vous avez écrites, et d'autres provenant de bibliothèques vous étant fournies. La première ligne du programme :

  1. #include <stdio.h>

indique au compilateur d'inclure des informations sur la bibliothèque d'entrée/sortie standard; la ligne apparaît au début de nombreux fichiers source C.

Une méthode de communication de données entre fonctions consiste pour la fonction appelante à fournir une liste de valeurs, appelées paramètres (ou arguments en anglais), à la fonction qu'elle appelle. Les parenthèses après le nom de la fonction entourent la liste de paramètres. Dans cet exemple, main est définie comme une fonction n'attendant aucun paramètre, ce qui est indiqué par la liste vide ().

Les instructions d'une fonction sont placées entre accolades { }. La fonction main ne contient qu'une seule instruction :

  1. printf("Bonjour le monde !\n");

Une fonction est appelée en la nommant, suivie d'une liste de paramètres entre parenthèses, ce qui appelle la fonction printf avec le paramètre "Bonjour le monde !\n". printf est une fonction de bibliothèque affichant la sortie, dans ce cas la chaîne de caractères entre les guillemets.

Une séquence de caractères entre guillemets, comme "Bonjour le monde !\n", est appelée une chaîne de caractères ou une constante de chaîne de caractères. Pour le moment, la seule utilisation des chaînes de caractères sera comme paramètres pour printf et d'autres fonctions.

La séquence \n dans la chaîne de caractères est la notation C pour le caractère de nouvelle ligne, qui, une fois affiché, fait avancer la sortie vers la marge gauche de la ligne suivante. Si vous omettez le \n (une expérience intéressante), vous constaterez qu'il n'y a pas d'avance de ligne après l'affichage de la sortie. Vous devez utiliser \n pour inclure un caractère de nouvelle ligne dans le paramètre printf ; si vous essayez quelque chose comme :

  1. printf("Bonjour le monde !
  2. ");

le compilateur C produira un message d'erreur.

printf ne fournit jamais automatiquement de caractère de nouvelle ligne, donc plusieurs appels peuvent être utilisés pour construire une ligne de sortie par étapes. Le premier programme aurait tout aussi bien pu être écrit :

  1. #include <stdio.h>
  2.  
  3. main() {
  4.  printf("Bonjour le ");
  5.  printf("monde !");
  6.  printf("\n");
  7. }    

pour produire une sortie identique.

Notez que \n ne représente qu'un seul caractère. Une séquence d'échappement comme \n fournit un mécanisme général et extensible pour représenter des caractères difficiles à saisir ou invisibles. Parmi les autres que C fournit, on trouve \t pour la tabulation, \b pour la touche de retour arrière, \" pour le guillemet double et \\ pour la barre oblique inverse elle-même.

Variables et expressions arithmétiques

Le programme suivant utilise la formule °C=(5/9)(°F-32) pour afficher le tableau suivant des températures en degrés Fahrenheit et leurs équivalents en degrés Celsius ou centigrades :

1    -17
20   -6
40   4
60   15
80   26
100  37
120  48
140  60
160  71
180  82
200  93
220  104
240  115
260  126
280  137
300  148

Le programme lui-même consiste toujours en la définition d'une seule fonction nommée main. Elle est plus longue que celle ayant affiché «Bonjour le monde !», mais pas compliquée. Elle introduit plusieurs nouvelles idées, notamment les commentaires, les déclarations, les variables, les expressions arithmétiques, les boucles et la sortie formatée :

  1. #include <stdio.h>
  2.  
  3. /* Affiche le tableau Fahrenheit-Celsius pour 
  4. fahr = 0, 20, ..., 300 */
  5.  
  6. main() {
  7.  int fahr, celsius;
  8.  int lower, upper, step;
  9.  
  10.  lower = 0; /* limite inférieure de l'échelle de température */
  11.  upper = 300; /* limite supérieure */
  12.  step = 20; /* taille du saut */
  13.  
  14.  fahr = lower;
  15.  while(fahr <= upper) {
  16.   celsius = 5 * (fahr-32) / 9;
  17.   printf("%d\t%d\n", fahr, celsius);
  18.   fahr = fahr + step;
  19.  }
  20. }

Les deux lignes :

  1. /* Affiche le tableau Fahrenheit-Celsius pour 
  2. fahr = 0, 20, ..., 300 */ 

sont des commentaires qui, dans ce cas, expliquent brièvement ce que fait le programme. Tous les caractères compris entre /* et */ sont ignorés par le compilateur; ils peuvent être utilisés librement pour rendre un programme plus facile à comprendre. Les commentaires peuvent apparaître partout où un espace, une tabulation ou une nouvelle ligne peuvent apparaître.

En C, toutes les variables doivent être déclarées avant d'être utilisées, généralement au début de la fonction avant toute instruction exécutable. Une déclaration annonce les propriétés des variables ; elle se compose d'un nom et d'une liste de variables, telles que :

  1. int fahr, celsius;
  2. int lower, upper, step;

Le type int signifie que les variables listées sont des entiers, contrairement à float, signifiant des nombres à virgule flottante, c'est-à-dire des nombres pouvant avoir une partie fractionnaire. L'intervalle de valeurs de int et float dépend de la machine que vous utilisez ; les entiers de 16 bits, se situant entre -32768 et +32767, sont courants, tout comme les entiers de 32 bits. Un nombre float est généralement une quantité de 32 bits, avec au moins six chiffres significatifs et une grandeur généralement comprise entre environ 10-38 et 1038.

C fournit plusieurs autres types de données en plus de int et float, notamment :

Type de données Description
char Caractère - un seul octet
short Entier court
long Entier long
double Virgule flottante de double précision

La taille de ces objets dépend également de la machine. Il existe également des tableaux, des structures et des unions de ces types de base, des pointeurs vers eux et des fonctions les renvoyant, que nous rencontrerons tous en temps voulu.

Le calcul dans le programme de conversion de température commence par les instructions d'affectation :

  1. lower = 0;
  2. upper = 300;
  3. step = 20;

définissant les variables à leurs valeurs initiales. Les instructions individuelles sont terminées par des points-virgules.

Chaque ligne du tableau est calculée de la même manière, nous utilisons donc une boucle qui se répète une fois par ligne de sortie ; c'est le but de la boucle while :

  1. while (fahr <= upper) {
  2. ...
  3. }

La boucle while fonctionne comme suit : la condition entre parenthèses est testée. Si elle est vraie (fahr est inférieur ou égal à upper), le corps de la boucle (les trois instructions entre accolades) est exécuté. Ensuite, la condition est re-testée et si elle est vraie, le corps est exécuté à nouveau. Lorsque le test devient faux (fahr dépasse upper), la boucle se termine et l'exécution continue à l'instruction suivant la boucle. Il n'y a pas d'autres instructions dans ce programme, il se termine donc.

Le corps d'un while peut être une ou plusieurs instructions entre accolades, comme dans le convertisseur de température, ou une seule instruction sans accolades, comme dans :

  1. while (i < j)
  2.     i = 2 * i;

Dans les deux cas, nous indenterons toujours les instructions contrôlées par le while d'une tabulation (que nous avons représentée par quatre espaces) afin que vous puissiez voir en un coup d'oeil quelles instructions se trouvent à l'intérieur de la boucle. L'indentation met l'accent sur la structure logique du programme. Bien que les compilateurs C ne se soucient pas de l'apparence d'un programme, une indentation et un espacement appropriés sont essentiels pour rendre les programmes faciles à lire. Il est recommandé d'écrire une seule instruction par ligne et d'utiliser des espaces autour des opérateurs pour clarifier le regroupement. La position des accolades est moins importante, bien que les gens aient des convictions passionnées. Il a été choisi l'un des nombreux styles populaires. Choisissez un style vous convenant, puis utilisez-le de manière cohérente.

La majeure partie du travail est effectuée dans le corps de la boucle. La température en Celsius est calculée et affectée à la variable celsius par l'instruction :

  1. celsius = 5 * (fahr-32) / 9;

La raison pour laquelle on multiplie par 5 et on divise par 9 au lieu de simplement multiplier par 5/9 est qu'en C, comme dans de nombreux autres langages de programmation, la division entière tronque : toute partie fractionnaire est ignorée. Puisque 5 et 9 sont des entiers, 5/9 serait tronqué à zéro et donc toutes les températures en Celsius seraient rapportées comme zéro.

Cet exemple montre également un peu plus comment fonctionne printf. printf est une fonction de formatage de sortie à usage général. Son premier paramètre est une chaîne de caractères à afficher, chaque % indiquant où l'un des autres paramètres (deuxième, troisième, ...) doit être substitué, et sous quelle forme il doit être affiché. Par exemple, %d spécifie un paramètre entier, donc l'instruction :

  1. printf("%d\t%d\n", fahr, celsius);

provoque l'affichage des valeurs des deux entiers fahr et celsius, avec une tabulation (\t) entre eux.

Chaque construction % dans le premier paramètre de printf est associée au deuxième paramètre, au troisième paramètre,... correspondants ; ils doivent correspondre correctement par numéro et par type, sinon vous obtiendrez de mauvaises réponses.

Au fait, printf ne fait pas partie du langage de programmation C ; il n'y a pas d'entrée ou de sortie définie dans le C lui-même. printf n'est qu'une fonction utile de la bibliothèque standard de fonctions étant normalement accessibles aux programmes C. Le comportement de printf est défini dans la norme ANSI, cependant, ses propriétés doivent donc être les mêmes avec tout compilateur et toute bibliothèque conformes à la norme.

Le programme de conversion de température présente quelques problèmes. Le plus simple est que la sortie n'est pas très jolie car les nombres ne sont pas justifiés à droite. C'est facile à résoudre ; si nous augmentons chaque %d dans l'instruction printf avec une largeur, les nombres affichés seront justifiés à droite dans leurs champs. Par exemple, nous pourrions dire :

  1. printf("%3d %6d\n", fahr, celsius);

pour afficher le premier numéro de chaque ligne dans un champ de trois chiffres de large, et le second dans un champ de six chiffres de large, comme ceci :

  0    -17
 20     -6
 40      4
 60     15
 80     26
100     37
...

Le problème le plus sérieux est que, comme nous avons utilisé l'arithmétique des nombres entiers, les températures en Celsius ne sont pas très précises ; par exemple, 0°F correspond en fait à environ -17,8°C, et non -17. Pour obtenir des réponses plus précises, nous devrions utiliser l'arithmétique à virgule flottante au lieu de l'arithmétique des nombres entiers. Cela nécessite quelques modifications dans le programme. Voici la deuxième version :

  1. #include <stdio.h>
  2.  
  3. /* afficher le tableau Fahrenheit-Celsius
  4. pour fahr = 0, 20, ..., 300 ; version à virgule flottante */
  5.  
  6. main() {
  7.  float fahr, celsius;
  8.  float lower, upper, step;
  9.  
  10.  lower = 0;   /* limite inférieure de l'échelle de température */
  11.  upper = 300; /* limite supérieure */
  12.  step = 20;   /* taille de pas */
  13.  fahr = lower;
  14.  while (fahr <= upper) {
  15.   celsius = (5.0/9.0) * (fahr-32.0);
  16.   printf("%3.0f %6.1f\n", fahr, celsius);
  17.   fahr = fahr + step;
  18.  }
  19. }

C'est à peu près la même chose qu'avant, sauf que fahr et celsius sont déclarés comme étant des nombres à virgule flottante et que la formule de conversion est écrite de manière plus naturelle. Nous n'avons pas pu utiliser 5/9 dans la version précédente car la division entière le tronquerait à zéro. Un point décimal dans une constante indique qu'il s'agit d'une valeur à virgule flottante, cependant, 5,0/9,0 n'est pas tronqué car il s'agit du rapport de deux valeurs à virgule flottante.

Si un opérateur arithmétique a des opérandes entiers, une opération entière est effectuée. Si un opérateur arithmétique a un opérande à virgule flottante et un opérande entier, cependant, l'entier sera converti en virgule flottante avant que l'opération ne soit effectuée. Si nous avions écrit (fahr-32), le 32 serait automatiquement converti en virgule flottante. Néanmoins, l'écriture de constantes à virgule flottante avec des points décimaux explicites même lorsqu'elles ont des valeurs entières met l'accent sur leur nature à virgule flottante pour les lecteurs humains.

Pour l'instant, notez que l'affectation :

  1. fahr = lower;

et le test :

  1. while (fahr <= upper)

fonctionne également de manière naturelle : l'int est converti en float avant que l'opération ne soit effectuée.

La spécification de conversion printf %3.0f indique qu'un nombre à virgule flottante (ici fahr) doit être affiché sur au moins trois caractères de large, sans point décimal et sans chiffres de fraction. %6.1f décrit un autre nombre (celsius) devant être affiché sur au moins six caractères de large, avec 1 chiffre après le point décimal. Le résultat ressemble à ceci :

  0 -17.8
 20  -6.7
 40   4.4
...

La largeur et la précision peuvent être omises d'une spécification : %6f indique que le nombre doit avoir au moins six caractères de large ; %.2f spécifie deux caractères après la virgule décimale, mais la largeur n'est pas limitée ; et %f indique simplement d'afficher le nombre sous forme de virgule flottante :

Format Description
%d Afficher sous forme d'entier décimal.
%6d Afficher sous forme d'entier décimal, d'au moins 6 caractères de large.
%f Afficher en virgule flottante
%6f Afficher en virgule flottante, au moins 6 caractères de large.
%.2f Afficher en virgule flottante, 2 caractères après la virgule décimale.
%6.2f Afficher en virgule flottante, au moins 6 de large et 2 après la virgule décimale.

Entre autres, printf reconnaît également %o pour octal, %x pour hexadécimal, %c pour caractère, %s pour chaîne de caractères et %% pour lui-même.

L'instruction for

Il existe de nombreuses façons différentes d'écrire un programme pour une tâche particulière. Essayons une variante du convertisseur de température :

  1. #include <stdio.h>
  2.  
  3. /* Afficher le tableau Fahrenheit-Celsius */
  4.  
  5. main() {
  6.  int fahr;
  7.  for (fahr = 0; fahr <= 300; fahr = fahr + 20)
  8.   printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
  9. }

Cela produit les mêmes réponses, mais cela semble certainement différent. Un changement majeur est l'élimination de la plupart des variables; seule fahr reste, et nous en avons fait un int. Les limites inférieure et supérieure et la taille du pas n'apparaissent que comme des constantes dans l'instruction for, elle-même une nouvelle construction, et l'expression calculant la température en Celsius apparaît maintenant comme le troisième paramètre de printf au lieu d'une instruction d'affectation séparée.

Ce dernier changement est un exemple d'une règle générale - dans tout contexte où il est permis d'utiliser la valeur d'un certain type, vous pouvez utiliser une expression plus compliquée de ce type. Étant donné que le troisième argument de printf doit être une valeur à virgule flottante pour correspondre à %6.1f, n'importe quelle expression à virgule flottante peut se produire ici.

L'instruction for est une boucle, une généralisation du while. Si vous la comparez au while précédent, son fonctionnement devrait être clair. Entre les parenthèses, il y a trois parties, séparées par des points-virgules. La première partie, l'initialisation :

  1. fahr = 0

est effectuée une fois, avant d'entrer dans la boucle proprement dite. La deuxième partie est le test ou la condition contrôlant la boucle :

  1. fahr <= 300

Cette condition est évaluée ; si elle est vraie, le corps de la boucle (ici un seul printf) est exécuté. Puis le pas d'incrémentation :

  1. fahr = fahr + 20

est exécuté et la condition réévaluée. La boucle se termine si la condition est devenue fausse. Comme avec le while, le corps de la boucle peut être une instruction unique ou un groupe d'instructions entre accolades. L'initialisation, la condition et l'incrément peuvent être n'importe quelles expressions.

Le choix entre while et for est arbitraire, en fonction de ce qui semble le plus clair. Le for est généralement approprié pour les boucles dans lesquelles l'initialisation et l'incrément sont des instructions uniques et logiquement liées, car il est plus compact que le while et il garde les instructions de contrôle de boucle ensemble au même endroit.

Constantes symboliques

Une dernière observation avant de laisser la conversion de température de côté pour toujours. C'est une mauvaise pratique d'enfouir des «nombres magiques» comme 300 et 20 dans un programme ; ils ne transmettent que peu d'informations à quelqu'un pouvant avoir à lire le programme plus tard, et ils sont difficiles à modifier de manière systématique. Une façon de gérer les nombres magiques est de leur donner des noms significatifs. Une ligne #define définit un nom symbolique ou une constante symbolique comme étant une chaîne de caractères particulière :

#define name replacement list

Par la suite, toute occurrence de name (non entre guillemets et ne faisant pas partie d'un autre nom) sera remplacée par le texte replacement correspondant. Le name a la même forme qu'un nom de variable : une séquence de lettres et de chiffres qui commence par une lettre. Le texte replacement peut être n'importe quelle séquence de caractères ; il n'est pas limité aux chiffres :

  1. #include <stdio.h>
  2.  
  3. #define LOWER 0 /* limite inférieure du tableau */
  4. #define UPPER 300 /* limite supérieure */
  5. #define STEP 20 /* taille de saut */
  6.  
  7. /* Afficher le tableau Fahrenheit-Celsius */
  8. main() {
  9.  int fahr;
  10.  for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP)
  11.   printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
  12. }    

Les quantités LOWER, UPPER et STEP sont des constantes symboliques et non des variables. Elles n'apparaissent donc pas dans les déclarations. Les noms de constantes symboliques sont traditionnellement écrits en majuscules afin de pouvoir les distinguer facilement des noms de variables en minuscules. Notez qu'il n'y a pas de point-virgule à la fin d'une ligne #define.

Entrée et sortie de caractères

Nous allons considérer une famille de programmes apparentés pour le traitement de données de caractères. Vous constaterez que de nombreux programmes ne sont que des versions étendues des prototypes dont nous discutons ici.

Le modèle d'entrée et de sortie pris en charge par la bibliothèque standard est très simple. L'entrée ou la sortie de texte, quelle que soit son origine ou sa destination, est traitée comme des flux de caractères. Un flux de texte est une séquence de caractères divisée en lignes ; chaque ligne se compose de zéro ou plusieurs caractères suivis d'un caractère de nouvelle ligne. Il est de la responsabilité de la bibliothèque de faire en sorte que chaque flux d'entrée ou de sortie confirme ce modèle ; le programmeur C utilisant la bibliothèque n'a pas à se soucier de la manière dont les lignes sont représentées en dehors du programme.

La bibliothèque standard fournit plusieurs fonctions pour lire ou écrire un caractère à la fois, dont getchar et putchar sont les plus simples. Chaque fois qu'elle est appelée, getchar lit le caractère d'entrée suivant dans un flux de texte et le renvoie comme valeur. C'est-à-dire après :

  1. c = getchar();

la variable c contient le prochain caractère de la saisie. Les caractères proviennent normalement du clavier.

La fonction putchar affiche un caractère à chaque fois qu'elle est appelée :

  1. putchar(c);    

affiche le contenu de la variable entière c sous forme de caractère, généralement à l'écran. Les appels à putchar et printf peuvent être entrelacés ; la sortie apparaîtra dans l'ordre dans lequel les appels sont effectués.

Copie de fichiers

Étant donné getchar et putchar, vous pouvez écrire une quantité surprenante de code utile sans rien savoir de plus sur l'entrée et la sortie. L'exemple le plus simple est un programme copiant son entrée vers sa sortie un caractère à la fois :

Lire un caractère
BOUCLE FAIRE TANT QUE (le caractère n'est pas un indicateur de fin de fichier)
   Afficher le caractère venant d'être lu
   Lire un caractère

La conversion en C donne :

  1. #include <stdio.h>
  2.  
  3. /* copier l'entrée vers la sortie ; 1ère version */
  4.  
  5. main() {
  6.  int c;
  7.  c = getchar();
  8.  while (c != EOF) {
  9.   putchar(c);
  10.   c = getchar();
  11.  }
  12. }

L'opérateur relationnel != signifie «différent de».

Ce qui apparaît comme un caractère sur le clavier ou l'écran est bien sûr, comme tout le reste, entreposé en interne comme un motif de bits. Le type char est spécifiquement destiné à entreposer de telles données de type caractère, mais n'importe quel type entier peut être utilisé. On utilise int pour une raison subtile mais importante.

Le problème est de distinguer la fin de l'entrée des données valides. La solution est que getchar renvoie une valeur distinctive lorsqu'il n'y a plus d'entrée, une valeur ne pouvant être confondue avec aucun caractère réel. Cette valeur est appelée EOF, pour «End Of File». On déclare c comme étant un type suffisamment grand pour contenir toute valeur renvoyée par getchar. On ne peut pas utiliser char car c doit être suffisamment grand pour contenir EOF en plus de tout char possible. Par conséquent, on utilise int.

EOF est un entier défini dans <stdio.h>, mais la valeur numérique spécifique n'a pas d'importance tant qu'elle n'est pas la même que n'importe quelle valeur char. En utilisant la constante symbolique, nous sommes assurés que rien dans le programme ne dépend de la valeur numérique spécifique.

Le programme de copie serait écrit de manière plus concise par des programmeurs C expérimentés. En C, toute affectation, telle que :

  1. c = getchar();

est une expression et a une valeur, étant la valeur du côté gauche après l'affectation. Cela signifie qu'une affectation peut apparaître comme une partie d'une expression plus grande. Si l'affectation d'un caractère à c est placée à l'intérieur de la partie test d'une boucle while, le programme de copie peut être écrit de cette façon :

  1. #include <stdio.h>
  2.  
  3. /* copier l'entrée vers la sortie ; 2e version */
  4.  
  5. main() {
  6.  int c;
  7.  while ((c = getchar()) != EOF)
  8.   putchar(c);
  9. }

Le while récupère un caractère, l'assigne à c, puis teste si le caractère était le signal de fin de fichier. Si ce n'était pas le cas, le corps du while est exécuté, en affichant le caractère. Le while se répète alors. Lorsque la fin de l'entrée est finalement atteinte, le while se termine et main aussi.

Cette version centralise l'entrée - il n'y a maintenant qu'une seule référence à getchar - et réduit le programme. Le programme résultant est plus compact et, une fois l'idiome maîtrisé, plus facile à lire. Vous verrez souvent ce style. (Il est possible de s'emporter et de créer du code impénétrable, cependant, une tendance que nous allons essayer de freiner.)

Les parenthèses autour de l'affectation, dans la condition, sont nécessaires. La priorité de != est supérieure à celle de =, ce qui signifie qu'en l'absence de parenthèses, le test relationnel != serait effectué avant l'affectation =. Donc l'instruction :

  1. c = getchar() != EOF

est équivalent à :

  1. c = (getchar() != EOF)

Cela a pour effet indésirable de définir c sur 0 ou 1, selon que l'appel de getchar a renvoyé ou non la fin du fichier.

Comptage des caractères

Le programme suivant compte les caractères ; il est similaire au programme de copie :

  1. #include <stdio.h>
  2.  
  3. /* Compter les caractères en entrée ; 1ère version */
  4.  
  5. main() {
  6.  long nc;
  7.  nc = 0;
  8.  while (getchar() != EOF)
  9.   ++nc;
  10.  printf("%ld\n", nc);
  11. }

L'instruction :

  1. ++nc;

présente un nouvel opérateur, ++, signifiant incrémenter de un. Vous pourriez plutôt écrire nc = nc + 1 mais ++nc est plus concis et souvent plus efficace. Il existe un opérateur correspondant -- pour décrémenter de 1. Les opérateurs ++ et -- peuvent être soit des opérateurs préfixes (++nc) soit des opérateurs postfixes (nc++) ; ces deux formes ont des valeurs différentes dans les expressions, mais ++nc et nc++ incrémentent tous deux nc. Pour le moment, nous nous en tiendrons à la forme préfixe.

Le programme de comptage de caractères accumule son compte dans une variable longue au lieu d'un int. Les entiers longs ont au moins 32 bits. Bien que sur certaines machines, int et long aient la même taille, sur d'autres un int a 16 bits, avec une valeur maximale de 32767, et il faudrait relativement peu d'entrées pour faire déborder un compteur int. La spécification de conversion %ld indique à printf que l'argument correspondant est un entier long.

Il peut être possible de gérer des nombres encore plus grands en utilisant un double (nombre à virgule de double précision). On utilisa également une instruction for au lieu d'un while, pour illustrer une autre façon d'écrire la boucle :

  1. #include <stdio.h>
  2.  
  3. /* compter les caractères en entrée ; 2e version */
  4.  
  5. main() {
  6.  double nc;
  7.  for (nc = 0; gechar() != EOF; ++nc)
  8.     ;
  9.  printf("%.0f\n", nc);
  10. }    

printf utilise %f pour float et double ; %.0f supprime l'affichage du point décimal et de la partie fractionnaire, étant zéro.

Le corps de cette boucle for est vide, car tout le travail est effectué dans les parties test et increment. Mais les règles grammaticales du C exigent qu'une instruction for ait un corps. Le point-virgule isolé, appelé instruction null, est là pour satisfaire cette exigence. Nous le mettons sur une ligne séparée pour le rendre visible.

Avant de quitter le programme de comptage de caractères, observez que si l'entrée ne contient aucun caractère, le test while ou for échoue au tout premier appel à getchar, et le programme produit zéro, la bonne réponse. C'est important. L'un des avantages de while et for est qu'ils testent en haut de la boucle, avant de passer au corps. S'il n'y a rien à faire, rien n'est fait, même si cela signifie ne jamais parcourir le corps de la boucle. Les programmes doivent agir intelligemment lorsqu'on leur donne une entrée de longueur nulle. Les instructions while et for aident à garantir que les programmes font des choses raisonnables avec des conditions limites.

Comptage des lignes

Le programme suivant compte les lignes d'entrée. Comme nous l'avons mentionné ci-dessus, la bibliothèque standard garantit qu'un flux de texte d'entrée apparaît comme une séquence de lignes, chacune terminée par un saut de ligne. Par conséquent, compter les lignes revient simplement à compter les sauts de ligne :

  1. #include <stdio.h>
  2.  
  3. /* Compte les lignes en entrée */
  4.  
  5. main() {
  6.  int c, nl;
  7.  nl = 0;
  8.  while ((c = getchar()) != EOF)
  9.   if (c == '\n')
  10.    ++nl;
  11.  printf("%d\n", nl);
  12. }

Le corps du while se compose maintenant d'un if, contrôlant à son tour l'incrément ++nl. L'instruction if teste la condition entre parenthèses, et si la condition est vraie, exécute l'instruction (ou le groupe d'instructions entre accolades) suivant. Nous avons de nouveau mis en retrait pour montrer ce qui est contrôlé par quoi.

Le double signe égal == est la notation C pour «est égal à» (comme le simple = de Pascal ou le .EQ. de Fortran). Ce symbole est utilisé pour distinguer le test d'égalité du simple = que le C utilise pour l'affectation. Un mot d'avertissement : les nouveaux venus en C écrivent parfois = quand ils veulent dire ==. Le résultat est généralement une expression légale, vous n'aurez donc aucun avertissement.

Un caractère écrit entre guillemets simples représente une valeur entière égale à la valeur numérique du caractère dans l'ensemble de caractères de la machine. C'est ce qu'on appelle une constante de caractère, bien que ce soit juste une autre façon d'écrire un petit entier. Ainsi, par exemple, «A» est une constante de caractère ; dans l'ensemble de caractères ASCII, sa valeur est 65, la représentation interne du caractère A. Bien sûr, «A» est préférable à 65 : sa signification est évidente et elle est indépendante d'un ensemble de caractères particulier.

Les séquences d'échappement utilisées dans les constantes de chaîne de caractères sont également autorisées dans les constantes de caractère, ainsi '\n' représente la valeur du caractère de nouvelle ligne, étant 10 en ASCII. Vous devez noter attentivement que '\n' est un caractère unique et que dans les expressions, il s'agit simplement d'un entier ; d'autre part, '\n' est une constante de chaîne de caractères ne contenant qu'un seul caractère.

Comptage de mots

Le quatrième programme de notre série compte les lignes, les mots et les caractères, avec la définition générale selon laquelle un mot est toute séquence de caractères ne contenant pas d'espace, de tabulation ou de saut de ligne. Il s'agit d'une version simplifiée du programme wc de UNIX :

  1. #include <stdio.h>
  2.  
  3. #define IN 1 /* à l'intérieur d'un mot */
  4. #define OUT 0 /* à l'extérieur d'un mot */
  5.  
  6. /* compter les lignes, les mots et les caractères en entrée */
  7.  
  8. main() {
  9.  int c, nl, nw, nc, state;
  10.  state = OUT;
  11.  nl = nw = nc = 0;
  12.  while ((c = getchar()) != EOF) {
  13.   ++nc;
  14.   if (c == '\n')
  15.    ++nl;
  16.   if (c == ' ' || c == '\n' || c = '\t')
  17.    state = OUT;
  18.   else if (state == OUT) {
  19.    state = IN;
  20.    ++nw;
  21.   }
  22.  }
  23.  printf("%d %d %d\n", nl, nw, nc);
  24. }

Chaque fois que le programme rencontre le premier caractère d'un mot, il compte un mot de plus. La variable state enregistre si le programme est actuellement dans un mot ou non ; initialement, il est «pas dans un mot», auquel est attribuée la valeur OUT. Nous préférons les constantes symboliques IN et OUT aux valeurs littérales 1 et 0 car elles rendent le programme plus lisible. Dans un programme aussi petit que celui-ci, cela ne fait pas beaucoup de différence, mais dans les programmes plus grands, l'augmentation de la clarté vaut bien le modeste effort supplémentaire que représente l'écriture de cette manière dès le début. Vous constaterez également qu'il est plus facile d'effectuer des modifications importantes dans les programmes où les nombres magiques n'apparaissent que sous forme de constantes symboliques.

La ligne :

  1. nl = nw = nc = 0;

met les trois variables à zéro. Ce n'est pas un cas particulier, mais une conséquence du fait qu'une affectation est une expression avec la valeur et les affectations associées de droite à gauche. C'est comme si on avait écrit :

  1. nl = (nw = (nc = 0));

L'opérateur || signifie OU, donc la ligne :

  1. if (c == ' ' || c == '\n' || c = '\t')

dit «si c est un espace ou c est un saut de ligne ou c est une tabulation». (Rappelons que la séquence d'échappement \t est une représentation visible du caractère de tabulation.) Il existe un opérateur correspondant && pour AND ; sa priorité est juste supérieure à ||. Les expressions connectées par && ou || sont évaluées de gauche à droite, et il est garanti que l'évaluation s'arrêtera dès que la vérité ou la fausseté sera connue. Si c est un espace, il n'est pas nécessaire de tester s'il s'agit d'un saut de ligne ou d'une tabulation, donc ces tests ne sont pas effectués. Ce n'est pas particulièrement important ici, mais est significatif dans des situations plus compliquées, comme nous le verrons bientôt.

L'exemple montre également un else, spécifiant une action alternative si la partie conditionnelle d'une instruction if est fausse. Le format général est :

if (expression)
    statement1
else
    statement2

Une seule et unique des deux instructions associées à un if-else est exécutée. Si l'expression est vraie, le statement1 est exécutée ; sinon, le statement2 est exécutée. Chaque instruction peut être une instruction unique ou plusieurs instructions entre accolades. Dans le programme de comptage de mots, celle suivant le else est un if contrôlant deux instructions entre accolades.

Les tableaux

Écrivons un programme pour compter le nombre d'occurrences de chaque chiffre, des caractères d'espacement (blanc, tabulation, nouvelle ligne) et de tous les autres caractères. C'est artificiel, mais cela nous permet d'illustrer plusieurs aspects du C dans un seul programme.

Il existe douze catégories d'entrées, il est donc pratique d'utiliser un tableau pour contenir le nombre d'occurrences de chaque chiffre, plutôt que dix variables individuelles. Voici une version du programme :

  1. #include <stdio.h>
  2.  
  3. /* compter les chiffres, les espaces blancs, autres */
  4.  
  5. main() {
  6.  int c, i, nwhite, nother;
  7.  int ndigit[10];
  8.  nwhite = nother = 0;
  9.  for (i = 0; i < 10; ++i) ndigit[i] = 0;
  10.  while ((c = getchar()) != EOF) if (c >= '0' && c <= '9') ++ndigit[c-'0'];
  11.  else if (c == ' ' || c == '\n' || c == '\t') ++nwhite;
  12.  else ++nother;
  13.  printf("chiffres =");
  14.  for (i = 0; i < 10; ++i) printf(" %d", ndigit[i]);
  15.  printf(", espace blanc = %d, autre = %d\n",nwhite, nother);
  16. }

La sortie de ce programme, lui-même, est :

chiffres = 9 3 0 0 0 0 0 0 0 1, espace blanc = 123, autre = 345

La déclaration :

  1. int ndigit[10];    

déclare ndigit comme étant un tableau de 10 entiers. Les indices de tableau commencent toujours à zéro en C, donc les éléments sont ndigit[0], ndigit[1], ..., ndigit[9]. Cela se reflète dans les boucles for initialisant et affichant le tableau.

Un indice peut être n'importe quelle expression entière, comprenant des variables entières comme i et des constantes entières.

Ce programme particulier s'appuie sur les propriétés de la représentation textuelle des chiffres. Par exemple, le test :

  1. if (c >= '0' && c <= '9')

détermine si le caractère dans c est un chiffre. Si c'est le cas, la valeur numérique de ce chiffre est :

  1. c - '0'

Cela ne fonctionne que si '0', '1', ..., '9' ont des valeurs croissantes consécutives. Heureusement, cela est vrai pour tous les ensembles de caractères.

Par définition, les caractères ne sont que de petits entiers, donc les variables et constantes char sont identiques aux entiers dans les expressions arithmétiques. C'est naturel et pratique ; par exemple, c-'0' est une expression entière avec une valeur comprise entre 0 et 9 correspondant au caractère '0' à '9' entreposé dans c, et donc un indice valide pour le tableau ndigit.

La décision de savoir si un caractère est un chiffre, un espace blanc ou autre chose est prise avec la séquence :

  1. if (c >= '0' && c <= '9')
  2.  ++ndigit[c-'0'];
  3. else if (c == ' ' || c == '\n' || c == '\t')
  4.  ++nwhite;
  5. else
  6.  ++nother;

Le modèle :

if (condition1)
   statement1
else if (condition2)
   statement2
   ...
   ...
else
   statementn

apparaît fréquemment dans les programmes comme un moyen d'exprimer une décision à plusieurs voies. Les condition sont évaluées dans l'ordre du haut jusqu'à ce qu'une condition soit satisfaite ; à ce stade, la partie statement correspondante est exécutée et la construction entière est terminée. (Toute instruction peut être constituée de plusieurs instructions entre accolades.) Si aucune des conditions n'est satisfaite, l'instruction après le else final est exécutée si elle est présente. Si le else final et le statement sont omis, comme dans le programme de comptage de mots, aucune action n'a lieu. Il peut y avoir n'importe quel nombre de :

else if(condition)
   statement

groupes entre le if initial et le else final.

Pour des raisons de style, il est conseillé de formater cette construction comme nous l'avons montré ; si chaque if était indenté au-delà du else précédent, une longue séquence de décisions sortirait du côté droit de la page.

L'instruction switch fournit une autre façon d'écrire une branche multidirectionnelle étant particulièrement adaptée lorsque la condition est de savoir si une expression entière ou de caractère correspond à l'une des constantes d'un ensemble.

Les fonctions

En C, une fonction est équivalente à une sous-routine ou à une fonction en Fortran, ou à une procédure ou à une fonction en Pascal. Une fonction fournit un moyen pratique d'encapsuler un calcul, pouvant ensuite être utilisé sans se soucier de son implémentation. Avec des fonctions correctement conçues, il est possible d'ignorer la manière dont une tâche est effectuée ; il suffit de savoir ce qui est fait. Le C rend l'utilisation des fonctions facile, pratique et efficace; vous verrez souvent une fonction courte définie et appelée une seule fois, simplement parce qu'elle clarifie un morceau de code.

Jusqu'à présent, nous n'avons utilisé que des fonctions comme printf, getchar et putchar nous ayant été fournies ; il est maintenant temps d'en écrire quelques-unes de nos propres. Comme C n'a pas d'opérateur d'exponentiation comme le ** de Fortran, illustrons la mécanique de la définition de fonction en écrivant une fonction power(m,n) pour élever un entier m à une puissance entière positive n. C'est-à-dire que la valeur de power(2,5) est 32. Cette fonction n'est pas une routine d'exponentiation pratique, car elle ne gère que les puissances positives de petits entiers, mais elle est suffisamment bonne pour l'illustration. (La bibliothèque standard contient une fonction pow(x,y) calculant xy.)

Voici la fonction power et un programme principal pour l'exercer, afin que vous puissiez voir toute la structure d'un coup :

  1. #include <stdio.h>
  2.  
  3. int power(int m, int n);
  4.  
  5. /* Tester la fonction de puissance */
  6.  
  7. main() {
  8.  int i;
  9.  for (i = 0; i < 10; ++i) printf("%d %d %d\n", i, power(2,i), power(-3,i));
  10.  return 0;
  11. }
  12.  
  13. /* puissance : élever la base à la puissance n ; n >= 0 */
  14. int power(int base, int n) {
  15.  int i, p;
  16.  p = 1;
  17.  for (i = 1; i <= n; ++i) p = p * base;
  18.  return p;
  19. }

Une définition de fonction a ce format :

type-de-retour nom-de-fonction(déclarations-de-paramètres, si-il-y-en-a)
{
   déclarations
   instructions
}

Les définitions de fonctions peuvent apparaître dans n'importe quel ordre, et dans un ou plusieurs fichiers source, bien qu'aucune fonction ne puisse être divisée entre plusieurs fichiers. Si le programme source apparaît dans plusieurs fichiers, vous devrez peut-être en dire plus pour le compiler et le charger que s'il apparaît dans un seul fichier, mais c'est une question de système d'exploitation, pas un attribut de langage. Pour le moment, nous supposerons que les deux fonctions se trouvent dans le même fichier, donc tout ce que vous avez appris sur l'exécution de programmes C fonctionnera toujours.

La fonction power est appelée deux fois par main, dans la ligne :

  1. printf("%d %d %d\n", i, power(2,i), power(-3,i));

Chaque appel passe deux paramètres à power, renvoyant à chaque fois un entier à formater et à afficher. Dans une expression, power(2,i) est un entier tout comme 2 et i. (Toutes les fonctions ne produisent pas une valeur entière.)

La première ligne de power elle-même :

  1. int power(int base, int n)

déclare les types et les noms des paramètres, ainsi que le type du résultat renvoyé par la fonction. Les noms utilisés par power pour ses paramètres sont locaux pour power et ne sont visibles par aucune autre fonction : d'autres routines peuvent utiliser les mêmes noms sans conflit. Ceci est également vrai pour les variables i et p : le i dans power n'est pas lié au i dans main.

Nous utiliserons généralement paramètre pour une variable nommée dans la liste entre parenthèses dans une fonction. Les termes argument formel et argument réel sont parfois utilisés pour la même distinction.

La valeur calculée par power est renvoyée à main par l'instruction return. Toute expression peut suivre return :

return expression;

Une fonction n'a pas besoin de renvoyer une valeur ; une instruction return sans expression entraîne le retour du contrôle, mais aucune valeur utile, à l'appelant, comme le fait de «tomber à la fin» d'une fonction en atteignant l'accolade droite de fin. Et la fonction appelante peut ignorer une valeur renvoyée par une fonction.

Vous avez peut-être remarqué qu'il y a une instruction return à la fin de main. Puisque main est une fonction comme n'importe quelle autre, elle peut renvoyer une valeur à son appelant, étant en fait l'environnement dans lequel le programme a été exécuté. En général, une valeur de retour de zéro implique une terminaison normale ; les valeurs non nulles signalent des conditions de terminaison inhabituelles ou erronées. Dans un souci de simplicité, nous avons omis les instructions return de nos fonctions principales jusqu'à présent, mais nous les inclurons ci-après, pour rappeler que les programmes doivent renvoyer l'état à leur environnement.

La déclaration :

  1. int power(int base, int n);

juste avant main dit que power est une fonction attendant deux paramètres int et renvoie un int. Cette déclaration, étant appelée prototype de fonction, doit être en accord avec la définition et les utilisations de power. C'est une erreur si la définition d'une fonction ou ses utilisations ne sont pas en accord avec son prototype.

Les noms de paramètres ne doivent pas nécessairement être en accord. En effet, les noms de paramètres sont facultatifs dans un prototype de fonction, donc pour le prototype nous aurions pu écrire :

  1. int power(int, int);

Les noms bien choisis constituent une bonne documentation, on les utilise donc souvent.

Un rappel historique : le plus grand changement entre le ANSI C et les versions antérieures concerne la manière dont les fonctions sont déclarées et définies. Dans la définition originale du C, la fonction puissance aurait été écrite comme ceci :

  1. /* puissance : élever la base à la puissance n ; n >= 0 */
  2. /* (version ancienne) */
  3.  
  4. power(base, n)
  5. int base, n;
  6. {
  7.  int i, p;
  8.  p = 1;
  9.  for (i = 1; i <= n; ++i) 
  10.   p = p * base;
  11.  return p;
  12. }

Les paramètres sont nommés entre parenthèses et leurs types sont déclarés avant d'ouvrir l'accolade gauche ; les paramètres non déclarés sont considérés comme des int. (Le corps de la fonction est le même qu'avant.)

La déclaration de puissance au début du programme aurait ressemblé à ceci :

  1. int power();

Aucune liste de paramètres n'était autorisée, de sorte que le compilateur ne pouvait pas vérifier facilement que power était appelé correctement. En effet, comme par défaut power aurait été supposé renvoyer un int, la déclaration entière aurait très bien pu être omise.

La nouvelle syntaxe des prototypes de fonctions permet à un compilateur de détecter beaucoup plus facilement les erreurs dans le nombre de paramètres ou leurs types. L'ancien style de déclaration et de définition fonctionne toujours en ANSI C, au moins pendant une période de transition, mais il est fortement recommandé d'utiliser la nouvelle forme lorsque vous disposez d'un compilateur la prenant en charge.

Paramètres - Appel par valeur

Un aspect des fonctions C peut être inconnu des programmeurs habitués à d'autres langages, notamment Fortran. En C, tous les paramètres de fonction sont passés «par valeur». Cela signifie que la fonction appelée reçoit les valeurs de ses paramètres dans des variables temporaires plutôt que dans les originaux. Cela conduit à des propriétés différentes de celles observées avec les langages «d'appel par référence» comme Fortran ou avec les paramètres var en Pascal, dans lesquels la routine appelée a accès au paramètre d'origine, et non à une copie locale.

L'appel par valeur est un atout, mais pas un inconvénient. Il conduit généralement à des programmes plus compacts avec moins de variables externes, car les paramètres peuvent être traités comme des variables locales initialisées de manière pratique dans la routine appelée. Par exemple, voici une version de power utilisant cette propriété :

  1. /* puissance : élever la base à la puissance n ; n >= 0 ; version 2 */
  2. int power(int base, int n) {
  3.  int p;
  4.  for (p = 1; n > 0; --n)
  5.  p = p * base;
  6.  return p;
  7. }

Le paramètre n est utilisé comme variable temporaire et est compté à rebours (une boucle for s'exécutant à rebours) jusqu'à ce qu'il atteigne zéro ; la variable i n'est plus nécessaire. Tout ce qui est fait à n dans power n'a aucun effet sur le paramètre avec lequel power a été appelé à l'origine.

Si nécessaire, il est possible de faire en sorte qu'une fonction modifie une variable dans une routine d'appel. L'appelant doit fournir l'adresse de la variable à définir (techniquement un pointeur vers la variable), et la fonction appelée doit déclarer le paramètre comme étant un pointeur et accéder à la variable indirectement par son intermédiaire.

L'histoire est différente pour les tableaux. Lorsque le nom d'un tableau est utilisé comme paramètre, la valeur passée à la fonction est l'emplacement ou l'adresse du début du tableau - il n'y a pas de copie des éléments du tableau. En souscrivant cette valeur, la fonction peut accéder à n'importe quel paramètre du tableau et le modifier. C'est le sujet de la section suivante.

Tableaux de caractères

Le type de tableau le plus courant en C est le tableau de caractères. Pour illustrer l'utilisation des tableaux de caractères et des fonctions permettant de les manipuler, écrivons un programme lisant un ensemble de lignes de texte et imprime la plus longue. Le schéma est assez simple :

TANT QUE (il y a une autre ligne)
   SI (c'est plus long que le précédent le plus long) ALORS
      (enregistrez-le)
      (enregistrez sa longueur)
   FIN SI
FIN TANT QUE
Afficher la ligne la plus longue

Ce schéma montre clairement que le programme se divise naturellement en morceaux. Un morceau reçoit une nouvelle ligne, un autre l'enregistre et le reste contrôle le processus.

Comme les choses se divisent si bien, il serait bien de les écrire de cette façon aussi. En conséquence, écrivons d'abord une fonction distincte getline pour récupérer la ligne d'entrée suivante. Nous essaierons de rendre la fonction utile dans d'autres contextes. Au minimum, getline doit renvoyer un signal sur une éventuelle fin de fichier ; une conception plus utile serait de renvoyer la longueur de la ligne, ou zéro si la fin du fichier est rencontrée. Zéro est un retour de fin de fichier acceptable car ce n'est jamais une longueur de ligne valide. Chaque ligne de texte a au moins un caractère ; même une ligne contenant uniquement une nouvelle ligne a une longueur de 1.

Lorsque nous trouvons une ligne plus longue que la ligne précédente la plus longue, elle doit être enregistrée quelque part. Cela suggère une deuxième fonction, copy, pour copier la nouvelle ligne dans un endroit sûr.

Enfin, nous avons besoin d'un programme principal pour contrôler getline et copy. Voici le résultat :

  1. #include <stdio.h>
  2.  
  3. #define MAXLINE 1000 /* longueur maximale de la ligne d'entrée */
  4.  
  5. int getline(char line[], int maxline);
  6. void copy(char to[], char from[]);
  7.  
  8. /* afficher la ligne d'entrée la plus longue */
  9. main() {
  10.  int len; /* longueur de ligne actuelle */
  11.  int max; /* longueur maximale observée jusqu'à présent */
  12.  char line[MAXLINE]; /* ligne d'entrée actuelle */
  13.  char longest[MAXLINE]; /* la plus longue ligne enregistrée ici */
  14.  
  15.  max = 0;
  16.  while ((len = getline(line, MAXLINE)) > 0)
  17.   if (len > max) {
  18.    max = len;
  19.    copy(longest, line);
  20.   }
  21.  if (max > 0) /* il y avait une ligne */
  22.  printf("%s", longest);
  23.  return 0;
  24. }
  25.  
  26. /* getline : lit une ligne dans s, renvoie la longueur */
  27. int getline(char s[],int lim) {
  28.  int c, i;
  29.  for (i=0; i < lim-1 && (c=getchar())!=EOF && c!='\n'; ++i)
  30.   s[i] = c;
  31.  if (c == '\n') {
  32.   s[i] = c;
  33.   ++i;
  34.  }
  35.  s[i] = '\0';
  36.  return i;
  37. }
  38.  
  39. /* copy : copier «to» dans «from» ; supposer que «to» est suffisamment grand */
  40. void copy(char to[], char from[]) {
  41.  int i;
  42.  i = 0;
  43.  while ((to[i] = from[i]) != '\0')
  44.  ++i;
  45. }

Les fonctions getline et copy sont déclarées au début du programme, que nous supposons contenu dans un fichier.

main et getline communiquent via une paire de paramètres et une valeur renvoyée. Dans getline, les paramètres sont déclarés par la ligne :

  1. int getline(char s[], int lim);    

spécifiant que le premier paramètre, s, est un tableau et le second, lim, un entier. Le but de fournir la taille d'un tableau dans une déclaration est de réserver de l'entreposage. La longueur d'un tableau s n'est pas nécessaire dans getline puisque sa taille est définie dans main. getline utilise return pour renvoyer une valeur à l'appelant, tout comme la fonction power l'a fait. Cette ligne déclare également que getline renvoie un int ; puisque int est le type de retour par défaut, il peut être omis.

Certaines fonctions renvoient une valeur utile ; d'autres, comme copy, ne sont utilisées que pour leur effet et ne renvoient aucune valeur. Le type de retour de copy est void, ce qui indique explicitement qu'aucune valeur n'est renvoyée.

getline place le caractère '\0' (le caractère nul, dont la valeur est zéro) à la fin du tableau qu'il crée, pour marquer la fin de la chaîne de caractères. Cette conversion est également utilisée par le langage de programmation C : lorsqu'une constante de chaîne comme :

"Bonjour\n"

apparaît dans un programme C, il est stocké sous la forme d'un tableau de caractères contenant les caractères de la chaîne de caractères et terminé par un '\0' pour marquer la fin :

La spécification de format %s dans printf s'attend à ce que le paramètre correspondant soit une chaîne de caractères représentée sous cette forme. copy s'appuie également sur le fait que son argument d'entrée se termine par un '\0' et copie ce caractère dans la sortie.

Il convient de mentionner en passant que même un programme aussi petit que celui-ci présente des problèmes de conception délicats. Par exemple, que doit faire main s'il rencontre une ligne plus grande que sa limite ? getline fonctionne en toute sécurité, dans la mesure où il arrête la collecte lorsque le tableau est plein, même si aucune nouvelle ligne n'a été vue. En testant la longueur et le dernier caractère renvoyé, main peut déterminer si la ligne était trop longue, puis gérer comme il le souhaite. Dans un souci de concision, nous avons ignoré ce problème.

Il n'y a aucun moyen pour un utilisateur de getline de savoir à l'avance quelle peut être la longueur d'une ligne d'entrée, donc getline vérifie le dépassement de capacité. D'un autre côté, l'utilisateur de copy sait déjà (ou peut découvrir) quelle est la taille des chaînes, nous avons donc choisi de ne pas y ajouter de vérification d'erreur.

Variables externes et portée

Les variables de main, telles que line, longest,..., sont privées ou locales à main. Comme elles sont déclarées dans main, aucune autre fonction ne peut y avoir un accès direct. Il en va de même pour les variables d'autres fonctions ; par exemple, la variable i de getline n'est pas liée à la variable i de copy. Chaque variable locale d'une fonction n'apparaît que lorsque la fonction est appelée et disparaît lorsque la fonction est quittée. C'est pourquoi ces variables sont généralement appelées variables automatiques, conformément à la terminologie utilisée dans d'autres langages. On utilisera désormais le terme automatique pour désigner ces variables locales.

Étant donné que les variables automatiques apparaissent et disparaissent avec l'appel de fonction, elles ne conservent pas leurs valeurs d'un appel à l'autre et doivent être explicitement définies à chaque entrée. Si elles ne sont pas définies, elles contiendront des données inutiles.

En guise d'alternative aux variables automatiques, il est possible de définir des variables externes à toutes les fonctions, c'est-à-dire des variables auxquelles n'importe quelle fonction peut accéder par leur nom. (Ce mécanisme ressemble un peu aux variables COMMON de Fortran ou aux variables Pascal déclarées dans le bloc le plus externe.) Étant donné que les variables externes sont accessibles globalement, elles peuvent être utilisées à la place des listes d'arguments pour communiquer des données entre fonctions. De plus, étant donné que les variables externes restent en existence de manière permanente, plutôt que d'apparaître et de disparaître lorsque les fonctions sont appelées et quittées, elles conservent leurs valeurs même après le retour des fonctions qui les ont définies.

Une variable externe doit être définie, exactement une fois, en dehors de toute fonction ; cela lui réserve un espace d'entreposage. La variable doit également être déclarée dans chaque fonction qui souhaite y accéder ; cela indique le type de la variable. La déclaration peut être une instruction extern explicite ou peut être implicite dans le contexte. Pour rendre la discussion concrète, réécrivons le programme de la plus longue ligne avec line, longest et max comme variables externes. Cela nécessite de modifier les appels, les déclarations et les corps des trois fonctions :

  1. #include <stdio.h>
  2.  
  3. #define MAXLINE 1000 /* taille maximale de la ligne d'entrée */
  4.  
  5. int max;               /* longueur maximale observée jusqu'à présent */
  6. char line[MAXLINE];    /* ligne d'entrée actuelle */
  7. char longest[MAXLINE]; /* la plus longue ligne enregistrée ici */
  8.  
  9. int getline(void);
  10. void copy(void);
  11.  
  12. /* afficher la ligne d'entrée la plus longue ; version spécialisée */
  13. main() {
  14.  int len;
  15.  extern int max;
  16.  extern char longest[];
  17.  
  18.  max = 0;
  19.  while ((len = getline()) > 0)
  20.   if (len > max) {
  21.    max = len;
  22.    copy();
  23.   }
  24.  if (max > 0) /* il y avait une ligne */
  25.  printf("%s", longest);
  26.  return 0;
  27. }
  28.  
  29. /* getline : version spécialisée */
  30. int getline(void) {
  31.  int c, i;
  32.  extern char line[];
  33.  for (i = 0; i < MAXLINE - 1 && (c=getchar)) != EOF && c != '\n'; ++i) line[i] = c;
  34.  if (c == '\n') {
  35.   line[i] = c;
  36.   ++i;
  37.  }
  38.  line[i] = '\0';
  39.  return i;
  40. }
  41.  
  42. /* copie : version spécialisée */
  43. void copy(void) {
  44.  int i;
  45.  extern char line[], longest[];
  46.  i = 0;
  47.  while ((longest[i] = line[i]) != '\0') ++i;
  48. }

Les variables externes dans main, getline et copy sont définies par les premières lignes de l'exemple ci-dessus, indiquant leur type et provoquent l'allocation d'entreposage pour elles. Syntaxiquement, les définitions externes sont comme les définitions de variables locales, mais comme elles se produisent en dehors des fonctions, les variables sont externes. Avant qu'une fonction puisse utiliser une variable externe, le nom de la variable doit être communiqué à la fonction ; la déclaration est la même qu'avant, à l'exception du mot-clef extern ajouté.

Dans certaines circonstances, la déclaration extern peut être omise. Si la définition de la variable externe se produit dans le fichier source avant son utilisation dans une fonction particulière, il n'est pas nécessaire d'avoir une déclaration extern dans la fonction. Les déclarations extern dans main, getline et copy sont donc redondantes. En fait, la pratique courante consiste à placer les définitions de toutes les variables externes au début du fichier source, puis à omettre toutes les déclarations extern. Si le programme est dans plusieurs fichiers sources et qu'une variable est définie dans file1 et utilisée dans file2 et file3, alors des déclarations externes sont nécessaires dans file2 et file3 pour connecter les occurrences de la variable. La pratique habituelle consiste à rassembler les déclarations externes de variables et de fonctions dans un fichier séparé, historiquement appelé entête, étant inclus par #include au début de chaque fichier source. Le suffixe .h est conventionnel pour les noms d'entête. Les fonctions de la bibliothèque standard, par exemple, sont déclarées dans des entêtes comme <stdio.h>.

Étant donné que les versions spécialisées de getline et copy n'ont pas de paramètres, la logique suggérerait que leurs prototypes au début du fichier soient getline() et copy(). Mais pour la compatibilité avec les anciens programmes C, la norme prend une liste vide comme déclaration à l'ancienne et désactive toute vérification de liste d'arguments ; le mot void doit être utilisé pour une liste explicitement vide.

Vous devez noter que nous utilisons les mots définition et déclaration avec précaution lorsque nous faisons référence aux variables externes dans cette section. «Définition» fait référence à l'endroit où la variable est créée ou à laquelle un espace d'entreposage est attribué ; « déclaration » fait référence aux endroits où la nature de la variable est indiquée mais où aucun espace d'entreposage n'est alloué.

Au fait, il existe une tendance à faire de tout ce qui est visible une variable externe car cela semble simplifier les communications - les listes de paramètres sont courtes et les variables sont toujours là quand on en a besoin. Mais les variables externes sont toujours là même quand on n'en a pas besoin. S'appuyer trop sur des variables externes est dangereux car cela conduit à des programmes dont les connexions de données ne sont pas toutes évidentes - les variables peuvent être modifiées de manière inattendue et même par inadvertance, et le programme est difficile à modifier. La deuxième version du programme le plus long est inférieure à la première, en partie pour ces raisons, et en partie parce qu'elle détruit la généralité de deux fonctions utiles en y écrivant les noms des variables qu'elles manipulent.



Dernière mise à jour : Mardi, le 19 novembre 2024