Adresses et pointeurs
Principe
Chaque variable possède donc une adresse qu'il est possible d'obtenir à l'aide de la syntaxe "&variable".
Exemple:
// Encode le nombre 5 sur 4 octets quelque part en mémoire
int a = 5;
// Affiche l'adresse en mémoire où le contenu de la variable a est stocké
std::cout<< &a;
L'adresse obtenue par "&variable" peut être vu comme un indice (/ un nombre entier positif) dans un tableau, où chaque case de ce tableau correspond à un octet dans votre mémoire RAM.
-
- Afficher cette adresse n'a que peu d'intérêt en soit, il s'agit d'un nombre exprimé par défault en hexadécimal (0x...).
-
- Cette valeur est une adresse logique (locale au programme en cours) et non pas directement l'adresse physique sur une barette de mémoire RAM. La conversion entre adresse logique et physique est réalisée par le système d'exploitation.
-
- Les adresses vont généralement être différentes à chaque nouveau lancement du programme, ce qui limite la possibilité de "hack" dans un programme (ex. en modifiant une valeur spécifiée à une adresse fixe).
-
- Sur une architecture 64 bits classique, les adresses sont des valeurs encodées sur 8 octets (64 bits). Il est donc possible d'adresser des indices allant de 0 à \(2^{64}\simeq 18.4 10^{18}\), ou encore 16 hexa-octets de "cases mémoires". Il faut par contre noter qu'il n'est pas possible de stocker une adresse dans une variable "int" - un int étant stocké sur 4 octets, la moitié de l'adresse serait alors perdue.
-
- Sur les anciennes architecture 32 bits, les adresses étaient stockées sur 4 octets, et il n'était possible de n'adresser que 4.2 milliards de valeurs - ou 4 giga-octets - qui explique certaines limite maximales de RAM et de taille des fichiers que l'on peut encore retrouver sur certains systèmes.
Il est possible d'utiliser cette adresse comme un moyen indirect de désigner et de modifier une valeur en mémoire à l'aide de ce que l'on apelle un
pointeur.
-
Un pointeur est une variable qui va stocker une adresse et indiquer le type associé à cette adresse.
-
-
- Le type d'un pointeur s'écrit "typePointé* nomVariable".
-
Le contenu de la mémoire désigné par l'adresse peut être accédé/modifié en déférencant le pointeur.
-
-
- Le déréférencement s'écrit "*nomVariable"
Exemple:
// Création d'une variable a dont le contenu vaut 5
int a = 5;
// Création d'un pointeur sur un entier
int* p;
// On place dans le contenu de p l'adresse de la variable a
// On dit que l'on fait "pointer p vers a"
p = &a;
// Déréférencer p, revient à accéder au contenu de la variable a
std::cout << *p; // affiche 5
// Si on modifie *p, on modifie le contenu de a
*p = 12;
std::cout << a; //affiche 12
// et inversement si on modifie a, la valeur de *p sera également modifiée
// dans cet exemple a et *p désigne le même endroit en mémoire.
// Création d'une autre variable b
int b = 8;
// On peut modifier p pour le faire pointer vers b
// c-a-d que le contenu de la variable p devient alors l'adresse de b
p = &b;
// Désormais b et *p désignent le même endroit en mémoire.
*p = 6; // b vaut désormais 6
// Attention à ne pas confondre
// p = &a -> p contient l'adresse de b (modifier *p revient à modifier a)
// *p = a -> le contenu de la variable pointé par p recoit
// la copie du contenu de la variable a
// mais modifier *p ne alors pas à modifier a
Second exemple:
struct vec3 {float x,y,z;};
// Création d'un vec3
vec3 v = {1.1f, 2.5f, 3.4f};
// On créé un pointeur sur la composante y et z:
float* py = &v.y;
float* pz = &v.z;
//py et pz contiennent respectivement l'adresse de la composante y et z de v
// Si on modifie *py ou *pz, on modifie directement les composantes du vecteur
*py = 4.1f; // même effet que v.y=4.1f;
*pz = -1.2f; // même effet que v.z=-1.2f;
Utilité des pointeurs
L'un des intérêts des pointeurs concerne le passage de paramètre modifiable dans une fonction.
On rappelle que lors d'une affectation ou d'une égalité entre deux variable, le contenu mémoire de l'une est copié dans le contenu mémoire de l'autre. Ce processus implique qu'une variable passé "par copie" dans une fonction ne pourra pas voir sa valeur modifiée par cette fonction.
Exemple:
void addOne(int x){
// modifie la valeur de x qui est une copie locale
// cela n'impacte pas la variable de la fonction appelante
x = x + 1;
}
int main()
{
int a = 5;
// le contenu de la variable a est passé en copie
// à l'argument de la fonction
addOne(a);
// le contenu de la variable a n'a pas été modifié
// a vaut toujours 5
std::cout<< a <<std::endl;
}
Pour permettre à une fonction de modifier la valeur de la variable d'origine, il est possible de passer l'adresse de celle-ci. Dans ce cas, la fonction prend comme argument un pointeur, dont la valeur est l'adresse de la variable - ce qui permet de modifier le contenu mémoire de cette variable.
Exemple:
void addOne(int* x){
// cette fois x est un pointeur, sa valeur est une adresse.
// Déréférencer x revient à accéder au contenu mémoire désigné à cette adresse
*x = *x + 1;
}
int main()
{
int a = 5;
// on passe l'adresse de la variable à modifier
addOne(&a);
// le contenu de la variable a est modifié
// a vaut désormais 6
std::cout<< a <<std::endl;
}