C'est un des points les plus troublants en C pour le débutant. Les
pointeurs ressemblent beaucoup à Pascal, seule la syntaxe diffère.
Les opérateurs de base sont & pour référencer un objet et
* pour déréférencer, écrits en préfixe. Ainsi
int x = 1, y = 2, z[10]; int *ip; /* - */ ip = &x; /* - */ y = *ip; /* - */ *ip = 0; /* - */ ip = &z[0]; /* - */
De même, la fonction d'échange s'écrira
int x, y;
void Echange (int *xp, int *yp)
{
int z = *xp;
*xp = *yp;
*yp = z;
}
...
Echange (&x, &y);
Nous verrons plus tard (cf. section
) les
fonctions malloc et free de la librairie C qui
permettent à un pointeur de manipuler des objets du tas de C (comme
new et dispose en Pascal). La constante NULL,
déinie dans le fichier <stdio.h> est la valeur standard pour
un pointeur vide (le nil de Pascal). Mais en C, en plus des
opérations usuelles d'égalité sur les pointeurs, on peut faire des
opérations arithmétiques (additions et soustractions). Nous
éviterons d'en faire trop dans ce cours. En général, il s'agit de
pointer sur l'élément d'un tableau et de passer sur ses éléments
voisins. En C, tout se dérive de l'égalité suivante, que l'on peut
considérer comme l'équation de C (pour le meilleur et pour le pire!)
Les corollaires de cette équation sont nombreux. D'abord si
, cela veut dire qu'une expression contenant le nom d'un
tableau a est un raccourci pour signifier la valeur d'un
pointeur pointant sur son premier élément (toujours à l'indice 0 en
C). Ainsi on comprend mieux les signatures des fonctions Erreur
de l'exemple du carré magique (page
),
de atoi (page
), de strlen (page
), Imprimer (page
). Ensuite, comme
, on a
. Nous
utiliserons peu cette écriture inélégante, mais le type de ces deux
notations permet d'écrire autrement la signature des fonctions
précédentes
void Erreur (char *s) {...}
Ecrire sous une forme ou l'autre est une affaire de goût,
mais dans le cas présent la première est plus naturelle, puisque
c'est vraiment une chaîne de caractères que l'on veut donner comme
argument. Enfin, si
,
l'instruction
p = p + j;
implique que
. Donc, si on incrémente un pointeur, on passe à
l'élément suivant du tableau, quelque soit le type de cet élément.
Il y a un type de tableau qui est très utilisé: les chaînes de
caractères. Une chaîne de caractères est un simple tableau de
caractères. La fin d'une chaîne de caractères est le caractère
'\0'.
Ainsi
char ch[256];
ch[0] = 'P'; ch[1] = 'a'; ch[2] = 'u'; ch[3] = 'l'; ch[4] = '\0';
décrit un tableau ch contenant la chaîne de
caractères "Paul" de longueur 4. On aurait pu obtenir le même
résultat avec la fonction strcpy de la bibliothèque C, dont
le prototype est dans <string.h>, en faisant
strcpy (ch, "Paul");
Le fichier include <string.h> contient des
opérations de comparaisons, de recherche de sous-chaîne.
Enfin, il faut bien comprendre qu'il existe des différences entre
tableaux et pointeurs. Au point de vue mémoire, un pointeur est un
simple ``mot-mémoire'' contenant une référence vers un élément du
langage. Un tableau est un ensemble de cases mémoire pour chacun de
ses éléments. Ainsi un tableau a de pointeurs vers des
caractères est différent, d'un tableau b de caractères à
deux dimensions
char *a[10]; char b[10][256];
Le premier occupe 10 cases mémoires, le deuxième 2560
octets, même si a et b peuvent être passés comme
argument d'une même procédure
void F(char **x) {...}
En Unix, la fonction main a comme argument le nombre
d'argument argc de la ligne de commande appelant le programme,
et un tableau de pointeurs vers les chaînes arguments argv.
(argv[0] est le nom de la commande elle-même). Ainsi la
commande echo du système Unix s'écrit
int main (int argc, char *argv[])
{
int i;
while (--argc > 0)
printf ("%s%s", argv[i], (i < argc - 1) ? " " : "");
printf ("\n");
return 0;
}
Il n'y a pas de variables fonctionnelles en C (comme en Scheme ou ML).
Mais, il existe des pointeurs sur des fonctions. Ainsi on peut faire
dépendre une fonction d'une autre fonction, en lui passant en
argument un pointeur vers une fonction. On peut donc reprendre le
programme de recherche de zéro d'une fonction quelconque (cf. page
).
#include <math.h>
#define Pi 3.14
#define Epsilon 1.0e-7
#define Nmax 100
double Zero (double (*f)(double), double a,b)
{
int n;
double m;
n = 1;
while (fabs (b - a) < Epsilon && n < Nmax) {
m = (a + b) / 2;
if ((*f) (m) > 0 == (*f) (a) > 0)
a = m;
else
b = m;
++n;
}
return a;
}
...
Zero (sin, Pi/2, 3*Pi/2);
Zero (cos, 0, Pi);
Par convention, une fonction représente un pointeur sur
elle-même, un peu comme pour les tableaux, et le déréférencement
implicite se fera (à un niveau). On peut donc simplement écrire
f(m) pour (*f)(m). La signature de Zero peut
aussi être simplifiée. L'écriture ressemble alors à celle de Pascal.