On a vu qu'un tableau est une série de variables d'un type
donné. Cette série est stockée en mémoire en un seul bloc. En
particulier, on peut le représenter entièrement par un pointeur sur sa
première valeur. Il est en effet facile de retrouver l'adresse de
l'élément numéro i
si on connaît l'adresse de l'élément
zéro: il suffit d'ajouter i
fois la taille d'un élément à
cette adresse. L'utilisation de pointeurs comme dans l'exemple
suivant,
float x[25] ; /* tableau de 25 réels */ float *adr_x ; /* pointeur vers un float */ adr_x = &x[0] ; /* faire pointer adr_x vers le premier élément de x */ x[0] = 5 ; /* ces deux instructions modifient la */ *adr_x = 5 ; /* valeur du premier élément de x */ x[10] = 5 ; /* ces deux instructions modifient la */ *(adr_x + 10) = 5 ;/* valeur du 10ème élément de x */
montre que l'écriture façon tableau et l'écriture en utilisant un
pointeur sont équivalentes. D'ailleurs, lorsqu'on utilise
x
seul (sans les crochets), par exemple dans un appel de
fonction, il est converti automatiquement en le pointeur
&x[0]
. La troisième ligne ci-dessus aurait pu
s'écrire adr_x = x
, et par conséquent x[10] =
5;
est équivalent à *(x+10) = 5;
.
Cette équivalence à deux intérêts:
Il y a deux moyens de créer un tableau de taille non connue à priori:
Déclaration d'un tableau de taille inconnue: float x[] ;
ou déclaration d'un pointeur: float *x ;
. Les deux
s'utilisent de la même manière par la suite.
Il est alors nécessaire d'utiliser l'allocation dynamique de
mémoire. Cela consiste à réserver de la place dans la mémoire au
moment de l'exécution. Cette opération s'effectue à l'aide des
fonctions malloc
et free
, définies dans la
librairie standard (il faut donc inclure le fichier #include
<stdlib.h>
au début du programme), qui nécessite
l'utilisation des pointeurs.
La fonction d'allocation dynamique de mémoire se nomme
malloc
. Elle a pour déclaration (dans
stdlib.h
):
void *malloc (size_t size) ;
La fonction malloc
a pour argument la taille (en octets)
du bloc que l'on souhaite réserver en mémoire, et renvoie un pointeur
sur le début du bloc. L'argument est du type size_t
, qui
est en fait un entier. Il faut ici faire attention au fait que le
nombre d'octets demandé n'est pas, en général, égal au nombre
d'éléments du tableau que l'on veut créer. En effet, nous avons vu
que chaque type de variable occupe une taille en mémoire
différente. Cette taille peut, de plus, dépendre de la machine
utilisée, c'est pourquoi il faut utiliser la fonction
sizeof
pour la détéminer. Par exemple la quantité de
mémoire nécessaire à stoquer n
variables d'un type donné
se calcule de la façon suivante:
n*sizeof(char) pour des caractères n*sizeof(int) pour des entiers n*sizeof(float) pour des réels simples précision etc...
La fonction renvoit un pointeur de type non défini (void
*
), qu'il faut convertir en pointeur du type de la variable
demandée. Ainsi pour allouer un tableau de n
variables
d'un type donné, l'appel de malloc
se fera de la façon
suivante:
int n ; /* taille du tableau */ float *fptr ; /* pointeur reel simple precision */ int *iptr ; /* pointeur entier */ char *cptr ; /* pointeur caractere */ double *dptr ; /* pointeur reel double precision */ n = 100 ; /* 100 variables dans le tableau */ /* allocation dynamique */ fptr = (float *)malloc (n * sizeof (float)) ; iptr = (int *)malloc (n * sizeof (int)) ; cptr = (char *)malloc (n * sizeof (char)) ; dptr = (double *)malloc (n * sizeof (double)) ;
Une fois la fonction malloc
appelée, il est impératif de
vérifier que le système a trouvé la quantité de mémoire souhaitée (la
mémoire n'est pas infinie). Lorsque la tentative d'allocation échoue,
la fonction malloc
renvoit un pointeur
NULL
. Il faut donc tester si le pointeur retourné est
égal à NULL
; si c'est le cas, on ne peut pas et on ne
doit pas l'utiliser:
int n ; /* taille du tableau */ float *fptr ; /* pointeur réel simple précision */ n = 100 ; /* 100 variables dans le tableau */ /* allocation dynamique */ fptr = (float *)malloc (n * sizeof (float)) ; /* test du résultat */ if (fptr == NULL) { /* l'allocation a echoué: */ /* message d'erreur et arrêt du programme */ printf ("l'allocation a echoue\n") ; exit (1) ; }
Les tableaux alloués de cette façon s'utilisent exactement comme les tableaux standards.
Lorsqu'on n'a plus besoin d'un tableau alloué dynamiquement, il est
impératif de libérer la mémoire. Cette opération se fait avec la
fonction free
(fichier en-tête stdlib.h). Sa déclaration
est:
void free (void *ptr) ;
son seul argument est le pointeur qui a été retourné par
malloc
. Le bloc de mémoire vers lequel il pointe est
ainsi libéré par la fonction free
. Cette fonction n'a pas
de valeur de retour (void
). Reprenons notre exemple:
int n ; /* taille du tableau */ int i ; /* un indice */ float *fptr ; /* pointeur reel simple precision */ n = 100 ; /* 100 variables dans le tableau */ /* allocation dynamique */ fptr = (float *)malloc (n * sizeof (float)) ; /* test du resultat */ if (fptr == NULL) { /* l'allocation a echoué: message d'erreur et arrêt du programme */ perror ("echec de l'allocation") ; exit (0) ; } /* on peut utiliser le tableau... */ /* (l'exécution de ce qui suit n'a lieu que si (fptr != NULL)) */ /* exemple: on affecte des valeurs aux éléments du tableau */ for (i=0 ; i<n ; i++) /* boucle de 0 à n-1 (n elements) */ { fptr[i] = (float)i ; /* chaque element a la valeur de l'indice */ } /* noter la conversion en réel */ /* ... */ /* Quand on n'a plus besoin du tableau, on libere la memoire */ free (fptr) ;
Les chaînes de caractères sont le principal exemple d'utilisation des
pointeurs. En effet, le C n'a pas, contrairement à d'autres langages,
de type spécifique pour manipuler les chaînes de caractères. Leur
représentation se fait par un tableau de caractères (type
char
) terminé par le caractère nul, qui en C signifie la
fin d'une chaîne de caractères. On les manipule ensuite à l'aide d'un
pointeur sur le premier caractère de la chaîne, ce qui permet
d'accéder à la chaîne en entier. Ainsi, toutes les fonctions C servant
à panipuler les chaînes de caractères utilisent comme arguments des
pointeurs sur des variables de type char
ces fonctions
sont définies dans le fichier en-tête string.h
. Nous
n'entrerons pas dans les détails ici, pour un exemple, voir charex.c
Il est possible de faire certaines opérations sur des pointeurs, mais leur interprétation est différente des opérations sur des variables, du fait de leur nature. En effet, un pointeur représente l'adresse mémoire d'une variable d'un type donné (donc stocké en mémoire sur un nombre donné d'octets). De ce fait, augmenter de 1 un pointeur revient à le faire pointer sur l'élement suivant dudit type, ce qui est différent d'augmenter l'adresse mémoire de 1 octet. Ce type de manipulation n'est utilisée que dans des cas où la rapidité d'exécution est essentielle (c'est difficilement lisible) vous en trouverez un exemple à la fin du programme charex.c