Bonjour,
Moi, je pense qu'il ne faut pas faire comme vous le faites et essayer de comparer avec tous les éléments qui nous entourent. La machine ne marche nécessairement pas de la même façon.
Il y a plusieurs mécanismes qu'il faut garder en tête :
La mémoire
Tout ce qui existe dans un programme correspond à des emplacements en mémoire. La mémoire n'est autre qu'un grand tableau où l'on peut placer des nombres. Voici une mémoire de cinq cases :
Ça veut dire que je peux mettre cinq nombres dedans.
À un moment, je déclare une variable :
Dans la mémoire d'un ordinateur, cela correspond à :
(Actuellement, mon nombre '42' peut être n'importe où, c'est géré par le système d'exploitation, et peut importe où mon nombre est stocké (tant qu'il est stocké cela suffira).
Pour une raison ou une autre, nous avons besoin de savoir où, dans la mémoire, est stocké notre nombre. En code C, on peut faire :
int* pFoo = &foo; // Récupération de l'emplacement mémoire de foo
En français, pFoo, est équivalent à "case numéro 2".
En informatique, déjà, l'ordinateur ne gère pas des nombres décimaux, mais des hexa décimaux, mais en plus, il compte à partir de 0, il va me dire :
0x00000001
On ajoute 0x devant le nombre, pour indiquer que c'est de l'hexadécimal. Pour l'ordinateur, c'est la case 1, car il existe une case 0. Et cette représentation d'exemple n'est valide que sur les architectures 32 bits, en effet, le nombre 0x00000001 est un nombre sur 32 bits.
Voilà, cette variable pFoo, qui définit l'emplacement d'une chose en mémoire, s'appelle un pointeur, car ce n'est pas une variable comme les autres.
Toutefois, même si le langage ne considère pas un pointeur comme une variable de type int/double ou autre (pour de multiples raisons), un pointeur est un nombre. Cela veut dire que si je fais :
J'accède à la case suivante.
---
Les pointeurs
Ok, tout cela est bien cool, je pense maintenant que l'on sait ce qu'est un pointeur. Mais, qu'elle est l'utilité ?
Un secret que l'on ne dit peut être pas assez tôt, c'est que les variables que vous passez en arguments à une fonction comme ici :
1 2 3 4 5 6 7 8 9 10 11 12
|
void bar(int foo)
{
// Affiche foo
// Ici, foo est une copie du foo présent dans la fonction main
}
int main(void)
{
int fooMain = 42;
bar(fooMain);
} |
sont des copies.
Cela veut dire que lorsque le programme se trouve dans la fonction bar(), dans mon tableau de cinq cases, je trouverai deux fois '42'.
Maintenant, imaginons que nous souhaitons avoir une fonction bar, qui incrémente la variable. C'est cool, on pourrait faire ça :
1 2 3 4 5 6 7 8 9 10 11 12 13
|
void bar(int foo)
{
// Ici, foo est une copie du foo présent dans la fonction main
foo = foo+1;
}
int main(void)
{
int fooMain = 42;
bar(fooMain);
// Ici, fooMain vaut toujours 42 ! Nous n'avons modifié que la copie de la variable et non la variable
} |
Dans la fonction bar, je modifie la copie de la variable fooMain, du main(). Cette copie est une copie temporaire, présente uniquement lors de l'exécution de la fonction. Il n'y a pas de mécanisme où, la modification effectuée sur foo, serait répercutée sur fooMain, car cela apporterait bien trop de soucis (complexité de mise en place et des bogues supplémentaires).
Du coup, comment pourrais-je faire pour que ce code marche ?
Au lieu de passer ma variable fooMain, directement, je vais donner son adresse.
Ainsi, la fonction bar() pourra lire l'adresse, aller dans la bonne case de mon tableau et modifier la variable.
Pour rappel, une adresse, en C, c'est un pointeur. Du coup, on écrit ça :
1 2 3 4 5 6 7 8 9 10 11 12 13
|
void bar(int* pFoo)
{
// Ici, pFoo est une copie de l'adresse de la variable, donc c'est bon, on peut toujours accéder la variable en lisant l'adresse
*pFoo = *pFoo+1;
}
int main(void)
{
int fooMain = 42;
bar(&fooMain);
// Ici, fooMain vaut 43 ! Nous sommes super bons \ o /
} |
Par contre, si on essaie de modifier pFoo, alors, la modification ne sera pas répercutée, car, on retombe dans le même cas problématique que précédemment.
---
Les tableaux
Maintenant, qu'en est t-il si nous voulons transférer un tableau à une fonction ? Ou même comment pouvons-nous spécifier un tableau dans un programme ?
On ne va pas faire N variables, pour les N cases de notre tableau, c'est trop lourd à gérer, c'est trop pénible et on ne peut pas automatiser les traitements avec des boucles (for).
Du coup, on va juste garder un pointeur sur le premier élément du tableau.
En C, que ce soit l'écriture :
ou
int* pTab; // Après cette ligne, il faut demander de la mémoire au système avec malloc
Le principe est le même. Les deux variables sont des pointeurs, pointant sur la première case du tableau à gérer.
On peut passer ce pointeur à une fonction, comme précédemment, si on veut utiliser le tableau dans une fonction.
Lorsque l'on veut accéder à un élément d'un tableau, on écrit généralement :
En réalité, le compilateur (ou l'ordinateur), que va t-il faire ? Il va prendre l'adresse pTab, puis se déplacer de 5 cases (on compte toujours à partir de zéro dans le monde de l'ordinateur).
Comme je l'ai dit plus haut, un pointeur, ça reste un simple nombre, donc l'ordinateur va faire :
Mais bon, cette écriture étant moins lisible, on ne la privilégie pas.
Voilà, je pense avoir tout dit sur les pointeurs. Je pense avoir montré deux utilités flagrantes des pointeurs, même si j'ai été un peu rapide sur les tableaux.
Pour les programmeurs un peu plus avancés, les pointeurs peuvent aussi éviter les copies des structures (optimisation). En effet, une structure peut contenir des éléments qui prendront 100 octets en mémoire (par exemple). Si on passe la structure à une fonction, on va copier ces 100 octets, comme je l'ai dit tout à l'heure. Par contre, si on ne donne qu'un pointeur à la fonction, pointant sur la structure, alors dans ce cas, on ne copie que le pointeur (4 octets en 32 bits). Le gain est conséquent (surtout si la fonction est appelée des milliers de fois).
Partager