Les pointeurs

I Qu'est-ce que c'est ?

Variables dont le contenu est une adresse

Le type de la variable contenue à l'adresse contenue dans une variable pointeur est le type du pointeur.

Entité qui permet d'accéder indirectement à une information : l'information n'est pas contenue dans la variable, mais à l'adresse contenue dans la variable.

L'accès se fait donc en deux temps.

II La syntaxe

II.1 Déclaration

Si le pointeur ne doit pas contenir l'adresse d'une variable de type connu, son type est void.

Il n'y a pas de conversion implicite entre les types de pointeur, sauf de void vers un autre type.

Attention : cette déclaration définit la zone mémoire portant le nom nom où pourra être mémorisée l'adresse d'une variable de type type_pointé. La zone mémoire qui sera utilisée par cette variable n'est pas réservée par cette déclaration.

Une grande liberté est ainsi laissée à l'utilisateur pour la gestion de cette variable.

II.2 Utilisation

a Affectation-initialisation

Initialiser un pointeur : lui affecter une valeur qui soit une adresse valide. Il faut toujours initialiser un pointeur.

i.Adresse d'une variable existante

ii.La valeur NULL

Il est dangereux de laisser un pointeur sans affectation ni initialisation : il risque de contenir une adresse invalide, et toute tentative de lecture ou écriture à cette adresse bloquera le programme.

Si on ne peut pas momentanément lui affecter une valeur valide, on lui affecte la valeur NULL. On pourra faire précéder toute tentative d'utilisation du pointeur par un test sur cette valeur.

iii.Adresse non prévue à la compilation

Il faut une adresse valide : qui soit dans la zone mémoire affecté à l'exécution du programme. Une partie de cette zone est dédiée à ce type de manipulation : le tas. Cette gestion de la mémoire pendant l'exécution s'appelle la gestion dynamique de la mémoire.

iii.a. Allocation de mémoire à un pointeur : la fonction malloc

La fonction malloc

Fonction de la bibliothèque standard

fichier d'en-tête : malloc.h

paramètre : le nombre d'octets à allouer.

retourne un pointeur sur void contenant une adresse valide

adresse valide : adresse autorisé, non déjà utilisée, début d'un bloc valide de N octets, si N est le nombre d'octets à allouer.

Il y a donc :

L'opérateur sizeof

Retourne le nombre d'octets occupé par son opérande, variable dont le nom suit l'opérateur, ou bien type, dont le nom est dans un typecast qui suit l'opérateur :

Allocation

Dans l'exemple ci-dessus, l'application de l'opérateur sizeof au pointeur donnera la valeur 20 (pourquoi ?).

iii.b. Libération de la mémoire

Elle est nécessaire, sinon on arrive plus ou moins vite à saturation du tas.

fonction free

iv.Affectation de la valeur d'un autre pointeur

Dans ce cas les deux pointeurs pointent sur le même bloc de mémoire, ce qui est dangereux.

b Accès au bloc mémoire pointé

Le rôle essentiel des pointeurs est de donner un autre moyen d'accès aux variables du programme, sans utiliser leur nom : les variables peuvent ne pas avoir de nom.

i.L'opérateur *, cas d'une variable - accès à une variable pointée

Appelé parfois opérateur de déréférencement ou opérateur d'indirection, car il donne accès indirectement (en passant par un pointeur) à une variable pointée ou à une variable d'un bloc pointé.

Dans cet exemple, on peut accéder à la variable x, en écriture ou en lecture :

*ptrDble est synonyme de x dans cet exemple

ii.Cas d'un bloc de plusieurs variables (de même type)

C'est le cas par exemple quand la fonction malloc a alloué un multiple plus grand que 1 de la taille de la variable pointée :

Dans cet exemple, ptrDble pointe sur un bloc de 5 doubles qui lui a été alloué : c'est donc un tableau de doubles. Il est possible d'accéder individuellement à chacune des variables qui constituent ce bloc. Il y a deux façons d'obtenir ce résultat :

ii.a. Opérateurs + et -

Attention, l'opérateur * vient avant l'opérateur + dans l'ordre des priorités : la parenthèse de l'exemple est indispensable.

ii.b. Opérateur []

ptrDble[i] est la i + 1 ème variable du bloc

ptr[i] est synonyme de *(ptr+i)

iii.Le mot-clé const
iii.a. Rappel

Ce mot clé intervient lors d'une déclaration-initialisation pour interdire toute modification ultérieure de la variable à laquelle il s'applique :

