Les Pointeurs


sommaire du chapitre :

Introduction

1. Adressage de variables
1a. Adressage direct
1b. Adressage indirect

2. Les pointeurs
2a. Les opérateurs de base
2b. Les opérations élémentaires sur pointeurs

3. Pointeurs et tableaux
3a. Adressage des composantes d'un tableau
3b. Arithmétique des pointeurs
3c. Pointeurs et chaînes de caractères
3d. Pointeurs et tableaux à deux dimensions

4. Tableaux de pointeurs

5. Allocation dynamique de mémoire
5a. Déclaration statique de données
5b. Allocation dynamique
5c. La fonction malloc et l'opérateur sizeof
5d. La fonction free


Introduction

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, soit à l'aide de variables auxquelles on peut attribuer les adresses d'autres variables.

En C, les pointeurs jouent un rôle primordial dans la définition de fonctions: comme le passage des paramètres en C se fait toujours par la valeur, les pointeurs sont le seul moyen de changer le contenu de variables déclarées dans d'autres fonctions. Ainsi le traitement de tableaux et de chaînes de caractères dans des fonctions serait impossible sans l'utilisation de pointeurs.

En outre, les pointeurs nous permettent d'écrire des programmes plus compacts et plus efficients et fournissent souvent la seule solution raisonnable à un problème.
Ainsi, la majorité des applications écrites en C profitent des pointeurs.

Malheureusement, il devient très facile de créer un pointeurs qui ne pointe sur rien, entrainant inlassablement un plantage du programme. Il est donc indispensable de procéder avec une extrême vigilance lors de l'implementation d'une application utilisant les pointeurs.

Cette constatation a motivé les créateurs du standard ANSI-C à prescrire des règles explicites pour la manipulation des pointeurs.

retour au sommaire


1. Adressage de variables

Avant de parler de pointeurs, il est indiqué de brièvement passer en revue les deux modes d'adressage principaux, qui vont d'ailleurs nous accompagner tout au long des chapitres suivants.

1a. Adressage direct

Dans la programmation, nous utilisons des variables pour stocker des informations.
La valeur d'une variable se trouve à un endroit spécifique dans la mémoire interne de l'ordinateur.
Le nom de la variable nous permet alors d'accéder directement à cette valeur.

Définition

Adressage direct : Accès au contenu d'une variable par le nom de la variable.
Exemple :
short A;
A = 10;

retour au sommaire

1b. Adressage indirect

Si nous ne voulons ou ne pouvons pas utiliser le nom d'une variable A, nous pouvons copier l'adresse de cette variable dans une variable spéciale P, appelée pointeur.
Ensuite, nous pouvons retrouver l'information de la variable A en passant par le pointeur P.

Définition

Adressage indirect : Accès au contenu d'une variable, en passant par un pointeur qui contient l'adresse de la variable.

retour au sommaire


2. Les pointeurs

Définition

Un pointeur est une variable spéciale qui peut contenir l'adresse d'une autre variable.
En C, chaque pointeur est limité à un type de données.
Il peut contenir l'adresse d'une variable simple de ce type ou l'adresse d'une composante d'un tableau de ce type.
Si un pointeur P contient l'adresse d'une variable A, on dit que "P pointe sur A".

Remarque
Les pointeurs et les noms de variables ont le même rôle : ils donnent accès à un emplacement dans la mémoire interne de l'ordinateur.
Il faut quand même bien faire la différence :
-Un pointeur est une variable qui peut "pointer" sur différentes adresses (soit une variable de type décimale qui contient une adresse du type 3F06 par exemple).
-Le nom d'une variable reste toujours lié à la même adresse.

retour au sommaire

2a. Les opérateurs de base

Lors du travail avec des pointeurs, nous avons besoin
- d'un opérateur "adresse de": & pour obtenir l'adresse d'une variable (&<NomDeVariable> désigne l'adresse de <NomVariable>).
- d'un opérateur "contenu de": * pour accéder au contenu d'une adresse (*<Adresse> désigne le contenu de <Adresse>).
- d'une syntaxe de déclaration pour pouvoir déclarer un pointeur.

Exemples :
L'opérateur &
int N;
printf("Entrez un nombre entier : ");
scanf("%d", &N);
L'opérateur *
<NomVariable> = 20
<PointeurSurVariable> = &<NomVariable>;
<ValeurVariable> = *<PointeurSurVariable>;
On a
*<ValeurVariable> = 20;

ATTENTION ! :
L'opérateur & peut seulement être appliqué à des objets qui se trouvent dans la mémoire interne, soit à des variables et des tableaux.
Il ne peut pas être appliqué à des constantes ou des expressions.

Déclaration d'un pointeur

