Accès rapide :
Qu'est-ce qu'un pointeur ?
Passage de paramètres de fonctions par pointeurs
L'arithmétique des pointeurs
Comparatif entre tableaux et pointeurs
Exemples d'utilisation de pointeurs appliqués aux chaînes de caractères
Souvent les débutants pensent que le concept de pointeur est quelque chose de très complexe à appréhender. C'est dommage, car en réalité, il n'en est rien. Un pointeur est tout simplement une adresse en mémoire. Afin de bien comprendre le concept, nous allons étudier quelques premiers exemples simples avant de passer à des exemples plus sérieux dans les chapitres qui suivent.
Donc, un pointeur est une adresse en mémoire. En C, un pointeur est associé à un type. Analysons l'exemple suivant :
int value = 10; int * pointer = &value; // Contient l'adresse en mémoire de la variable value printf( "value == %d at %p\n", value, pointer ); *pointer = 15; printf( "value == %d at %p\n", value, pointer );
L'exemple précédent créé une variable entière, appelée value
et y stocke la valeur 10. La seconde variable est de type pointeur sur entier
(int *
). Pour calculer l'adresse de quelque chose, il faut utiliser l'opérateur unaire (qui accepte un seul opérande) &
.
La variable pointer
contient donc l'adresse de la variable value
. Notez que le premier appel à la fonction printf
affiche bien la valeur de la variable ainsi que l'adresse (%p
) du mémoire de cette variable.
Ensuite, on cherche à changer la valeur référencée par le pointeur. Pour ce faire, on utilise la syntaxe *pointer
qui représente la donnée
pointée, que l'on remplace ici par 15. Pour le second appel à la fonction printf
, l'adresse n'aura donc par changée, par contre la valeur
de la variable value
sera maintenant 15
.
Si ce petit exemple est bien compris, on peut passer à la suite : à savoir, à quoi peut concrètement servir un pointeur ? Pour répondre à cette question, nous allons, dans un premier temps, parler de passage de paramètres de fonctions.
Pour comprendre en quoi les pointeurs peuvent être utiles pour un passage de paramètres d'une fonction, analysons le programme suivant : il cherche à permuter les valeurs de deux variables.
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 |
#include <stdio.h> #include <stdlib.h> /* * Une fonction de permutation de ses paramètres. */ void swap( int first, int second ) { printf( "first==%d, second==%d\n", first, second ); // Résultat: first==10, second==20 int temp = first; first = second; second = temp; printf( "first==%d, second==%d\n", first, second ); // Résultat: first==20, second==10 } /* * Fonction main : le point d'entrée de votre programme. */ int main() { int a = 10; int b = 20; swap( a, b ); printf( "a==%d, b==%d\n", a, b ); // Résultat: a==10, b==20 return EXIT_SUCCESS; } |
Si vous compilez de programme (gcc -o pointer pointer.c
) et que vous le lancez, le résultat sera sans appel : ça ne marche pas.
Effectivement, la variable a
contiendra toujours la valeur 10 et la variable b
contiendra 20. Mais pourquoi ?
En fait, les choses sont simples : durant l'appel de la fonction swap
, les paramètres correspondent à des nouveaux espaces mémoires
empilés sur la « stack » (la pile d'exécution). Ces espaces mémoire sont initialisés en y copiant les valeurs des variables d'origines
(celle définies dans la fonction main
). Du coup, ce que l'on permute, ce sont les copies des variables, mais pas celles d'origines.
Si l'on veut que la permutation soit effective, ce qu'il faut passer à la fonction swap
, ce ne sont pas des copies des valeurs
initiales, mais des pointeurs vers ces variables. Ce nouvel exemple montre comment utiliser des pointeurs pour répondre à notre besoin de
permutation.
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 |
#include <stdio.h> #include <stdlib.h> /* * Une fonction de permutation de ses paramètres. */ void swap( int * first, int * second ) { int temp = *first; *first = *second; *second = temp; } /* * Fonction main : le point d'entrée de votre programme. */ int main() { int a = 10; int b = 20; swap( &a, &b ); printf( "a==%d, b==%d\n", a, b ); // Résultat: a==10, b==20 return EXIT_SUCCESS; } |
Voici le résultat produit par cet exemple :
$> gcc -o pointer pointer.c $> pointer a==20, b==10 $>
Une autre situation ou le passage de paramètres pourra être réalisé par pointeurs, c'est quand la fonction attend des données basées sur des types structures. Effectivement, une structure peut occuper plusieurs octets en mémoire. Le fait de passer un pointeur en lieu et place d'une valeur basée sur une structure, permettra de réduire la consommation de données sur la pile d'exécution. Nous verrons des exemples présentant cette possibilité dans un prochain chapitre (Manipulation de listes chaînées).
Eventuellement, il vous sera possible qualifier la zone pointée comme étant constante (via le mot clé const
) si vous ne souhaitez
pas permettre de modification de la donnée durant l'appel de la fonction.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <stdio.h> #include <stdlib.h> void function( const int * value ) { printf( "%d\n", *value ); // Can read *value = 0; // Cannot write. Content of value cannot be writed. } /* * Fonction main : le point d'entrée de votre programme. */ int main() { int a = 10; function( &a ); return EXIT_SUCCESS; } |
Voici ce qui se passe si l'on cherche à compiler cet exemple.
$> gcc -o pointer pointer.c pointer.c: Dans la fonction 'function': pointer.c:7:12: erreur : assignment of read-only location '*value' *value = 0; // Cannot write. Content of value cannot be writed. ^ $>
Il vous est possible d'utiliser des opérateurs directement sur des pointeurs (pas sur les valeurs pointées, mais bien sur les pointeurs) :
on parle d'arithmétique des pointeurs. L'exemple ci-dessous présente cette possibilité. Il faut comprendre que un +1
sur un
pointeur ne déplace pas le pointeur d'un octet en mémoire, mais de la taille d'un élément derrière le pointeur (en fonction de son type).
Donc dans l'exemple proposé, on déplace le pointeur de sizeof(int)
octets : on se retrouve donc sur le second entier du tableau.
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 |
#include <stdio.h> #include <stdlib.h> #define SIZE 3 /* * Fonction main : le point d'entrée de votre programme. */ int main() { // Un tableau est un pointeur int tb[] = { 10, 20, 30 }; // On affiche tous les éléments du tableau for( int i=0; i<SIZE; i++ ) { printf( "%d ", *(tb+i) ); } printf( "\n" ); // On modifie la valeur du second élément du tableau *(tb+1) = 100; // On affiche de nouveau le tableau for( int i=0; i<SIZE; i++ ) { printf( "%d ", *(tb+i) ); } printf( "\n" ); return EXIT_SUCCESS; } |
Les opérateurs suivants peuvent donc être utilisés sur un pointeur : +
, -
, ++
et --
.
Donc, comme nous venons de le voir, un tableau est en fait un pointeur vers le premier élément de la collection. Une première conclusion s'impose : comme un tableau n'est qu'un pointeur, il n'est pas possible de retrouver la taille du tableau via ce pointeur. En d'autres termes, comme le tableau ne stocke pas sa taille, il est de votre responsabilité de maintenir quelque part cette information.
Autre remarque : pour manipuler un tableau, vous aurez toujours le choix entre deux syntaxes : la syntaxe tableau ou la syntaxe pointeur. C'est que nous tentons de démontrer avec l'exemple suivant. Un tableau est créer avec un certain nombre de valeurs, puis nous tentons de mettre la valeur de chaque élément du tableau à zéro. Deux fonctions (avec deux syntaxes différentes) vous sont proposées pour effectuer cette tâche : en fait les deux formes d'écriture sont strictement équivalentes, ce n'est qu'une histoire de point de vue (de syntaxe préférée). Après compilation, les deux fonctions produites sont strictement les mêmes.
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
#include <stdio.h> #include <stdlib.h> #define SIZE 5 /* * Fonction d'affichage d'un tableau (syntaxe pointeur) */ void displayIntArray_PTR( int * array, size_t size ) { for( size_t i=0; i<size; i++ ) { printf( "%d ", *(array+i) ); } printf( "\n" ); } /* * Fonction d'affichage d'un tableau (syntaxe tableau) */ void displayIntArray_TB( int * array, size_t size ) { for( size_t i=0; i<size; i++ ) { printf( "%d ", array[i] ); } printf( "\n" ); } /* * Fonction de réinitialisation d'un tableau (syntaxe pointeur) */ void clearIntArray_PTR( int * array, size_t size ) { for( size_t i=0; i<size; i++ ) { *(array+i) = 0; } } /* * Fonction de réinitialisation d'un tableau (syntaxe tableau) */ void clearIntArray_TB( int * array, size_t size ) { for( size_t i=0; i<size; i++ ) { array[i] = 0; } } /* * Fonction main : le point d'entrée de votre programme. */ int main() { // Un tableau est un pointeur int tb[] = { 10, 20, 30, 40, 50 }; displayIntArray_PTR( tb, SIZE ); clearIntArray_PTR( tb, SIZE ); displayIntArray_PTR( tb, SIZE ); //displayIntArray_TB( tb, SIZE ); //clearIntArray_TB( tb, SIZE ); //displayIntArray_TB( tb, SIZE ); return EXIT_SUCCESS; } |
Note : le type size_t
est défini par la librairie standard et est équivalent à un entier non signé (donc forcément positif).
C'est donc le type adéquat pour stocker une information de taille (ici de taille de notre tableau).
Pour finir cette présentation des pointeurs, nous allons travailler sur un dernier exemple appliqué aux chaînes de caractères. Une chaîne de caractères est en fait un tableau de caractères : donc les pointeurs vont pouvoir aussi être utilisés pour cet exemple. Il existe néanmoins une différence avec l'exemple précédent : une chaîne de caractères en C est qualifiée d'AZT (A Zéro Terminal). Cela veut dire qu'un code ASCII (American Standard Coding for Information Interchange) 0 est utilisé pour terminer la chaîne. Il n'est donc pas requis de conserver la taille de la chaîne en permanence car elle peut être retrouvée par ce caractère ASCII de fin de chaîne.
Note : le code ASCII nul n'étant pas simplement accessible à partir de votre clavier, il a été proposé de le représenté par la séquence de
caractères suivante : '\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 40 41 42 43 44 45 46 47 48 49 50 |
#include <stdio.h> #include <stdlib.h> /* * Fonction de calcul de la taille d'une chaine de caractères : * on boucle jusqu'à atteindre le caractère '\0' (la fin de chaîne). */ size_t myStrLen( const char * string ) { size_t size = 0; while ( *string != '\0' ) { string++; // On déplace le pointeur sur le caractère suivant size++; } return size; } /* * Fonction de mise en majuscules d'une chaîne de caractères ASCII. * Dans la table ASCII : * 'A' == 65 'a' == '97' * Il y a donc un écart de 32 (2^5) entre les deux plages de caractères. * En retirant le bit 5 d'une lettre minuscule on passe donc en majuscule. */ void toUpperCase( char * string ) { while ( *string != '\0' ) { if ( *string >= 'a' && *string <= 'z' ) { *string &= 223; // 223 == 255 - 32 == 255 - 2^5 } string++; // On déplace le pointeur sur le caractère suivant } } /* * Fonction main : le point d'entrée de votre programme. */ int main() { char str[] = { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0' }; printf( "%s\n", str ); printf( "myStrLen(str) == %lu\n", myStrLen( str ) ); toUpperCase( str ); printf( "%s\n", str ); return EXIT_SUCCESS; } |
ATTENTION : ne tentez pas de définir la variable str
ainsi : char * str = "Hello World";
. Dans ce
cas la variable str
pointe sur une chaine de caractères constante de votre programme : elle est alors noyée dans la zone de mémoire
du code exécutable de votre programme et cette zone de mémoire est READ ONLY. Si vous le faite, vous aboutirez à un plantage de
votre programme. Avec la déclaration proposée, la chaîne de caractères est placée sur la stack (la pile d'exécution). C'est une zone de
mémoire en accès lecture/écriture : il sera donc possible de transformer la chaîne de caractères en majuscules.
Voici ce que donne l'exécution de ce programme :
$> gcc -o pointer pointer.c $> pointer Hello World myStrLen(str) == 11 HELLO WORLD $>
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :