Ce qui suit se veut un bref survol des pointeurs, destiné à celles et ceux parmi vous qui sont pas encore habiles pour les comprendre et les manipuler.
Les symboles et concepts clés à comprendre sont les suivants.
Note : pour en savoir plus sur sizeof, voir ../Divers--cplusplus/sizeof.html
En C et en C++ (et en C#, d'ailleurs, même si on ne le voit pas à moins d'écrire du code de bas niveau), le type utilisé pour les grosseurs est std::size_t.
Le type std::size_t est un alias (un typedef ou, en termes plus contemporains, un using) pour un type entier non-signé qui dépend de la plateforme (ça peut être unsigned int, unsigned long, unsigned long long, peu importe). On ne veut pas savoir lequel.
L'opérateur sizeof(expr), qui peut aussi s'exprimer sizeof x si x est un objet, retourne dès la compilation la taille en bytes de expr; cet opérateur retourne donc un size_t. Ainsi, à droite, l'affichage de sizeof(int) nous montera combien de bytes occupe un int dans notre programme, alors que l'affichage de sizeof d nous indiquera combien de bytes occupe un double dans notre programme puisque d est un double. |
|
Lors d'une déclaration de variable, placer * entre le type et le nom déclare un pointeur.
Ainsi, à droite :
|
|
Une fois un pointeur déclaré, il peut être utilisé en tant que pointeur (donc en tant qu'adresse typée) comme en tant qu'outil d'accès indirect vers un pointé.
Quand on souhaite utiliser ce vers quoi il pointe, il faut le déréférencer (le précéder d'un *). Tous les pointeurs sont des adresses, donc tous les pointeurs occupent la même quantité d'espace en mémoire. À titre d'exemple, sizeof(string*)==sizeof(char*) même s'il est clair que sizeof(string)>sizeof(char). |
|
Lorsque le pointé est un objet complexe, il est aussi possible d'accéder à ses membres à travers l'opérateur -> comme le montre l'exemple à droite. |
|
Pour faire pointer un pointeur sur un objet, il est souvent utile de pouvoir prendre l'adresse de cet objet.
Une fois une variable déclarée, la précéder d'un & signifie prendre son adresse. Ainsi, à droite :
|
|
Il est possible de prendre l'adresse d'un pointeur, même si on le fait rarement en pratique. Dans le code à droite, p pointe sur i alors que pp pointe sur p (pp est un int**, donc un pointeur sur un pointeur de int). On utilise souvent des alias pour alléger la syntaxe quand on a besoin de jouer dans ces eaux-là. |
|
Note : pour en savoir plus sur les références, voir ../Divers--cplusplus/References.html
Il est facile de confondre « prendre l'adresse » et « déclarer une référence », les deux écritures étant semblables et reposant sur le symbole &. La différence entre les deux se situe au contexte d'utilisation : est-ce que & apparaît dans une déclaration ou est-ce que & est utilisé sur un objet existant?
Comme vu plus haut, appliquer & sur un objet existant signifie prendre son adresse.
Lors d'une déclaration de variable, placer & entre le type et le nom déclare une référence. Ainsi, à droite : |
|
Examinons maintenant comment il est possible de bien utiliser les pointeurs et, en comparaison, de bien utiliser les références.
Une référence est une sorte d'alias pour ce à quoi elle réfère. Une fois associée à quelque chose, elle y est liée pour toute son existence. Un pointeur peut se déplacer en mémoire. C'est ce qui le rend souple... et dangereux. Dans le code à droite, la dernière instruction écrit la valeur 3 à ce qui serait la position 6 de vals, or les positions légales dans vals vont de 0 à 4 inclusivement. Ce programme est brisé. |
|
Comm vue plus haut, quand un pointeur mène sur un objet, il est possible d'accéder aux membres de cet objet par l'opérateur -> ou en déréférençant le pointeur puis en utilisant l'opérateur . comme pour les objets accédés directement. Évidemment, avec une référence, la syntaxe est la même qu'avec un référé. |
|
Un pointeur permet de gérer de la mémoire allouée dynamiquement, pour faire en sorte que le pointé survive à la portée du pointeur. En C++ contemporain, il est rare qu'on manipule de la mémoire de cette manière. On privilégie les conteneurs tels que vector ou string, ou les pointeurs intelligents comme unique_ptr. |
|
Un void* est un pointeur abstrait. Tout pointeur peut être implicitement converti en void* (tout pointeur est une adresse). Par contre, pour utiliser un void* autrement que comme une vulgaire adresse, il faut le transtyper (faire un Cast). |
|
L'arithmétique sur les pointeurs stresse des gens, mais c'est assez simple :
Quand on veut avancer d'un byte à la fois, un utilise des char* car sizeof(char)==1. On ne peut pas faire d'arithmétique sur des void* car sizeof(void) est illégal. |
|
Dans le code à droite, expliquez :
|
|
Réécrivez le code à droite pour qu'il fasse la même chose sans avoir recours à un indice. Si votre fonction ne trouve pas d'élément négatif, elle devrait retourner un pointeur sur « l'élément » qui serait juste après le dernier élément du tableau. |
|
Qu'affichera le programme à droite? Expliquez pourquoi. |
|
Qu'affichera le programme à droite? Expliquez pourquoi. |
|
Dans le programme à droite, faitres en sorte que main() appelle f() en lui passant l'adresse de i. |
|
Dans le programme à droite, écrivez une fonction vlimeuse() qui fera en sorte que, suite à l'appel dans dans main(), p pointera sur glob. |
|
Quelques liens pour enrichir le propos.