• Aucun résultat trouvé

Les pointeurs et les références 1 Introduction

• Un pointeur est une donnée (constante ou variable) qui représente l'adresse d'une variable.

• Pour que l'adresse stockée dans un pointeur soit exploitable, il faut connaître le type de l'information qui se situe au niveau de cette adresse afin de pouvoir l'interpréter convenablement. C'est pour cette raison qu'un pointeur doit être toujours associé à un type donné.

• Les pointeurs, tout comme les tableaux, les références et les structures sont considérés comme des outils de construction de types étendus (types construits sur la base d'autres types).

• Un pointeur est dit qu'il pointe (ou renvoie) vers la variable dont il stocke l'adresse.

2 - Déclaration

Formellement la syntaxe de déclaration d'un pointeur est la suivante : Type * NomPointeur ;

NomPointeur désigne le nom d'une variable de type pointeur.

Exemples :

char *pc; // définit un pointeur vers une donnée de type caractère. int * pi; // définit un pointeur vers une donnée de type int

double * pdr; // définit un pointeur vers une donnée de type réel double

unsigned long * pli; // définit un pointeur vers une donnée de type unsigned long

Remarque :

Les pointeurs occupent tous la même taille en mémoire indépendamment de la taille du type de l'objet pointé. Cela signifie par exemple qu'une variable de type pointeur sur un long double possède la même taille en mémoire qu'une variable de type pointeur sur un caractère.

Déclaration multiple

int *p1, *p2; // p1 et p2 sont deux pointeurs sur des entiers.

int *p1, p2; // p1 est un pointeur sur un entier alors que p2 est une variable entière. int p1, *p2 // p1 est une variable entière alors que p2 est un pointeur sur un entier.

3 - Initialisation des pointeurs

• Comme pour les autres variables, il est possible d'initialiser les variables de type pointeur. La valeur initiale est dans ce cas, l'adresse d'une donnée possédant comme type, le type vers lequel pointe le pointeur en question.

• L'initialisation d'un pointeur ne peut s'effectuer qu'en lui affectant comme valeur l'adresse d'une variable déjà existante.

Exemple :

short s; short *ps1 = &s;

Les pointeurs et les références Programmation orientée objet (C++) _________________________________________________________________________________________________________________

Remarque :

Si l'on indique comme valeur d'initialisation pour un pointeur l'adresse d'une variable ayant un autre type de données que celui vers lequel le pointeur peut renvoyer, le compilateur affiche alors une erreur lors de la compilation.

Exemple :

long l, *pl=&l;

unsigned long *pul=&l; // erreur: pul n'est un pointeur vers un long

4 - Affectation des pointeurs et conversion

Une variable de type pointeur peut obtenir sa valeur non seulement par une initialisation, mais également par une opération d'affectation.

Exemples :

float f1; float *pf1,*pf2;

pf1 =&f1; // pf1 contient l'adresse de f1. pf2=pf1; // pf2 contient l'adresse de f1.

Conversion

Il n'existe aucune conversion implicite d'un type pointeur vers un autre type pointeur. Le seul moyen pour faire des conversions entre types pointeurs est le casting.

int i,*pi=&i; unsigned int *pui; pui = pi; // erreur pui=&i; // erreur

pui=(unsigned int *)pi; // OK

5 - Accès indirect aux variables

• Il est possible d'accéder à une variable à travers un pointeur qui pointe vers cette variable. Il suffit d'utiliser pour cela l'opérateur * de la manière suivante :

Type Variable, *NomPointeur=&Variable;

Variable ⇔⇔⇔ * NomPointeur ⇔

• L'opérateur * est appelé dans ce cas opérateur d'indirection car il permet d'accéder d'une manière indirecte au contenu de la variable (à travers le pointeur).

Exemple 1 :

int i; int *pi; i =1234;

pi= &i // pi contient l'adresse de i

// *pi désigne d'une manière indirecte le contenu de i cout<<i;

cout<<*pi;

Exemple 2 :

Cet exemple montre la saisie et l'affichage de la valeur d'une variable à travers un pointeur :

int i,*pi=&i; scanf("%d",pi); printf("%d",*pi);

Ou également en utilisant les fonctions d'E/S du C++

int i,*pi=&i; cin>>*pi; cout<<*pi;

Les pointeurs et les références Programmation orientée objet (C++) _________________________________________________________________________________________________________________ Exemple 3 : #include<stdio.h> void main() {

int i=125; int *pi=&i;

printf("la valeur de i est: %d",i); //125 printf("la valeur du *pi est %d",*pi); //125 i++;

printf("la valeur de i incrémentée est: %d",i); //126 printf("la valeur du *pi est %d",*pi); //126 (*pi)++;

printf("la valeur de i est: %d",i); //127 printf("la valeur du *pi est %d",*pi); //127 *pi=2*(*pi);

printf("la valeur de i est: %d",i); //254 printf("la valeur du *pi est %d",*pi); //254 }

6 - Incrémentation de pointeurs et addition

• L'incrémentation d'un pointeur donne l'adresse située à sizeof(TYPE) octets à partir de la valeur courante du pointeur. TYPE étant le type de la variable pointée par le pointeur.

• L'addition entre un pointeur et un entier N donne l'adresse située à N*sizeof(TYPE) à partir de la valeur courante du pointeur.

Exemple 1:

int i; int *p=&i;

p++; // incrémente p de 4 octets en décimal.

Exemple 2: int * i,* j; int k; i=&k; j=i+10; j++;

/* Si i contient par exemple 1600 alors j vaut 1600+10*sizeof(int)=1640. j++ donne 1644 */

Remarque :

Pour les pointeurs, l'addition n'est définie qu'entre un pointeur et un entier. Elle n'est pas définie entre deux pointeurs ni entre un pointeur et un nombre en virgule flottante.

7 - Décrémentation de pointeurs et soustraction

• Contrairement à l'addition, on peut soustraire d'un pointeur non seulement un nombre entier mais aussi un autre pointeur de même type.

• La soustraction d'un nombre entier à un pointeur fonctionne d'une manière analogue à l'addition. De même la décrémentation fonctionne d'une manière analogue à l'incrémentation.

• La soustraction entre deux pointeurs (de même type) fournit le nombre d'éléments, du type en question, situés entre les deux adresses correspondantes (Ce n'est pas le nombre d'octets).

Exemple: int *a, *b; int tab[6]; a= &tab[5]; b=&tab[1]; cout<<a-b; //donne 4 cout<<b-a; //donne -4

Les pointeurs et les références Programmation orientée objet (C++) _________________________________________________________________________________________________________________

8 - Comparaison entre pointeurs

La comparaison de pointeurs est possible mais seulement entre pointeurs de même type.

Exemple 1 :

int *pa, *pb; … … …

if(pa = = pb)

cout<<"les deux pointeurs pointent vers la même donnée"; else

cout<<"les deux pointeurs pointent vers des données différentes"; … … …

Exemple 2 :

if(pa < pb)

cout<<"le pointeur pb contient une adresse plus grande que le pointeur pa";

9 - Pointeur nul

• Il existe en C/C++ un pointeur particulier qui pointe vers l'adresse 0, appelé le pointeur nul. En C/C++ cette adresse ne contient aucune donnée, par conséquent le pointeur nul ne pourra en aucun cas pointer vers une donnée.

• Chaque pointeur vers n'importe quel type peut être comparé au pointeur nul.

Exemple :

int *pi; if(pi==0)

cout<<"error";

• La constante symbolique NULL peut être utilisée à la place de la constante numérique 0. Elle est prédéfinie dans le fichier stdio.h.

• NULL peut être également définie par la directive define comme suit :

#define NULL 0 ou également #define NULL 0L selon que les adresses sur la machine sont représentées

par des int ou des long.

10 - Pointeurs génériques

• Il existe en C/C++ des pointeurs particuliers appelés pointeurs génériques qui peuvent pointer vers des données de n’importe quel type.

• Les pointeurs génériques s'avèrent utiles si par exemple l'adresse d'une zone mémoire doit être enregistrée, mais qu'il n'est pas encore établi quel type de données cette zone doit accueillir ou si le programmeur veut se réserver la possibilité d'enregistrer (successivement) des types de données différents ou bien encore s'il souhaite pour d'autres raisons ne pas se fixer dans l'immédiat sur un quelconque type de données.

Déclaration

Les pointeurs génériques sont déclarés à l'aide du type : void*

La déclaration se fait comme suit :

void* PointeurGenerique;

Exemple:

int a; double d;

void * vp; // pointeur générique

vp=&a; // vp stocke l'adresse d'un int vp=&d; // vp stocke l'adresse d'un double

Les pointeurs et les références Programmation orientée objet (C++) _________________________________________________________________________________________________________________

Restrictions dans l'utilisation des pointeurs génériques

• Un pointeur générique ne peut pas servir pour faire des accès indirects aux contenus des variables.

Exemple :

double d; void* pg=&d;

Ainsi *vp=1.234 génère une erreur. En effet, même si vp stocke l'adresse d'un double, le type void* ne donne

aucune infromation sur la taille mémoire qu'occupe la variable pointée. Pour résoudre ce problème, il faut explicitement convertir vp comme suit :

*(double * )vp=1.234;

• Par ailleurs, vp++ ou toute autre opération arithmétique sur les pointeurs génériques génère une erreur car le compilateur ne peut pas savoir de combien d'octets se déplacer.

Affectation d'un pointeur générique à un pointeur d'un autre type (type* ←←←← void*)

• En C++, l'affectation du contenu d'un pointeur générique (void*) à un pointeur (type*) doit obligatoirement passer par le casting.

• En C, le casting n'est pas obligatoire pour faire ce genre d'opérations même s'il reste conseillé.

Exemple :

int i=5;

int *pi1=&i, *pi2; void* vp;

vp=pi1; // Ok C et C++

pi2=vp; // Ok en C erreur en C++ pi2=(int*)vp // Ok C et C++

cout<<*pi2; // opération possible car le type de pi2 est connu int* pi2++; // opération possible car le type de pi2 est connu int*

Remarque

La conversion implicite entre pointeur générique et un pointeur type se fait se fait donc dans les cas suivants : T* vers void* // légale en C et C++

void* vers T* // légale en C seulement en C++ le casting est obligatoire

11 - Pointeurs et tableaux

• En C/C++, le nom d'un tableau est considéré comme une constante d'adresse définissant l'emplacement de début à partir duquel vont se succéder les éléments du tableau.

• Ainsi, lorsqu'il est employé seul, l'identificateur d'un tableau, est considéré comme un pointeur constant.

Notation d'accès aux éléments d'un tableau Si on considère la déclaration suivante :

int T[10];

Alors on a les équivalences de notations suivantes :

T ⇔ &T[0], T+1 ⇔ &T[1], T+i ⇔ &T[i] *T ⇔ T[0], *(T+1) ⇔ T[1], *(T+i) ⇔ T[i]

Exemple 1 :

Cet exemple montre un programme de remplissage des éléments d'un tableau d'entiers avec des 1.

int T[10],i; for(i=0;i<10;i++) T[i]=1; int T[10],i; for(i=0;i<10;i++) *(T+i) = 1; int T[10],i, *p;

for(p=T,i=0; i<10 ; i++,p++) *p=1;

Les pointeurs et les références Programmation orientée objet (C++) _________________________________________________________________________________________________________________

Le nom du tableau ne peut pas être utilisé pour faire le parcours des éléments (l'incrémentation T++ est incorrecte) car T est dans ce cas considéré comme un pointeur constant. Ceci explique l'utilisation d'une variable de type pointeur dans l'exemple 3 pour faire ce parcours.

Exemple 2 :

L'accès aux éléments peut se faire à l'aide d'indices négatifs si ces éléments sont situés à une adresse précédant l'adresse contenue dans le pointeur permettant de faire l'accès.

int T[5]; int *p=&T[4];

P[-1] ⇔ T[3], P[-2] ⇔ T[2], …, P[-4] ⇔ T[0].

Cas d'un tableau à deux dimensions:

Toute déclaration d'un tableau à N dimensions est considérée comme une déclaration d'un pointeur constant. Cependant, la référence des éléments à l'aide des pointeurs diffère de celle des tableaux à une dimension. En effet si on considère le cas particulier où N=2, la déclaration int T[3][4] est interprétée comme étant un

tableau de 3 éléments, chaque élément de ce tableau étant lui-même un tableau de 4 entiers.

Exemple :

typedef int QuatreEntiers[4]; QuatreEntier Tab[3];

Le tableau tab est équivalent au tableau T déclaré d'une manière classique comme suit : int T[3][4]

Les éléments de T étant des int[4].

De ce qui précède T+1 correspond à l'adresse de T+4*sizeof(int) octets (car l'élément du tableau correspond à 4

entiers et non pas à un seul).

De même les écritures suivantes sont équivalentes :

T ⇔ &T[0][0] ⇔ T[0] T+1 ⇔ &T[1][0] ⇔ T[1]

Pointeurs et constantes

Pointeur en lecture seule

Un pointeur en lecture seule peut pointer sur n'importe quelle variable. Il ne permet toutefois que l'accès en lecture à la variable pointée. Toute tentative de modification (accès en écriture) est signalée comme erreur.

Déclaration

const Type* NomPointeur;

Exemple :

int i=5,j=10;

const int* p = &i; // pointeur sur un entier cout<<"Valeur pointée :"<<*p<<endl;

p = &j; // ok

cout<<"Valeur pointée :"<<*p<<endl;

*p = 8; // erreur (tentative de modification) cout<<"Valeur pointée :"<<*p<<endl;

Remarque :

On ne peut pas affecter l'adresse d'un objet constant à un pointeur sur un type non constant, car une telle affectation pourrait permettre de modifier cet objet par l'intermédiaire du pointeur.

Exemple :

const char espace=' ' ;

const char *p = &espace ; // ok ; char *q = & espace ;; //erreur

Les pointeurs et les références Programmation orientée objet (C++) _________________________________________________________________________________________________________________

Pointeur constant

Un pointeur constant est un pointeur qui ne peut pointer que sur une seule variable. Le contenu de la variable pointée n'est pas nécessairement constant et peut par conséquent être modifié.

Déclaration

Type* const NomPointeur;

Exemple :

int i=5,j=10; int* const p = &i;

cout<<"Valeur pointée :"<<*p<<endl; i=8;

cout<<"Valeur pointée :"<<*p<<endl; p = &j; // erreur

cout<<"Valeur pointée :"<<*p<<endl;

Les références

• Le C++ introduit un nouvel outil de construction de types dérivés appelé référence. Une valeur de type référence est une adresse unique (qui ne change pas) et qui désigne une variable bien déterminée.

• Une référence doit obligatoirement être initialisée par une variable. Elle joue dès lors le rôle d'alias de cette variable (cette variable peut être manipulée à travers sa référence).

• Les références sont définies selon la syntaxe suivante :

Type &NomReference = VariableInitialisation;

• Les références sont à la fois similaires aux pointeurs du fait qu'ils stockent des adresses en mémoire et différents de ces derniers du fait que le contenu d'un pointeur peut être variable alors que celui d'une référence est constant.

• Les références peuvent être considérées comme des "pointeurs constants" qui sont manipulés, d'un point de vue syntaxique comme des variables ordinaires.

Exemple :

int i;

int &r1 = i; // r1 est une référence de i. Elle le restera pour tout le programme. r1 peut // désormais être utilisée en tant qu'autre nom de i.

int &r2; // Erreur, une référence doit être initialisée

Déclaration multiple de références

int a=1;

int &r1=a, &r2=a; // r1 et r2 référencent a.

Remarques :

• Il n'est pas possible de créer une référence générique. Ainsi void& n'est pas valide.

• Il n'est pas possible de créer des pointeurs vers des références, ni des références de références.

int a=1; int &r =a; int & *ptr; // Erreur int &&rr; // Erreur

• Il n'est pas possible de déclarer des tableaux de références. • Il est possible de déclarer des références de pointeurs.

int i,*p=&i;

int* &r=p; // r est une référence d'un pointeur sur entier i=5;

cout<<*r; // Affiche le contenu de i à savoir 5

Les références de constantes

En plus des références de variables, il est également possible de créer des références de constantes en ajoutant const à la déclaration.

Les pointeurs et les références Programmation orientée objet (C++) _________________________________________________________________________________________________________________

Exemple :

const int c=2;

const int &rc=c; //rc est une référence de c. Elle peut la remplacer

Opérations sur les références

Une fois qu'une référence a été déclarée et initialisée, toutes les opérations effectuées par la suite sur cette référence se rapporteront exclusivement à "l'objet" référencé et non à la référence elle-même. (En effet, le contenu d'une référence ne peut pas être modifié après son initialisation vu qu'il est constant).

Exemples :

int x; int *p; int &r=x;

r=1; // x reçoit la valeur 1 et r reste inchangé et contient toujours l'adresse de x. r++; // incrémente x de 1.

L'exemple ci-dessus montre que les références sont bien manipulées comme des variables sans utilisation de l'opérateur d'indirection).

Dans les expressions d'adressage x peut également être remplacée par r, ainsi :

p=&r; // affecte l'adresse de x à p.

*p=r; // copie le contenu de x dans l'emplacement pointé par p;

cout<<&x<<'\t'<<p<<'\t'<<&r; //Affiche 3 fois la même valeur qui est l'adresse de x

Initialisation des références

• Une référence non constante ne peut être initialisée qu'avec une lvalue de même type.

unsigned char uc; double d1,d2;

int &ri =1024; // error : not lvalue char &rc =uc // error inexact types double &dr=d1+d2; // error not an lvalue

• Une référence sur un objet constant peut être initialisée aussi bien avec une rvalue qu'avec une lvalue.

const unsigned char uc; const double d1;

const double d2;

const int &ri=1024; // OK : 1024 c'est l'adresse const unsigned char &rc = uc; // OK