talpa

About Benoit

Benoit

Send message

Biography

Voici l’explication sur les pointeurs que j’ai rédigé pour l’exercice du cours sur le C.

Mémoire et adresse

Pour bien comprendre les pointeurs, il faut avoir compris que tout se passe en mémoire. Lorsque l’on défini une variable comme ceci :

int ma_variable = 10 ;

Celle-ci est stocké quelque part dans la mémoire, la question est de savoir où. C’est ici qu’intervient la notion d’adresse. La mémoire pour un ordinateur est un gigantesque tableau d’octets. Une adresse est alors simplement un numéro, l’indice d’une case dans ce tableau. Quand on stocke une donnée qui est plus grande qu’un octet (et ça arrive souvent), celle-ci s’étale sur plusieurs case et par abus de langage on dit que l’adresse de cette donnée est le numéro de la première case qu’elle utilise.

Exemple : supposons que sur notre machine, une variable de type int fasse 4 octets, et que ma_variable soit stocké à l’adresse 2686700, on a alors quelque chose comme ceci :

   adresse       contenu
   
|     .     |      ???      |
|     .     |      ???      |
+-----------+      ???      +
|  2686699  |      ???      |
+-----------+---------------+
|  2686700  |               |
+-----------+               +
|  2686701  |  ma_variable  |
+-----------+               +
|  2686702  |               |
+-----------+     (10)      +
|  2686703  |               |
+-----------+---------------+
|  2686704  |      ???      |
+-----------+      ???      +
|     .     |      ???      |
|     .     |      ???      |

On n’a pas vraiment à savoir comment fait l’ordinateur pour savoir qui est où dans la mémoire, et ça dépasse de toute façon largement le cadre du C. Il nous suffit de savoir, en tant que programmeur, que nos variables sont stocké à une certaine adresse.

En C, on peut connaître l’adresse d’une variable à l’aide de l’opérateur &.

#include <stdio.h> 

int main(void)
{
    int ma_variable = 10;
    
    printf("ma_variable: %d\n", ma_variable);
    printf("adresse de ma_variable: %d\n", &ma_variable);
    return 0; 
}

Ce code donne chez moi :

ma_variable: 10
adresse de ma_variable: 2686700

Il y a de forte chance pour l’adresse affiché ne soit pas la même chez toi, il est même possible que l’adresse ne soit pas la même si tu lance le programme plusieurs fois. Mais peu importe, ce qui est important de constater c’est que l’adresse est un nombre, j’allais dire comme un autre mais ce n’est pas tout à fait vrai. D’ailleurs le code que j’ai donné est faux, ton compilateur a peut être un peu pleurniché avec une histoire de format et de int*. C’est tout à fait normal et je vais expliquer pourquoi.

Le problème est que le format %d de printf() sert à afficher un entier de type int, or pour afficher une adresse il faut utiliser le format %p. Modifie donc le code précédent en conséquence, si tu le relance tu vas voir que l’adresse est un truc bizarre avec des lettres. En fait c’est juste un nombre, mais écrit en base 16 (hexadécimal), c’est simplement une habitude de programmeur que d’écrire les adresses en hexadécimal.

Ce qu’il faut retenir de tout ça c’est que :

  • Les variables ont une adresse
  • Ces adresses sont des nombres entier (mais pas de type int)

Mais alors, si une adresse est un nombre ça veut dire qu’on peut le mettre dans une variable. Pas dans un int par contre, ce n’est pas le bon type, c’est ici qu’on va avoir besoin des pointeurs.

Les types pointeurs

Un pointeur est une variable contenant une adresse. Le type exact du pointeur dépend du type de la variable pointé. L’adresse d’un int est de type int*, l’adresse d’un float est de type float*, l’adresse d’un char est de type char*, etc.. Ainsi parler simplement d’un « pointeur » est un peu ambigüe, il faudrait dire « pointeur sur int » ou « pointeur sur float », etc..

Par exemple dans le code précédent, l’expression &ma_variable est de type int*, on peut donc la stocker dans un pointeur sur int, on peut également l’afficher.

int* pt_int = &ma_variable;
printf("contenu de pt_int: %p\n", pt_int);

