Un pointeur est un objet qui permet d’accéder à un autre objet,
l’objet pointé. Un objet pointeur peut changer de valeurs en
cours d’exécution et donc pointer sur un autre objet.
Un objet pointeur peut :
Soit contenir la valeur NULL (0). L’objet ne pointe alors sur
aucun objet ;
Soit contenir une adresse mémoire. En C,
les adresses sont des adresses d’octets, la mémoire étant vue
comme un tableau à une
dimension d’octets. Mais attention, une pointeur n’est pas
qu’une adresse, du fait que pointeur est typé (voir déclaration de
pointeur).
Dans beaucoup de langages, les pointeurs pointent sur des objets
particuliers résidant dans une zone mémoire appellée
tas (heap) et alloués
sur demande de l'utilisateur appelant des fonctions
particulières. C'est certes le cas en C qui possèdent 2 fonctions pour
cela, malloc et calloc. Mais, et
c'est peu fréquent dans les langages, un pointeur, dit pointeur
parasite, peut pointer en C sur n'importe quel objet, voire sur
n'importe quel octet d'un objet.
Il y a aussi en des pointeurs sur des fonctions. Un usage
très intéressant peut en être fait en liaison avec les typesunions et structures, pour
implémenter à la main le mécanisme
d’héritage et de liaison dynamique des langages objets.
En C, un pointeur possède un type (ce n’est pas
qu’une adresse en mémoire) ; cela
signifie que l’on précise à la déclaration du
pointeur le type de l’objet
pointé.
int *p ; /*p est un objet qui ne peut pointer que sur un objet
de type int : p est
dit de type int* */
int *px, *py, n ; /* px et py sont des pointeurs d’entiers, n
est un entier*/
char *pchar ;
/*pchar est un pointeur de caractères */
int ** pt_de_pt ; /* pt_de_pt est un pointeur de pointeur sur
un entier ? Cela cache aussi un pointeur sur un tableau d’int.*/
tydef int *ptint; /* Utilisation de typedef pour
simplifier les écritures ultérieures*/
pt_int q, *r ; /* q est un pointeur de int, r est un pointeur
de pointeur sur un int */
Par la déclaration int *p,
on considère implicitement la mémoire comme étant
un tableau d’objets de
type int (un objet
int occupant un mot, à savoir sizeof(int) == 4
octets sur une machine de 32 bits). Un pointeur contient
l’adresse du premier octet de l’entier.
Dangers de la programmation avec les pointeurs :
notion d’adresse valide et ordre des octets dans un
mot.
Des contraintes d’alignement des entiers existent.
L’adresse d’un objet de type int ne
peut pas être quelconque, cela doit être un multiple
d’une certaine valeur (4 sur une machine de 32 bits).
L’effet d’un accès via un pointeur de type int *
qui ne contient pas une adresse valide est indéfini.
Ceci est possible, bien que l’objet pointeur est typé, du
fait des conversions explicites de type (px =
(int *) pchar ; De plus, les machines ne numérotent pas
tous de la mêm façon les octets d’un mout mémoire.
Les machines «petits-boutistes (little-endians)
commencent par l’octet de poids faible. Les machines
«grands-boutistes (big-endians) commencent par l’octet de
poids fort.
Une programmation trop prêt de la machine conduit souvent
à des programmes non portables (la portabilité d’un
programme est la propriété de fonctionner
de la même façon sur des machines différentes).
Qui dit pointeurs typés dit vérification de
types.
pchar et px, déclarés ci-dessus, sont de types
différents. Le compilateur
n’accepte pas que l’on écrive (il n’y a pas de conversion
automatique dans ce cas) :
pchar = px ; ou px = pchar ;
Le langage permet de convertir explicitement (donc avec
un peu d’effort de la part du programmeur).
pchar = (char *)
px ; /* en anglais, cette opération s’appelle cast */
Pointeurs constants et pointeurs sur des « constantes »
En écrivant :
const int *p ;
on indique que l’on ne veut pas que l’on puisse modifier la
valeur de l’objet pointé en passant par le pointeur. On a affaire
à un pointeur sur une constante. Ceci est
particulièrement utile dans le cas des paramètres tableaux (on passe
en paramètre l’adresse du premier élément). On ne veut pas que la
fonction modifie le
tableau. On accepte
cependant de modifier la valeur du pointeur pour parcourir le
tableau.
int strlen (const
char *s) ; /*
Rend la longueur d’une chaîne ; s est un pointeur sur le 1er
caractère */
On peut aussi indiquer qu’un pointeur est constant. Il pointera
toujours sur le même objet.
int * const p = &x ; /* p est un pointeur constant que l’on
initialise avec l’adresse de x */
const int * const p = &x ; /* q est un pointeur constant
sur le même objet x . Via p, on peut modifier l’objet pointé.
Via q, on ne le peut pas*/
Les opérations sur les pointeurs
Il y a 4 types d’opérateurs
agissant sur les pointeurs et donc sur les adresses (puisque la
valeur d’un pointeur est une adresse) :
L’opérateur & permettant d’obtenir l’adresse d’un objet
L’opérateur * permettant d’accéder à l’objet pointé.
Les opérations de comparaison de pointeurs (==, !=, < ….)
L’opérateur & (ou les pointeurs parasites)
L’opérateur unaire &, placé devant le nom d’un objet, donne
l’adresse de celui-ci. Ainsi l’instruction
suivante : px = &x ; affecte à px l’adresse de x, on dit que
px pointe sur x. (en fait, px contient l’adresse du premier octet
de x). L’expression &x est typé. Si x est de type toto, alors
&x est de type *toto.
Attention, l’opérateur & ne s’applique qu’aux objets en
mémoire. ; il ne
peut donc pas s’appliquer à des expressions ou des variables de
type registre.
Les usages de l’opérateur &
Cet opérateur n’existe pas dans les langages de haut niveau (ou,
tout au moins, n’y joue pas un rôle important).
Un pointeur en C peut parasiter une autre variable. Ceci
n’est pas dangereux si la variable pointée
est statique (car sa durée de vie est
celle de tout le programme). Dans le cas d’une variable pointée de
classe automatique (allouée dans la pile) , il y a un risque
qu’elle disparaisse alors que le pointeur contient encore son
adresse. Une faute non détectée interviendra lors d’un accès via
le pointeur (accès à un mot mémoire qui a pu
être réalloué entre temps pour un autre usage).
Un pointeur parasite est utilisé quand on veut accéder plusieurs
fois à un même élément d’une structure de donnée
complexe. Le calcul d’adresse ne sera exécuté qu’une fois (Ne pas
oublier cependant qu’un compilateur moderne
est capable de faire des optimisations, du genre sortir d’une
boucle des calculs
constants), et on écrit qu’une seule fois une expression
complexe.
int * const element-singulier = a [i] [j] [k] ;
Un autre usage des pointeurs est lié au passage de paramètres. Le
passage de paramètres se faisant par valeur en C, il faut,
lorsque l’on veut que le paramètre effectif
puisse être modifié par la fonction appelée,
utiliser des pointeurs et passer l’adresse du paramètre effectif.
En C++, un grand nombre des usages de l’opérateur &
disparaissent du fait de l’introduction des objets référence et
du passage des paramètres par référence.
L’opérateur *
Si pentier est un pointeur sur un entier, alors *pentier est de
type entier, et
représente l’entier pointé par pentier.
int nombre, *pentier ; pentier = &nombre ; • pentier = 4 ;
/* équivalent à nombre = 4 ; */
Attention, danger, un pointeur est indéfini avant son
initialisation.
Ecrire *pentier = 4 ; entraîne un effet non défini
(détection ou non par le matériel) si pentier n’est pas
initialisé auparavant. Pour rappel, il n’y pas d’initialisation
par défaut.
Affectation de pointeurs et affectation
d’objets pointés
L’instruction py = px
constitue une affectation de
pointeurs (copie d’adresse). Py est fait pointer sur l’objet sur
lequel pointe x.
Ne pas confondre cette instruction avec
*py = * px ; qui recopie le contenu de l’objet pointé par px dans
l’objet pointé par py.
Addition d’un pointeur et d’un entier, la soustraction de deux
pointeurs
Si px est de type *int, alors
px++ incrémente px, non pas de 1, mais de la taille de l’objet
pointé, à savoir de sizeof (int), a
priori 4. px pointe sur l’élément suivant celui sur lequel il
pointait auparavant.
On entrevoit le lien qu’il y a en C entre la notion de pointeur
et la notion de tableau. Plus
généralement, px + i (in étant un entier) donne l’adresse du ième
élément suivant px.
La soustraction de deux pointeurs de même type, p et q, donne
un résultat entier i tel que :
p-q == i si et seulement si p+i == q.
i est le nombre d’éléments typés séparant les objets pointés par
p et q.
Via un pointeur * int, la mémoire est donc
bien vue come formant un tableau d’entiers
et non d’octets.
Danger : les opérations arithmétiques sur pointeurs, interdites
dans bien des langages, donnent accès libre à toute la mémoire. Mais, ces
opérations sont à la base du concept de tableau en C.
Les comparaisons de pointeurs
On peut comparer deux pointeurs (==, !=, <, <=, >,
>=). Il s’agit bien d’une comparaison d’adresse. Seuls les
opérateurs == et != sont vraiment utiles.
Les conversions explicites (cast) de pointeurs
On peut convertir un entier et un pointeur (et vice versa), et
également un pointeur d’un certain type en un pointeur
d’un autre type.
Attention, cependant, aux contraintes d’alignement.
A côté des objets déclarés (statiques ou dynamiques), il y a ceux
créés dynamiquement, suit à l’appel par le programmeur des
fonctionscalloc et mallloc,
dans une zone de mémoire, appelée
tas (ou heap). On accède à
ces objets obligatoirement via des pointeurs.
Le tas est utilisé
pour la construction de structure de
données complexes (listes, arbres, ensembles, …) dont on ne
connaît pas a priori le nombre d’éléments. Les tableaux, de taille
fixe, ne permettent pas de les construire.