Introduction#
Nous avons vu dans le premier article de cette série, en quoi les appels systèmes étaient essentiels pour nos programmes. Dans cet article, nous allons voir en quoi les pointeurs sont indispensables à la création de choses plus complexes.
Pour mieux comprendre leur utilité, je vais poser un petit problème auquel seuls les pointeurs peuvent permettre de répondre :
Je veux réaliser une fonction qui prend en entrée un nombre, qui le divise par 5 et qui retourne deux valeurs, le quotient ainsi que le reste.
En C, il est impossible pour une fonction de renvoyer deux valeurs distinctes (sans parler des structures). Essayons donc d’écrire un tel programme mais sans se servir du mot clé return
:
#include <stdio.h> // Permet d'utiliser printf
void divisionPar5(int nombre, int quotient, int reste) {
quotient = nombre / 5;
reste = nombre % 5;
}
int main() {
int nombre = 158;
int quotient = 0;
int reste = 0;
divisionPar5(nombre, quotient, reste);
printf("%d == 5*%d+%d\n", nombre, quotient, reste);
}
À l’exécution, notre programme va donner :
158 == 5*0+0
Comme prévu, le résultat n’est pas bon… Voyons donc en quoi ces fameux pointeurs peuvent nous aider à résoudre notre problème.
Deuxième brique : les pointeurs#
Un sujet qui génère beaucoup d’appréhension et d’incompréhension chez pas mal de débutants, les pointeurs sont en réalité très (très) simples et assez logiques à comprendre.
Définitions#
Avant d’attaquer les pointeurs, nous allons voir quelques rappels sur le fonctionnement des variables.
Variables et mémoire#
Lorsqu’un programme est exécuté, les variables qu’il va utiliser seront stockées dans la mémoire vive a.k.a. la RAM. Pour simplifier, une variable associée à une valeur possèdera une adresse dans la mémoire (que le noyau lui aura fourni).
Par exemple, si on déclare la variable a
, on aura :
int a = 42;
Donnera en mémoire (l’adresse indiquée ici est totalement arbitraire) :
Nom | Adresse | Valeur |
---|---|---|
a | 0x3021 | 42 |
La variable possèdera donc la valeur 42
à l’adresse 0x3021
en mémoire. Le 0x
dans l’adresse signifie que le nombre est représenté sous forme hexadécimale (base 16), ce qui donnerait 12321
en décimal (base 10).
Comment vérifier cette valeur ?
On pourrait utiliser un déboggeur, mais c’est tricher. Heureusement, le C nous donne la possiblité de récupérer l’adresse d’une variable en mémoire grâce à l’opérateur &
, appelé référencement, par exemple :
#include <stdio.h>
int main() {
int a = 42;
printf("Adresse de a: %p", &a);
}
Donnera à l’exécution (encore une fois la valeur est arbitraire) :
Adresse de a: 0x3021
C’est bien beau de récupérer l’adresse, mais est-ce qu’on ne pourrait pas faire l’action inverse ? En d’autres termes, récupérer une variable disponible à une adresse donnée ?
Encore une fois (quel crack), le C nous met à disposition l’opérateur de déréférencement : *
.
Reprenons encore une fois notre variable a
, affichons son adresse mémoire et essayons de récupérer sa valeur en utilisant l’opérateur de déréférencement :
#include <stdio.h>
int main() {
int a = 42;
printf("Adresse de a: %p\n", &a);
printf("Valeur de a: %d", *(&a)); // on peut aussi écrire *&a
}
Ce qui va donner :
Adresse de a: 0x3021
Valeur de a: 42
Pas mal non ? Mais, est-ce qu’on ne pourrait pas stocker cette adresse dans une autre variable ?
Bonne question ! On est tous d’accord pour dire que *(&a)
n’est ni très élégant, ni pratique comparé à un magnifique *b
, tel que b = &a
.
Essayons donc de stocker l’adresse de a
dans une nouvelle variable b
:
int main() {
int a = 42;
int b = &a;
}
Et là, catastrophe, notre compilateur nous crie dessus et on ne comprend pas pourquoi…
error: incompatible pointer to integer conversion initializing 'int' with an expression of type 'int *'
Pas de panique, pour rentrer un peu dans le détail, si on regarde la signature de l’opérateur &
:
R* operator &(K a);
&
va retourner une donnée de type R*
, avec R
le type de la variable à laquelle on a appliqué l’opérateur.
Donc, dans notre cas, &a
va retourner une valeur de type int *
, mais qu’est-ce donc que cette petite étoile ? (Spoiler : ce n’est pas l’opérateur de déréférencement vu plus tôt.)
Pointeurs#
Un pointeur, c’est tout simplement une variable dans laquelle on va stocker l’adresse en mémoire d’une autre variable (exactement ce qu’on a voulu faire juste avant).
Ils s’écrivent en indiquant x
, le type de variable pour laquelle le pointeur va contenir l’adresse et le symbole *
pour indiquer que notre variable est de type pointeur de type x. Cette notation peut porter à confusion avec l’opérateur de déréférencement mais ces notations ne s’utilisent pas de la même manière.
int *p;
On peut aussi écrire
int* p
, mais par convention c’est l’autre version qui prime.
Ici nous avons choisi int pour signifier que p pourra contenir l’adresse d’une variable de type int (nous verrons dans un article bonus en quoi ce typage est intéressant).
Désormais, on comprend un peu mieux l’erreur que nous avions eu plus tôt. Le compilateur s’attendait à ce b
soit un pointeur, mais nous avions défini b
comme une variable de type int.
Reprenons notre variable a
et créons le pointeur ptr
de type int, qui va contenir l’adresse de a
:
int a = 42;
int *ptr = &a;
Ce qui va donner en mémoire :
Nom | Adresse | Valeur |
---|---|---|
a | 0x3021 | 42 |
ptr | 0x3025 | 0x3021 |
Pour vérifier, on pourra exécuter le code suivant :
#include <stdio.h>
int main() {
int a = 42;
printf("Adresse de a: %p\n", &a);
int *ptr = &a;
printf("Valeur de ptr: %p", ptr);
}
Donnera logiquement les mêmes valeurs :
Adresse de a: 0x3021
Valeur de ptr: 0x3021
Et donc, finalement, est-ce qu’on ne pourrait pas essayer d’afficher la valeur de la variable stockée à l’adresse indiquée par le pointeur (oui oui la phrase a un sens) :
#include <stdio.h>
int main() {
int a = 42;
int *ptr = &a;
printf("Adresse de a: %p\n", &a);
printf("Valeur de a: %d\n", *(&a));
printf("Valeur de ptr: %p\n", ptr);
printf("Déréférencement de ptr: %d", *ptr);
}
Qui va donner :
Adresse de a: 0x3021
Valeur de a: 42
Valeur de ptr: 0x3021
Déréférencement de ptr: 42
C’est bien tout ça mais concrètement, ça sert à quoi ?
Résolution du problème#
Reprenons le problème posé dans l’introduction :
Je veux réaliser une fonction qui prend en entrée un nombre, qui le divise par 5 et qui retourne deux valeurs, le quotient ainsi que le reste.
#include <stdio.h> // Permet d'utiliser printf
void divisionPar5(int nombre, int quotient, int reste) {
quotient = nombre / 5;
reste = nombre % 5;
}
int main() {
int nombre = 158;
int quotient = 0;
int reste = 0;
divisionPar5(nombre, quotient, reste);
printf("%d == 5*%d+%d\n", nombre, quotient, reste);
}
Nous avions vu que le résultat n’était pas du tout celui que nous voulions, essayons maintenant d’y répondre en utilisant les magnifiques pointeurs :
#include <stdio.h> // Permet d'utiliser printf
void divisionPar5(int nombre, int *ptr_quotient, int *ptr_reste) {
*ptr_quotient = nombre / 5; // utilisation de l'opérateur de déréférencement
*ptr_reste = nombre % 5; // utilisation de l'opérateur de déréférencement
}
int main() {
int nombre = 158;
int quotient = 0;
int reste = 0;
int *ptr_quotient = "ient; // sauvegarde de l'adresse du quotient
int *ptr_reste = &reste; // sauvegarde de l'adresse du reste
divisionPar5(nombre, ptr_quotient, ptr_reste);
printf("%d == 5*%d+%d\n", nombre, quotient, reste);
}
Ce qui va donner à l’exécution :
158 == 5*31+3
Notre fonction divisionPar5
va prendre en paramètre notre nombre, un pointeur vers l’adresse du quotient et un pointeur vers l’adresse du reste.
Cela va nous permettre modifier directement la valeur du quotient et du reste en mémoire, depuis l’intérieur de la fonction, et ceci grâce à l’opérateur de déréférencement sur nos pointeurs.
Conclusion#
Voilà donc la seconde brique élémentaire du C, celle-ci va nous permettre de faire mumuse avec la mémoire facilement et ainsi de pouvoir créer des choses plus complexes.
Le prochain article sera un bonus sur la représentation des types dans la mémoire, ainsi que sur pourquoi le typage des pointeurs est intéressant.