Ce qui est vraiment intéressant, c’est que l’on peut accéder une variable via son adresse, on appel ça un déréférencement et en C cela se fait avec l’opérateur *. Ici, *pt_int vaut donc le contenu de la variable pointé, par pt_int, c’est à dire 10. Et modifier *pt_int, revient à modifier directement la variable pointé. Voici un exemple complet :

#include <stdio.h> 

int main(void)
{
    int ma_variable = 10;
    int* pt_int = &ma_variable;
    printf("ma_variable: %d\n", ma_variable);
    printf("adresse de ma_variable: %p\n", &ma_variable);
    printf("contenu de pt_int: %p\n", pt_int);
    printf("dereferencement de pt_int: %d\n", *pt_int);
    
    *pt_int = 13;
    printf("ma_variable vaut maintenant %d\n", ma_variable);
    return 0; 
}

Attention aux différents cas d’utilisation de l’étoile :

  • dans une déclaration c’est pour dénoter un type pointeur, ex : int* pt;

  • En tant qu’opérateur unaire dans une expression c’est un déréférencement, ex : printf("%d\n", *pt);

  • En tant qu’opérateur binaire dans une expression c’est une multiplication, ex : printf("%d\n", a*b);

Il est maintenant peut être plus facile de comprendre pourquoi un pointeur doit toujours être associé à un type, c’est pour être sûr à quoi on a affaire lorsque l’on déréférence celui-ci. Si on stocke l’adresse d’un float dans un int*, alors lorsque l’on va déréférencer ce pointeur, le compilateur croira avoir affaire à un int et le résultat sera n’importe quoi (cela peut même faire planter purement et simplement le programme).

Histoire d’être tordu, on peut considérer l’adresse de pt_int dans le code précédent. C’est une variable après tout, on peut donc prendre son adresse, et le résultat obtenu est alors un pointeur de pointeur de int donc de type int**.

Si ça commence à faire des nœuds au cerveau c’est que le concept n’est pas bien intégré. Il ne faut pas hésiter à y réfléchir longtemps, laissez passer quelques heures/jours et y revenir.

Passage par copie et références

Ici on va voir une des principales utilités des pointeurs.

En C, le passage d’argument à une fonction se fait toujours par copie (on dit aussi par valeur), c’est à dire que on n’envoi pas de variable mais seulement une copie de celle-ci. Considère le code suivant par exemple :

#include <stdio.h> 

void incremente(int n)
{
    n += 1;
}

int main(void)
{
    int ma_variable = 10;
    printf("%d\n", ma_variable); // 10
    incremente(ma_variable);
    printf("%d\n", ma_variable); // toujours 10
    return 0; 
}

La fonction incremente() n’affecte pas la variable ma_variable puisque seule une copie de celle-ci est passé en paramètre. Pour qu’une fonction puisse effectivement agir sur une variable extérieur, avoir sa valeur ne suffit pas, elle doit avoir une référence vers cette variable.

Malheureusement la notion de référence n’existe pas en C, mais on peut ruser. L’idée est de passer en paramètre l’adresse de la variable, c’est à dire de passer un pointeur (avec le type approprié) contenant l’adresse de notre variable. Bien sur la fonction ne va avoir qu’une copie du pointeur mais en le déréférençant on va bien retrouver la variable qui nous intéresse et pouvoir faire les modifications que l’on veut.

#include <stdio.h> 

void incremente(int* n)
{
    *n += 1;
}

int main(void)
{
    int ma_variable = 10;
    printf("%d\n", ma_variable); // 10
    incremente(&ma_variable); // on passe l’adresse (de type int*)
    printf("%d\n", ma_variable); // 11
    return 0; 
}

Ici on aurait pu se débrouiller sans pointeurs, en faisant renvoyer par incremente() la nouvelle valeur mais il y des cas où ce n’est pas possible, si on veut modifier plusieurs variables par exemple. Ce cas n’est pas si tordu qu’il n’y parait, la fonction scanf() utilise bien ce principe.

Signature

per aspera ad astra – comp.lang.c FAQexplication pointeur

Account information

Signup date: January 31, 2014

Last sign-in: June 21, 2016