Adresses

Chaque octet qui compose la mémoire est identifié par une valeur numérique : c'est son adresse. Le premier octet a pour adresse 0, et chaque octet qui suit est numéroté ainsi dans l'ordre croissant.

int main(void) {
  int x = 12;
  ...
}
À l'exécution de ce programme, il choisit quatre octets consécutifs pour stocker la valeur de x. Un test de ce programme révéle que x occupe les octets numéro 140737081300956, 140737081300957, 140737081300958, et 140737081300959. On dit alors que l'adresse de x est 140737081300956.

Ce n'est qu'un exemple : si on exécute à nouveau le même programme, il choisira un endroit différent pour stocker x. On ne peut pas prédire le choix d'une adresse.

int main(void) {
  int x;
  printf("%p\n", &x);
  return EXIT_SUCCESS;
}
Nous avons déjà vu que l'on peut obtenir l'adresse choisie pour une variable grâce à l'opérateur &. C'est par ce biais que nous pouvons par exemple transmettre à la fonction scanf l'adresse de la variable dont on souhaite modifier la valeur.

L'affichage d'une adresse requiert un format spécifique dans printf : %p. Comme les adresses sont souvent de très grands entiers, ils sont automatiquement affichés en représentation héxadécimale.

Une adresse est une donnée comme une autre, et comme toutes les autres données du programme, elle peut donc être stockée dans une variable.

int main(void) {
  double x;
  double* ptr;
 
  ptr = &x;
  printf("%p\n", ptr);
  return EXIT_SUCCESS;
}
Un pointeur est tout simplement une variable utilisée pour stocker une adresse (comme ptr dans l'exemple ci-dessus). Lorsqu'un pointeur contient l'adresse d'une variable, on dit qu'il pointe sur cette variable (dans l'exemple, ptr pointe vers x).

Chaque type de donnée correspond à un type d'adresse différent : l'adresse d'une donnée de type double est elle-même une donnée de type double*. Il suffit tout bêtement de rajouter une étoile à la fin d'un nom de type pour obtenir le nom du type d'adresse associé.

Malgré cela, tous les types d'adresse sont affichés grâce au format %p dans printf. Il n'y a pas besoin de plus parce que dans tous les cas, ce sont des entiers naturels stockés sur 8 octets (ou 4 sur une machine 32 bits).

Comme les autres variables, un pointeur devrait toujours être initialisé. Si vous n'avez pas de valeur à lui donner au moment de la déclaration, il est recommandé d'utiliser la constante NULL définie dans stdlib.h. Celle-ci vaut 0, et son type (void*) est converti sans inconvénient vers n'importe quel type d'adresse.

int main(void) {
  double* ptr = NULL;
  ...
}

Nous savons que l'opérateur & nous donne l'adresse d'une variable, mais il nous reste à voir comment nous en servir.

int main(void) {
  double x = 7.63;
  double* ptr = &x;
 
  *ptr = 5.41;
  printf("%f\n", x);
  return EXIT_SUCCESS;
}
L'opérateur * (qui n'a rien à voir avec la multiplication dans ce cas) permet de manipuler une variable à travers son adresse plutôt que son nom.

Vous pouvez lire l'expression &x comme « l'adresse de x » et l'expression *ptr comme « le pointé de ptr ». Ces deux opérateurs sont donc exactement l'inverse l'un de l'autre.

Remarque Attention aux conséquences désastreuses de l'usage de * sur une mauvaise adresse. Au mieux, l'adresse défectueuse appartient à un segment protégé et le programme sera interrompu par une Segmentation Fault (c'est le cas pour l'adresse NULL). Au pire, la mauvaise variable sera modifiée, provoquant un comportement anormal très difficile à corriger.

  1. Cartographie. Écrivez un programme qui déclare des variables de type

    • float
    • double
    • long double
    • char
    • short int
    • int
    • unsigned long int
    puis qui affiche l'adresse de chacune d'entre elles.

    Faites le plan de la mémoire comme si c'était une avenue et placez-y les variables à l'aide des adresses obtenues. Que constatez-vous ? Faites plusieurs exécutions d'affilée : qu'est-ce qui ne change pas ?

  2. Alphabet. Sans l'exécuter, prédisez ce que ce programme va afficher :

    int main(void) {
      char min, maj;
      char *p = NULL;
     
      for(min = 'a', maj = 'A'; maj <= 'Z'; min++, maj++) {
        p = (p == &min) ? &maj : &min;
        putchar(*p);
      }
      putchar('\n');
      return EXIT_SUCCESS;
    }
    Une fois votre prédiction couchée par écrit, exécutez ce programme pour vérifier votre raisonnement.

  3. Mort-vivant. Considérez le programme suivant :

    #include <stdlib.h>
    #include <stdio.h>
    #include <time.h>
     
    int main(void) {
      int* p;
     
      if(time(NULL)%2) {
        int x = 59;
        p = &x;
      } else {
        int y = 31;
        p = &y;
      }
      /* fragment inactif
      printf("x=%d\n", x);
      printf("y=%d\n", y);
      */
      printf("%d\n", *p);
      return EXIT_SUCCESS;
    }
    Quelle valeur pensez-vous voir s'afficher ? Dans quelle variable cette valeur était-elle stockée au moment de l'affichage ? Si vous remettez dans le programme les instructions en commentaire, que constatez-vous ? Avez-vous repéré le mort-vivant ?

    Remarque Une telle situation est nommée « dangling pointer» (pointeur ballant). Il faut à tout prix l'éviter, car utiliser de la mémoire non réservée ne peut amener que des ennuis.

  4. Conversion. Voici une façon très étrange d'afficher π :

    int main(void) {
      long int n = 4614256656552045848L;
      double* p = (double*) &n;
      printf("π = %f\n", *p);
      return EXIT_SUCCESS;
    }
    Le format %f et le type du pointeur p conduisent printf à penser que la valeur qui lui est fournie est un double. Il interprète donc les bits qui la composent (qui sont pourtant les bits de n) comme s'ils décrivaient un réel. Il ne s'agit pas vraiment d'une conversion mais plutôt d'une réinterprétation.

    Quelle valeur faut-il donner à n pour que ce programme affiche 2π ? Vous pouvez écrire un autre programme pour vous aider à trouver la réponse.

  5. Étapes. Considérez (sans l'exécuter) le programme suivant :

    int main(void) {
      int a = 1, b = 2, c = 3;
      int *p, *q;
     
      p=&a;
      q=&c;
      *p=(*q)++;
      p=q;
      q=&b;
      *p-=*q;
      ++*q;
      *p*=*q;
      a=++*q**p;
      p=&a;
      *q=*p/(*q);
      return EXIT_SUCCESS;
    }
    Simulez sur papier le déroulement du programme. Après l'exécution de chaque instruction, répondez aux questions suivantes :
    • Que contiennent a, b et c ?
    • Vers quoi pointent p et q ?

    Vous pouvez tester vos réponses en exécutant réellement le programme, auquel vous aurez rajouté quelques appels à printf.

    Remarque Ce programme est spécifiquement écrit pour vous donner du fil à retordre. Il n'est absolument pas représentatif de l'usage habituel des pointeurs.

retour à la page d'accueil

retour au sommet