Cours BUT Info

Ch. 2-3 — Mémoire et pointeurs

La grande rue de la mémoire : adresses, & et sizeof, little-endian — pointeurs et déréférencement, passage par valeur, malloc/free et ses pièges, structures, puis registres du processeur et interruptions.

La grande rue de la mémoire

La mémoire est comme une grande rue : chaque maison — chaque case mémoire — contient 8 bits (un octet) et possède une adresse, généralement notée en hexadécimal. On peut y stocker une variable :

char car = 'B';          /* le code ascii de 'B' est 0x42 */
printf("adresse de car : %p\n", &car);   /* ex. 0x7ffc063697b7 */

Le contenu de car s'obtient en écrivant car ; son adresse, en écrivant &car. Si l'on modifie car (car = 'a';), l'adresse ne change pas — il redécore chez lui, mais il habite toujours à la même adresse.

Une seule maison ne stocke que 8 bits (de 0 à 255) : pour des valeurs plus grandes, il faut plus de maisons. L'opérateur sizeof donne le nombre d'octets d'un type — il retourne un long unsigned int, affiché avec %ld :

printf("size of char : %ld octet\n", sizeof(char));    /* 1 */
printf("size of int : %ld octets\n", sizeof(int));     /* 4 */
printf("size of float : %ld octets\n", sizeof(float)); /* 4 */

Un int entier = 10; occupe 4 cases contiguës, dont l'adresse d'entier est la première. L'entier est écrit en little-endian — octets de poids faible en premier (rappel du m99 de R1.03) : 0x0000000A est rangé 0x0A 0x00 0x00 0x00. Modifier le contenu d'une des cases intermédiaires modifie donc aussi entier.

Les pointeurs

L'idée : créer une variable qui contient l'adresse d'une autre. Si car habite à l'adresse 2, un pointeur pt peut contenir 2, et *pt désigne alors le contenu de car — on parle de déréférencer un pointeur. Un pointeur se déclare selon sa cible : char* pt;, int* pt;, float* pt; — globalement type* pt; déclare un pointeur vers une case contenant un objet de ce type.

int entier = 10;
int* pointeur;
pointeur = &entier;
printf("entier vaut : %d\n", entier);              /* 10  */
printf("*pointeur vaut : %d\n", *pointeur);        /* 10  */
*pointeur = 12;
printf("entier vaut : %d\n", entier);              /* 12 ! */

Deux étoiles à ne pas confondre

Le * de la déclaration (int* pt;) n'est pas l'opérateur de déréférencement (*pt = 12;). Les écritures int* pt; et int *pt; sont toutes deux correctes ; ce cours utilise int* pt;.

Pointeurs et fonctions

Deux règles encadrent les fonctions : les arguments sont passés par valeur — la fonction travaille sur une copie de la case reçue, à considérer comme une constante — et on ne retourne qu'une seule valeur. L'astuce du cours : vous ne pouvez pas modifier un argument, mais vous pouvez toujours modifier la case ciblée par un pointeur. On déclare donc l'argument comme pointeur et la fonction écrit *pointeur = 4; :

int par_valeur (int a)       { a = a + 1; return 0; }    /* le main ne voit rien */
int par_pointeur (int * b)   { *b = *b + 1; return 0; }  /* b du main passe à 1  */

Dans le main, après par_valeur(a) puis par_pointeur(pt_b), a vaut toujours 0 et b vaut 1.

Les tableaux comme pointeurs

Un tableau de taille 4, char tab[4] = {'a','b','c','d'};, place ses cases à la suite dans la mémoire ; en pratique, tab est simplement un pointeur vers son premier élément. L'adresse de tab[i] vaut donc tab + i, et son contenu *(tab + i) :

char* pt = &tab[0];
*(pt + 2) = 'e';    /* tab[2] vaut 'e'  */
*(tab + 3) = 'f';   /* tab[3] vaut 'f'  */

Pour passer un tableau en argument, on donne le pointeur vers son début — et comme il n'existe pas de fonction length en C, on passe souvent la taille en deuxième argument. Dans la fonction, on s'en sert comme d'un tableau normal ou avec la notation pointeur, en sachant toujours où est la fin du tableau. Attention : un tableau à deux dimensions (ou plus) passé en argument perd son aspect bidimensionnel.

malloc, ou comment demander de la mémoire

Retournons la logique : on déclare d'abord le pointeur, puis on lui donne l'adresse d'une case inutilisée, qui devient la cible de *pt — désormais considérée comme occupée par le programme. Pour demander de la mémoire : void* malloc(int size);, dans la bibliothèque stdlib.h. malloc réserve size octets à la suite et retourne un pointeur vers le premier octet réservé :

#include <stdlib.h>

