pointeur

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).

Il n’y a pas d’initialisation par défaut des pointeurs.

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 types unions et structures, pour implémenter à la main le mécanisme d’héritage et de liaison dynamique des langages objets.

Les déclarations de pointeurs

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é.
  • L’opération = d’affectation de pointeurs
  • 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.

void echange (int * pi, int * pj) ; echange (&i, &j) ; echange (a[i] , a[j]) ; /* …..*/

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

Soit la déclaration suivante :

int *px, *py ;

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.

char c, d, e, f, *pc = d ;

int *pi = (*int) pc ; /* pc contient une adresse valide pour un caractère. Cette adresse n’est pas obligatoirement valide pour un entier */

L’allocation dynamique dans le tas

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 fonctions calloc 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.

Le langage C comporte des fonctions comme malloc et calloc (via la bibliothèque standard C) pour allouer de la mémoire, et une fonction free (dangereuse d’emploi) pour libérer cette mémoire.

Les fonctions d’allocation, malloc et calloc

La fonction de libération free

Les pointeurs génériques

Les pointeurs de fonction

» Glossaire du langage C