Participer au site avec un Tip
Rechercher
 

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 :

Utilisation de pointeurs

Les fonctions Gestion dynamique de la mémoire


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.

Qu'est-ce qu'un pointeur ?

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.

Passage de paramètres de fonctions par pointeurs

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;

}
Tentative de codage d'une fonction de permutation de paramètres.

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;

}
Fonction de permutation de paramètres passés par pointeurs.

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;

}
Exemple d'un pointeur sur zone constante.

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.
            ^
$> 

L'arithmétique des pointeurs

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;

}
Exemple d'utilisation de l'arithmétique des pointeurs

Les opérateurs suivants peuvent donc être utilisés sur un pointeur : +, -, ++ et --.

Comparatif entre tableaux et pointeurs

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;

}
Exemple de manipulation de tableaux via la « syntaxe pointeurs »

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

Exemples d'utilisation de pointeurs appliqués aux chaînes de caractères

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;

}
Exemple de manipulation de chaînes de caractères via les pointeurs

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
$> 


Les fonctions Gestion dynamique de la mémoire