Des pointeurs et des hommes (1)

Next

Chez la plupart des étudiants, le pointeur est un des concepts du C qui fait peur. Et le pire, c’est que je ne peux même pas leur jeter la pierre pour ça : si vous jetez un petit coup d’œil aux supports de cours qui traînent ça et là sur le web, certains sont vraiment merdiques et la grande majorité utilise un vocabulaire à coucher dehors. Pourtant, il n’y a pas plus simple ! Alors installez vous confortablement, prenez un petit café, et voyons ensemble si je peux faire un peu mieux que le professeur Fomblu moyen !

Tout est mémoire

Commençons par un petit programme tout simple :

#include <stdio.h>

int main() {
    int A ;
    A = 42 ;
    printf(" A vaut %d \n", A ) ;
    return 0 ;
}

Rien de bien folichon ici : nous avons demandé à l’ordinateur de nous réserver un peu de mémoire, suffisamment pour stocker un int, et nous lui avons dit que désormais nous allions désigner cet entier sous le nom de A. Tout ça, en une seule instruction :

    int A ;

Tiens d’ailleurs, combien d’octets a-t-on réservé ? Facile, il n’y a qu’à demander à la machine avec l’opérateur sizeof() :

#include <stdio.h>

int main() {
    int A ;
    A = 42 ;
    printf("A vaut %d \n", A ) ;
    printf("A occupe %ld octets \n", sizeof(A) ) ;
    return 0 ;
}
$gcc -o pouet main.c
$./pouet
A vaut 42
A occupe 4 octets

A occupe donc 4 octets ! Si on se rappelle que notre RAM n’est qu’un loooooooong ruban d’octets, cela signifie simplement qu’en déclarant la variable A, nous avons décidé que nous allions utiliser 4 des octets de notre RAM pour stocker notre entier.

Un ptit dessin permettant de fixer un peu les choses, voici notre RAM :

ram

Elle est constituée de cases d’un octet, qui sont numérotées. Pour simplifier le schéma, j’ai représenté ci-dessus une “petite” RAM de 256 Octets. Les numéros vont donc en hexadecimal de 0x00 à 0xff. Dans la réalité, votre mémoire est bien plus grande, et les adresses sont représentées sur 8 Octets ( de 0 à 0xffffffffffffffff ) sur votre belle machine 64 bits :)

Finalement, déclarer notre variable A, ce n’est que choisir 4 cases mémoires (4 octets) pour stocker le contenu de A :

ram

Le nom de la variable, “A”, n’est là que pour les pauvres humains que nous sommes. Pour la machine, l’entier que nous appelons A c’est “l’entier stocké dans les cases 0xA2 et suivantes”. Ou, Si vous préférez, “l’entier stocké à l’adresse 0xA2”.

L’adresse d’une donnée, finalement, ce n’est que ça : C’est le numéro de case mémoire du premier octet de la donnée.

L’opérateur &

Je vous voir venir : “d’accord, c’est bien beau tout ça … Mais montre-moi où est A, en pratique !”. Tout d’abord, sachez que votre manque de foi en moi me peine profondément. Mais puisque vous le demandez, on peut le déterminer grâce à l’opérateur “&”.

Cet opérateur permet de déterminer l’adresse d’une donnée en mémoire. Un petit exemple ?

#include <stdio.h>

int main() {
    int A ;
    A = 42 ;
    printf("A vaut %d \n", A ) ;
    printf("A occupe %ld octets \n", sizeof(A) ) ;
    printf("A est situé à l'adresse mémoire %p \n", &A ) ;
    return 0 ;
}
$gcc -o pouet main.c
$./pouet
A vaut 42
A occupe 4 octets
A est situé à l'adresse mémoire 0x7ffeca7b6334

Notre variable A a donc été placée à l’adresse 0x7ffeca7b6334 de la mémoire … Classe non ?

Je sais que la notation hexadécimale peut vous troubler, mais n’y prêtez pas attention. C’est juste une façon pour les informaticiens de manipuler des chiffres sous une forme un peu plus réduite. Après tout, que je vous dise que A est en 0x7ffeca7b6334 ou à la case numéro 140732295504692, c’est strictement la même chose ! Et je vous rappelle que de toute façon, votre ordinateur ne parle que binaire, au final !

Ceci dit, pour le moment, tout cela n’est pas très très utile. Je vois pas trop comment le glisser dans une conversation autour d’un apéro, et pour draguer, je suis sûr que vous pouvez surement trouver mieux :) Nous allons donc essayer de jouer un peu avec nos adresses.

Manipulons les adresses

Dans quoi pourrait-on stocker l’adresse d’une variable ? Souvent, la première idée qui vient à l’esprit est de se dire “Ben c’est qu’un entier quoi ! On a qu’à mettre ça dans un int !”.

Oui, mais non ! Parce que c’est un gros entier. Notre int, sur une architecture PC, fait 32 bits, alors que nos adresses en font 64 ! Un long int alors ? Mouais … mais c’est pas hyper portable tout ça !

On a donc décidé de créer un nouveau type, un type “Variable qui contient l’adresse d’un entier”. En C, ça se note de la façon suivante :

int * addr ;

La variable addr est une variable tout à fait habituelle. Simplement, son type est “int*”, c’est à dire qu’elle contient l’adresse d’un int.

C’est ça un pointeur, tout simplement ! Une variable qui contient une adresse.

Un exemple ? Allez, puisque vous le demandez gentiment :

#include <stdio.h>

