
OK, parlons de quelque chose dont tout le monde s’accorde pour dire que c’est une des notions les plus délicates à comprendre, mais ô combien puissante !
Il est temps de parler des pointeurs ! C’est parti !!!!
Sommaire
C’est quoi un pointeur?
Les pointeurs sont des variables qu’on voit notamment en C et C++, mais elles ne sont pas comme les autres variables classiques. On connait tous les variables et leurs rôles : stocker un caractère, un chiffre, un nombre à virgule bref quelque chose qu’on appelle une valeur. Les pointeurs c’est la même chose sauf qu’elle stocke une adresse.
Une adresse ? C’est quoi ça ?
Pas possible de parler des pointeurs sans parler de mémoire !
L’ordinateur est une grosse machine à calculer, c’est le processeur qui s’occupe de traiter toutes les instructions que lui fournit un programme. La machine à différente façon de stocker des informations, pour les données qui ne sont pas utilisées, l’ordinateur utilise des supports de stockage comme le disque dur, les dvd ou les clés usb. Ces mémoires sont lentes, mais possèdent un très grand espace de stockage, on y trouve donc tous nos fichiers, documents, programmes, etc.
Ensuite, l’ordinateur possède un deuxième type de mémoire, les registres qui se trouvent au cœur même du processeur. Celle-ci est l’inverse des mémoires de type disque dur, car elle possède un espace de stockage très très petit, mais c’est la plus rapide de toutes. Les registres ne sont pas des espaces de stockage à proprement parler, cela ressemble plus à une mémoire tampon qui aide grandement le processeur à faire ses opérations.
Et ce qui nous intéresse, c’est la mémoire qui est entre les deux, celle qui joue l’intermédiaire entre le disque dur et le processeur, la mémoire virtuelle alias la RAM. C’est la fameuse petite barrette de RAM que vous mettez dans le PC et son rôle est de contenir les programmes qui sont lancés. En effet, quand vous exécutez un programme, tout le code est chargé dans la mémoire virtuelle de façon à ce qu’il puisse être exécuté par le processeur. Le disque dur étant un support beaucoup plus lent comparé à une mémoire RAM, le processeur prendrait énormément de temps s’il devait faire un traitement directement dessus. Dans la mémoire virtuelle, nous avons le code qui va donner les instructions au processeur, mais aussi nos variables, car tout ce qui constitue un programme est chargé en mémoire.
Chaque instruction, chaque variable est positionnée quelque part dans la mémoire virtuelle, cette position on l’appelle l’adresse. C’est essentiel, l’ordinateur ne pourrait pas se repérer dans sa mémoire sans ça. On pourrait considérer la mémoire comme un très grand livre où les variables et les instructions sont des pages. L’adresse serait le n° de la page, et la valeur serait le contenu de la page.
Pour résumer, chaque variable est composée d’une valeur et d’une adresse ! Et les pointeurs sont des variables comme les autres, avec une adresse et une valeur, seulement ce n’est pas un chiffre ou un caractère qu’il contient, mais une adresse !
Une phrase à retenir :
Un pointeur est une variable qui contient l’adresse d’une autre variable !!!
Une fois cette phrase ancrée dans votre mémoire ( la vôtre, hein !), nous allons voir comment en déclarer une.
Déclarons un pointeur !
Un pointeur peut être typé, ce qui veut dire que le rôle de ce pointeur est de contenir l’adresse d’une variable d’un type spécifique. Un pointeur typé « int » ne pourra que contenir l’adresse d’une variable « int », si on lui assigne l’adresse d’un « char » par exemple, cela va générer une erreur de compilation.
Comment déclare-t-on un pointeur ? C’est simple, il suffit de mettre une étoile collée au type.
Ici, des déclarations de diverses variables tout à fait classiques :
int unChiffre; char uneLettre; float uneDecimale;
Et là, des déclarations de pointeurs :
int* unPointeurSurChiffre; char* unPointeurSurLettre; float* un PointeurSurDecimal;
On peut aussi déclarer un pointeur non typé, ce qui veut dire qu’il n’a pas de type de données spécifique et qu’il peut contenir l’adresse de n’importe quel type de variable. Pour ça, on utilise le mot clé « void » suivi de l’étoile.
void* unPointeurToutCourt;
Pratique quand on ne sait pas sur quel type de données il pointera.
Les deux opérateurs.
OK mes amis, on vient de voir un nouveau type de variable et on sait comment le déclarer. Mais concrètement, on en fait quoi ? Souvenez-vous, son rôle est de contenir une adresse, il suffit de savoir comment obtenir l’adresse d’une variable.
Pour commencer, un pointeur peut s’utiliser comme n’importe quelle variable, à savoir l’initialiser, faire des additions, des soustractions, des multiplications, etc. Mais il est censé manipuler des adresses, les opérateurs mathématiques n’ont que peu d’intérêt sur un pointeur.
Comment obtenir l’adresse d’une variable ? C’est là qu’on va faire la découverte de la fameuse esperluette alias « & » qui est l’opérateur d’adresse.
L’opérateur d’adresse permet d’obtenir l’adresse d’une variable en le plaçant derrière celle-ci.
int unChiffre = 10; cout << &unChiffre << endl // Affiche l'adresse de la variable déclarée en haut, non il n'affichera pas 10 int* unPointeurSurChiffre = &unChiffre; // Déclaration d'un pointeur, initialisé sur l'adresse de "unChiffre"
Quand un pointeur contient l’adresse d’une variable, on dit que ce pointeur pointe sur la variable « xxx », dans notre cas, unPointeurSurChiffre pointe sur la variable unChiffre.
Il existe en plus de l’esperluette, un autre opérateur qui permet d’obtenir une valeur à partir d’une adresse. Pour cela, on utilise l’opérateur de déréférencement qui est l’étoile. Oui encore lui 🙂 ! En plus d’obtenir la valeur grâce à un pointeur ou une adresse, on peut la modifier par le même opérateur !
int unChiffre = 10; int* unPointeurSurChiffre = &unChiffre; cout << &unPointeurSurChiffre << endl // Affiche l'adresse de "unChiffre" cout << *unPointeurSurChiffre << endl // Affiche la valeur de "unChiffre" à savoir 10. *unPointeurSurChiffre += 15; // Modification de la variable unChiffre par son adresse, on lui rajoute +15. cout << *unPointeurSurChiffre << endl // Affiche la valeur de "unChiffre" à savoir 25.
De ce fait, nous pouvons obtenir la valeur d’une variable par un pointeur et en faire tout ce qu’on veut. Pour déréférencer un pointeur non typé, c’est la même chose sauf qu’il faut faire un cast.
Les dangers d’une telle puissance !
Attention maintenant, l’utilisation de l’opérateur « * » peut porter à confusion, car elle n’a pas la même signification où on l’utilise. En effet, elle sert à déréférencer et à déclarer un pointeur, toute utilisation de l’étoile en dehors d’une déclaration veut dire qu’on déréférence !
int unChiffre = 10; // un chiffre au hasard, car il nous faut bien un chiffre :) int* encoreUnPointeur = NULL; // Utilisation de l'étoile dans ce contexte veut dire qu'on déclare un pointeur int* deuxiemePointeur = &unChiffre // Utilisation de l'étoile dans ce contexte veut dire qu'on déclare un pointeur et de l'esperluette veut dire qu'on affiche l'adresse d'une variable int* troisiemePointeur = *deuxiemePointeur
Une autre attention, nous avons vu que nous pouvons modifier une variable rien qu’en ayant son adresse et c’est d’ailleurs de cette fonctionnalité que les pointeurs sont puissants, mais que se passe-t-il si vous vous trompez d’adresse? Et bien vous allez forcément modifier quelque chose quelque part dans la mémoire, ce qui va conduire au mieux à des erreurs de l’outre monde dans votre programme jusqu’au crash complet du logiciel.
L’ordinateur empêche tout accès et modification par un programme dont la mémoire ne lui ait pas été attribuée, et si jamais c’est le cas, l’ordinateur fait planter très méchamment le logiciel, une manière efficace quand un programme fait trop son malin.
Alors, comment s’assurer qu’on n’utilise pas un pointeur à tort et à travers ? Il faut savoir qu’en déclarant simplement une variable sans l’initialiser, sa valeur prend une valeur aléatoire ce qui peut être source d’erreurs.
Donc on déclare les pointeurs en veillant toujours à initialiser sa valeur :
int* unPointeurSurChiffre = NULL; // Pointeur initialisé a NULL pour éviter le plantage int unChiffre = 10; unPointeurSurChiffre = &unChiffre;
De plus, une fois qu’on ne se sert plus d’un pointeur, on remet sa valeur à NULL.
Ça sert à quoi un pointeur ?
Le rôle d’un pointeur est de pouvoir stocker une adresse et donc d’effectuer des manipulations directement sur la mémoire. Il y a 3 cas où vous pouvez vous en servir.
L’utilisation la plus courante des pointeurs est lorsqu’on les envoie dans les paramètres d’une fonction. En effet, la fonction à la possibilité de modifier le contenu d’une variable via le déréférencent sans utiliser le « return », ce qui permet d’avoir une fonction capable de modifier plusieurs variables.
Une autre utilisation est lorsqu’on fait de l’allocation dynamique en C, dont la fonction « malloc » et ses cousins renvoi l’adresse de la mémoire qui a été allouée. Cette adresse, il faut bien la stocker quelque part et savoir l’utiliser 8).
Enfin, on peut aussi s’en servir aussi pour manipuler des tableaux, leur comportement étant similaire à quelques différences près.
Nous avons vu les pointeurs simples, typés et non typés, mais il existe des types de pointeurs encore plus complexes comme des doubles pointeurs, des pointeurs sur fonctions, des pointeurs sur tableau, sans parler des doubles déréférence. En général, c’est à partir de là que la vraie prise de tête commence quand on a pas l’habitude.
Pour faire simple :
Un pointeur est une variable qui contient l’adresse d’une autre variable !!! 8)
Mais un pointeur n’est rien sans ses amis l’esperluette et l’étoile.
Au final, on retient que :
- Un pointeur est une variable qui contient l’adresse d’une autre variable
- La récupération de l’adresse d’une variable se fait par l’opérateur d’adresse « & » quand il est utilisé en dehors d’une déclaration
- Le déréférencement qui est le fait d’obtenir la valeur par une adresse se fait avec son opérateur « * » quand il est utilisé en dehors d’une déclaration
- L’utilisation de l’étoile dans une déclaration veut dire « je déclare un pointeur de type ‘xxx' »
- Les pointeurs servent pour l’allocation dynamique, la modification des variables dans une fonction, la manipulation de tableau.
A++ 🙂
Laisser un commentaire