Pointeurs et tableaux
Un pointeur est une variable contenant l'adresse d'une variable. Les pointeurs sont très utiles en C, en partie parce qu'ils sont parfois le seul moyen d'exprimer un calcul, et en partie parce qu'ils conduisent généralement à un code plus compact et plus efficace que ce qui peut être obtenu par d'autres moyens. Les pointeurs et les tableaux sont étroitement liés. Les pointeurs ont été regroupés avec l'instruction goto comme un moyen merveilleux de créer des programmes impossibles à comprendre. Il est certainement vrai lorsqu'ils sont utilisés avec négligence, et il est facile de créer des pointeurs pointant vers un endroit inattendu. Avec le déplacement, cependant, les pointeurs peuvent également être utilisés pour obtenir clarté et simplicité. Le principal changement dans ANSI C est de rendre explicites les règles sur la façon dont les pointeurs peuvent être manipulés, en imposant en fait ce que les bons programmeurs pratiquent déjà et les bons compilateurs appliquent déjà. De plus, le type void * (pointeur vers void) remplace char * comme type approprié pour un pointeur générique.
Pointeurs et adresses
Commençons par une image simplifiée de l'organisation de la mémoire. Une machine typique a un réseau de cellules de mémoire numérotées ou adressées consécutivement qui peuvent être manipulées individuellement ou en groupes contigus. Une situation courante est que n'importe quel octet peut être un char, une paire de cellules d'un octet peut être traitée comme un entier short et quatre octets adjacents forment un long. Un pointeur est un groupe de cellules (souvent deux ou quatre) pouvant contenir une adresse. Donc, si c est un char et ptr est un pointeur pointant vers lui, nous pourrions représenter la situation de cette façon :
L'opérateur binaire & donne l'adresse d'un objet, donc l'instruction :
- ptr = &c;
assigne l'adresse de c à la variable ptr, et ptr est dit pointeur vers c. L'opérateur & s'applique uniquement aux objets en mémoire : variables et éléments de tableau. Il ne peut pas être appliqué aux expressions, aux constantes ou aux variables register. L'opérateur binaire * est l'opérateur d'indirection ou de déréférencement; lorsqu'il est appliqué à un pointeur, il accède à l'objet sur lequel pointe le pointeur. Les premières choses à faire avec les pointeurs sont de déclarer une variable de pointeur, de la définir pour qu'elle pointe quelque part, et enfin de manipuler la valeur vers laquelle elle pointe. Une simple déclaration de pointeur ressemble à ceci :
- int *ip;
Supposons que x et y sont des entiers et que ip est un pointeur vers int. Cette séquence artificielle montre comment déclarer un pointeur et comment utiliser & et * :
Les déclarations de x, y et z sont ce que nous avons vu depuis le début. La déclaration de l'ip du pointeur :
- int *ip;
est conçu comme un mnémonique; il dit que l'expression *ip est un int. La syntaxe de la déclaration d'une variable imite la syntaxe des expressions dans lesquelles la variable peut apparaître. Ce raisonnement s'applique également aux déclarations de fonction. Par exemple :
indique que dans une expression *dp et atof(s) ont des valeurs de type double, et que le paramètre de atof est un pointeur vers char. Vous devez également noter l'implication qu'un pointeur est contraint de pointer vers un type particulier d'objet : chaque pointeur pointe vers un type de données spécifique. (Il y a une exception : un pointeur vers void est utilisé pour contenir tout type de pointeur mais ne peut pas être déréférencé lui-même. Si ip pointe vers l'entier x, alors *ip peut se produire dans n'importe quel contexte où x pourrait, donc :
- *ip = *ip + 10;
incrémente *ip de 10. Les opérateurs binaires * et & se lient plus étroitement que les opérateurs arithmétiques, donc l'affectation :
- y = *ip + 1;
prend tout ip pointe vers, ajoute 1 et attribue le résultat à y, tandis que :
- *ip += 1;
incrémente ce vers quoi pointe ip, comme le font :
- ++*ip;
et
- (*ip)++;
Les parenthèses sont nécessaires dans ce dernier exemple; sans eux, l'expression incrémenterait ip au lieu de ce qu'elle pointe, car les opérateurs binaires comme * et += s'associent de droite à gauche. Enfin, comme les pointeurs sont des variables, ils peuvent être utilisés sans déréférencement. Par exemple, si iq est un autre pointeur vers int :
- iq = ip;
copie le contenu de ip dans iq, faisant ainsi pointer iq vers tout ip pointé.
Pointeurs et paramètres de fonction
Puisque C passe des paramètres aux fonctions par valeur, il n'y a aucun moyen direct pour la fonction appelée de modifier une variable dans la fonction appelante. Par exemple, une routine de tri peut échanger deux éléments dans le désordre avec une fonction appelée swap. Il ne suffit pas d'écrire :
- swap(a,b);
où la fonction swap est définie comme
En raison de l'appel par valeur, swap ne peut pas affecter les paramètres a et b dans la routine l'ayant appelé. La fonction ci-dessus n'échange que les copies de a et b. La manière d'obtenir l'effet souhaité est que le programme appelant passe des pointeurs vers les valeurs à modifier :
- swap(&a,&b);
Puisque l'opérateur & produit l'adresse d'une variable, &a est un pointeur vers a. Dans la fonction swap lui-même, les paramètres sont déclarés comme des pointeurs et les opérandes sont accessibles indirectement via eux.
Pointeurs et tableaux
En C, il existe une relation forte entre les pointeurs et les tableaux, suffisamment forte pour que les pointeurs et les tableaux soient discutés simultanément. Toute opération pouvant être réalisée par l'indice de tableau peut également être effectuée avec des pointeurs. La version du pointeur sera en général plus rapide mais, au moins pour les non-initiés, un peu plus difficile à comprendre. La déclaration :
- int a[10];
définit un tableau a de taille 10, c'est-à-dire un bloc de 10 objets consécutifs nommés a[0], a[1], ..., a[9].
La notation a[i] fait référence au i-ème élément du tableau. Si pa est un pointeur vers un entier, déclaré comme :
- int *pa;
puis puis affectation :
- pa = &a[0];
définir pa pour qu'il pointe sur l'élément zéro de a; autrement dit, pa contient l'adresse d'un a[0].
Maintenant l'assignement :
- x = *pa;
copiera le contenu d'un [0] dans x.
Si pa pointe vers un élément particulier d'un tableau, alors par définition pa+1 pointe vers l'élément suivant, pa+i pointe vers les éléments i après pa, et pa-i pointe vers les éléments i avant. Ainsi, si pa pointe vers a[0] :
- *(pa+1)
fait référence au contenu de a[1], pa+i est l'adresse de a[i], et *(pa+i) est le contenu de a[i].
Ces remarques sont vraies quel que soit le type ou la taille des variables du tableau a. La signification de «ajouter 1 à un pointeur», et par extension, de toute arithmétique de pointeur, est que pa+1 pointe vers l'objet suivant, et pa+i pointe vers le i-ème objet au-delà de pa.
La correspondance entre l'indexation et l'arithmétique des pointeurs est très étroite. Par définition, la valeur d'une variable ou d'une expression de type tableau est l'adresse de l'élément zéro du tableau. Ainsi après l'affectation :
- pa = &a[0];
pa et a ont des valeurs identiques. Étant donné que le nom d'un tableau est synonyme de l'emplacement de l'élément initial, l'affectation pa=&a[0] peut également s'écrire comme suit :
- pa = a;
Ce qui est plus surprenant, à première vue, c'est qu'une référence à a[i] peut aussi s'écrire *(a+i). En évaluant a[i], C le convertit immédiatement en *(a+i) ; les deux formes sont équivalentes. En appliquant l'opérateur & aux deux parties de cette équivalence, il s'ensuit que &a[i] et a+i sont également identiques : a+i est l'adresse du i-ème élément au-delà de a. De l'autre côté de la médaille, si pa est un pointeur, les expressions peuvent l'utiliser avec un indice ; pa[i] est identique à *(pa+i). En bref, une expression de tableau et d'index est équivalente à une expression écrite sous la forme d'un pointeur et d'un déplacement.
Il y a une différence entre un nom de tableau et un pointeur qu'il faut garder à l'esprit. Un pointeur est une variable, donc pa=a et pa++ sont légaux. Mais un nom de tableau n'est pas une variable ; des constructions comme a=pa et a++ sont illégales.
Lorsqu'un nom de tableau est passé à une fonction, ce qui est passé est l'emplacement de l'élément initial. Dans la fonction appelée, ce paramètre est une variable locale, et donc un paramètre de nom de tableau est un pointeur, c'est-à-dire une variable contenant une adresse. Nous pouvons utiliser ce fait pour écrire une autre version de strlen, calculant la longueur d'une chaîne de caractères :
Puisque s est un pointeur, son incrémentation est parfaitement légale ; s++ n'a aucun effet sur la chaîne de caractères de la fonction ayant appelé strlen, mais incrémente simplement la copie privée du pointeur de strlen. Cela signifie que des appels comme :
tout fonctionne.
En tant que paramètres formels dans une définition de fonction :
- char s[];
et :
- char *s;
sont équivalentes ; on préférera la seconde car elle indique plus explicitement que la variable est un pointeur. Lorsqu'un nom de tableau est passé à une fonction, la fonction peut, à sa convenance, croire qu'on lui a transmis soit un tableau, soit un pointeur, et le manipuler en conséquence. Elle peut même utiliser les deux notations si cela semble approprié et clair.
Il est possible de passer une partie d'un tableau à une fonction, en passant un pointeur vers le début du sous-tableau. Par exemple, si a est un tableau :
- f(&a[2])
et :
- f(a+2)
les deux transmettent à la fonction f l'adresse du sous-tableau commençant à a[2]. Dans f, la déclaration de paramètre peut se lire :
- f(int arr[]) { ... }
ou :
- f(int *arr) { ... }
Ainsi, en ce qui concerne f, le fait que le paramètre fasse référence à une partie d'un tableau plus grand n'a aucune conséquence.
Si l'on est sûr que les éléments existent, il est également possible d'indexer vers l'arrière dans un tableau ; p[-1], p[-2],... sont syntaxiquement légaux et font référence aux éléments qui précèdent immédiatement p[0]. Bien entendu, il est illégal de faire référence à des objets n'étant pas dans les limites du tableau.
Arithmétique d'adresse
Si p est un pointeur vers un élément d'un tableau, alors p++ incrémente p pour pointer vers l'élément suivant, et p+=i l'incrémente pour pointer i éléments au-delà de l'endroit où il le fait actuellement. Ces constructions et d'autres similaires sont les formes simples de l'arithmétique de pointeur ou d'adresse. C est cohérent et régulier dans son approche de l'arithmétique d'adresse ; son intégration des pointeurs, des tableaux et de l'arithmétique d'adresse est l'une des forces du langage. Illustrons cela en écrivant un allocateur d'entreposage rudimentaire. Il existe deux routines. La première, alloc(n), renvoie un pointeur vers n positions de caractères consécutives, pouvant être utilisé par l'appelant d'alloc pour entreposer des caractères. La seconde, afree(p), libère l'entreposage ainsi acquis afin qu'il puisse être réutilisé ultérieurement. Les routines sont «rudimentaires» car les appels à afree doivent être effectués dans l'ordre inverse des appels effectués sur alloc. Autrement dit, l'entreposage géré par alloc et afree est une pile, ou le dernier entré, premier sorti. La bibliothèque standard fournit des fonctions analogues appelées malloc et free n'ayant pas de telles restrictions.
L'implémentation la plus simple consiste à demander à alloc de distribuer des morceaux d'un grand tableau de caractères que nous appellerons allocbuf. Ce tableau est privé pour alloc et afree. Comme ils traitent des pointeurs, pas des indices de tableau, aucune autre routine n'a besoin de connaître le nom du tableau, qui peut être déclaré statique dans le fichier source contenant alloc et afree, et donc être invisible en dehors de celui-ci. Dans les implémentations pratiques, le tableau peut très bien ne pas avoir de nom ; il peut plutôt être obtenu en appelant malloc ou en demandant au système d'exploitation un pointeur vers un bloc d'entreposage sans nom.
L'autre information nécessaire est la quantité d'allocbuf ayant été utilisée. Nous utilisons un pointeur, appelé allocp, pointant vers le prochain élément libre. Lorsque l'on demande à alloc n caractères, il vérifie s'il reste suffisamment de place dans allocbuf. Si c'est le cas, alloc renvoie la valeur actuelle d'allocp (c'est-à-dire le début du bloc libre), puis l'incrémente de n pour pointer vers la prochaine zone libre. S'il n'y a pas de place, alloc renvoie zéro. afree(p) définit simplement allocp sur p si p est à l'intérieur d'allocbuf.
- #define ALLOCSIZE 10000 /* Taille de l'espace disponible */
-
- static char allocbuf[ALLOCSIZE]; /* Entreposage pour alloc */
- static char *allocp = allocbuf; /* Prochaine position libre */
-
- char *alloc(int n) { /* renvoie le pointeur sur n caractères */
- if (allocbuf + ALLOCSIZE - allocp >= n) { /* il rentre */
- allocp += n;
- return allocp - n; /* vieux p */
- } else /* pas assez de place */
- return 0;
- }
-
- void afree(char *p) { /* Entreposage libre pointé par p*/
- if (p >= allocbuf && p < allocbuf + ALLOCSIZE) allocp = p;
- }
En général, un pointeur peut être initialisé comme n'importe quelle autre variable, bien que normalement les seules valeurs significatives soient zéro ou une expression impliquant l'adresse de données précédemment définies de type approprié. La déclaration :
- static char *allocp = allocbuf;
définit allocp comme étant un pointeur de caractère et l'initialise pour pointer vers le début de allocbuf, étant la prochaine position libre au démarrage du programme. Cela aurait aussi pu être écrit :
- static char *allocp = &allocbuf[0];
puisque le nom du tableau est l'adresse du zéroième élément.
Le test :
- if (allocbuf + ALLOCSIZE - allocp >= n) { /* il rentre */
vérifie s'il y a suffisamment de place pour satisfaire une requête de n caractères. Si c'est le cas, la nouvelle valeur de allocp sera au plus un au-delà de la fin de allocbuf. Si la requête peut être satisfaite, alloc renvoie un pointeur vers le début d'un bloc de caractères (remarquez la déclaration de la fonction elle-même). Sinon, alloc doit renvoyer un signal indiquant qu'il n'y a plus d'espace. C garantit que zéro n'est jamais une adresse valide pour les données, donc une valeur de retour de zéro peut être utilisée pour signaler un événement anormal, dans ce cas, pas d'espace.
Les pointeurs et les entiers ne sont pas interchangeables. Zéro est la seule exception : la constante zéro peut être assignée à un pointeur, et un pointeur peut être comparé à la constante zéro. La constante symbolique NULL est souvent utilisée à la place de zéro, comme mnémonique pour indiquer plus clairement qu'il s'agit d'une valeur spéciale pour un pointeur. NULL est défini dans <stdio.h>. Nous utiliserons NULL à partir de maintenant.
Des tests comme :
- if (allocbuf + ALLOCSIZE - allocp >= n) { /* il rentre */
et :
- if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
montre plusieurs facettes importantes de l'arithmétique des pointeurs. Tout d'abord, les pointeurs peuvent être comparés dans certaines circonstances. Si p et q pointent vers des membres du même tableau, alors les relations telles que ==, !=, <, >=,..., fonctionnent correctement. Par exemple :
- p < q
est vrai si p pointe vers un élément antérieur du tableau par rapport à q. Tout pointeur peut être comparé de manière significative pour l'égalité ou l'inégalité avec zéro. Mais le comportement est indéfini pour l'arithmétique ou les comparaisons avec des pointeurs ne pointant pas vers des membres du même tableau. (Il existe une exception : l'adresse du premier élément après la fin d'un tableau peut être utilisée dans l'arithmétique des pointeurs.)
Deuxièmement, nous avons déjà observé qu'un pointeur et un entier peuvent être additionnés ou soustraits. La construction :
- p + n
signifie l'adresse du n-ième objet au-delà de celui vers lequel p pointe actuellement. Ceci est vrai quel que soit le type d'objet vers lequel p pointe ; n est mis à l'échelle en fonction de la taille des objets vers lesquels p pointe, étant déterminée par la déclaration de p. Si un int est de quatre octets, par exemple, l'int sera mis à l'échelle de quatre.
La soustraction de pointeur est également valide : si p et q pointent vers des éléments du même tableau, et p<q, alors q-p+1 est le nombre d'éléments de p à q inclus. Ce fait peut être utilisé pour écrire une autre version de strlen :
Dans sa déclaration, p est initialisé à s, c'est-à-dire qu'il pointe vers le premier caractère de la chaîne de caractères. Dans la boucle while, chaque caractère est examiné à son tour jusqu'à ce que le '\0' à la fin soit visible. Comme p pointe vers des caractères, p++ avance p jusqu'au caractère suivant à chaque fois, et p-s donne le nombre de caractères avancés, c'est-à-dire la longueur de la chaîne de caractères. (Le nombre de caractères de la chaîne de caractères pourrait être trop grand pour être entreposé dans un int. L'entête <stddef.h> définit un type ptrdiff_t suffisamment grand pour contenir la différence signée de deux valeurs de pointeur. Si nous étions prudents, cependant, nous utiliserions size_t pour la valeur de retour de strlen, pour correspondre à la version de la bibliothèque standard. size_t est le type entier non signé renvoyé par l'opérateur sizeof.
L'arithmétique des pointeurs est cohérente : si nous avions eu affaire à des float, occupant plus d'entreposage que les char, et si p était un pointeur vers float, p++ avancerait jusqu'au float suivant. Ainsi, nous pourrions écrire une autre version d'alloc conservant les float au lieu des chars, simplement en changeant char en float dans alloc et afree. Toutes les manipulations de pointeurs prennent automatiquement en compte la taille des objets pointés.
Les opérations de pointeurs valides sont l'affectation de pointeurs du même type, l'ajout ou la soustraction d'un pointeur et d'un entier, la soustraction ou la comparaison de deux pointeurs vers des membres du même tableau et l'affectation ou en les comparant à zéro. Toute autre opération arithmétique de pointeur est illégale. Il n'est pas légal d'ajouter deux pointeurs, ou de les multiplier, de les diviser, de les décaler ou de les masquer, ou de leur ajouter float ou double, ou même, à l'exception de void *, d'assigner un pointeur d'un type à un pointeur d'un autre type sans transtypage.
Pointeurs et fonctions de caractères
Une constante de chaîne de caractères, écrite comme suit :
- "Je suis une chaîne de caractères"
est un tableau de caractères. Dans la représentation interne, le tableau se termine par le caractère nul '\0' afin que les programmes puissent trouver la fin. La longueur en mémoire est donc supérieure d'un au nombre de caractères entre les guillemets.
L'occurrence la plus courante des constantes de chaîne de caractères est peut-être celle des paramètres de fonctions, comme dans :
- printf("Bonjour le monde !\n");
Lorsqu'une chaîne de caractères comme celle-ci apparaît dans un programme, l'accès à celle-ci se fait via un pointeur de caractère ; printf reçoit un pointeur vers le début du tableau de caractères. Autrement dit, une constante de chaîne de caractères est accessible via un pointeur vers son premier élément.
Les constantes de chaîne de caractères ne doivent pas nécessairement être des paramètres de fonction. Si pmessage est déclaré comme :
- char *pmessage;
alors la déclaration :
- pmessage = "C'est le moment";
assigne à pmessage un pointeur vers le tableau de caractères. Il ne s'agit pas d'une copie de chaîne de caractères ; seuls les pointeurs sont impliqués. C ne fournit aucun opérateur pour traiter une chaîne entière de caractères comme une unité.
Il existe une différence importante entre ces définitions :
amessage est un tableau, juste assez grand pour contenir la séquence de caractères et '\0' l'initialisant. Les caractères individuels du tableau peuvent être modifiés, mais amessage fera toujours référence au même entreposage. D'autre part, pmessage est un pointeur, initialisé pour pointer vers une constante de chaîne de caractères ; le pointeur peut ensuite être modifié pour pointer ailleurs, mais le résultat est indéfini si vous essayez de modifier le contenu de la chaîne de caractères.
Nous allons illustrer d'autres aspects des pointeurs et des tableaux en étudiant les versions de deux fonctions utiles adaptées de la bibliothèque standard. La première fonction est strcpy(s,t), copiant la chaîne de caractères t dans la chaîne de caractères s. Il serait bien de dire simplement s=t mais cela copie le pointeur, pas les caractères. Pour copier les caractères, nous avons besoin d'une boucle. La version tableau en premier :
En guise de contraste, voici une version de strcpy avec des pointeurs :
Comme les paramètres sont passés par valeur, strcpy peut utiliser les paramètres s et t comme bon lui semble. Ici, ce sont des pointeurs initialisés de manière pratique, étant déplacés le long des tableaux un caractère à la fois, jusqu'à ce que le '\0' qui termine t ait été copié dans s.
En pratique, strcpy ne s'écrirait pas comme nous l'avons montré ci-dessus. Les programmeurs C expérimentés préféreraient :
Cela déplace l'incrément de s et t dans la partie test de la boucle. La valeur de *t++ est le caractère vers lequel t pointait avant que t ne soit incrémenté ; le suffixe ++ ne change pas t jusqu'à ce que ce caractère ait été récupéré. De la même manière, le caractère est entreposé dans l'ancienne position s avant que s ne soit incrémenté. Ce caractère est également la valeur étant comparée à '\0' pour contrôler la boucle. L'effet net est que les caractères sont copiés de t vers s, en remontant et en incluant le '\0' de fin.
Comme abréviation finale, notez qu'une comparaison avec '\0' est redondante, puisque la question est simplement de savoir si l'expression est nulle. La fonction s'écrirait donc probablement comme suit :
Bien que cela puisse paraître cryptique à première vue, la commodité de notation est considérable et l'idiome doit être maîtrisé, car vous le verrez fréquemment dans les programmes C. La fonction strcpy de la bibliothèque standard (<string.h>) renvoie la chaîne de caractères cible comme valeur de sa fonction.
La deuxième routine que nous examinerons est strcmp(s,t), comparant les chaînes de caractères s et t et renvoie une valeur négative, nulle ou positive si s est lexicographiquement inférieur, égal ou supérieur à t. La valeur est obtenue en soustrayant les caractères de la première position où s et t ne sont pas correspondant.
La version pointeur de strcmp :
Étant donné que ++ et -- sont des opérateurs préfixes ou postfixes, d'autres combinaisons de * et ++ et -- se produisent, bien que moins fréquemment. Par exemple :
- *--p
décrémente p avant de récupérer le caractère vers lequel p pointe. En fait, la paire d'expressions :
- *p++ = val; /* pousser val sur la pile */
- val = *--p; /* faire apparaître le haut de la pile dans la valeur */
sont l'idiome standard pour pousser et faire sortir une pile.
L'entête <string.h> contient des déclarations pour les fonctions mentionnées dans cette section, ainsi qu'une variété d'autres fonctions de gestion de chaînes de la bibliothèque standard.
Tableaux de pointeurs ; pointeurs vers pointeurs
Étant donné que les pointeurs sont eux-mêmes des variables, ils peuvent être entreposés dans des tableaux comme d'autres variables. Illustrons cela en écrivant un programme triant un ensemble de lignes de texte par ordre alphabétique, une version simplifiée du programme sort de UNIX.
Dans la page Contrôle du flux, nous avons présenté une fonction Shell sort triant un tableau d'entiers, et dans la page Fonctions et structure du programme nous l'avons améliorée avec un tri rapide. Les mêmes algorithmes fonctionneront, sauf que nous devons maintenant gérer des lignes de texte, étant de longueurs différentes, et qui, contrairement aux entiers, ne peuvent pas être comparées ou déplacées en une seule opération. Nous avons besoin d'une représentation des données gérant efficacement et facilement les lignes de texte de longueur variable.
C'est là qu'intervient le tableau de pointeurs. Si les lignes à trier sont stockées de bout en bout dans un long tableau de caractères, alors chaque ligne est accessible par un pointeur vers son premier caractère. Les pointeurs eux-mêmes peuvent être stockés dans un tableau. Deux lignes peuvent être comparées en passant leurs pointeurs à strcmp. Lorsque deux lignes désordonnées doivent être échangées, les pointeurs du tableau de pointeurs sont échangés, et non les lignes de texte elles-mêmes.
Cela élimine le double problème de gestion complexe d'entreposage et de frais généraux élevés accompagnant le déplacement des lignes elles-mêmes.
Le processus de tri comporte trois étapes :
lire toutes les lignes d'entrée les trier les afficher dans l'ordre |
Comme d'habitude, il est préférable de diviser le programme en fonctions correspondant à cette division naturelle, la routine principale contrôlant les autres fonctions. Laissons de côté l'étape de tri pour un moment et concentrons-nous sur la structure des données et sur les entrées et sorties.
La routine d'entrée doit collecter et sauvegarder les caractères de chaque ligne, et construire un tableau de pointeurs vers les lignes. Elle devra également compter le nombre de lignes d'entrée, puisque cette information est nécessaire pour le tri et l'impression. Comme la fonction d'entrée ne peut gérer qu'un nombre fini de lignes d'entrée, elle peut renvoyer un nombre illégal comme -1 si trop d'entrées sont présentées.
La routine de sortie n'a qu'à afficher les lignes dans l'ordre dans lequel elles apparaissent dans le tableau de pointeurs :
- #include <stdio.h>
- #include <string.h>
-
- #define MAXLINES 5000 /* max #lignes à trier */
-
- char *lineptr[MAXLINES]; /* pointeurs vers des lignes de texte */
-
- int readlines(char *lineptr[], int nlines);
- void writelines(char *lineptr[], int nlines);
- void qsort(char *lineptr[], int left, int right);
-
- /* trier les lignes d'entrée */
- main() {
- int nlines; /* nombre de lignes d'entrée lues */
- if ((nlines = readlines(lineptr, MAXLINES)) >= 0) {
- qsort(lineptr, 0, nlines-1);
- writelines(lineptr, nlines);
- return 0;
- } else {
- printf("erreur : entrée trop grande pour être triée\n");
- return 1;
- }
- }
-
- #define MAXLEN 1000 /* longueur maximale de toute ligne d'entrée */
-
- int getline(char *, int);
-
- char *alloc(int);
-
- /* readlines : lire les lignes d'entrée */
- int readlines(char *lineptr[], int maxlines) {
- int len, nlines;
- char *p, line[MAXLEN];
- nlines = 0;
- while ((len = getline(line, MAXLEN)) > 0)
- if (nlines >= maxlines || p = alloc(len) == NULL) return -1;
- else {
- line[len-1] = '\0'; /* supprimer la nouvelle ligne */
- strcpy(p, line);
- lineptr[nlines++] = p;
- }
- return nlines;
- }
-
- /* writelines: écrire des lignes de sortie */
- void writelines(char *lineptr[], int nlines) {
- int i;
- for (i = 0; i < nlines; i++) printf("%s\n", lineptr[i]);
- }
La principale nouveauté est la déclaration pour lineptr :
- char *lineptr[MAXLINES]
dit que lineptr est un tableau de MAXLINES éléments, dont chaque élément est un pointeur vers un caractère. Autrement dit, lineptr[i] est un pointeur de caractère et *lineptr[i] est le caractère vers lequel il pointe, le premier caractère de la i-ème ligne de texte enregistrée.
Étant donné que lineptr est lui-même le nom d'un tableau, il peut être traité comme un pointeur de la même manière que dans nos exemples précédents, et writelines peut être écrit à la place comme :
Au départ, *lineptr pointe vers la première ligne; chaque élément avance jusqu'au pointeur de ligne suivant tandis que nlines est décompté.
Avec l'entrée et la sortie sous contrôle, nous pouvons procéder au tri. Le tri rapide du chapitre 4 nécessite des modifications mineures : les déclarations doivent être modifiées et l'opération de comparaison doit être effectuée en appelant strcmp. L'algorithme reste le même, ce qui nous donne une certaine confiance dans son bon fonctionnement.
- /* qsort: trier v[left]...v[right] dans l'ordre croissant */
- void qsort(char *v[], int left, int right) {
- int i, last;
- void swap(char *v[], int i, int j);
- if (left >= right) return; /* ne rien faire si le tableau contient moins de deux éléments */
- swap(v, left, (left + right)/2);
- last = left;
- for (i = left+1; i <= right; i++) if (strcmp(v[i], v[left]) < 0) swap(v, ++last, i);
- swap(v, left, last);
- qsort(v, left, last-1);
- qsort(v, last+1, right);
- }
De même, la routine d'échange ne nécessite que des modifications triviales :
Étant donné que tout élément individuel de v (alias lineptr) est un pointeur de caractère, temp doit l'être également, de sorte que l'un puisse être copié vers l'autre.
Tableaux multidimensionnels
Le C fournit des tableaux multidimensionnels rectangulaires, bien qu'en pratique ils soient beaucoup moins utilisés que les tableaux de pointeurs. Dans cette section, nous allons montrer certaines de leurs propriétés.
Considérons le problème de la conversion de date, du jour du mois au jour de l'année et vice versa. Par exemple, le 1er mars est le 60e jour d'une année non bissextile et le 61e jour d'une année bissextile. Définissons deux fonctions pour effectuer les conversions : day_of_year convertit le mois et le jour en jour de l'année, et month_day convertit le jour de l'année en mois et jour. Comme cette dernière fonction calcule deux valeurs, les paramètres month et day seront des pointeurs :
- month_day(1988, 60, &m, &d)
définit m sur 2 et d sur 29 (29 février).
Ces deux fonctions ont besoin des mêmes informations, un tableau du nombre de jours de chaque mois («trente jours ont septembre...»). Étant donné que le nombre de jours par mois diffère pour les années bissextiles et non bissextiles, il est plus facile de les séparer en deux lignes d'un tableau bidimensionnel que de suivre ce qui se passe en février pendant le calcul. Le tableau et les fonctions permettant d'effectuer les transformations sont les suivants :
- static char daytab[2][13] = {
- {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
- {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
- };
-
- /* day_of_year: définir le jour de l'année à partir du mois et du jour */
- int day_of_year(int year, int month, int day) {
- int i, leap;
- leap = year%4 == 0 && year%100 != 0 || year%400 == 0;
- for (i = 1; i < month; i++) day += daytab[leap][i];
- return day;
- }
-
- /* month_day: définir le mois et le jour à partir du jour de l'année */
- void month_day(int year, int yearday, int *pmonth, int *pday) {
- int i, leap;
- leap = year%4 == 0 && year%100 != 0 || year%400 == 0;
- for (i = 1; yearday > daytab[leap][i]; i++) yearday -= daytab[leap][i];
- *pmonth = i;
- *pday = yearday;
- }
Rappelons que la valeur arithmétique d'une expression logique, telle que celle de leap, est soit zéro (false) soit un (true), elle peut donc être utilisée comme indice du tableau daytab.
Le tableau daytab doit être externe à day_of_year et month_day, afin qu'ils puissent tous les deux l'utiliser. On la fait en utilisant char pour illustrer une utilisation légitime de char pour entreposer de petits entiers non-caractères.
daytab est le premier tableau bidimensionnel que nous avons traité. En C, un tableau bidimensionnel est en réalité un tableau unidimensionnel, dont chacun des éléments est un tableau. Par conséquent, les indices sont écrits comme suit :
- daytab[i][j] /* [ligne][colonne] */
plutôt que :
- daytab[i,j] /* MAUVAIS */
Hormis cette distinction de notation, un tableau bidimensionnel peut être traité de la même manière que dans d'autres langages de programmation. Les éléments sont entreposés par lignes, de sorte que l'indice le plus à droite, ou la colonne, varie plus rapidement lorsque les éléments sont accédés dans l'ordre d'entreposage.
Un tableau est initialisé par une liste d'initialiseurs entre accolades ; chaque ligne d'un tableau bidimensionnel est initialisée par une sous-liste correspondante. Nous avons démarré le tableau daytab avec une colonne de zéro afin que les numéros de mois puissent aller du 1 naturel au 12 au lieu de 0 à 11. Comme l'espace n'est pas un facteur important ici, cela est plus clair que l'ajustement des indices.
Si un tableau bidimensionnel doit être passé à une fonction, la déclaration de paramètre dans la fonction doit inclure le nombre de colonnes ; le nombre de lignes n'est pas pertinent, car ce qui est passé est, comme précédemment, un pointeur vers un tableau de lignes, où chaque ligne est un tableau de 13 int. Dans ce cas particulier, il s'agit d'un pointeur vers des objets qui sont des tableaux de 13 int. Ainsi, si le tableau daytab doit être passé à une fonction f, la déclaration de f serait :
- f(int daytab[2][13]) { ... }
Cela pourrait aussi être :
- f(int daytab[][13]) { ... }
puisque le nombre de lignes n'a pas d'importance, ou cela pourrait être :
- f(int (*daytab)[13]) { ... }
disant que le paramètre est un pointeur vers un tableau de 13 entiers. Les parenthèses sont nécessaires car les crochets [] ont une priorité plus élevée que *. Sans parenthèses, la déclaration :
- int *daytab[13]
est un tableau de 13 pointeurs vers des entiers. Plus généralement, seule la première dimension (indice) d'un tableau est libre ; toutes les autres doivent être précisées.
Initialisation des tableaux de pointeurs
Considérons le problème de l'écriture d'une fonction month_name(n), renvoyant un pointeur vers une chaîne de caractères contenant le nom du n-ième mois. Il s'agit d'une application idéale pour un tableau statique interne. month_name contient un tableau privé de chaînes de caractères et renvoie un pointeur vers la chaîne appropriée lorsqu'elle est appelée. Cette section montre comment ce tableau de noms est initialisé.
La syntaxe est similaire aux initialisations précédentes :
La déclaration de name, étant un tableau de pointeurs de caractères, est la même que celle de lineptr dans l'exemple de tri. L'initialiseur est une liste de chaînes de caractères ; chacune est affectée à la position correspondante dans le tableau. Les caractères de la i-ème chaîne de caractères sont placés quelque part, et un pointeur vers eux est entreposé dans name[i]. Comme la taille du tableau name n'est pas spécifiée, le compilateur compte les initialiseurs et renseigne le nombre correct.
Pointeurs et tableaux multidimensionnels
Les débutants en C sont parfois confus quant à la différence entre un tableau bidimensionnel et un tableau de pointeurs, comme name dans l'exemple ci-dessus. Étant donné les définitions :
alors a[3][4] et b[3][4] sont tous deux des références syntaxiquement légales à un seul int. Mais a est un véritable tableau bidimensionnel : 200 emplacements de la taille d'un int ont été mis de côté, et le calcul d'indice rectangulaire conventionnel 20 * row + col est utilisé pour trouver l'élément a[row, col]. Pour b, cependant, la définition n'alloue que 10 pointeurs et ne les initialise pas ; l'initialisation doit être effectuée explicitement, soit de manière statique, soit avec du code. En supposant que chaque élément de b pointe vers un tableau de vingt éléments, alors il y aura 200 int mis de côté, plus dix cellules pour les pointeurs. L'avantage important du tableau de pointeurs est que les lignes du tableau peuvent être de longueurs différentes. C'est-à-dire que chaque élément de b n'a pas besoin de pointer vers un vecteur de vingt éléments ; certains peuvent pointer vers deux éléments, certains vers cinquante et certains vers aucun du tout.
Bien que nous ayons formulé cette discussion en termes d'entiers, l'utilisation la plus fréquente des tableaux de pointeurs est de loin l'entreposage de chaînes de caractères de différentes longueurs, comme dans la fonction month_name. Comparez la déclaration et l'image pour un tableau de pointeurs :
- char *name[] = { "Mois illégal", "Jan", "Fév", "Mar" };
avec ceux pour un tableau à deux dimensions :
- char aname[][15] = { "Mois illégal", "Jan", "Fév", "Mar" };
Paramètres de ligne de commande
Dans les environnements prenant en charge C, il existe un moyen de transmettre des paramètres ou des paramètres de ligne de commande à un programme lorsqu'il commence à s'exécuter. Lorsque main est appelé, il l'est avec deux paramètres. Le premier (appelé traditionnellement argc, pour paramètre count) est le nombre de paramètres de ligne de commande avec lesquels le programme a été appelé; le second (argv, pour paramètre vector) est un pointeur vers un tableau de chaînes de caractères contenant les paramètres, un par chaîne de caractères. Nous utilisons généralement plusieurs niveaux de pointeurs pour manipuler ces chaînes de caractères.
L'illustration la plus simple est le programme echo, affichant ses paramètres de ligne de commande sur une seule ligne, séparés par des espaces. C'est-à-dire la commande :
echo Bonjour, monde |
affichera la sortie :
Bonjour, mondePar convention, argv[0] est le nom par lequel le programme a été appelé, donc argc vaut au moins 1. Si argc vaut 1, il n'y a pas de paramètre de ligne de commande après le nom du programme. Dans l'exemple ci-dessus, argc vaut 3, et argv[0], argv[1] et argv[2] sont respectivement «echo», «Bonjour» et «monde». Le premier paramètre optionnel est argv[1] et le dernier est argv[argc-1]; de plus, la norme exige que argv[argc] soit un pointeur nul.
La première version d'echo traite argv comme un tableau de pointeurs de caractères :
Étant donné que argv est un pointeur vers un tableau de pointeurs, nous pouvons manipuler le pointeur plutôt que d'indexer le tableau. Cette variante suivante est basée sur l'incrémentation d'argv, étant un pointeur vers un pointeur vers char, tandis que argc est décompté :
Étant donné que argv est un pointeur vers le début du tableau de chaînes de caractères de paramètres, l'incrémenter de 1 (++argv) le fait pointer vers l'argv[1] d'origine au lieu de argv[0]. Chaque incrément successif le déplace vers le paramètre suivant ; *argv est alors le pointeur vers ce paramètre. En même temps, argc est décrémenté ; lorsqu'il devient zéro, il n'y a plus de paramètres à afficher.
Alternativement, nous pourrions écrire l'instruction printf comme suit :
- printf((argc > 1) ? "%s " : "%s", *++argv);
Cela montre que le paramètre format de printf peut aussi être une expression.
Comme deuxième exemple, apportons quelques améliorations au programme de recherche de motifs de la section précédente. Si vous vous souvenez, nous avons intégré le motif de recherche au coeur du programme, une disposition manifestement insatisfaisante. En suivant l'exemple du programme grep de UNIX, améliorons le programme de sorte que le motif à rechercher soit spécifié par le premier paramètre de la ligne de commande.
- #include <stdio.h>
- #include <string.h>
-
- #define MAXLINE 1000
-
- int getline(char *line, int max);
- /* find: afficher les lignes correspondant au motif du premier paramètre */
- main(int argc, char *argv[]) {
- char line[MAXLINE];
- int found = 0;
- if (argc != 2)
- printf("Syntaxe : find motif\n");
- else
- while (getline(line, MAXLINE) > 0)
- if (strstr(line, argv[1]) != NULL) {
- printf("%s", line);
- found++;
- }
- return found;
- }
La fonction de bibliothèque standard strstr(s,t) renvoie un pointeur vers la première occurrence de la chaîne de caractères t dans la chaîne de caractères s, ou NULL s'il n'y en a pas. Elle est déclarée dans <string.h>. Le modèle peut maintenant être élaboré pour illustrer d'autres constructions de pointeurs. Supposons que nous voulions autoriser deux paramètres optionnels. L'un dit «afficher toutes les lignes sauf celles correspondant au motif» ; le second dit «précéder chaque ligne affichée de son numéro de ligne». Une convention courante pour les programmes C sur les systèmes UNIX est qu'un paramètre commençant par un signe moins introduit un indicateur ou un paramètre optionnel. Si nous choisissons -x (pour «sauf») pour signaler l'inversion, et -n ("numéro") pour demander la numérotation des lignes, alors la commande :
find -x -npattern |
affiche chaque ligne ne correspondant pas au modèle, précédée de son numéro de ligne. Les paramètres optionnels doivent être autorisés dans n'importe quel ordre, et le reste du programme doit être indépendant du nombre d'arguments que nous présentons. De plus, il est pratique pour les utilisateurs que les paramètres optionnels puissent être combinés, comme dans :
find -nx pattern |
Voici le programme :
- #include <stdio.h>
- #include <string.h>
-
- #define MAXLINE 1000
-
- int getline(char *line, int max);
- /* find: afficher les lignes correspondant au motif du 1er paramètre */
- main(int argc, char *argv[]) {
- char line[MAXLINE];
- long lineno = 0;
- int c, except = 0, number = 0, found = 0;
- while (--argc > 0 && (*++argv)[0] == '-')
- while (c = *++argv[0])
- switch (c) {
- case 'x':
- except = 1;
- break;
- case 'n':
- number = 1;
- break;
- default:
- printf("find: Option illégal %c\n", c);
- argc = 0;
- found = -1;
- break;
- }
- if (argc != 1) printf("Syntaxe: find -x -n motif\n");
- else
- while (getline(line, MAXLINE) > 0) {
- lineno++;
- if ((strstr(line, *argv) != NULL) != except) {
- if (number) printf("%ld:", lineno);
- printf("%s", line);
- found++;
- }
- }
- return found;
- }
argc est décrémenté et argv est incrémenté avant chaque paramètre optionnel. A la fin de la boucle, s'il n'y a pas d'erreur, argc indique combien de paramètres restent non traités et argv pointe vers le premier d'entre eux. Ainsi, argc doit être 1 et *argv doit pointer vers le motif. Notez que *++argv est un pointeur vers une chaîne de caractères de paramètre, donc (*++argv)[0] est son premier caractère. (Une autre forme valide serait **++argv.) Comme [] lie plus étroitement que * et ++, les parenthèses sont nécessaires ; sans elles, l'expression serait considérée comme *++(argv[0]). En fait, c'est ce que nous avons utilisé dans la boucle interne, où la tâche consiste à parcourir une chaîne de caractères de paramètres spécifique. Dans la boucle interne, l'expression *++argv[0] incrémente le pointeur argv[0] !
Il est rare que l'on utilise des expressions de pointeur plus compliquées que celles-ci ; dans de tels cas, les diviser en deux ou trois étapes sera plus intuitif.
Pointeurs vers des fonctions
En C, une fonction elle-même n'est pas une variable, mais il est possible de définir des pointeurs vers des fonctions, pouvant être assignées, placées dans des tableaux, passées à des fonctions, renvoyées par des fonctions,... Nous illustrerons cela en modifiant la procédure de tri écrite plus tôt dans cette page de sorte que si le paramètre optionnel -n est donné, il triera les lignes d'entrée numériquement au lieu de lexicographiquement.
Un tri se compose souvent de trois parties : une comparaison déterminant l'ordre de n'importe quelle paire d'objets, un échange inversant leur ordre et un algorithme de tri qui effectue des comparaisons et des échanges jusqu'à ce que les objets soient dans l'ordre. L'algorithme de tri est indépendant des opérations de comparaison et d'échange, donc en lui passant différentes fonctions de comparaison et d'échange, nous pouvons organiser le tri selon différents critères. C'est l'approche adoptée dans notre nouveau tri.
La comparaison lexicographique de deux lignes est effectuée par strcmp, comme auparavant ; nous aurons également besoin d'une routine numcmp comparant deux lignes sur la base de valeurs numériques et renvoie le même type d'indication de condition que strcmp. Ces fonctions sont déclarées avant main et un pointeur vers la fonction appropriée est passé à qsort. Nous avons fait l'impasse sur le traitement des erreurs pour les paramètres, afin de nous concentrer sur les problèmes principaux.
- #include <stdio.h>
- #include <string.h>
-
- #define MAXLINES 5000 /* nombre maximal de lignes à trier */
- char *lineptr[MAXLINES]; /* pointeurs vers des lignes de texte */
-
- int readlines(char *lineptr[], int nlines);
- void writelines(char *lineptr[], int nlines);
-
- void qsort(void *lineptr[], int left, int right, int (*comp)(void *, void *));
- int numcmp(char *, char *);
-
- /* trier les lignes d'entrée */
- main(int argc, char *argv[]) {
- int nlines; /* nombre de lignes d'entrée lues */
- int numeric = 0; /* 1 si tri numérique */
- if (argc > 1 && strcmp(argv[1], "-n") == 0) numeric = 1;
- if ((nlines = readlines(lineptr, MAXLINES)) >= 0) {
- qsort((void**) lineptr, 0, nlines-1, (int (*)(void*,void*))(numeric ? numcmp : strcmp));
- writelines(lineptr, nlines);
- return 0;
- } else {
- printf("entrée trop grande pour être triée\n");
- return 1;
- }
- }
Dans l'appel à qsort, strcmp et numcmp sont des adresses de fonctions. Puisqu'elles sont connues pour être des fonctions, le & n'est pas nécessaire, de la même manière qu'il n'est pas nécessaire avant un nom de tableau.
Nous avons écrit qsort de manière à ce qu'il puisse traiter n'importe quel type de données, pas seulement des chaînes de caractères. Comme indiqué par le prototype de fonction, qsort attend un tableau de pointeurs, deux entiers et une fonction avec deux paramètres de pointeur. Le type de pointeur générique void * est utilisé pour les paramètres de pointeur. Tout pointeur peut être converti en void * et inversement sans perte d'informations, nous pouvons donc appeler qsort en convertissant les paramètres en void *. Le cast élaboré de l'argument de fonction convertit les paramètres de la fonction de comparaison. Ceux-ci n'auront généralement aucun effet sur la représentation réelle, mais assureront au compilateur que tout va bien.
- /* qsort: trier v[gauche]...v[droite] dans l'ordre croissant */
-
- void qsort(void *v[], int left, int right, int (*comp)(void *, void *)) {
- int i, last;
- void swap(void *v[], int, int);
-
- if (left >= right) /* ne rien faire si le tableau contient */
- return; /* moins de deux éléments */
- swap(v, left, (left + right)/2);
- last = left;
- for (i = left+1; i <= right; i++) if ((*comp)(v[i], v[left]) < 0) swap(v, ++last, i);
- swap(v, left, last);
- qsort(v, left, last-1, comp);
- qsort(v, last+1, right, comp);
- }
Les déclarations doivent être étudiées avec une certaine attention. Le quatrième paramètre de qsort est :
disant que comp est un pointeur vers une fonction ayant deux paramètres void * et renvoyant un int.
L'utilisation de comp dans la ligne :
- if ((*comp)(v[i], v[left]) < 0)
est cohérent avec la déclaration : comp est un pointeur vers une fonction, *comp est la fonction, et :
- (*comp)(v[i], v[left])
est l'appel à celui-ci. Les parenthèses sont nécessaires pour que les composants soient correctement associés ; sans elles :
dit que comp est une fonction renvoyant un pointeur vers un int, ce qui est très différent.
Nous avons déjà montré strcmp, comparant deux chaînes de caractères. Voici numcmp, comparant deux chaînes de caractères sur une valeur numérique de tête, calculée en appelant atof :
La fonction swap, échangeant deux pointeurs, est identique à ce que nous avons présenté plus tôt dans cette page, sauf que les déclarations sont modifiées en void * :
Déclarations compliquées
C est parfois critiqué pour la syntaxe de ses déclarations, en particulier celles qui impliquent des pointeurs vers des fonctions. La syntaxe est une tentative de faire concorder la déclaration et l'utilisation ; elle fonctionne bien pour les cas simples, mais elle peut être déroutante pour les cas plus difficiles, car les déclarations ne peuvent pas être lues de gauche à droite et parce que les parenthèses sont surutilisées. La différence entre :
- int *f(); /* f: fonction renvoyant un pointeur vers int */
et :
- int (*pf)(); /* pf: pointeur vers une fonction renvoyant int */
illustre le problème : * est un opérateur de préfixe et il a une priorité inférieure à (), donc les parenthèses sont nécessaires pour forcer l'association correcte.
Bien que les déclarations vraiment compliquées se produisent rarement dans la pratique, il est important de savoir comment les comprendre et, si nécessaire, comment les créer. Une bonne façon de synthétiser les déclarations est de procéder par petites étapes avec typedef. En guise d'alternative, dans cette section, nous présenterons une paire de programmes convertissant du C valide en une description de mot et vice versa. La description de mot se lit de gauche à droite.
Le premier, dcl, est le plus complexe. Il convertit une déclaration C en une description de mot, comme dans ces exemples :
char **argv argv: pointeur vers char int (*daytab)[13] daytab: pointeur vers array[13] de int int *daytab[13] daytab: tableau[13] de pointeur vers int void *comp() comp: fonction retournant un pointeur vers void void (*comp)() comp: pointeur vers une fonction retournant void char (*(*x())[])() x: fonction retournant un pointeur vers array[] de pointeur vers fonction retournant char char (*(*x[3])())[5] x: tableau[3] de pointeur vers fonction retournant pointeur vers tableau[5] de char |
dcl est basé sur la grammaire spécifiant un déclarateur; il s'agit d'une forme simplifiée :
dcl: optionnel *'s direct-dcl direct-dcl name (dcl) direct-dcl() direct-dcl[taille optionnel] |
En d'autres termes, un dcl est un dcl direct, peut-être précédé de *. Un dcl direct est un nom, ou un dcl entre parenthèses, ou un dcl direct suivi de parenthèses, ou un dcl direct suivi de crochets avec une taille facultative.
Cette grammaire peut être utilisée pour analyser des fonctions. Par exemple, considérez ce déclarateur :
- (*pfa[])()
pfa sera identifié comme un nom et donc comme un direct-dcl. Alors pfa[] est aussi un direct-dcl. Alors *pfa[] est reconnu comme un dcl, donc (*pfa[]) est un direct-dcl. Alors (*pfa[])() est un direct-dcl et donc un dcl. Nous pouvons également illustrer l'analyse avec un arbre comme celui-ci (où direct-dcl a été abrégé en dir-dcl) :
Le coeur du programme dcl est constitué d'une paire de fonctions, dcl et dirdcl, analysant une déclaration selon cette grammaire. Comme la grammaire est définie de manière récursive, les fonctions s'appellent mutuellement de manière récursive lorsqu'elles reconnaissent des parties d'une déclaration ; le programme est appelé un analyseur à descente récursive :
- /* dcl: analyser un déclarateur */
- void dcl(void) {
- int ns;
- for (ns = 0; gettoken() == '*'; ) ns++; /* compteur *'s */
- dirdcl();
- while (ns-- > 0) strcat(out, " pointeur vers");
- }
-
- /* dirdcl: analyser un déclarant direct */
- void dirdcl(void) {
- int type;
- if (tokentype == '(') { /* ( dcl ) */
- dcl();
- if (tokentype != ')')
- printf("erreur: attendu )\n");
- } else if (tokentype == NAME) /* nom de variable */
- strcpy(name, token);
- else
- printf("ereur: nom attendue ou (dcl)\n");
- while ((type=gettoken()) == PARENS || type == BRACKETS)
- if (type == PARENS)
- strcat(out, " fonction retourne ");
- else {
- strcat(out, " tableau");
- strcat(out, token);
- strcat(out, " de");
- }
- }
Étant donné que les programmes sont destinés à être illustratifs et non infaillibles, dcl est soumis à des restrictions importantes. Il ne peut gérer qu'un type de données simple, comme une ligne char ou int. Il ne gère pas les types de paramètres dans les fonctions, ni les qualificateurs comme const. Les espaces parasites le perturbent. Il ne fait pas beaucoup de récupération d'erreur, donc les déclarations invalides le perturberont également.
Voici les variables globales et la routine principale :
- #include <stdio.h>
- #include <string.h>
- #include <ctype.h>
-
- #define MAXTOKEN 100
-
- enum { NAME, PARENS, BRACKETS };
-
- void dcl(void);
- void dirdcl(void);
-
- int gettoken(void);
- int tokentype; /* type de dernier jeton */
- char token[MAXTOKEN]; /* dernière chaîne de jeton */
- char name[MAXTOKEN]; /* nom d'identifiant */
- char datatype[MAXTOKEN]; /* type de données = char, int,... */
- char out[1000];
-
- main() { /* convertir la déclaration en mots */
- while (gettoken() != EOF) { /* 1er jeton en ligne */
- strcpy(datatype, token); /* est le type de données */
- out[0] = '\0';
- dcl(); /* analyser le reste de la ligne */
- if (tokentype != '\n')
- printf("erreur de syntaxe\n");
- printf("%s: %s %s\n", name, out, datatype);
- }
- return 0;
- }
La fonction gettoken ignore les espaces et les tabulations, puis trouve le jeton suivant dans l'entrée ; un «jeton» est un nom, une paire de parenthèses, une paire de crochets incluant peut-être un nombre, ou tout autre caractère unique :
- int gettoken(void) { /* retourner le jeton suivant */
- int c, getch(void);
- void ungetch(int);
- char *p = token;
-
- while ((c = getch()) == ' ' || c == '\t')
- ;
- if (c == '(') {
- if ((c = getch()) == ')') {
- strcpy(token, "()");
- return tokentype = PARENS;
- } else {
- ungetch(c);
- return tokentype = '(';
- }
- } else if (c == '[') {
- for (*p++ = c; (*p++ = getch()) != ']'; )
- ;
- *p = '\0';
- return tokentype = BRACKETS;
- } else if (isalpha(c)) {
- for (*p++ = c; isalnum(c = getch()); ) *p++ = c;
- *p = '\0';
- ungetch(c);
- return tokentype = NAME;
- }
- else
- return tokentype = c;
- }
Aller dans l'autre sens est plus facile, surtout si l'on ne s'inquiète pas de générer des parenthèses redondantes. Le programme undcl convertit une description de mot comme «x est une fonction renvoyant un pointeur vers un tableau de pointeurs vers des fonctions renvoyant char», que nous exprimerons comme suit :
- x () * [] * () char
vers :
- char (*(*x())[]) ()
La syntaxe d'entrée abrégée nous permet de réutiliser la fonction gettoken. undcl utilise également les mêmes variables externes que dcl :
- /* undcl: convertir les descriptions de mots en déclarations */
- main() {
- int type;
- char temp[MAXTOKEN];
- while (gettoken() != EOF) {
- strcpy(out, token);
- while ((type = gettoken()) != '\n')
- if (type == PARENS || type == BRACKETS)
- strcat(out, token);
- else if (type == '*') {
- sprintf(temp, "(*%s)", out);
- strcpy(out, temp);
- } else if (type == NAME) {
- sprintf(temp, "%s %s", token, out);
- strcpy(out, temp);
- } else
- printf("Entrée invalide à %s\n", token);
- }
- return 0;
- }