En C, on déclare un pointeur comme suit :
<Type> *<NomPointeur>
Cela déclare un pointeur qui peut recevoir des adresses de variables du type <Type>.

Une déclaration comme
int *PNUM;
peut être interprétée comme suit:
"*PNUM est du type int" ou
"PNUM est un pointeur sur int" ou
"PNUM peut contenir l'adresse d'une variable du type int"

Remarque : Nous allons voir que la limitation d'un pointeur à un type de variables n'élimine pas seulement un grand nombre de sources d'erreurs très désagréables, mais permet une série d'opérations très pratiques sur les pointeurs.

retour au sommaire

2b. Les opérations élémentaires sur les pointeurs

Le pointeur NULL

On peut uniquement passer des adresses dans un pointeur, sauf dans le cas d'un pointeur NULL, dans quel cas la valeur du pointeur est 0 :
int *Pointeur;
Pointeur = 0;

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.

retour au sommaire


3. Pointeurs et tableaux

En C, il existe une relation très étroite entre tableaux et pointeurs.
Ainsi, chaque opération avec des indices de tableaux peut aussi être exprimée à l'aide de pointeurs.
En général, les versions formulées avec des pointeurs sont plus compactes et plus efficientes, surtout à l'intérieur de fonctions.
Mais, du moins pour des débutants, le "formalisme pointeur" est un peu inhabituel.

retour au sommaire

3a. Adressage des composantes d'un tableau

Comme nous l'avons déjà constaté plus tôt, 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.

Exemple :
En déclarant un tableau A de type int et un pointeur P sur int :
int A[10];
int *P;
l'instruction: P = A; est équivalente à P = &A[0];

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
P-i pointe sur la i-ième composante devant P

Ainsi, après l'instruction, P = A de l'exemple précédent
le pointeur P pointe sur A[0]
*(P+1) désigne le contenu de A[1]
*(P+2) désigne le contenu de A[2]
...
*(P+i) désigne le contenu de A[i]

On peut donc considérer 2 grandes façons d'utiliser un tableau en C :

Le 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;
}

Le 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;
}

retour au sommaire

3b. Arithmétique des pointeurs

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 fait que toutes les opérations avec les pointeurs tiennent compte automatiquement du type et de la grandeur des objets pointés.

Affectation par un pointeur sur le même type

Soient P1 et P2 deux pointeurs sur le même type de données, alors l'instruction P1 = P2 fait pointer P1 sur le même objet que P2.

Addition et soustraction d'un nombre entier

Si P pointe sur l'élément A[i] d'un tableau, alors
P+n pointe sur A[i+n]
P-n pointe sur A[i-n]

Incrémentation et décrémentation d'un pointeur

Si P pointe sur l'élément A[i] d'un tableau, alors après l'instruction
P++; P pointe sur A[i+1]
P+=n; P pointe sur A[i+n]
P--; P pointe sur A[i-1]
P-=n; P pointe sur A[i-n]

Domaine des opérations

L'addition, la soustraction, l'incrémentation et la décrémentation sur les pointeurs sont seulement définies à l'intérieur d'un tableau.
Si l'adresse formée par le pointeur et l'indice sort du domaine du tableau, alors le résultat n'est pas défini.

Seule exception :
Il est permis de "pointer" sur le premier octet derrière un tableau (à condition que cet octet se trouve dans le même segment de mémoire que le tableau).
Cette règle, introduite avec le standard ANSI-C, légalise la définition de boucles qui incrémentent le pointeur avant l'évaluation de la condition d'arrêt.

Soustraction de deux pointeurs

Soient P1 et P2 deux pointeurs qui pointent dans le même tableau :
P1-P2 fournit le nombre de composantes comprises entre P1 et P2.

Le résultat de la soustraction P1-P2 est
- 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.

Comparaison de deux pointeurs

On peut comparer deux pointeurs par <, >, <=, >=, ==, !=.
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).

retour au sommaire

3c. Pointeurs et chaînes de caractères

De la même façon qu'un pointeur sur int peut contenir l'adresse d'un nombre isolé ou d'une composante d'un tableau, un pointeur sur char peut pointer sur un caractère isolé ou sur les éléments d'un tableau de caractères.
Un pointeur sur char peut en plus contenir l'adresse d'une chaîne de caractères constante et il peut même être initialisé avec une telle adresse.

Pointeurs sur char et chaînes de caractères constantes

Affectation

On peut attribuer l'adresse d'une chaîne de caractères constante à un pointeur sur char :
char *C;
C = "Ceci est une chaîne de caractères constante";