char* pt;
pt = malloc(1);    /* un char fait 1 octet */
*pt = 'B';

Un appel malloc(4) réserve 4 octets — de quoi stocker un int ou un tableau de 4 char.

free, ou comment rendre la mémoire

Une case réservée est une case bloquée : si le programme ne l'utilise plus, il faut le dire au système. free libère l'espace ciblé par un pointeur — et une fois libérées, les cases peuvent changer de valeurs.

La règle d'or

Toute case mémoire réservée pour le programme doit être libérée par le programme : pour tout malloc, il doit y avoir un free.

sizeof : les pièges

sizeof est un opérateur (comme +, = ou ==) qui prend exactement un argument — le nom d'un type (int, char, int*…) — et renvoie le nombre d'octets d'un élément de ce type. Ne pas l'appliquer à une variable : risque important de mal interpréter le résultat.

printf("size of char* : %ld octets\n", sizeof(char *));  /* 8 octets */

char tab[100];
/* dans main :            sizeof(tab)      -> 100 octets */
/* dans la fonction       sizeof(tableau)  ->   8 octets  */

sizeof(char*) vaut 8 octets (la taille d'un pointeur), et le sizeof d'un tableau reçu en paramètre donne… la taille du pointeur, pas celle du tableau.

Les structures

Une structure est un regroupement d'autres types. Exemple du cours : un point sur une grille a une position x et une position y — on veut un type qui regroupe les deux :

typedef struct {
    int x;
    int y;
} point_t;

struct définit la structure, typedef annonce qu'on la nomme, point_t est ce nom ; en mémoire, ses éléments sont stockés à la suite. On l'utilise comme un type normal : point_t point; puis point.x et point.y — et le plus souvent via des pointeurs :

point_t* pt_point;
pt_point = malloc(sizeof(point_t));  /* l'espace d'un point_t */
pt_point->x = 6;                     /* équivaut à (*pt_point).x */
pt_point->y = 7;
free(pt_point);

pt_point->x désigne la valeur en x de l'élément de type point_t ciblé par pt_point.

Le processeur et les interruptions

Retour sur le m99 de R1.03. Le processeur contient des cases mémoire particulières, les registres — réservés au processeur, très rapides d'accès (pour m99 : R, A, B, PC, inst). Son cycle : charger une instruction depuis la mémoire, la décoder, l'exécuter, passer à la suivante. Son jeu d'instructions — l'ensemble des instructions exécutables — diffère d'un modèle à l'autre. Les cases mémoire peuvent donc aussi stocker des instructions en langage machine : la RAM contient le programme compilé, chargé en mémoire, et le processeur vient l'y lire.

Chaque composant d'entrée/sortie possède son driver. En sortie, le processeur envoie l'information et le périphérique, qui attend, la reçoit puis l'interprète. En entrée (une touche du clavier appuyée), deux mécanismes s'opposent :

  • le polling : un bit en mémoire vaut 0 touche relâchée, 1 touche appuyée ; le programme va le lire quand il veut (initiative du programme). Problème : il peut rater un clic s'il ne regarde pas au bon moment ;
  • les interruptions : le clavier envoie un message prioritaire au processeur, qui s'arrête pour le lire et le traiter, puis reprend son travail. Avantage : tous les signaux entrants sont captés. Inconvénient : trop de signaux, et la tâche en cours peut ne jamais se réaliser. C'est ce mécanisme qu'utilisent les ordinateurs.

Quand le processeur reçoit une interruption : il sauvegarde son état en mémoire, traite l'interruption — ce qui écrase tout ce qu'il faisait — puis recharge son état depuis les sauvegardes et continue dans le même état qu'avant.

A retenir

  • la mémoire est une rue de cases de 1 octet, chacune adressée ; &var donne l'adresse, sizeof(type) la taille (char 1, int 4, float 4, pointeur 8) ;
  • un int tient dans 4 cases contiguës, écrites en little-endian ;
  • un pointeur contient une adresse ; *pt déréférence — le seul moyen de faire « sortir » une valeur d'une fonction, les arguments étant passés par valeur ;
  • un tableau est un pointeur vers son premier élément : tab[i] ≡ *(tab + i), taille à passer à part ;
  • tout malloc appelle son free ; champs d'une structure : point.x ou pt->x ;
  • les entrées se gèrent par interruptions : sauvegarde de l'état, traitement, restauration.

Cours 02.pdf

R2.04 · Cours 2 — Mémoire et pointeurs · 20 pages

Consultation seule — téléchargement désactivé

Chargement du document…

Cours 03.pdf

R2.04 · Cours 3 — Gestion de la mémoire en C · 23 pages

Consultation seule — téléchargement désactivé

Chargement du document…

Dernière mise à jour le

Sur cette page