# Pointeurs / adresses Ce mini cours a pour but de rééxpliquer la notion de pointeurs vue en INF203 en C. Les pointeurs sont une fonctionnalité très puissante et cruciale pour n'importe quel programme complexe. Il est cependant vrai qu'elle peut être compliquée à comprendre, en sachant qu'une fois acquie, son usage est très naturel. Le cours traitera d'abord des opérateurs: * `&` (référence) * `*` (déréférence) * `*` (modifieur de type) :::info On remarque déjà une des difficultés; le `*` a deux utilisations différentes. On reviendra dessus juste après. ::: On finira par parler un tout petit peu de `malloc()` et `free()`, mais ces deux fonctions sont simplissimes à comprendre une fois les 3 opérateurs compris. ## Sommaire [TOC] --- ## Théorie: Memoire vive d'un processus _[source partielle](https://manybutfinite.com/post/anatomy-of-a-program-in-memory/)_ :::warning Cette partie n'est pas super importante mais elle peut aider un peu à comprendre ::: Un processus (un programme en cours d'exécution) a besoin de mémoire vive pour deux raisons principales: * Stocker temporairement son code compilé (en langage machine) qui sera lu par le processeur après que l'OS l'ait copié depuis le disque dur * Stocker son "état": toutes les variables que vous créez et l'endroit auquel le processeur est rendu dans l'exécution. Quand vous lancez un processus, le système d'exploitation lui alloue plusieurs zones (=segments) juste pour lui dans la mémoire vive: * **Text** (_Lecture-seule_, _Taille constante_) (le nom porte à confusion, ça n'est pas du texte): L'endroit où est chargé tout le code compilé du programme en langage machine. L'OS fait presque un copier-coller du contenu du fichier dans cet endroit. C'est ici que le processeur lit les instructions en langage machine * **Data** (_Lecture / écriture_, _Taille constante_): Variables globales du programme (qui ne dépendent pas d'une fonction). Exemple: ```c= int a = 19; // Variable globale [segment "Data"] int faireDesTrucs() { int b = 23; // Variable locale [segment "Stack", c.f. juste après] return a + b; } ``` * **Heap** ou tas (_Lecture / écriture_, _Taille variable_): ==Le tas est initialisé à une taille de 0== et le processus peux demander à avoir accès à une quantité donnée de mémoire ici (c.f. `malloc()`) * **Stack** ou pile (_Lecture / écriture_, _Taille variable_): La pile stocke l'historique des appels de fonctions à l'instant actuel de l'exécution. Chaque "étage" de la pile (="cadre de pile") correspond à un appel, et contient toutes les variables locales et les paramètres passés à la fonction lors de cet appel. Quand une fonction est appellée, un cadre de pile est inséré tout en haut avec les paramètres passés à la fonction et de l'espace libre de taille connue à l'avance pour les variables locales (par exemple une fonction qui manipule deux variables locales de type `char` aura 2 octets). Quand l'exécution de la fonction actuelle se termine, le cadre de pile correspondant est supprimé du haut de la pile. ![](https://miro.medium.com/max/1200/1*B2v5_pqD-zbzJNOhAc-A2A.gif) Quand vous écrivez un programme en C, les valeurs des variables que vous manipulez, à votre niveau actuel, se situent soit dans **la pile**, soit dans "**data**". Si vous maîtrisez `malloc()`, vous utilisez également **le tas**. ## Le vif du sujet, les ++==pOiNtEUrS==++ L'idée derrière les pointeurs est très simple. Plutôt que de stocker une valeur dans une variable, on va stocker l'adresse de cette valeur au sein de la mémoire vive. ### Adresses :::danger TODO / W.I.P: Expliquer la notion d'adresse ::: ```c= int main() { char a = 12; // on met en pause ici } ``` | Adresse: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |----------|----|---|----|----|-----|-----|---|----|-----|-----| | Valeur: | 16 | 0 | 75 | 43 | 100 | 156 | 0 | 12 | 201 | 203 | ### Théorie: Adresses et C Souvenez vous, il y a trois opérateurs intéressants: `&`, `<type> *` et `*`. Voyons un par un leur utilité. #### Référencement ou `&` En C, il est tout à fait possible de récupérer l'adresse d'une variable, c'est à dire l'endroit ou est stocké le premier octet de la valeur de cette variable. On utilise l'opérateur unaire `&` pour récupérer l'adresse où une variable stocke sa valeur. On appelle ça le référencement, on récupère un "référence" (~=adresse) à une variable. ```c= #include <stdio.h> int main() { int a = 12; printf("valeur de a: %d\n", a); // (long int) permet de convertir l'adresse en long, sinon // le compilateur refuse de compiler. C'est logique, en temps // normal, on a aucune raison d'afficher une adresse printf("adresse de a: %ld\n", (long int) &a); } ``` #### Modificateur de type ou `<type> *` Maintenant qu'on a récupéré l'adresse, plutôt que de l'afficher, on peut avoir envie de la stocker elle même dans une variable. En C, vous savez qu'une variable est toujours déclarée avec un type spécifié explicitement. Pour stocker un pointeur vers quelque chose, on va noter le type de quelque chose et rajouter un `*` après. On appelle ce type de variables des **pointeurs**, car elles contiennent une adresse qui pointe vers une valeur. Ici, `a` est un `int`. L'adresse de `a` peut donc être stockée dans une variable de type `int*`, comme ci-dessous: ```c= #include <stdio.h> int main() { int a = 12; int *adresseDeA = &a; // On peut aussi écrire `int* adresseDeA` // Ou encore `int*adresseDeA` // L'espacement n'a aucune importance printf("valeur de a: %d\n", a); printf("adresse de a: %ld\n", (long int) adresseDeA); } ``` :::spoiler Plus d'info sur la conversion vers `(long int)` ligne 12 > Si vous printez la valeur de `sizeof(int *)`, vous obtiendrez toujours 8 peut importe ce avec quoi vous remplacez `int`. En fait, sur un processeur 64 bits, toutes les adresses sont stockées sur un entier sur 8 octets, c'est à dire un `long int`. C'est logique finalement, la longueur d'une adresse ne dépend pas de ce qui s'y trouve. ::: <br/> Pour résumer, `adresseDeA` contient la position dans la mémoire vive de la variable `a`. Maintenant, comment faire des choses utiles avec cette adresse, c'est à dire retrouver la valeur qui s'y trouve ou y remplacer la valeur actuelle ? #### Le déréférencement ou `*` L'opérateur `*` placé avant une variable **de type pointeur** permet d'accéder à la valeur qui s'y trouve, que ce soit pour la lire ou la modifier. On appelle ça déréférencement, car on récupère une valeur à partir d'une référence (~=adresse). ```c= #include <stdio.h> int main() { int a = 12; int *adresseDeA = &a; *adresseDeA += 30; // Pareil que a += 30; printf("valeur de a: %d\n", a); } ``` Dans une certaine mesure, on peut dire que `&` et `*` sont inverses (au sens mathématique). En effet `*(&a)` est équivalent à `a`, car on récupère l'adresse de `a`, puis on déréférence immédiatement cette adresse. #### Synthèse * L'opérateur `&` peut être appliqué sur n'importe quelle variable de n'importe quel `<type>` et retourne un `<type> *`, c'est à dire un pointeur vers ce type. * L'opérateur `*` peut être appliqué uniquement sur des types pointeurs de la forme `<type> *` et retourne un `<type>`. Si vous n'êtes pas perdu ici, vous avez tout compris. ## Applications concrètes _Vide_ ## Allouer de la mémoire dynamiquement Des fois, vous avez besoin d'avoir accès à une quantité arbitraire de mémoire (pour un tableau de taille inconnue par exemple). Pour ce faire, vous allez avoir besoin d'utiliser `malloc()`. Cette fonction a la signature suivante: ```c= void *malloc(size_t size); ``` Elle prend en unique paramètre la taille dont vous avez besoin (`size_t` est un type numérique). Elle retourne un `void *`, c'est un pointeur vers un type arbitraire. En fait, vous pouvez stocker sa valeur de retour dans n'importe quel pointeur. Exemple: ```c= #include <stdlib.h> int main() { // On alloue 4 octets, malloc renvoie l'adresse vers le premier int* addresseVersUnIntAlloue = malloc(4); *addresseVersUnIntAlloue = 42; } ``` Comme la taille de `int` dépend elle aussi du processeur vers lequel on compile, il est plus prudent d'écrire: ```c= #include <stdlib.h> int main() { // sizeof(int) va être remplacé par la taille d'un int int* addresseVersUnIntAlloue = malloc(sizeof(int)); *addresseVersUnIntAlloue = 42; } ``` Pour finir, le segment de memoire alloué n'est pas remis à 0, il peu y avoir des données résiduelles de ce qui y était stocké avant. Il est donc important d'initialiser correctement. Par exemple, reprenons l'exemple précédent: ```c= #include <stdlib.h> #include <stdio.h> int main() { int* addresseVersUnIntAlloue = malloc(sizeof(int)); printf("int alloué: %d\n", *addresseVersUnIntAlloue); } ``` Même si ça peut être le cas, il n'y aucune garantie que `addresseVersUnIntAlloue` pointe vers un int nul, car le segment peut ne pas contenir que des octets nuls. La fonction `calloc`, qui peut se substituer à `malloc` et possède la même signature, remet à zero le segment alloué. ### Libérer la mémoire allouée Allouer c'est sympa, mais C n'a aucun moyen de savoir que vous n'utilisez plus un segment alloué. Si vous continuez d'allouer sans rien faire, vous allez petit à petit remplir toute la RAM, ce qui s'appelle une fuite de mémoire. Pour régler ce problème, **il faut toujours libérer la mémoire allouée** avec la fonction `free()`: ```c= void free(void *ptr); ``` Elle prend comme unique paramètre un pointeur précédemment alloué et libère tout le segment d'un coup. Note: quand un processus se termine, toute sa mémoire est libérée, il n'est donc pas essentiel de libérer la mémoire si elle est utilisée du début à la fin du programme et n'est allouée qu'une fois. Il reste cependant plus prudent de libérer la mémoire tout le temps. ### Application concrète `malloc` pour un tableau de taille dynamiquement connue Ce programme demande un nombre n à l'utilisateur, et construit une chaine constituée, dans l'ordre ascii, des n caractères à partir de l'espace. ```c= #include <stdlib.h> #include <stdio.h> int main() { int taille; scanf("%d", &taille); // Lire depuis la console // taille + 1 pour pouvoir mettre le dernier élément à 0 // car une chaine est toujours terminée par 0 char *tab = malloc((taille + 1) * sizeof(char)); tab[taille] = 0; for (int i = 0; i < taille; i++) { tab[i] = i + ' '; } printf("%s\n", tab); // Pas essentiel car le segment va être libéré à la sortie // du programme quoi qu'il en soit... free(tab); } ```