Les Pointeurs
Les Pointeurs
LES POINTEURS
[Link].
La plupart des langages de programmation offrent la possibilité d'accéder aux données dans la
mémoire de l'ordinateur à l'aide de pointeurs, c.-à-d. à l'aide de variables auxquelles on peut
attribuer les adresses d'autres variables.
- d'un opérateur 'adresse de': & pour obtenir l'adresse d'une variable.
L'opérateur & nous est déjà familier par la fonction scanf, qui a besoin de l'adresse de ses
arguments pour pouvoir leur attribuer de nouvelles valeurs.
Exemple
int N;
printf("Entrez un nombre entier : ");
scanf("%d", &N);
Attention !
L'opérateur & peut seulement être appliqué à des objets qui se trouvent dans la mémoire
interne, c.-à-d. à des variables et des tableaux. Il ne peut pas être appliqué à des constantes ou
des expressions.
1
Représentation schématique
Alors l'instruction
P = &A;
affecte l'adresse de la variable A à la variable P. Dans notre représentation schématique, nous
pouvons illustrer le fait que 'P pointe sur A' par une flèche:
Exemple
Soit A une variable contenant la valeur 10, B une variable contenant la valeur 50 et P un
pointeur non initialisé:
P = &A;
B = *P;
*P = 20;
- P pointe sur A,
2
- le contenu de A (référencé par *P) est affecté à B, et
3
3. Déclaration d'un pointeur
type *NomPointeur ;
déclare un pointeur NomPointeur qui peut recevoir des adresses de variables
du type Type
Une déclaration comme int *PTI; peut être interprétée comme suit:
Exemple
Remarque
4
3.1. Priorités des opérateurs * et &
En travaillant avec des pointeurs, nous devons observer les règles suivantes:
Les opérateurs * et & ont la même priorité que les autres opérateurs unaires (la négation !,
l'incrémentation ++, la décrémentation --). Dans une même expression, les opérateurs unaires
*, &, !, ++, -- sont évalués de droite à gauche.
* Si un pointeur P pointe sur une variable X, alors *P peut être utilisé partout où on peut
écrire X.
Exemple
Après l'instruction
P = &X;
Y = *P+1 Y = X+1
*P = *P+10 X = X+10
*P += 2 X += 2
++*P ++X
(*P)++ X++
Comme les opérateurs unaires * et ++ sont évalués de droite à gauche, sans les parenthèses
le pointeur P serait incrémenté, non pas l'objet sur lequel P pointe.
La valeur NULL est utilisée pour indiquer qu'un pointeur ne pointe 'nulle part'.
int *P;
P = NULL;
5
Finalement, les pointeurs sont aussi des variables et peuvent être utilisés comme telles. Soit
P1 et P2 deux pointeurs sur int, alors l'affectation
P1 = P2;
copie le contenu de P2 vers P1. P1 pointe alors sur le même objet que P2.
Résumons:
int A;
int *P;
P = &A;
A désigne le contenu de A
&A désigne l'adresse de A
P désigne l'adresse de A
*P désigne le contenu de A
En outre:
6
4. Pointeurs et Tableaux
Le nom d'un tableau représente l'adresse de son premier élément. En d'autre termes:
&tableau[0] et tableau
sont une seule et même adresse.
En simplifiant, nous pouvons retenir que le nom d'un tableau est un pointeur constant sur le
premier élément du tableau.
int A[10];
int *P;
l'instruction:
Si P pointe sur une composante quelconque d'un tableau, alors P+1 pointe sur la composante
suivante. Plus généralement,
P+i Pointe sur la i-ième composante derrière P et
P-i Pointe sur la i-ième composante devant P.
P = A;
Remarque :Au premier coup d'oeil, il est bien surprenant que P+i n'adresse pas le i-ième octet
derrière P, mais la i-ième composante derrière P ...
7
- chaque pointeur est limité à un seul type de données, et
Exemple
Soit A un tableau contenant des éléments du type float et P un pointeur sur float:
float A[20], X;
float *P;
Après les instructions,
P = A;
X = *(P+9);
X contient la valeur du 10-ième élément de A, (c.-à-d. celle de A[9]). Une donnée du type
float ayant besoin de 4 octets, le compilateur obtient l'adresse P+9 en ajoutant 9 * 4 = 36
octets à l'adresse dans P.
Attention !
Il existe toujours une différence essentielle entre un pointeur et le nom d'un tableau:
Lors de la première phase de la compilation, toutes les expressions de la forme A[i] sont
traduites en *(A+i). En multipliant l'indice i par la grandeur d'une composante, on obtient un
indice en octets: <indice en octets> = <indice élément> * <grandeur élément>
Résumons Soit un tableau A d'un type quelconque et i un indice pour les composantes de A,
alors
8
Si P = A, alors
Exemple
Les deux programmes suivants copient les éléments positifs d'un tableau T dans un deuxième
tableau POS.
Formalisme tableau
main()
{
int T[10] = {-3, 4, 0, -7, 3, 8, 0, -1, 4, -9};
int POS[10];
int I,J; /* indices courants dans T et POS */
for (J=0,I=0 ; I<10 ; I++)
if (T[I]>0)
{
POS[J] = T[I];
J++;
}
return 0;
}
Nous pouvons remplacer systématiquement la notation tableau[I] par
*(tableau + I), ce qui conduit à ce programme:
Formalisme pointeur
main()
{
int T[10] = {-3, 4, 0, -7, 3, 8, 0, -1, 4, -9};
int POS[10];
int I,J; /* indices courants dans T et POS */
for (J=0,I=0 ; I<10 ; I++)
if (*(T+I)>0)
{
*(POS+J) = *(T+I);
J++;
}
return 0;
}
int TAB[];
déclare un tableau d'éléments du type int
9
B désigne l'adresse du premier élémént de TAB.
(Cette adresse est toujours constante)
TAB[i] désigne le contenu de la composante i du tableau
&TAB[i] désigne l'adresse de la composante i du tableau
int *P;
déclare un pointeur sur des éléments du type int.
Comme les pointeurs jouent un rôle si important, le langage C soutient une série d'opérations
arithmétiques sur les pointeurs que l'on ne rencontre en général que dans les langages
machines. Le confort de ces opérations en C est basé sur le principe suivant:
Toutes les opérations avec les pointeurs tiennent compte automatiquement du type et de la
grandeur des objets pointés.
P1 = P2;
Exemples
int A[10];
int *P;
P = A+9; /* dernier élément -> légal */
P = A+10; /* dernier élément + 1 -> légal */
P = A+11; /* dernier élément + 2 -> illégal */
P = A-1; /* premier élément - 1 -> illégal */
- négatif, si P1 précède P2
- zéro, si P1 = P2
- positif, si P2 precède P1
- indéfini, si P1 et P2 ne pointent pas dans le même tableau
Plus généralement, la soustraction de deux pointeurs qui pointent dans le même tableau est
équivalente à la soustraction des indices correspondants.
On peut comparer deux pointeurs par <, >, <=, >=, ==, !=.
11
La comparaison de deux pointeurs qui pointent dans le même tableau est équivalente à la
comparaison des indices correspondants. (Si les pointeurs ne pointent pas dans le même
tableau, alors le résultat est donné par leurs positions relatives dans la mémoire).
Affectation
a) On peut attribuer l'adresse d'une chaîne de caractères constante à un pointeur sur char:
Exemple
char *C;
C = "Ceci est une chaîne de caractères constante";
Nous pouvons lire cette chaîne constante ([Link]: pour l'afficher), mais il n'est pas recommandé
de la modifier, parce que le résultat d'un programme qui essaie de modifier une chaîne de
caractères constante n'est pas prévisible en ANSI-C.
Initialisation
b) Un pointeur sur char peut être initialisé lors de la déclaration si on lui affecte l'adresse
d'une chaîne de caractères constante:
Attention !
12
A est un tableau qui a exactement la grandeur pour contenir la chaîne de caractères et la
terminaison '\0'. Les caractères de la chaîne peuvent être changés, mais le nom A va toujours
pointer sur la même adresse en mémoire.
B est un pointeur qui est initialisé de façon à ce qu'il pointe sur une chaîne de caractères
constante stockée quelque part en mémoire. Le pointeur peut être modifié et pointer sur autre
chose. La chaîne constante peut être lue, copiée ou affichée, mais pas modifiée.
Modification
c) Si nous affectons une nouvelle valeur à un pointeur sur une chaîne de caractères constante,
nous risquons de perdre la chaîne constante. D'autre part, un pointeur sur char a l'avantage de
pouvoir pointer sur des chaînes de n'importe quelle longueur:
Exemple
Attention !
Les affectations discutées ci-dessus ne peuvent pas être effectuées avec des tableaux de
caractères:
Exemple
13
Dans cet exemple, nous essayons de copier l'adresse de B dans A, respectivement l'adresse de
la chaîne constante dans C. Ces opérations sont impossibles et illégales parce que l'adresse
représentée par le nom d'un tableau reste toujours constante.
Pour changer le contenu d'un tableau, nous devons changer les composantes du tableau l'une
après l'autre ([Link]. dans une boucle) ou déléguer cette charge à une fonction de <stdio.h> ou
<string.h>.
Conclusions:
Utilisons des tableaux de caractères pour déclarer les chaînes de caractères que nous
voulons modifier.
Utilisons des pointeurs sur char pour manipuler des chaînes de caractères constantes
(dont le contenu ne change pas).
Utilisons de préférence des pointeurs pour effectuer les manipulations à l'intérieur des
tableaux de caractères.
Comme la fin des chaînes de caractères est marquée par un symbole spécial, nous n'avons pas
besoin de connaître la longueur des chaînes de caractères; nous pouvons même laisser de côté
les indices d'aide et parcourir les chaînes à l'aide de pointeurs.
Cette façon de procéder est indispensable pour traiter de chaînes de caractères dans des
fonctions. En anticipant sur la matière du chapitre 10, nous pouvons ouvrir une petite
parenthèse pour illustrer les avantages des pointeurs dans la définition de fonctions traitant
des chaînes de caractères:
Pour fournir un tableau comme paramètre à une fonction, il faut passer l'adresse du tableau à
la fonction. Le nom du tableau est l’adresse du tableau.
14
6. Allocation dynamique de mémoire
Nous avons vu que l'utilisation de pointeurs nous permet de mémoriser économiquement des
données de différentes grandeurs. Si nous générons ces données pendant l'exécution du
programme, il nous faut des moyens pour réserver et libérer de la mémoire au fur et à mesure
que nous en avons besoin. Nous parlons alors de l'allocation dynamique de la mémoire.
Revoyons d'abord de quelle façon la mémoire a été réservée dans les programmes que nous
avons écrits jusqu'ici.
Chaque variable dans un programme a besoin d'un certain nombre d'octets en mémoire.
Jusqu'ici, la réservation de la mémoire s'est déroulée automatiquement par l'emploi des
déclarations des données. Dans tous ces cas, le nombre d'octets à réserver était déjà connu
pendant la compilation. Nous parlons alors de la déclaration statique des variables.
Exemples
Pointeurs
Exemples
15
Chaînes de caractères constantes
L'espace pour les chaînes de caractères constantes qui sont affectées à des pointeurs ou
utilisées pour initialiser des pointeurs sur char est aussi réservé automatiquement:
Exemples
Souvent, nous devons travailler avec des données dont nous ne pouvons pas prévoir le nombre
et la grandeur lors de la programmation. Ce serait alors un gaspillage de réserver toujours
l'espace maximal prévisible. Il nous faut donc un moyen de gérer la mémoire lors de
l'exécution du programme.
Exemple
Nous voulons lire 10 phrases au clavier et mémoriser les phrases en utilisant un tableau de
pointeurs sur char. Nous déclarons ce tableau de pointeurs par:
char *TEXTE[10];
Pour les 10 pointeurs, nous avons besoin de 10*p octets. Ce nombre est connu dès le départ et
les octets sont réservés automatiquement. Il nous est cependant impossible de prévoir à
l'avance le nombre d'octets à réserver pour les phrases elles-mêmes qui seront introduites lors
de l'exécution du programme ...
Allocation dynamique
La réservation de la mémoire pour les 10 phrases peut donc seulement se faire pendant
l'exécution du programme. Nous parlons dans ce cas de l'allocation dynamique de la
mémoire.
16
La fonction malloc et l'opérateur sizeof
La fonction malloc
malloc( <N> )
fournit l'adresse d'un bloc en mémoire de <N> octets libres ou la valeur
zéro s'il n'y a pas assez de mémoire.
Cast de malloc()
La fonction malloc() donne l'adresse d'une zone de mémoire, mais cette adresse
retournée ne permet pas de savoir quel est le type des valeurs qui seront stockées dans cette
zone. En effet, ce que l'on indique à malloc(), c'est simplement le nombre d'octets
demandés. Dans ce sens, on dit que l'adresse renvoyée ou donnée par malloc() est
générique : cela se traduit en langage C en disant que l'adresse de la zone donnée par
malloc() est de type void* : pointeur générique. Or cette adresse sera affectée à un
pointeur du programme, dont le type sera long* ou char* ou encore double* : au sens
strict, ces types sont différents. Pour éviter les messages d'avertissement et garder une bonne
compatibilité, on prend systématiquement la précaution de faire précéder l'utilisation de
malloc() d'un transtypage, en indiquant entre parenthèses le type du pointeur dans lequel
sera affecté l'adresse que la commande fournira.
De même, selon le type du pointeur dans lequel on affectera la valeur donnée par la
commande.
Exemple :
float *ptrF;
La fonction free
Si nous n'avons plus besoin d'un bloc de mémoire que nous avons réservé à l'aide de malloc,
alors nous pouvons le libérer à l'aide de la fonction free de la bibliothèque <stdlib>.
17
free( Pointeur ) ;
libère le bloc de mémoire désigné par le <Pointeur>; n'a pas d'effet si le
pointeur a la valeur zéro.
* La fonction free ne change pas le contenu du pointeur; il est conseillé d'affecter la valeur
NULL au pointeur immédiatement après avoir exécuter.
* Si nous ne libérons pas explicitement la mémoire à l'aide free, alors elle est libérée
automatiquement à la fin du programme.
La fonction calloc
calloc() donne l'adresse générique (donc de type void*) d'une zone permettant de stocker
le nombre de variables demandé, chacune de ces variables occupant la taille, en octets,
précisée.
int nb=10 ;
7. Pointeurs et structures.
Soit les déclarations des structures suivantes
Structures :
typedef struct date {
int jour;
char mois[20];
int annee;
} date;
typedef struct {
char nom[32];
char prenom[32];
date date_naissance;
} eleve;
Les champs peuvent donc être de n'importe quel type connu : types de bases, tableaux,
pointeurs ou autre structure.
Soit la variable :
eleve E1; /* un eleve */
eleve *ptr ;
7. Pointeurs et fonctions.
7.1. Relation entre paramètres d'une fonction et pointeurs
A part les tableaux, les paramètres des fonctions sont des variables locales : modifications
faites dans la fonction
Le paramètre d'une fonction est la copie de l'argument : l'argument n'est pas modifié, on dit
que l’on fait un passage par valeur.
Ce mécanisme est appelé passage par adresse ou passage par pointeur (c'est la même chose,
car un pointeur est une adresse).
Plutôt que de fournir à la fonction une valeur, on va fournir son adresse ou un pointeur sur
cette valeur.
Rappel : une constante n'a pas d'adresse, ce mécanisme n'est utilisable qu'avec des variables.
Exemple
X = X +1; *X = *X +1;
} }
19
7.3. Passage d'une structure en paramètre
ou par adresse :
void Affiche( eleve *pE );
On préférera toujours la deuxième solution, qui évite la duplication de la structure sur la pile
(opération qui peut être couteuse,).
Dans la fonction, on utilise alors la notation ->.
Il convient d'être prudent lors de l'utilisation d'une fonction retournant un pointeur. Il faudra
éviter l'erreur qui consiste à retourner l'adresse d'une variable temporaire.
Exemple
#include <stdio.h>
void main(){
char *p;
char *ini_car(void);
p = ini_car();
printf("%c\n", *p);
}
char *ini_car(void){
char c;
c = '#';
return(&c); <=== ERREUR
}
20
7.5. Pointeur de fonction
Une fonction en C n'est pas une variable. Cependant on peut accéder à son adresse et
manipuler des pointeurs de fonction.
Déclaration
Voici trois exemples de pointeurs de fonction :
pf1 pointe sur une fonction qui a int pour type de retour et qui n'a pas de paramètres.
On associe pointeur de fonction et fonction comme cela : (les deux lignes sont équivalentes)
Maintenant (*pf2)(10.5,21.0); ,
pf2(10.5,21.0)
La forme avec * est préconisée : la deuxième forme est plus difficile à repérer en cas d'erreur
car on ne voit pas directement qu'il s'agit d'un pointeur, sauf si le nom est explicite.
21