Comprenez le problème posé
Un des plus gros problèmes avec les pointeurs, en plus d'être assez délicats à assimiler pour des débutants, c'est qu'on a du mal à comprendre à quoi ils peuvent bien servir.
Alors bien sûr, je pourrais vous dire : "Les pointeurs sont totalement indispensables, on s'en sert tout le temps, croyez-moi !", mais je sais que cela ne vous suffira pas.
Je vais donc vous poser un problème que vous ne pourrez pas résoudre sans utiliser de pointeurs. Ce sera en quelque sorte le fil rouge du chapitre.
Voici le problème : je veux écrire une fonction qui renvoie deux valeurs. "Impossible", me direz-vous ! En effet, on ne peut renvoyer qu'une valeur par fonction :
int fonction()
{
return valeur;
}
Si on indique int
, on renverra un nombre de type int
(grâce à l'instruction return
).
On peut aussi écrire une fonction qui ne renvoie aucune valeur avec le mot-clé void
:
void fonction()
{
}
Mais renvoyer deux valeurs à la fois… c'est impossible. On ne peut pas faire deux return
.
Supposons que je veuille écrire une fonction à laquelle on envoie un nombre de minutes. Celle-ci renverrait le nombre d'heures et minutes correspondant :
Si on envoie 45, la fonction renvoie 0 heure et 45 minutes.
Si on envoie 60, la fonction renvoie 1 heure et 0 minute.
Si on envoie 90, la fonction renvoie 1 heure et 30 minutes.
Soyons fous, tentons le coup :
#include <stdio.h>
#include <stdlib.h>
/* Je mets le prototype en haut. Comme c'est un tout
petit programme je ne le mets pas dans un .h, mais
en temps normal (dans un vrai programme), j'aurais placé
le prototype dans un fichier .h bien entendu */
void decoupeMinutes(int heures, int minutes);
int main(int argc, char *argv[])
{
int heures = 0, minutes = 90;
/* On a une variable minutes qui vaut 90.
Après appel de la fonction, je veux que ma variable
"heures" vaille 1 et que ma variable "minutes" vaille 30 */
decoupeMinutes(heures, minutes);
printf("%d heures et %d minutes", heures, minutes);
return 0;
}
void decoupeMinutes(int heures, int minutes)
{
heures = minutes / 60; // 90 / 60 = 1
minutes = minutes % 60; // 90 % 60 = 30
}
Résultat :
0 heures et 90 minutes
Zut, zut, zut et rezut, ça n'a pas marché !
Que s'est-il passé ?
En fait, quand vous "envoyez" une variable à une fonction, une copie de la variable est réalisée : la variable heures
dans la fonction decoupeMinutes
n'est pas la même que celle de la fonction main
!
Votre fonction decoupeMinutes
fait son travail : à l'intérieur de decoupeMinutes
, les variables heures
et minutes
ont les bonnes valeurs : 1 et 30.
Mais ensuite, la fonction s'arrête lorsqu'on arrive à l'accolade fermante. Comme on l'a appris dans les chapitres précédents, toutes les variables créées dans une fonction sont détruites à la fin de cette fonction. Vos copies de heures
et de minutes
sont donc supprimées.
On retourne ensuite à la fonction main
, dans laquelle vos variables heures
et minutes
valent toujours 0 et 90. C'est un échec !
Bref, vous aurez beau retourner le problème dans tous les sens… vous pouvez essayer de renvoyer une valeur avec la fonction (en utilisant un return
et en mettant le type int
à la fonction), mais vous n'arriverez à renvoyer qu'une des deux valeurs. Vous ne pouvez pas renvoyer les deux valeurs à la fois. De plus, vous ne pouvez pas utiliser de variables globales car, comme on l'a vu, cette pratique est fortement déconseillée.
Voilà, le problème est posé. Voyons comment les pointeurs vont nous permettre de le résoudre !
Souvenez-vous du chapitre sur les variables
Analysez le schéma de la mémoire vive
La première ligne représente la "cellule" du tout début de la mémoire vive.
Pour contourner ce problème, on a inventé une table qui fait la liaison entre les nombres et les lettres. Cette table dit par exemple : « Le nombre 89 représente la lettre Y ».
Rappelez-vous comment faire afficher la valeur d'une variable
Quand vous créez une variable age
de type int
en tapant ceci :
int age = 10;
… votre programme demande au système d'exploitation (Windows, par exemple) la permission d'utiliser un peu de mémoire. Le système d'exploitation répond en indiquant à quelle adresse en mémoire il vous laisse le droit d'inscrire votre nombre.
Revenons à notre variable age
. La valeur 10 a été inscrite quelque part en mémoire, disons par exemple à l'adresse n° 4655.
Ce qu'il se passe (et c'est le rôle du compilateur), c'est que le mot age
dans votre programme est remplacé par l'adresse 4655 à l'exécution. Cela fait que, à chaque fois que vous avez tapé le mot age
dans votre code source, il est remplacé par "4655", et votre ordinateur voit ainsi à quelle adresse il doit aller chercher en mémoire ! Du coup, l'ordinateur se rend en mémoire à l'adresse 4655, et répond fièrement : "La variable age
vaut 10".
On sait donc comment récupérer la valeur de la variable : il suffit de taper age
dans son code source. Si on veut afficher l'âge, on peut utiliser la fonction printf
:
printf("La variable age vaut : %d", age);
Résultat à l'écran :
La variable age vaut : 10
Bon, rien de bien nouveau jusque-là : on sait afficher la valeur de la variable, mais saviez-vous que l'on peut aussi afficher l'adresse correspondante ?
Faites afficher l'adresse d'une variable
Pour afficher l'adresse de la variable, on doit :
Utiliser le symbole
%p
(le p du mot « pointeur ») dans leprintf
.Envoyer à la fonction
printf
non pas la variableage
, mais son adresse… Et pour faire cela, vous devez mettre le symbole&
devant la variableage
, comme je vous avais demandé de le faire pour lesscanf
, il y a quelque temps, sans vous expliquer pourquoi.
Tapez donc :
printf("L'adresse de la variable age est : %p", &age);
Résultat :
L'adresse de la variable age est : 0x0023FF74
Ce que vous voyez là est l'adresse de la variable age
au moment où j'ai lancé le programme sur mon ordinateur. Oui, oui, 0x0023FF74 est un nombre, il est simplement écrit dans le système hexadécimal, au lieu du système décimal dont nous avons l'habitude. Le préfixe "0x" indique que les symboles suivants sont écrits en hexadécimal. Si vous remplacez %p
par %d
, vous obtiendrez un nombre décimal que vous connaissez.
OK, mais où on veut en venir avec tout ça ?
Eh bien en fait, je veux vous faire retenir ceci :
age
désigne la valeur de la variable ;&age
désigne l'adresse de la variable.
Utilisez des pointeurs
Mais… Les adresses sont des nombres aussi, non ? Ça revient à stocker des nombres encore et toujours !
C'est exact. Mais ces nombres auront une signification particulière : ils indiqueront l'adresse d'une autre variable en mémoire.
Créez un pointeur et donnez-lui une valeur par défaut
int *monPointeur;
Notez qu'on peut aussi écrire int* monPointeur;
. Cela revient exactement au même.
Pour initialiser un pointeur, c'est-à-dire lui donner une valeur par défaut, on n'utilise généralement pas le nombre 0 mais le mot-clé NULL
(veillez à l'écrire en majuscules) :
int *monPointeur = NULL;
Là, vous avez un pointeur initialisé à NULL
. Comme ça, vous saurez dans la suite de votre programme que votre pointeur ne contient aucune adresse.
Que se passe-t-il ?
Ce code va réserver une case en mémoire comme si vous aviez créé une variable normale.
Cependant, et c'est ce qui change, la valeur du pointeur est faite pour contenir une adresse. L'adresse… d'une autre variable.
Vous savez maintenant comment indiquer l'adresse d'une variable (au lieu de sa valeur) en utilisant le symbole &
, alors allons-y :
int age = 10;
int *pointeurSurAge = &age;
Qu'est-ce que ça veut dire ?
La première ligne signifie : "Créer une variable de type
int
dont la valeur vaut 10".La seconde ligne signifie : "Créer une variable de type pointeur dont la valeur vaut l'adresse de la variable
age
". La seconde ligne fait donc deux choses à la fois. Si vous le souhaitez, pour ne pas tout mélanger, sachez qu'on peut la découper en deux temps :
int age = 10;
int *pointeurSurAge; // 1) signifie "Je crée un pointeur"
pointeurSurAge = &age; // 2) signifie "pointeurSurAge contient l'adresse de la variable age"
Vous avez remarqué qu'il n'y a pas de type "pointeur" comme il y a un type int
et un type double
.
Au lieu de ça, on utilise le symbole *
, mais on continue à écrire int
.
Qu'est-ce que ça signifie ?
La schéma suivant résume ce qu'il s'est passé dans la mémoire :
Dans ce schéma :
la variable
age
a été placée à l'adresse 177450 (vous voyez d'ailleurs que sa valeur est 10) ;et le pointeur
pointeurSurAge
a été placé à l'adresse 3 (c'est tout à fait le fruit du hasard).
Et… ça sert à quoi ?
Maintenant, on a un pointeurSurAge
qui contient l'adresse de la variable age
.
Essayons de voir ce que contient le pointeur à l'aide d'un printf
:
int age = 10;
int *pointeurSurAge = &age;
printf("%d", pointeurSurAge);
177450
Hum. En fait, cela n'est pas très étonnant. On demande la valeur de pointeurSurAge
, et sa valeur, c'est l'adresse de la variable age
(177450).
Comment faire pour demander à avoir la valeur de la variable se trouvant à l'adresse indiquée dans pointeurSurAge
?
Il faut placer le symbole *
devant le nom du pointeur :
int age = 10;
int *pointeurSurAge = &age;
printf("%d", *pointeurSurAge);
10
Hourra ! Nous y sommes arrivés !
Qu'est-ce qu'on y gagne ? On a simplement réussi à compliquer les choses ici. On n'avait pas besoin d'un pointeur pour afficher la valeur de la variable age
!
Cette question (que vous devez inévitablement vous poser) est légitime. Actuellement l'intérêt n'est pas évident, mais petit à petit, tout au long des chapitres suivants, vous comprendrez que tout cela n'a pas été inventé par pur plaisir de compliquer les choses.
Faites l'impasse sur la frustration que vous devez ressentir. Si vous avez compris le principe, c'est l'essentiel. Les choses s'éclairciront d'elles-mêmes par la suite.
Retenez le principe de base d'un pointeur
Voici ce qu'il faut avoir compris et ce qu'il faut retenir pour la suite de ce chapitre :
Contentez-vous de bien retenir ces quatre points. Faites des tests et vérifiez que ça marche.
Ce schéma devrait bien vous aider à situer chacun de ces éléments.
Ce n’est sans doute pas facile d'assimiler toutes ces notions d’un coup. Je vous propose une petite vidéo pour résumer tous ces points :
En résumé
Chaque variable est stockée à une adresse précise en mémoire.
Les pointeurs sont semblables aux variables, à ceci près qu'au lieu de stocker un nombre, ils stockent l'adresse à laquelle se trouve une variable en mémoire.
Si on place un symbole
&
devant un nom de variable, on obtient son adresse au lieu de sa valeur (ex. :&age
).Si on place un symbole
*
devant un nom de pointeur, on obtient la valeur de la variable stockée à l'adresse indiquée par le pointeur.
Félicitations, vous êtes arrivé à la fin de ce chapitre ! Dans le prochain, on continue sur notre lancée et on récupère le problème que l'on avait posé pour comprendre l'intérêt des pointeurs. C'est parti !