iii.b. Application aux pointeurs

Selon sa position dans la déclaration, ce mot clé permet d'interdire toute modification ultérieure :

La règle de syntaxe est la suivante : le mot const s'applique à l'entité placée immédiatement à sa droite :

Les deux premières lignes indiquent que le double pointé est constant : on ne pourra le modifier par l'intermédiaire du pointeur déclaré dans chacune des deux lignes (mais on pourra le modifier s'il existe une autre façon d'y accéder).

La troisième ligne indique que ptr3Dble, qui est un pointeur, ne pourra être modifé. On peut par contre modifier le contenu de la variable pointée.

c Arithmétique et comparaison

On a vu que l'opérateur + et l'opérateur - peuvent s'applique aux pointeurs. La signification de ces opérateurs est adaptée à leur utilisation :

i.Pointeur + entier, pointeur - entier

l'addition (ou soustraction) d'un entier N à un pointeur ajoute (ou soustrait) au contenu du pointeur la quantité d'octets qui lui permet ou permettrait) de pointer sur la variable de même type située au Nème rang derrière (ou devant) elle dans l'ordre des adresses. Il lui est donc ajouté (ou soustrait) N fois la taille de la variable pointée.

Attention :

ii.Pointeur – pointeur

Cette expression n'est permise qu'entre deux pointeurs de même type, non void. Elle retourne le rang du premier objet pointé par rapport au deuxième (supposé l'objet numéro 0).

iii.Comparaison

L'opérateur == permet de tester si deux pointeurs contiennent la même adresse.

III À quoi ça sert ?

III.1 Échanges d'informations entre fonctions

a Paramètres pointeurs

On a vu qu'une fonction ne peut retourner qu'une valeur. Elle peut cependant posséder autant de variables de sortie que nécessaire si elle reçoit en paramètre les adresses de ces variables : il lui sera alors possible d'écrire les valeurs de sortie dans ces variables. Puisque la fonction peut tout aussi bien lire la valeur de ces variables ce sont en fait des variables d'entrée/sortie

Les variables du programme appelant dont les adresses initialisent les paramètres de cette fonction voient leurs valeurs échangées par l'appel à cette fonction.

Dans cet exemple, N doubles successifs de la fonction appelante, situés aux adresses derrière l'adresse dont la valeur est transmise comme paramètre à l'appel de la fonction, se voient affecter des valeurs à l'appel de la fonction remplit.

b Pointeur valeur de retour

Une fonction peut bien sûr retourner un pointeur. Il faut cependant se souvenir que ce pointeur sera sans doute utilisé pour accéder à une variable, et qu'il faut donc qu'il reste valide après exécution de la fonction.

Règle simple : ne pas retourner l'adresse d'une variable locale. Cette adresse sera en effet utilisée à autre chose après retour de la fonction et aura perdu sa signification, puisque c'est celle d'une variable locale (souvenez vous que les paramètres sont aussi des variables locales initialisées par les valeurs des variables transmises à l'appel de la fonction).

Il est par contre possible de retourner un pointeur déclaré localement et initialisé par appel à malloc dans le corps de la fonction. En effet, cette allocation lui a affecté la valeur d'une adresse dans le tas, qui reste donc valide après retour de la fonction. C'est l'adresse de ce pointeur qui n'a plus de signification, mais pas son contenu.

c Paramètre fonction

On peut transmettre à une fonction l'adresse d'une fonction qu'elle pourra alors utiliser à sa guise.

i.Syntaxe
i.a. Pointeur sur fonction

La déclaration d'un pointeur sur fonction reprend toutes les informations de l'interface de la fonction : un nom, le nom du pointeur, le type retourné et les types des paramètres.

i.b. Adresse d'une fonction

Le nom d'une fonction est l'adresse de cette fonction, c'est à dire l'adresse de branchement lors de l'appel de la fonction.

i.c. Opérations

Les opérations permises sont l'affectation (=) et la comparaison (==), entre pointeurs sur fonctions de même prototype

III.2 Gestion dynamique de la mémoire

Les pointeurs permettent de faire face à des situations où le nombre de variables à manipuler ne peut être connu au moment où l'on écrit le programme. La mémoire est donc gérée à l'exécution et non pas à la compilation : c'est la gestion dynamique de la mémoire.

On prévoit que le programme aura à manipuler une collection de N variables d'un type donné, mais la valeur de N dépendra d'événement survenus au cours du déroulement du programme.

La solution est simple : un appel à malloc lorsque N est connu.