1.34k likes | 1.5k Vues
Les adresses et les pointeurs. Les adresses. Les cases mémoires ont toutes un numéro qui les distingue les une des autres: ce numéro est appelé adresse . C’est par cette adresse que le processeur peut communiquer avec la mémoire. 0. 1. 2. 3. 4. max. Les adresses et les variables.
E N D
Les adresses Les cases mémoires ont toutes un numéro qui les distingue les une des autres: ce numéro est appelé adresse. C’est par cette adresse que le processeur peut communiquer avec la mémoire. 0 1 2 3 4 . . . . . . max
Les adresses et les variables Le nom que l’on donne au cases mémoires est traduit en une adresse juste avant l’exécution d’un programme. Cela est nécessaire afin que le processeur sache à quelle case mémoire est associée chaque variable. En général il est impossible de prévoir à quelle adresse sera placée une variable. Le nom des variables est donc nécessaire. 0 c1: 1 char 2 c1 = c2; Lire le contenu de la case 3. Mettre ce qui a été lu dans la case 1. c2: 3 char 1324 4 . . . . . . max
Le partage de la mémoire Sur les systèmes modernes, il peut y avoir plusieurs usagers se partageant la mémoire et chaque usager peut exécuter plusieurs programmes simultanément. Cela signifie que l’on n’est pas libre d’utiliser toutes les cases mémoires comme on le veut. Une case peut être occupée par un programme à un certain moment et libre à un autre. Cette situation est aléatoire. Pour cette raison, on ne mentionne jamais explicitement une adresse dans un programme même si cela est théoriquement possible.
Adresses valides et non valides Exemple. Dans le pseudo-code suivant: Lire le contenu de la case 3. Mettre ce qui a été lu dans la case 1. Que se passe t-il si au moment de l’exécution la case mémoire 1 est déja utilisée par un autre programme. La case est alors non valide et il y aura erreur à l’exécution. C’est pour cette raison que l’on utilise des variables. Avant l’exécution, une adresse valide est associée à chaque variable. Seul notre programme pourra utiliser ces cases mémoire.
Position des variables dans la mémoire a int . . . . . . Sauf pour les tableaux, il n’y a aucune garantie que les variables occupent des cases adjacentes en mémoire. Exemple. int a,b[4],c,d[3]; int b[0] int b[1] int b[2] int b[3] int c . . . . . . d[0] int int d[1] int d[2] . . . . . .
Les adresses et les types Une des principales fonctions des types est d’indiquer le nombre d’octets utilisés par une variable. Par exemple nous avons vu que: un caractère prend 1 octet (8 bits) un entier prend 4 octets (32 bits). Cela signifie que si on divise la mémoire en case d’un octet alors: un char utilise 1 case un int utilise 4 cases adjacentes n: 0 00000000 int 1 00000000 2 00000000 3 00001101 c1: 4 00011011 char . . . c2: 5 00011100 char . . . . . . max
Remarque On peut aussi voir la mémoire comme une suite de cases de taille variable. n: 0 00000000000000000000000000001101 int int c1: 4 00011011 char . . . c2: 5 00011100 char . . . . . . max
Les adresses et les tableaux Le nom d’un tableau correspond à l’adresse du début du tableau. Exemple: char tab[5]; printf(“%p\n”, tab); 4027630992 printf(“%p\n”, tab+1); 4027630993 printf(“%p\n”, tab+2); 4027630994 Note: ‘%p’ sert à afficher les pointeurs.
Les tableaux d’entiers Exemple: int tab[5]; printf(“%p\n”, tab); 4027630976 printf(“%p\n”, tab+1); 4027630980 printf(“%p\n”, tab+2); 4027630984 Question: Pourquoi? +4 +4
L’incrémentation d’une adresse L’adresse a+1 n’est pas valide a: 16216 int . . . . . . int b[0]: b=24600 b[1]: b+1=24604 int int b[2]: b+2=24608 Incrémenter une adresse ne veux pas dire ajouter 1, cela veut dire aller à l’adresse suivant la variable courante. En général cela n’a du sens que si on est dans un tableau. int b[3]: b+3=24612 . . . . . . d[0]: d=54316 char char d[1]: d+1=54317 char d[2]: d+2=54318 . . . . . .
Remarque Si Tab est un tableau alors L’adresse de Tab[0] est Tab, l’adresse de Tab[1] est Tab + 1, l’adresse de Tab[2] est Tab + 2, etc. Cela est vrai quelque soit le type des éléments de Tab.
L’opérateur & Il est possible de connaître, pendant l’exécution d’un programme, l’adresse associée à une variable. En C, cela est possible à l’aide de l’opérateur unaire & Exemple: char c; int n, tab[1000]; L’adresse de c est &c L’adresse de n est &n L’adresse de tab[3] est &tab[3]
L’opérateur * Il est aussi possible de connaître, pendant l’exécution d’un programme, le contenu de la case mémoire située à une adresse donnée. En C, cela est possible à l’aide de l’opérateur unaire * • Exemple: • char c; • int tab[1000]; • Le contenu de l’adresse tab + 25 est *(tab + 25) • *(tab + 25) est donc identique à tab[25] • *(&c) est identique à c • *c n’a aucun sens
Exemple • Les expressions logiques suivantes sont vraies: • &n == 12556 • *(12560) == 60 • *(12560) < c2 • *(&r) == 12.345 . . . . . . int n: 12556 5000000 c1: 12560 char 60 c2: 12561 char 61 r: 12562 double 12.345 . . . . . .
Résumé des opérations sur les adresses • On peut: • Additionner une adresse et un entier • Déterminer l’adresse d’une variable • Déterminer le contenu d’une adresse • Mais, on ne peut pas • Additionner deux adresses • (mais on peut soustraire deux adresses d’un même tableau)
Les pointeurs Un pointeur est une variable pouvant contenir une adresse. Exemple: int *pn; pointeur sur une valeur entière char *pc; pointeur sur un caractère double *pr; pointeur sur un réel
Les pointeurs: exemple pn: 12556 int* Exemple: int *pn, m; . . . . . . m: 65710 int . . . . . .
Les pointeurs: exemple 65710 pn: 12556 int* Exemple: int *pn, m; pn = &m; . . . . . . m: 65710 int . . . . . .
Les pointeurs: exemple 65710 pn: 12556 int* Exemple: int *pn, m; pn = &m; m = 6; . . . . . . m: 65710 6 int . . . . . .
Les pointeurs: exemple 65710 pn: 12556 int* Exemple: int *pn, m; pn = &m; m = 6; *pn = 38; . . . . . . m: 65710 38 int . . . . . .
Les pointeurs et les tableaux En C les pointeurs sont intimement liés aux tableaux. Exemple: int tab[10], *p; p=tab; tab[3] = 70; *(tab + 3) = 70; p[3] = 70; *(p + 3) = 70; tous équivalents:
Remarque Le nom d’un tableau est une adresse constante et non pas un pointeur qui est une variable. Exemple: int tab[10], *p; p=tab; p = tab; /*Valide */ tab = p; /*Non valide */ 0 1 2 3 4 5 6 7 8 9 10 tab: p:
Quelques utilités des pointeurs • Pour implanter le passage de paramètres par référence • Pour implanter le passage de tableaux en paramètre • Pour utiliser des indices négatifs au tableaux • Fondamental en structure de données
Les pointeurs comme paramètres En C++: echanger(a, b) void echanger( int &x, int &y){ int tmp; tmp = x; x = y; y = tmp; }
Les pointeurs comme paramètres En C: echanger(&a, &b) void echanger( int *x, int *y){ int tmp; tmp = *x; *x = *y; *y = tmp; }
Les tableaux passés en paramètre Une des plus importantes utilisation des pointeurs réside dans le passage des tableaux en paramètre. Lorsque l’on passe le nom d’un tableau en paramètre, on passe une adresse. La fonction appelée reçoit donc une adresse qu’elle met dans une variable: cette variable doit donc être un pointeur.
Les tableaux passés en paramètre void liretab(int tab[], int max) { int c; int i=0; while ((c=getchar()) != EOF){ tab[i] = c; i = i + 1; } }
Les tableaux passés en paramètre void liretab(int *tab, int max) { int c; int i=0; while ((c=getchar()) != EOF){ *(tab+i) = c; i = i + 1; } }
Les indices de tableaux Exemple 1 Tableau d’entiers dont les indices vont de -5 à 5: int tab[11]; int *ptab; ptab = tab + 5; ptab[0] est identique à tab[5] ptab[-5] est identique à tab[0] ptab[5] est identique à tab[10] 0 1 2 3 4 5 6 7 8 9 10 tab: ptab:
Les indices de tableaux Exemple 2 Tableau d’entiers dont les indices vont de ‘A’ à ‘Z’: int tab[26]; int *ptab; ptab = tab - ‘A’; /* A vaut 65 en ASCII */ ptab[‘A’] est identique à tab[0] ptab[‘Z’] est identique à tab[25] -65 0 1 2 3 4 21 22 23 24 25 tab: ptab:
Allocation statique et dynamique Jusqu’à maintenant, toutes la mémoire que nous avons utilisée dans nos programmes devait avoir été allouée avant l'exécution à l’aide des déclarations de variables. Il est parfois utile d’allouer une partie de l’espace mémoire en cours d’exécution.
Exemple 1 Par exemple, si on a besoin de mémoriser un certain nombre d’objets mais que ce nombre n’est pas connu avant l’exécution du programme. Il faut alors allouer suffisamment d’espace au cas où le nombre d’objets est grand. Si le nombre d’objets est petit, on gaspille inutilement de l’espace mémoire.
Le fichier d’entête stdlib.h Le fichier d’entête stdlib.h contient des déclarations de fonctions traitant, entre autres, de l’allocation de la mémoire: - malloc - free - calloc - realloc
(void *)malloc(size_t size) size_t est un type d’entiers positifs. malloc retourne un pointeur sur un espace mémoire réservé à un objet de taille size, ou bien NULL si cette demande ne peut être satisfaite. La mémoire allouée n’est pas initialisée. Bien entendu, quand on travaille avec des pointeurs en C, ces derniers possédent chacun un type. Par conséquent, le type de données (void *) retourné par malloc doit toujours être mis dans le type de donnée avec lequel nous voulons travailler..
Exemple 2 int *p; *p = 10; /* INVALIDE puisque p ne pointe sur */ /* aucune case mémoire valide */ p = (int*) malloc(sizeof(int)) *p = 10; /* VALIDE */ p: Avant malloc Après malloc p:
Exemple 2 int *p; *p = 10; /* INVALIDE puisque p ne pointe sur */ /* aucune case mémoire valide */ p = (int) malloc(sizeof(int)) *p = 10; /* VALIDE */ Pourquoi? p: Avant malloc Après malloc p:
Pointeurs sur void La fonction malloc ne sait pas à quoi servira l’espace mémoire qui lui est demandé. Elle ne sait pas quel type d’objet utilisera cet espace. Alors, elle retourne un pointeur générique qui peut être convertien n’importe quel type de pointeur: un pointeur sur void
Conversion implicite En C, certaines conversions de type sont implicites: double x; int n; char c; n = c; /* un char est converti en un int */ c = n; /* un int est converti en un char */ x = n; /* un int est converti en un double */ n = x; /* un double est converti en un int */
Conversion explicite Dans toute expression, on peut forcer explicitement des conversions de types grâce à un opérateur unaire appelé cast. Dans la construction (nom de type) expression l’expression est convertie dans le type précisé (selon certaines règles).
Exemple 3 printf(“%d”, pow(2,3)); /* mauvaise façon */ printf(“%d”, (int) pow(2,3)); /* bonne façon */ int *p; struct complexe *cplx; p = (int *) malloc(sizeof(int)); cplx = (struct complexe *) malloc(sizeof(struct complexe));
void free(void * p) free libère l’espace mémoire pointé par p; elle ne fait rien si p vaut NULL. p doit être un pointeur sur un espace mémoire alloué par malloc, calloc ou realloc.
void *calloc(size_t nobj, size_t size) calloc retourne un pointeur sur un espace mémoire réservé à un tableau de nobj objets, tous de taille size, ou bien NULL si cette demande ne peut pas être satisfaite. La mémoire allouée est initialisée par des zéros.
void *realloc(void *p, size_t size) realloc change en size la taille de l’objet pointé par p. Si la nouvelle taille est plus petite que l’ancienne, seul le début du contenu de l’objet est conservé. Si la nouvelle taille est plus grande, le contenu de l’objet est conservé, et l’espace mémoire supplémentaire n’est pas initialisé. realloc retourne un pointeur sur un nouvel espace mémoire, ou bien NULL si cette demande ne peut pas être satisfaite, auquel cas *p n’est pas modifié.
Exemple 4 On veut lire des entiers et les mettre en mémoire. Plutôt que de créer un tableau avant l’exécution, on utilise calloc pendant l’exécution. int *p, n; scanf(“%d”, &n); p = (int *) calloc(n, sizeof(int)); si plus tard cet espace n’est plus suffisant, alors on utilise: p = (int *) realloc(p, 2*n); … p:
Exemple 5 Liste chaînée struct noeud{ int valeur; noeud *suivant; }; struct noeud *chaine, *p; chaine: p:
Exemple 5 chaine = (struct noeud) malloc(sizeof(struct noeud)); valeur suivant chaine: p:
Exemple 5 chaine = (struct noeud) malloc(sizeof(struct noeud)); chaine -> valeur = 8; 8 chaine: p:
Exemple 5 chaine -> suivant = (struct noeud) malloc(sizeof(struct noeud)); 8 chaine: p:
Exemple 5 p = chaine -> suivant; 8 chaine: p: