Les pointeurs :
Dans certaines circonstances, travailler avec les adresses de variables au lieu de leur valeur s'avère utile. De telles manipulations sont possibles en langage C, grâce aux pointeurs.

Un pointeur est une variable ou une constante qui permet de stocker l’adresse d’une autre variable ou d’une fonction. Il permet d’effectuer ce qui s’appelle par analogie au langage Assembleur, l’adressage indirect, et d’accéder indirectement à la variable.
L’utilisation d’un pointeur donne l’accès au contenu de la variable vers laquelle il pointe ; il est plus aisé de changer le contenu du pointeur afin que celui-ci puisse pointer sur une autre variable. Ce mécanisme simple a une infinité d’applications en langage C.
Avec un pointeur l’écriture des boucles est simplifiée pour la même tâche sur une plage d'emplacements mémoire ou sur une série de variables.
Quelques utilisations possibles:
- Passages d’arguments par références aux fonctions ;
- Transfert de plusieurs informations vers des fonctions ;
- Accès aux tableaux (un tableau est un pointeur constant qui pointe sur son premier indice), et dans certains cas, un pointeur peut être un moyen d’accès efficace aux chaînes de caractères ;
- Passages de tableaux en argument aux fonctions.
Déclaration d’une variable pointeur :
Syntaxe :
type *nomDuPointeur ;
L’opérateur « * », dans un contexte de déclaration, indique que la variable déclarée est un pointeur.
type, c’est le type de la variable pointée.
Exemple :
int *myPtr ; //myPtr est un pointeur vers des entiers.
float *flPtr ; //flPtr est un pointeur vers des décimaux.
Pour initialiser un pointeur, il suffit de lui affecter l’adresse d’une autre variable. Ceci est réalisé en associant l’opérateur & en C pour désigner une adresse.
Exemple :
Ptr = &x ; //Ptr est utilisé ici sans * et il devra être du même type que x (par exemple : int *Ptr ; et int x ;).
Pour accéder à la variable pointée par un pointeur, on utilise l’opérateur « * ».
Exemple :
y = *Ptr; // ici on affecte à y le contenu de la variable pointée par Ptr.
Autres exemples d’interprétations des pointeurs :
int x, *Ptr; // x : variable d’entiers, et Ptr : pointeur d’entiers
Ptr = &x; // affecte à Ptr l’adresse de x
*Ptr = 5; // ici x prend la valeur 5
&x est un pointeur constant, sur la variable x ; car l’opérateur & signifie « adresse de ». Cette adresse ne pourra pas changer ; alors que Ptr est un pointeur variable.
L’instruction Ptr = &x; affecte à Ptr l’adresse de la variable x. Le contenu de Ptr quant à lui, peut changer durant le déroulement du programme.
La dernière ligne de l’exemple : *Ptr = 5; nous avons l’opérateur * (ou opérateur de déréférencement ou encore opérateur d’indirection);
*Ptr représente la donnée pointée par Ptr. Le terme *Ptr peut être utilisé partout où l’on veut utiliser x dans le programme. Ainsi les instructions y = *Ptr; et y = x; produisent le même résultat.
Fonctionnement des pointeurs :
En exemple supposons les lignes de codes ci-dessous :
{
int x, y;
int *Ptr;
x = 0xDA;
y = 0xBF;
Ptr = &x;
*Ptr = 0x20;
Ptr = &y;
*Ptr = 0x30;
}
La première instruction : int x, y; déclare x et y comme des entiers. Ensuite int *Ptr déclare Ptr comme un pointeur d’entiers.
On affecte la valeur hexadécimale 0xDA à x, puis la valeur 0xBF à y; et à Ptr l’adresse de x à l’aide de l’opérateur unaire & (Ptr = &x). La valeur stockée dans Ptr est égale à 3 (adresse de x).
L’instruction *Ptr = 0x20 affecte la valeur hexadécimale 0x20 au contenu de l’adresse pointée par Ptr, soit x.
Ensuite on affecte à Ptr l’adresse de y (c'est à dire 5), puis l’instruction *Ptr = 0x30 ; affecte la valeur 0x30 au contenu de l’adresse pointée par Ptr.
Pointeur de tableau :
Exemple :
L’instruction : int t[3] = {1, 2, 3}; déclare un tableau à trois éléments, puis l'initialise avec les valeurs contenues dans les accolades.
Tous les éléments de ce tableau occupent des cases mémoire consécutives.
Comme on peut le constater, int t[] est un tableau d’entiers (16bits d’espace mémoire pour chaque élément), ainsi le compilateur 8 bits réserve une adresse supplémentaire à chaque élément du tableau. Même si les adresses des ces éléments ne sont pas contiguës, elles sont bien consécutives.
Pour cela si on connait l’adresse du premier élément d’un tableau, il est plus facile de le parcourir en incrémentant ou en ajoutant un offset à cette adresse.
Cette opération peut aussi être réalisée avec des pointeurs.
Exemple :
Soient les deux déclarations suivantes :
int t[4] = {1, 2, 3, 4};
int *Ptr;
Il est possible d’initialiser le pointeur afin de pointer le tableau t, en utilisant une des trois méthodes suivantes :
Ptr = t ; // uniquement parce que t est un tableau
Ptr = &t ; // notation valable pour les tableaux et les variables
Ptr = &t[0] ; // car l’adresse d’un tableau c’est l’adresse de son premier élément
La première méthode affecte simplement la variable tableau à la variable pointeur. En langage C, lorsque l’identifiant d’un tableau est employé sans crochets et sans indices, il est considéré comme l’adresse du tableau, c’est-à-dire l’adresse de son premier élément. Donc on peut simplement écrire Ptr = t sans l’opérateur d’adresse &.
La deuxième méthode est plus explicite, avec la présence de l’opérateur &. Elle intéressante, dans un programme car on ne sera pas obligé de se souvenir si on a affaire à une variable ou pas.
La troisième méthode produit le même résultat que les deux autres méthodes.
Opérations arithmétiques avec les pointeurs :
Exemple :
int t[3] = {1, 2, 3} ;
int *Ptr ;
Ptr = &t ;
Ptr++ ;
Dans les lignes de codes précédentes, nous avons un tableau et un pointeur dont le rôle est d’identifier certains éléments en particulier dans le tableau. Un aspect très important sur l’usage des pointeurs avec les tableaux, est dû au fait qu’il suffit d’incrémenter un pointeur pour passer à l’élément qui suit dans le tableau, sans tenir compte du nombre d’octets occupés par cet élément.
L’instruction Ptr = &x ; met dans la variable pointeur, l’adresse du premier élément du tableau.
Ensuite Ptr++, déplace le pointeur sur l’élément suivant dans le tableau.
Incrémentation ou décrémentation selon le type de variable :
Une incrémentation ou une décrémentation d’un pointeur pourra ajouter ou soustraire le nombre d’octets occupé en mémoire par le type vers lequel il pointe.
Exemple :
Float y ;
Float *Ptr = &y ;
Ptr++ ;
Après l’instruction Ptr++, le pointeur Ptr va pointer vers l’adresse &y + 4, car les données du type float occupent 4 octets en mémoire. Ce calcul est fait automatiquement par le compilateur.
Saut selon le type de variable :
Additionner ou soustraire un nombre entier quelconque avec un pointeur, pourra modifier son contenu par un multiple du nombre d’octets occupé en mémoire par le type de données vers lequel il pointe.
Exemple :
int x ;
int *Ptr = &x ;
Ptr += 3 ;
Après l’instruction Ptr += 3 ; Ptr contiendra : &x + 6. Puisque le type int occupe 2 octets mémoire (donc 3 x 2 octets).
De même pour un type float, Ptr contiendra &x + 12 (3 x 4 octets).
Autre exemple :
{
int t[3] = {1, 2, 3} ;
int *Ptr = &t ;
*Ptr +=4 ;
Ptr++ ;
*Ptr = 0xFA ;
Ptr++ ;
*Ptr = 0x30 ;
Ptr -=2 ;
*Ptr = 0x20 ;
}
Après l’initialisation du tableau, puis du pointeur, celui-ci, contient l’adresse mémoire du premier élément de t soit t[0] ; on affecte à t[0] la valeur du contenu t[0] + 4 ; soit (1 + 4} = 5. Le contenu de t[0] vaut désormais 5.
Ensuite Ptr est incrémenté par l’instruction : Ptr++ ; Ptr contient maintenant l’adresse &t[0] + 2. Le pointeur Ptr pointe à présent sur l’adresse &t[1] ; l’instruction *Ptr = 0xFA ; affecte la valeur 0xFA au contenu de t[1]. De nouveau Ptr est incrémenté (Ptr++), puis Ptr pointe sur &t[2], puis on affecte au contenu de t[2] la valeur 0x30. L’instruction Ptr -=2, ramène le contenu du pointeur 4 adresses plus haut (car chaque élément de type int occupe 2 octets), soit &t[0] ; le contenu de cette adresse est écrasé par la valeur 0x20.
Règles de post incrémentation – décrémentation sur les pointeurs :
| Syntaxe | Opération | Exemple |
|
Ptr++ *Ptr++ *(Ptr++) |
Post incrémentation du pointeur |
z = *(Ptr++) ; Equivaut à : z = *Ptr ; Ptr = Ptr +1 ; |
| (*Ptr)++ | Post incrément des données pointées par le pointeur |
z = (*Ptr)++ ; Equivaut à : z = *Ptr ; *Ptr = *Ptr + 1 ; |
L’association de l’opérateur * et l’opérateur ++ pour un pointeur pose problème car tous les deux ont le même niveau de priorité. Dans ce cas c’est la règle d’associativité qui prévaut, ainsi ++ est prioritaire sur *. Lorsque le terme *(Ptr++) est utilisé dans une expression, la valeur des données pointées par Ptr sera d’abord extraite, puis le pointeur sera incrémenté.
Pour opérer sur les données pointées par le pointeur, il est nécessaire de mettre l’opérateur * et le pointeur entre parenthèses. Ainsi (*Ptr)++ équivaut à x++, si Ptr pointe vers x.
Exemple :
Soit le code suivant :
{
int x[3] = {1, 2, 3} ;
int y ;
int *Ptr = &x ;
y = 5 + *(Ptr++) ;
y = 5 + (*Ptr)++ ;
}
Le tableau x à trois éléments est déclaré ; ainsi que la variable y. Le pointeur Ptr est déclaré et aussitôt affecté par l’adresse de x (qui n’est rien d’autre que l’adresse du premier élément du tableau).
Dans l’instruction y = 5 + *(Ptr++) ; la valeur de la donnée pointée par Ptr (=1) est d’abord extraite, puis ajoutée à 5, après l’addition, puis p est incrémenté. y = 6.
A présent Ptr pointe désormais sur l’adresse du deuxième élément du tableau (x[1]).
L’instruction y = 5 + (*Ptr)++ ; la valeur pointée par Ptr est d’abord extraite puis ajoutée à 5 ; y = 7. Ensuite le contenu de l’adresse pointée par Ptr est incrémenté ; x[1] = 2+1 = 3.
Règles de pré incrémentation – décrémentation :
| Syntaxe | Opération | Exemple |
|
++Ptr ++*Ptr+ *(++Ptr) |
Pré incrémentation du pointeur |
z = *(++Ptr) ; Equivaut à : Ptr = Ptr + 1 ; z =*Ptr ; |
| ++(*Ptr) | Pré incrémentation de données pointées par le pointeur |
z = ++(*Ptr) ; Equivaut à : *Ptr = *Ptr + 1 ; z = *Ptr ; |
L’opération de pré incrémentation fonctionne comme la post incrémentation, sauf que dans le cas présent, l’incrémentation se fait avant toute opération sur Ptr.
Exemple :
{
int x[3] = {1, 2, 3} ;
int y ;
int *Ptr = &x ;
y = 5 + *(++Ptr) ;
y = 5 + ++(*Ptr) ;
}
Dans cet exemple, le préfix signifie qu’il sera utilisé avant l’utilisation du pointeur. Après les deux premières lignes de déclaration de x et de y, le pointeur est déclaré puis initialisée à l’adresse de x. Il pointe sur x[0].
Le pointeur est d’abord incrémenté ; il pointe sur x[1].
y = 5 + *Ptr = 5 + 2 = 7.
Pour l’instruction qui suit : y = 5 + ++(*Ptr) ; on va d’abord incrémenter les données pointées par Ptr (données de x[1] : 2 + 1) ; ensuite la valeur pointée par Ptr est ajoutée à 5, soit :
y = 5 + *Ptr
y = 5 + 3 = 8.
En résumé, les parenthèses déterminent ce qui pourra être incrémenté ou décrémenté :
Si c’est le pointeur Ptr:
En pré incrémentation ce sera : *(++Ptr) ou *++Ptr ou ++Ptr ;
En post incrémentation : *(Ptr++) ou *Ptr++ ou Ptr++ ;
Si c’est la valeur des données x pointées par le pointeur Ptr :
Ce sera ++(*Ptr) en pré incrémentation et (*Ptr)++ en post incrémentation.
Pointeurs et fonctions :
Soit le programme ci-dessous :
int x = 2, y = 0 ;
// Et la fonction
int carre(int n)
{
return n *=n ;
}
int main(void)
{
y = carre(x) ;
}
Lorsque le programme principal main() appelle la fonction carre(), elle lui passe la variable x comme paramètre, puis carre() en fait une copie pour effectuer le traitement dont elle en a la charge et la variable originale ne change pas. En somme, x = 2. Cette valeur servira pour calculer y après l’appel de la fonction carre() ; et la valeur de x n’a pas été changée par la fonction.
Mais dans certaines situations, le programmeur souhaiterait que la fonction puisse modifier une variable. Pour ce faire on utilise un pointeur en paramètre de cette fonction. En d’autres termes c’est un passage par référence.
Exemple :
int x = 2, y = 0 ;
// Et la fonction
int carre(int *n)
{
return n *= *n ;
}
int main(void)
{
y = carre(&x) ;
}
Remarque : une fonction avec un pointeur en paramètre peut être appelée de deux façon différentes :
- Soit en passant l’adresse de la variable en paramètre ;
- Soit en passant le pointeur en paramètre.
Chaînes de caractères et pointeurs :
Précédemment nous avons vu qu’une chaîne de caractères n’est rien d’autre qu’un tableau de caractères.
Un pointeur peut servir pour manipuler une chaîne de caractères. La chaîne crée est stockée en mémoire, tant dis que le pointeur pointera sur l’adresse du premier caractère de la chaîne.
Exemple de déclarations d’une chaîne de caractères avec un pointeur:
char *str = "Hello" ;
char *str = "Processor" ;
Le pointeur str pointe sur le premier caractère de la chaîne. On peut accéder à un caractère quelconque de la chaîne en ajoutant un offset au pointeur :
Ainsi (*str + 4) correspond à la lettre e.
L’initialisation d’un pointeur et d’un tableau par une chaîne de caractères se fait de la même manière.
Par exemple pour un pointeur :
char *str = "Hello" ;
Pour un tableau :
char str[] = "Hello" ; ou char str[6] = "Hello" ;
Toute chaîne de caractère se terminera dans les deux cas par le caractère null \0.
Comparaison de deux chaînes de caractères :
Supposons que la variable str contient la chaîne Hello ; pour effectuer une comparaison entre la chaîne Hello et une autre chaîne allo par exemple, on serait tenté de faire le test :
if(str == "allo")
{
…;
}
Mais cela pose problème car le compilateur va comparer l’adresse contenue dans str et celle de la chaîne "allo".
Pour comparer deux chaînes de caractères en langage C, il faudrait utiliser la fonction prédéfinie strcmp(), de la librairie string.h.
Cette fonction peut comparer deux chaînes de caractères, caractère par caractère. Elle renvoie la valeur 0 si les deux chaînes sont égales, ou un résultat différent de 0 dans le cas contraire.
Exemple :
# include "string.h"
# include "stdio.h"
char *str = "Processor" ;
int main(void)
{
if ( 0 == strcmp(str, "Processor"))
{
printf("Ok! \n") ;
}
while(1) ;
}
Tableaux de pointeurs :
Un tableau de pointeur est un simple tableau dont les éléments sont des pointeurs.
Exemple e déclaration :
char *Ptr[4] ;
Ptr est un tableau de 4 pointeurs de caractères. Chaque pointeur peut être initialisé par une chaîne de caractère :
Ptr[0] = "Yes" ;
Ptr[1] = "No" ;
Ptr[2] = "Power" ;
Ptr[3] = "Off" ;
On peut aussi appliquer les règles de déréférencement, pour utiliser une valeur pointée, par un élément d’un tableau de pointeurs en écrivant par exemple : x = *Ptr[0].
Exemple :
int i = 0 ;
char *str = [] = {"Zero", "One", "Two", "Tree", "Four", "\0"} ;
int main(void)
{
while(*str[i] != « \0 »
{
printf(« %s\n », str[i++]) ;
while(1) ;
}
}
Pour finir :
Les tableaux sont fréquemment traité comme des pointeurs ;
L’adresse d’un tableau équivaut à l’adresse de son premier élément.
Il est possible à l’aide d’un pointeur de passer une variable par référence à une fonction.
Pointeurs de fonctions :
En langage C, il est possible de placer le nom d’une fonction dans une variable : de plus ce nom est traduit par le compilateur en adresse de cette fonction.
D’où la possibilité des pointeurs à pouvoir pointer avec des adresses de fonctions, ou dans certaines situations de passer une fonction à une autre fonction.
Une fonction pointeur est déclarée comme un prototype de fonction.
Exemple :
int (*fptr) (int x) ;
Cette déclaration spécifie que :
fptr est un pointeur sur une fonction à un paramètre x de type int, fournissant un résultat de type int.
Une fonction pointeur (comme (*fptr)), est initialisée en configurant le nom de la fonction égal au nom du pointeur.
Exemple :
Si on effectue la déclaration suivante :
int (*fp) (int x) ; // fonction pointeur
int baar(int x) ; // prototype de fonction
La fonction pointeur peut être initialisée comme suit :
fp = foo ; // A présent fp pointe vers foo
Appel de fonction via une fonction pointeur :
La fonction pointée par fp peut être appelée de la façon suivante :
y = fp(x) ;
Ce qui pourrait se faire directement par :
y = baar(x) ;
Exemple :
int x, y ;
int add(int a, int b) ; // prototype de fonction
int sub(int a, intb) ; // prototype de fonction
// Déclaration d’une fonction avec un pointeur de fonction
int foo(int a, int b, int (*fp) (int, int)) ;
{
return (fp(a, b));
}
int main(void)
{
x = foo(5, 12, &add) ; // passe l’adresse de add
y = foo(5, 12, &sub) ; // passe l’adresse de sub
}