Les pointeurs
Description du chapitre et des ses objectifs :
Voici le moment tant redouté par les débutants, les pointeurs ont la facheuse habitude de dérouter bon nombre de jeunes programmeurs. De plus, le C est le seul langage à mntrer aussi fièrement ses pointeurs, les autres préférant les cacher à vos yeux. J'essayerai donc de détailler au maximum ce point
très important de la programmation en C qu'il est nécessaire de maîtriser au maximum.
Si vous ne comprennez pas tout du premier coup, ne vous en faîtes pas, c'est en pratiquant et en relisant que leur utilisation deviendra naturelle. De plus, n'oubliez pas de vous reporter à la section consacrée aux exercices pour vérifier vos connaissances.
Bonne lecture et bon courrage.
Accéder directement à une des parties du cours :
A quoi servent les pointeurs?
S'il y a bien une question récurrente, c'est "à quoi servent les pointeurs?". La réponse est pourtant simple: pouvoir modifier le contenu des variables envoyées à une fonction. Et oui! On ne va plus travailler sur des valeurs, mais sur le contenu d'une variable! En effet, on ne copie donc plus les valeurs dans d'autres variables, on va faire un peu comme avec les tableaux, travailler avec l'adresse des variables.
D'ailleurs en parlant de tableau, on ne savait pas comment retourner un tableau? Et bien voilà qui va changer. Vous comprendrez aussi toute la nature des tableaux avec l'allocation dynamique.
Un exemple qui prouve l'utilitée des pointeurs? Très bien, essayez de faire une fonction qui inverse le contenu de 2 variables. Vous me dites:
void inverse(int a,int b)
{
int c;
c = a;
a = b;
b = c;
}
Et bien c'est perdu.
Là vous n'échangez que les valeurs des variables dans la fonction, mais pas le contenu des variables avec lesquelles on appelle la fonction.
Question : Est-il possible de faire un "gros" programme sans pointeur?
Oui et non, disons que du moment où vous ne vous limitez plus à de simples calculs, vous serez obligés de passer par là, alors autant le faire maintenant.
Aperçu de la syntaxe de base
Rien de tel pour commencer que de voir leur utilisation bien étrange.
Première chose à savoir, un pointeur donne deux informations, l'adresse d'une variable et son contenu.
Question : Hey, mais on a pas déjà vu ça? L'adresse d'une variable c'est &variable et son contenu, ben c'est la variable!
Et oui! Mais comment faites vous alors pour dire à une fonction que vous attendez une adresse par exemple? Et c'est là que ça coince. Cependant c'est tout à fait l'approche qu'il faut avoir.
En effet, l'utilisation de base d'un pointeur consiste à donner l'adresse d'une variable à notre pointeur et à en regarder le contenu.
Sans plus attendre, voici comment déclarer un pointeur:
Impressionant non? Comme vous pouvez le voir, c'est très proche d'une variable, la seule différence est l'ajout d'un "*" qui signifie: "je suis un pointeur".
Question : Pourquoi les pointeurs ont un type?
Et bien pour savoir ce qu'ils regardent, quel est leur contenu. En effet, rappeller vous qu'un
char ne tient que un octet et un
int tient 4 octets, et grâce au type, le pointeur saura quel taille il faudra qu'il regarde sur la mémoire, sinon, il ne sait pas.
Maintenant, il faut savoir qu'un pointeur ne peut pas prendre de valeur! En effet, il est obligé de regarder le contenu d'un espace mémoire, et comme je vous l'ai dit, le plus simple c'est de regarder l'espace mémoire sur lequel tient une variable.
Voici un exemple:
int *pointeur;
int variable = 12;
pointeur = & variable;
Si ça vous perturbe (ce qui est certainement le cas) voici l'explication:
Comme je l'ai dit, un pointeur c'est une adresse et un moyen de regarder le contenu. Soit un pointeur définit de la façon suivante:
alors dans l'utilisation:
---
p est l'adresse du pointeur
--- *
p est le contenu qui se situe sur l'adresse
p
---
int est le type de ce qui est contenu sur l'adresse
p, soit le type du contenu de
*p
Peut-être ferez vous le lien avec les tableaux:
--- tableau[x] est le contenu de la xième case
---
x est l'emplacement dans le tableau
C'est la même chose avec les pointeurs.
Reprennons notre code:
int *pointeur;
int variable = 12;
pointeur = & variable;
Comme vous pouvez le remarquer, j'affecte une adresse à une adresse (
pointeur et
&variable), je ne peux pas faire par exemple:
Erreur :
*pointeur = &variable;
Cela n'a pas de sens actuellement. Attention donc à ne pas mélanger les torchons et les serviettes, on affecte toujours une adresse à une adresse!
Voyons maintenant comment travailler avec le contenu de notre pointeur.
Une fois que notre pointeur a une adresse et seulement à ce moment là, nous pouvons travailler avec son contenu. Ensuite, il agit
comme s'il était la variable, il suffit pour travailler le contenu du pointeur de mettre * devant son nom!
Voici un bout de code qui vous donnera tout ce qu'il se passe:
int *pointeur;
int variable = 12;
pointeur = & variable; //le pointeur a maintenant une adresse, on peut travailler avec
*pointeur = 42; // maintenant, *pointeur vaut 42, mais variable vaut aussi 42
variable = variable - 78 // maintenant, variable vaut 42-78 soit -36, mais le pointeur regardant au même endroit, *pointeur vaut aussi -36
On peut presque dire que le pointeur est la variable maintenant. On dit aussi que le pointeur
pointe sur une variable. (cependant je trouve cette dernière forme absolument pas explicite)
Attention : Seuls les pointeurs sont habilités à changer d'adresse! En effet il est absolument impossible de faire quelquechose comme:
int * point;
int variable = 1, v2 = 5;
point = &v2;
&variable = point; // IMPOSSIBLE
&variable = &v2 // IMPOSSIBLE
Exemples d'utilisation plus poussée
Actuellement, je ne vous ai montré qu'un forme détaillée de l'utilisation des pointeurs. Il faut aussi savoir que par défaut, on initialise un pointeur sur NULL qui signifie l'emplacement 0 de la mémoire de l'ordinateur.
Ainsi, on doit dans la pratique initialiser un pointeur à l'adresse NULL avant de l'utiliser. C'est une norme d'utilisation.
La forme détaillée se ferait comme ça:
int *pointeur;
pointeur = NULL;
Il existe une forme contractée qui mêle initialisation et déclaration:
Attention : Il faut bien lire ce que j'ai écrit juste au dessus et non pas que le contenu du pointeur vaut NULL. Il est donc seulement possible d'utiliser la forme *p=adresse lors de la déclaration. Ensuite, on fai toujours du adresse = adresse.
De la même façon, on peut faire:
int variable = 1;
int *pointeur = &variable;
Cela revient exactement au même que faire:
int variable = 1;
int *pointeur = NULL;
pointeur = &variable;
Il est aussi possible de déclarer plusieurs pointeurs d'un même type sur un seule et même ligne comme pour les variables:
int *p1=NULL, *p2, *p3=NULL, a, b=12;
Comme vous pouvez le voir sur cette ligne, à chaque fois que l'on déclare un nouveau pointeur sur cette ligne, il faut mettre un "*". On a aussi la possibilité ou non d'initialiser ces pointeurs sur la ligne.
Attention cependant à ne pas confondre les pointeurs avec les variables simples sur une même ligne! En effet dans cet exemple, a et b sont de simples variables (que j'ai encore une fois le droit d'initialiser ou non). Il est cependant peu recommander de mélanger les déclarations de pointeurs/variables sur une même ligne pour des soucis de lisibilitée et de compréhension.
De plus les pointeurs peuvent changer quand bon vous semble d'adresse et sauter ainsi de variable en variable. En voici un exemple:
int a = 12, b = -5;
int * pointeur1 = NULL;
int * pointeur2 = NULL;
pointeur1 = &a; // *pointeur1 vaut maintenant la même chose que a, soit 12. pointeur1 a l'adresse de a
pointeur1 = &b; // *pointeur1 vaut maintenant la même chose que b, soit -5. pointeur1 a l'adresse de b
pointeur2 = pointeur1; // pointeur2 reçois la même adresse que pointeur1, *pointeur2 vaut donc la même chose que b et que *pointeur1...
Pour mieux comprendre tout ce qu'il se passe, rien de tel qu'un petit programme. Pour cela on va utiliser des formateurs spécifiques de la fonction
printf. Pour afficher l'adresse d'un pointeur (en hexadécimal, ou base 16), on utilise le formateur
%p, si l'on souhaite afficher l'adresse d'une variable en hexadecimal, on utilise le formateur
%x ou
%X (la différence sera que pour le premier les lettres seront en minuscule, en majuscule pour le second).
Vous n'avez qu'à essayer ce petit programme:
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
int a = 12, b = -5;
int * pointeur1 = NULL, * pointeur2 = NULL;
printf("1_ Initialisation:\n Adresse de a: %X\n Adresse de b: %X\n Adresse de *pointeur1: %p\n Adresse de *pointeur2: %p\n\n", &a, &b, pointeur1, pointeur2
);
pointeur1 = &a;
printf("2_ pointeur1= &a:\n Adresse de a: %X\n Adresse de b: %X\n Adresse de *pointeur1: %p\n Adresse de *pointeur2: %p\n\n", &a, &b, pointeur1, pointeur2
);
pointeur1 = &b;
printf("3_ pointeur1= &b:\n Adresse de a: %X\n Adresse de b: %X\n Adresse de *pointeur1: %p\n Adresse de *pointeur2: %p\n\n", &a, &b, pointeur1, pointeur2
);
pointeur2 = pointeur1;
printf("4_ pointeur2= pointeur1:\n Adresse de a: %X\n Adresse de b: %X\n Adresse de *pointeur1: %p\n Adresse de *pointeur2: %p\n\n", &a, &b, pointeur1, pointeur2
);
return 0;
}
Et tant qu'on y est, un autre qui va lui s'occuper de voir l'évolution des valeurs:
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
int a = 12, b = -5;
int * pointeur1 = &a, * pointeur2 = &b;
printf("1_ Initialisation:\n a= %d\n b= %d\n *pointeur1= %d\n *pointeur2= %d\n\n", a, b, *pointeur1, *pointeur2
);
pointeur1 = &b;
printf("2_ pointeur1 = &b;:\n a= %d\n b= %d\n *pointeur1= %d\n *pointeur2= %d\n\n", a, b, *pointeur1, *pointeur2
);
*pointeur1 = *pointeur1 + a;
printf("3_ *pointeur1 = *pointeur1 + a:\n a= %d\n b= %d\n *pointeur1= %d\n *pointeur2= %d\n\n", a, b, *pointeur1, *pointeur2
);
pointeur2 = &a;
printf("4_ pointeur2 = &a:\n a= %d\n b= %d\n *pointeur1= %d\n *pointeur2= %d\n\n", a, b, *pointeur1, *pointeur2
);
*pointeur2 = 59 + *pointeur1;
printf("5_ *pointeur2 = 59 + *pointeur1:\n a= %d\n b= %d\n *pointeur1= %d\n *pointeur2= %d\n\n", a, b, *pointeur1, *pointeur2
);
return 0;
}
Regardez bien ce qu'il se passe. Je vous propose d'ailleurs de dessinner des cases qui vous permettront de suivre le contenu de chaque pointeur avec de jolies flêches. C'est une méthode qui peut bien vous aider à comprendre. D'ailleurs, si ma formulation des choses vous gène, n'hésitez pas à essayer de chercher autre part une façon d'expliquer qui vous conviendra mieux.
Les pointeurs et les fonctions
Voici la partie la plus interessante des pointeurs. Tout l'intérêt de leur utilisation réside d'ailleurs dans le fait de les utiliser dans les fonctions.
Premièrement, le prototype:
-
Comment demander en argument un pointeur?
Voici un prototype type:
Comme vous pouvez le constater, on demande un pointeur comme on demande une variable. Cependant, il y a des précisions à apporter sur ce point.
En effet, déclarer une telle fonction signifie que pour l'appeler il faudra envoyer une adresse à attribuer au pointeur. Pour faire plus clair, dans notre cas on va faire une copie de l'adresse qu'on envoit à la fonction et qui va être affectée au pointeur. J'ajouterai des précisions sur ce point un peu plus bas.
-
Comment retourner un pointeur?
La c'est assez évident:
Bien sur, remplacez
int par le type que vous souhaitez. La fonction renverra une
adresse encore une fois.
Deuxièmement, l'utilisation:
-
Comment utiliser un pointeur en argument?
L'utilisation est alors identique à n'importe quel pointeur. Vous pouvez manipuler comme bon vous semble. Cependant, bien que vous pouvez changer la valeur contenue à l'adresse du pointeur, si vous changez l'adresse du pointeur, les modifications ne seront pas prises en compte. En effet, comme dit un peu plus haut, on copie l'adresse envoyée dans le pointeur passé en argument à la fonction. (Vous pouvez faire l'analogie avec les valeurs des variables qui sont copiées dans une fonction). Voici un exemple de code utilisant les pointeurs:
void echange(int * a, int *b)
{
int temp = *a; // une varaible temporaire qui stocke le contenu de *a
*a = *b; // *a prend la valeur de *b
*b = temp; //*b prend la valeur de la variable temporaire (notez que je ne modifie pas l'adresse de b, mais bel et ben son contenu
}
-
Comment appeller la fonction?
Les fonctions prennant en argument un pointeur demandent en fait une adresse (avec les arguments classiques, les fonctions demandent des valeurs). Ainsi, si je veux utiliser la fonction que j'ai définie juste au-dessus, je vais par exemple envoyer l'adresse de 2 variables. Pour cela, j'ai 2 moyens:
int valeur1 = 8, valeur2 = 16;
echange( &valeur1, &valeur2);
int valeur1 = 8, valeur2 = 16;
int *p1 = NULL, *p2 = NULL;
p1 = &valeur1 ;
p2 = &valeur2;
echange( p1, p2);
Comme vous pouvez le constater, dans chaque cas j'envois une adresse.
La fonction reste cependant toujours bien hermétique. Ce n'est toujours pas la variable que j'envois dans la fonction, ici je fais une copie de l'adresse de la variable que j'envois à la fonction. On peut même dire que la valeur de l'adresse est copiée dans le pointeur de la fonction.
-
Comment récupérer l'adresse en retour d'une fonction?
Là pas de problème majeur, il s'agit d'une adresse comme une autre. Soit la fonction:
Pour récupérer le retour il suffit de créer un pointeur:
Ou encore
de façon
plus réduite:
Attention : Il est pour le moment hors de question de retourner l'adresse d'une variable crée dans une fonction. Je le répète, les fonctions sont des ensembles hermétiques, ce qui est créé dans la fonction n'en sort pas, d'ailleurs, tout ce qui est créé dans une fonction est détruit à la fin de l'execution de celle-ci. Pour le moment, retourner une adresse consistera donc à retourner une adresse d'un pointeur passé en argument. Nous verrons comment palier aux problèmes dans la partie suivante.
Les pointeurs... de pointeur
L'utilisation des pointeurs ne s'arrête pas la. En effet, vous avez entre autre la possibilité de pointer sur des pointeurs qui peuvent eux aussi pointer sur d'autres pointeurs qui peuvent pointer sur d'autres...
Question : A quoi peut bien servir cette sorcellerie?
Tout de suite les grands mots! Rappellez-vous de ce que j'ai dit un peu au-dessus lorsqu'on travaille dans une fonction:
Citation nepser : Cependant, bien que vous pouvez changer la valeur contenue à l'adresse du pointeur, si vous changez l'adresse du pointeur, les modifications ne seront pas prises en compte.
Et bien il existe une méthode pour pouvoir changer l'adresse à la sortie de la fonction. Et oui, vous l'aurez compris, on va créer un pointeur sur cette adresse. Cette méthode peut sembler assez étrange aux premiers abords, mais dans la pratique ça semble presque évident.
Voyons le cas de base, le pointeur de pointeur:
Avec un
exemple rapide:
Vous pouvez donc très bien faire un pointeur de pointeur de pointeur....
L'utilitée est proche du néan, mais c'est intéressant à savoir.
Je ne pense pas que vous montrer spécifiquement des exemples d'utilisation aidera plus qu'autre chose, quand vous sentirez que vous en avez besoin, vous les utiliserez (et surtout dans la partie avancée du cours). Les messages d'erreurs lors de la compilation vous permettront de savoir à quel niveau vous devez utiliser vos pointeurs. (dans le cas d'un
int **p, il y a une adresse de stockée à
*p et à
p)
Pointeurs chez les structures
La dernière syntaxe à connaître, c'est celle des structures.
En effet, on a ici droit à des cas particuliers du fait de la prioritée des opérateurs car comme dans un calcul banal où les parenthèses sont plus fortes que les multiplications et divisions qui eux mêmes ont la prioritée sur les additions et soustractions; en C il y a des prioritées spécifiques avec tous les opérateurs. Et les opérateurs, ce n'est pas seulement + - % / * ( ) , il y a aussi le "*" qui désigne un pointeur que vous venez de découvrir, le "&" qui signifie une adresse, et entre autre le "." pour accéder aux champs d'une structure.
Regardons un exemple, pour avoir tous les exemples possibles, je propose cette structre:
typedef struct {
int a;
int * p;
} structure;
Maintenant, le code pour l'utiliser qui semble le plus évident:
int main (void)
{
structure A; // je sais j'ai beaucoup d'imagination dans les noms
A.a = 18; // on met une valeur dans le champ a de la structure pour nos tests
structure *pointeur_structure = &A; // la façon de pointer une structure est strictement la même que pour n'importe quel type de variable, une structure est un type, je le rappelle.
printf("%d", *pointeur_structure.
a);
// ce qui peut sembler logique return 0;
}
Seulement voilà! Mon compilateur me dit:
Citation Mon ami le compilo : error: request for member `a' in something not a structure or union
Ha problème! Selon lui, je demande (request) le membre 'a' alors que je ne travaillerais pas dans une structure (ou une union)! Etrange non?
Et bien la solution se trouve bien dans la prioriété des opérateurs. Dans notre cas, le "." de la structure à une prioritée supérieure au "*" du pointeur. Finalement, que va lire le compilateur?
Et bien pour l'exercice, on va remplacer toutes les variables physiques (et structures) par le mot
objet et les adresses porteront le nom d'
adresse
Transformons cette ligne:
*pointeur_structure.a = 10;
Ca donne:
*adresse.objet = objet;
Or, prioritée oblige, on évalue d'abord "
adresse.objet" , ce qui signifie... absolument rien! Une adresse n'a pas de membre objet! Par contre
objet.objet = objet; est juste!
Alors pour transformer d'abord l'adresse en objet utilisable, on va mettre des parenthèses, l'opérateur ayant la plus haute prioritée.
Ce qui nous donne:
(*adresse).membre = objet; qui est juste.
*adresse est un
objet et on sait que
objet.objet est possible.
En C, on aura donc:
(*pointeur_structure).a = 10;
Ceci est une syntaxe correcte.
Pour palier à cette écriture un peu lourde, on a inventé l'opérateur "->" (un tiret "-" puis une flèche ">"). Il suffit de remplacer le "." traditionnel par ce nouvel opérateur. On a donc cette nouvelle syntaxe correcte:
pointeur_structure->a = 10;
Attention : Plus de "*" dans ce cas, "->" remplace entièrement l'écriture avec les parenthèses, le "." et le "*"
Regardons maintenant notre petit pointeur dans la structure que nous avons laissé de côté depuis un moment. Essayons ce code:
int main (void)
{
structure A;
int valeur = 25;
A.p = &valeur; //comme vous pouvez le constater, tant qu'on utilise que les adresses, pas beosin de s'inquiéter
printf("%d", *A.
p );
// essayons comme la dernière fois return 0;
}
Et comme vous vous y attendiez tous... ça marche!
Question : O_o ?
Et oui! Reprenons notre notation si spéciale:
Si on "traduit", on arrive à ce type d'expression:
*objet.adresse = objet;
Le compilateur va commencer par évaluer "
objet.adresse" . Est-ce faux? Que neni! Un objet peut très bien avoir l'un de ses champs qui soit une adresse. Ensuite le compilateur évaluer le tout:
"*(objet.adresse)" ce qui peut se simplifier par
"*(adresse)" ce qui est correct et qui, à fortiori, est un objet sur lequel on peut travailler.
On récapitule:
Si j'ai un pointeur sur une structure, j'utilise "->"
Si je veux avoir le contenu d'un pointeur contenu dans ma structure, j'utilise le classique "*".
Hum... tout ce texte récapitulé en 2 lignes...
Et voilà qui conclut le précieux chapitre sur les pointeurs, mais aussi la partie consacrée aux débutants. Vous voilà enfin prêt à affrontrer toutes les épreuves de programmation. Mais il reste encore beaucoup de choses à apprendre, rendez-vous dans la partie suivante.
Chapitre précédent - Sommaire
Nos rédacteurs et membres sont pour la plupart ouverts à des remarques constructives et servir à alerter le rédacteur du cours, des fautes éventuelles ou de propositions et nouvelles perspectives de cours etc ...
Pour ce faire cliquez ici
Postez vous aussi un commentaire à cette partie via le lien que voici