1. Adressage de variables
1a. Adressage direct
1b. Adressage indirect2. Les pointeurs
2a. Les opérateurs de base
2b. Les opérations élémentaires sur pointeurs3. 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 dimensions5. 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
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.
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.
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;
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.
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.
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
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.
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.
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.
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;
}
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).
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.
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)
Déclaration
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
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 */
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.
5c. La fonction malloc et l'opérateur sizeof
La fonction malloc de la bibliothèque
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
ATTENTION ! :
Sur notre système, le paramètre
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
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
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