Lorsqu'on déclare une variable, le programme (lors de son exécution)
réserve la quantité de mémoire nécessaire pour enregistrer le contenu
de la variable. L'unité de base de la mémoire est l'octet (huit bits)
et selon le type de variable, il faut plus ou moins d'octets: un
caractère (type char
) occupera typiquement 1 octet, un
entier 2 à 4, un réel en simple précision 4 octets, un réel en double
précision peut par exemple prendre 8 octets).
Il est possible d'utiliser des séries de variables du même type, c'est ce qu'on appelle des tableaux. Un tableau est déclaré en indiquant le nombre d'éléments qu'il va contenir:
int i[100] ; /* tableau de 100 entiers */ float x[25] ; /* tableau de 25 réels */
La taille doit être spécifiée par un entier positif, on ne peut utiliser de variable. Lorsqu'on ne connait pas a priori la taille du tableau à utiliser, il faut faire appel à l'allocation dynamique.
Pour utiliser un élément d'un tableau, on y fait référence par son indice placé entre crochets, les indices commencant toujours à 0:
i[0] = ... i[1] = ... . . . i[99] = ...
Attention piège: le dernier élément d'un tableau à 100 éléments a l'indice 99. Le piège est méchant car l'écriture dans l'élément numéro 100 conduit à un comportement totalement imprévisible du programme: plantage immédiat, plantage aléatoire/différé, pas de plantage, mais peut-être faux résultats, ... !
On peut utiliser une variable entière comme indice, en prenant garde de ne pas dépasser les valeurs permises (de 0 à la taille du tableau moins 1):
int i ; /* compteur de boucle */ float x[25] ; /* tableau de 25 réels */ for (i=0 ; i<25 ; i++) /* boucle de 0 à 24 inclus */ { x[i] = ... /* chaque élément est affecté */ }
Un pointeur est un type très particulier de variable, puisqu'il est déstiné à contenir une adresse mémoire. L'interêt de ce type de variable apparaît dans plusieurs cas:
En effet, comme nous l'avons déjà signalé, lorsqu'on appelle une fonction avec des arguments, ce sont des copies des arguments qui sont passés à la fonction. Ceci empèche la modification des variables à l'intérieur d'une fonction. L'idée est d'envoyer l'adresse de la variable dont on veut modifier le contenu. Même si c'est une copie de l'adresse que la fonction reçoit, ce qu'il y a à cette adresse correspond bien à la variable qu'on veut modifier. De même, si l'on veut envoyer à une fonction un objet de grande taille, on peut se contenter de lui envoyer son adresse mémoire, la fonction saura alors où se trouve l'objet, et pourra l'utiliser.
Un pointeur se déclare à l'aide du symbole *
qui
se place entre le type de la variable et le nom de la variable:
int *adr_entier ;
représente un pointeur sur une variable entière. Après la déclaration, le pointeur ne 'pointe' sur aucune variable. Il faut lui affecter une 'destination':
int *adr_entier, x ; x = 5 ; adr_entier = &x ;
on a ici une variable entière x
à laquelle on a affecté
la valeur 5
, et on fait pointer le pointeur
adr_entier
sur cette variable en utilisant le symbole
&
, qui, placé devant une variable, permet d'obtenir
l'adresse mémoire de la variable. Ainsi, le pointeur
adr_entier
contient l'adresse mémoire de la variable
x
.
Sur cet exemple, nous avons représenté la mémoire de l'ordinateur
comme une suite de 'cases'; chaque case a une adresse qui est indiquée
en dessous. La variable x
est (par exemple) stockée à
partir de la case numéro 105 (mais vu qu'il s'agit d'un entier de type
int
, la variable occupe les cases 105, 106, 107 et 108,
soit 4 octets en tout), qui contient donc la valeur 5. La variable
adr_entier
est (par exemple) stockée à la case 502 (le
pointeur occupe 8 cases dans notre exemple). Elle contient l'adresse
de la variable x
, donc 105. Sur un même ordinateur, un
pointeur occupe toujours la même quantité de cases (typiquement 4 ou
8), que ce soit un pointeur vers un entier, vers un caractère, vers un
flottant de double précision ou vers une grande structure de
données. Ce qui compte, c'est qu'il doit pouvoir stocker une
adresse. C'est le compilateur qui sait combien de place prend une
adresse, et qui, comme pour les types entiers et flottants, réserve la
place qu'il faut quand vous déclarez un pointeur.
Comme nous l'avons vu dans le paragraphe sur les fonctions, la modification du contenu d'une variable passée comme argument à une fonction n'a pas de portée en dehors de la fonction. Le seul moyen de modifier le contenu d'une variable par l'appel d'une fonction consiste à passer non pas la variable elle-même mais l'adresse de la variable à la fonction. Dans ce cas, la fonction a connaissance de l'endroit dans la mémoire où se trouve stockée la variable, et peut donc la modifier. L'argument de la fonction est alors un pointeur sur une variable du type que l'on souhaite modifier.
void doubler (float *adr_x) /* fonction qui double la valeur a l'adresse donnee */ { (*adr_x) = (*adr_x) * 2.0 ; /* le contenu de l'adresse est double ici */ return ; /* on ne retourne rien (void) */ } int main (void) { float z ; /* variable reelle */ z = 5.0 ; doubler (&z) ; /* on essaye de doubler z */ printf ("%f\n", z) ; /* z vaut maintenant 10 */ return 1 ; }
Dans cet exemple, la fonction doubler
demande un argument
qui est un pointeur sur une variable de type float
. Cette
fonction fait une opération sur le contenu de l'adresse, et non pas
sur l'adresse elle-même, c'est pourquoi on utilise *adr_x
et non pas adr_x
. En effet, adr_x
est un
pointeur (type float *
), c'est-à-dire une adresse mémoire
d'une variable de type float
. L'expression
*adr_x
représente le contenu de cette adresse, ce qui est
bien la variable à modifier. Noter l'usage des parenthèses autour de
(*adr_x)
qui permettent une meilleure lisibilité (par
rapport au symbole de mutliplication).
L'appel de la fonction doubler
dans le main
se fait
en passant l'adresse de la variable sur laquelle on veut effectuer l'opération.
La variable z
étant une variable de type float
,
on passe son adresse mémoire &z
à la fonction.
Affichez, compilez et executez le programme
prog11.c : Modification de variables par
une fonction / passage d'arguments par adresse.