int main() {
    int A ;
    int *P ;
    
    A = 42 ;
    P = &A ;
    
    printf("A vaut %d \n", A ) ;
    printf("A occupe %ld octets \n", sizeof(A) ) ;
    printf("A est situé à l'adresse mémoire %p \n\n", &A ) ;
    printf("P vaut %p \n", P ) ;
    printf("P occupe %ld octets \n", sizeof(P) ) ;

    return 0 ;
}

Dans ce petit programme, nous avons ajouté une variable P qui est un int*. Comme toutes les variables, P peut être remplie avec l’opérateur =. Ici, on y a stocké l’adresse de A grâce à la ligne :

P = &A ;

On dit que “P pointe vers A”. Perso, je trouve l’expression assez alambiquée. Je préfère dire “P contient l’adresse de A”. C’est plus simple, et je trouve ça plus clair !

Bon, on parle, on parle … mais si on exécutait ?

$gcc -o pouet main.c
$./pouet
A vaut 42
A occupe 4 octets
A est situé à l'adresse mémoire 0x7ffe78c958fc

P vaut 0x7ffe78c958fc
P occupe 8 octets

Comme on peut le voir, P, de type int*, fait 8 octets. Ce qui est bien la taille nécessaire pour stocker une adresse mémoire. Et il contient 0x7ffe78c958fc, ce qui est bien l’adresse de A. Graphiquement, ça donne une RAM qui ressemble à ça :

ram

Si A avait été un double, on aurait déclaré P comme double*, pour dire qu’il contient l’adresse d’un double. Si A avait été un unsigned char, P aurait été déclaré unsigned char*. Et ainsi de suite … Vous pouvez faire un pointeur vers n’importe quelle variable, quelquesoit son type !

Cependant, peut importe vers quel type de donnée pointe P, cela ne change pas sa nature profonde : Il contient une adresse ( un numéro de case mémoire si vous préférez ! ) … et donc il aura dans tous les cas une taille de 8 Octets sur cette machine.

Mais un pointeur, c’est vraiment une variable comme les autres ?

Et bien oui ! Rien ne distingue un pointeur de toutes les variables que vous manipulez depuis le début. Si vous avez du mal avec, dédramatisez le truc et dites vous que ce n’est qu’un gros entier … un “Numéro de case mémoire” !

Mais attendez … si c’est une variable … on peut demander son adresse ?????

OUI !

Dans notre exemple, P est forcément stocké quelquepart en RAM … et donc on peut utiliser notre opérateur “&” pour obtenir son adresse :

#include <stdio.h>

int main() {
    int A ;
    int *P ;
    
    A = 42 ;
    P = &A ;
    
    printf("A vaut %d \n", A ) ;
    printf("A occupe %ld octets \n", sizeof(A) ) ;
    printf("A est situé à l'adresse mémoire %p \n\n", &A ) ;
    printf("P vaut %p \n", P ) ;
    printf("P occupe %ld octets \n", sizeof(P) ) ;
    printf("P est situé à l'adresse mémoire %p \n", &P ) ;

    return 0 ;
}
$gcc -o pouet main.c
$./pouet
A vaut 42
A occupe 4 octets
A est situé à l'adresse mémoire 0x7ffeea36056c

P vaut 0x7ffeea36056c
P occupe 8 octets
P est situé à l'adresse mémoire 0x7ffeea360570

ram

Et si je voulais manipuler l’adresse de P, dans quoi je la stockerais ?

Vous l’avez deviné : C’est l’adresse d’un int*, je la stockerais donc dans un int** !

Et on peut continuer ainsi assez longtemps ! (Je ne sais même pas s’il y a une limite pratique !). Par contre, si vous dépassez trois ou quatre étoiles, c’est probablement que votre code mérite un deuxième coup d’oeil !

L’opérateur d’indirection *

Un dernier concept, et promis je vous laisse tranquille : L’opérateur “*”.

C’est un opérateur qui permet de désigner la donnée qui se trouve à l’adresse pointée par un pointeur. Ok, je sais, dit comme ça, c’est pas clair ! Mais rassurez vous, cela n’a rien de compliqué du tout !

Si je reprends l’exemple précédent :

    int A ;
    int *P ;
    
    A = 42 ;
    P = &A ;

P contient maintenant l’adresse de A.

Si nous tapons l’instruction suivante :

    *P = 69 ;

Alors nous demandons à stocker la valeur 69 à l’adresse contenue dans P.

Si l’on détaille le processus, le PC va :

Et du coup … nous venons de modifier A, et c’est dans A que le 69 est stocké !

Attention, ne confondez pas cette étoile, qui est un opérateur, avec l’étoile utilisée pour déclarer un pointeur !!!

Je vous laisse tester ce petit exemple pour bien comprendre :

#include <stdio.h>

int main() {
    int A ;
    int *P ;
    
    A = 42 ;
    P = &A ;
    
    printf("A vaut %d \n", A ) ;
   
    *P = 69 ;

    printf("A vaut maintenant %d \n", A ) ;
    return 0 ;
}

Le repos du guerrier

Cela fait déjà beaucoup à assimiler, et le chemin est encore long. Je vous propose donc de scinder cet article en plusieurs posts, pour prendre le temps de comprendre et ne pas vous assommer d’un coup. Promis, le prochain article vous montrera comment utiliser tout ça très pratiquement !

Au programme des articles suivants :

J’attaque la rédaction du tome 2 dès ce soir !

À Bientôt,

Rancune.