ATTENTION ! :Nous pouvons lire cette chaîne constante (pour l'afficher par exemple), 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

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 :
char *B = "Bonjour !";

ATTENTION ! : Faites bien la différence entre les deux initialisation suivantes :
char A[] = "Bonjour !"; /* c'est un tableau de caractères */
char *B = "Bonjour !"; /* c'est un pointeur */

Modification

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 :
char *A = "Petite chaîne";
char *B = "Chaîne un peu plus longue";
A = B;
Maintenant A et B pointent sur la même chaîne; la "Petite chaîne" est perdue:

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.

Perspectives et motivation

Avantages des pointeurs sur char

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 suivant, 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 ; or, les paramètres des fonctions sont des variables locales, que nous pouvons utiliser comme variables d'aide.
Bref, une fonction obtenant une chaîne de caractères comme paramètre, dispose d'une copie locale de l'adresse de la chaîne.
Cette copie peut remplacer les indices ou les variables d'aide du formalisme tableau.

retour au sommaire

3d. Pointeurs et tableaux à deux dimensions

L'arithmétique des pointeurs se laisse élargir avec toutes ses conséquences sur les tableaux à deux dimensions.
Voyons cela sur un exemple :

Le tableau M à deux dimensions est défini comme suit :
int M[4][10] =
{
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
{10,11,12,13,14,15,16,17,18,19},
{20,21,22,23,24,25,26,27,28,29},
{30,31,32,33,34,35,36,37,38,39}
}
;

Le nom du tableau M représente l'adresse du premier élément du tableau et pointe sur le tableau M[0] qui a la valeur : { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
L'expression (M+1) est l'adresse du deuxième élément du tableau et pointe sur M[1] qui a la valeur : {10,11,12,13,14,15,16,17,18,19}
etc...

Problème

Comment pouvons-nous accéder à l'aide de pointeurs aux éléments de chaque composante du tableau, soit aux éléments M[0][0], M[0][1], ... , M[3][9] ?

Une solution consiste à convertir la valeur de M (qui est un pointeur sur un tableau du type int) en un pointeur sur int.
On pourrait se contenter de procéder ainsi :
int M[4][10] =
{
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
{10,11,12,13,14,15,16,17,18,19},
{20,21,22,23,24,25,26,27,28,29},
{30,31,32,33,34,35,36,37,38,39}
}
;
int *P;
P = M; /* conversion automatique */

Cette dernière affectation entraîne une conversion automatique de l'adresse &M[0] dans l'adresse &M[0][0]. (Remarquez bien que l'adresse transmise reste la même, seule la nature du pointeur a changé).
Cette solution n'est pas satisfaisante à 100% : généralement, on gagne en lisibilité en explicitant la conversion mise en oeuvre par l'opérateur de conversion forcée qui évite en plus des messages d'avertissement de la part du compilateur.

Solution Voici finalement la version que je conseillerais :
int M[4][10] =
{
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
{10,11,12,13,14,15,16,17,18,19},
{20,21,22,23,24,25,26,27,28,29},
{30,31,32,33,34,35,36,37,38,39}
}
;
int *P;
P = (int *)M; /* conversion forcée */

Dû à la mémorisation ligne par ligne des tableaux à deux dimensions, il nous est maintenant possible traiter M à l'aide du pointeur P comme un tableau unidimensionnel de dimension 4*10.

ATTENTION ! : Lors de l'interprétation d'un tableau à deux dimensions comme tableau unidimensionnel il faut calculer avec le nombre de colonnes indiqué dans la déclaration du tableau.

Exemple :
Pour la matrice A, nous réservons de la mémoire pour 3 lignes et 4 colonnes, mais nous utilisons seulement 2 lignes et 2 colonnes :
int A[3][4];
A[0][0]=1;
A[0][1]=2;
A[1][0]=10;
A[1][1]=20;
L'adresse de l'élément A[I][J] se calcule alors par:
A + I*4 + J

Conclusion

Pour pouvoir travailler à l'aide de pointeurs dans un tableau à deux dimensions, nous avons besoin de quatre données :
-l'adresse du premier élément du tableau converti dans le type simple des éléments du tableau
-la longueur d'une ligne réservée en mémoire (ici : 4 colonnes)
-le nombre d'éléments effectivement utilisés dans une ligne (ici: 2 colonnes)
-le nombre de lignes effectivement utilisées (ici : 2 lignes)

retour au sommaire


4. Tableaux de pointeurs

Déclaration

<Type> *<NomTableau> [<EntierPositifN>] ;

Exemple :
double *A[10];
déclare un tableau de 10 pointeurs sur des rationnels du type double dont les adresses et les valeurs ne sont pas encore définies.

Remarque :
Le plus souvent, les tableaux de pointeurs sont utilisés pour mémoriser de façon économique des chaînes de caractères de différentes longueurs.
Dans la suite, nous allons surtout considérer les tableaux de pointeurs sur des chaînes de caractères.

Initialisation

Nous pouvons initialiser les pointeurs d'un tableau sur char par les adresses de chaînes de caractères constantes.

Exemple discuté

char *JOUR[] =
{
"dimanche",
"lundi",
"mardi",
"mercredi",
"jeudi",
"vendredi",
"samedi"
};
déclare un tableau JOUR[] de 7 pointeurs sur char.
Chacun des pointeurs est initialisé avec l'adresse de l'une des 7 chaînes de caractères.

On peut afficher les 7 chaînes de caractères en fournissant les adresses contenues dans le tableau JOUR à printf (ou puts) :
int I;
for (I=0; I<7; I++)
{ printf("%s \n", JOUR[I]); }

Comme JOUR[I] est un pointeur sur char, on peut afficher les premières lettres des jours de la semaine en utilisant l'opérateur "contenu de" :
int I;
for (I=0; I<7; I++)
{ printf("%c \n", *JOUR[I]); }

L'expression JOUR[I]+J désigne la J-ième lettre de la I-ième chaîne.
On peut afficher la troisième lettre de chaque jour de la semaine par :
int I;
for (I=0; I<7; I++)
{ printf("%c \n", *(JOUR[I]+2)); }

Conclusion

L'expression "int *D[]" déclare un tableau de pointeurs sur des éléments du type int
Si D[i] pointe dans un tableau :
D[i] désigne l'adresse de la première composante
D[i]+j désigne l'adresse de la j-ième composante
*(D[i]+j) désigne le contenu de la j-ième composante

retour au sommaire


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

5a. Déclaration statique de données

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 :
float A, B, C; /* réservation de 12 octets */
short D[10][20]; /* réservation de 400 octets */
char E[] = {"Bonjour !"}; /* réservation de 10 octets */
char F[][10] = {"un", "deux", "trois", "quatre"}; /* réservation de 40 octets */

Pointeurs

Le nombre d'octets à réserver pour un pointeur dépend de la machine et du "modèle" de mémoire choisi, mais il est déjà connu lors de la compilation.
Un pointeur est donc aussi déclaré statiquement.
Supposons dans la suite qu'un pointeur ait besoin de p octets en mémoire (en DOS: p =2 ou p = 4).

Exemples :
double *G; /* réservation de p octets */
char *H; /* réservation de p octets */
float *I[10]; /* réservation de 10*p octets */

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 :
char *J = "Bonjour !"; /* réservation de p+10 octets */
float *K[] = {"un", "deux", "trois", "quatre"}; /* réservation de 4*p+3+5+6+7 octets */

retour au sommaire

5b. Allocation dynamique

Problème

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.

retour au sommaire

5c. La fonction malloc et l'opérateur sizeof

La fonction malloc de la bibliothèque <Stdlib.h> nous aide à localiser et à réserver de la mémoire au cours d'un programme.
Elle nous donne accès au tas (heap); soit à l'espace en mémoire laissé libre une fois mis en place le DOS, les gestionnaires, les programmes résidents, le programme lui-même et la pile (stack).

La fonction malloc

malloc(<EntierPositifN>)
fournit l'adresse d'un bloc en mémoire de octets libres ou la valeur zéro s'il n'y a pas assez de mémoire.

ATTENTION ! :
Sur notre système, le paramètre est du type unsigned int.
A l'aide de malloc, nous ne pouvons donc pas réserver plus de 65535 octets à la fois !

L'opérateur sizeof

sizeof(<Variable>) renvoit la grandeur de <Variable>
sizeof(<Constante>) renvoit la grandeur de <Constante>
sizeof(<Type>) renvoit la grandeur d'un objet de <Type>

exit

S'il n'y a pas assez de mémoire pour effectuer une action avec succès, il est conseillé d'interrompre l'exécution du programme à l'aide de la commande exit (de <Stdlib.h>) et de renvoyer une valeur différente de zéro comme code d'erreur du programme.

retour au sommaire

5d. 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.h>.

free(<Pointeur>) ;
libère le bloc de mémoire désigné par le <Pointeur>.
Elle n'a pas d'effet si le <Pointeur> a la valeur zéro.

ATTENTION ! :
-La fonction free peut aboutir à un désastre si on essaie de libérer de la mémoire qui n'a pas été allouée par malloc
-La fonction free ne change pas le contenu du pointeur; il est conseillé d'affecter la valeur zéro au pointeur immédiatement après avoir libéré le bloc de mémoire qui y était attaché
- Si nous ne libérons pas explicitement la mémoire à l'aide free, alors elle est libérée automatiquement à la fin du programme

retour au sommaire
chapitre suivant