Essayer de coder ← Retour aux sujets

Adresse et déréférencement

& et *

Un pointeur est une variable qui contient l'adresse d'une autre variable. & donne l'adresse, * donne accès à la valeur qui s'y trouve.

int x = 5;
Une variable : une valeur rangée à une adresse
int *p = &x;
p contient l'adresse de x : p pointe vers x
*p
La valeur pointée (ici 5) : lecture
*p = 10;
Écriture : x vaut maintenant 10
int *q = NULL;
Pointeur qui ne pointe vers rien
printf("%p", (void *) p);
Affiche l'adresse contenue dans p
adresse.c
#include <stdio.h>

int main(void) {
    int x = 5;
    int *p = &x;

    printf("x = %d, *p = %d\n", x, *p);
    *p = 10;
    printf("x = %d\n", x);
    return 0;
}
Affichage
x = 5, *p = 5
x = 10

Tableaux et arithmétique

p + i

Le nom d'un tableau devient l'adresse de sa première case. Ajouter 1 à un pointeur le fait avancer d'un élément, pas d'un octet.

int t[4] = {10, 20, 30, 40};
Quatre entiers contigus
int *p = t;
p pointe vers t[0]
*(p + 2)
Équivaut à t[2] : 30
p++;
p pointe maintenant vers t[1]
t[i]
Écriture abrégée de *(t + i)
parcours.c
#include <stdio.h>

int main(void) {
    int t[4] = {10, 20, 30, 40};
    int *p = t;

    for (int i = 0; i < 4; i++) {
        printf("%d ", *(p + i));
    }
    printf("\n");
    printf("%d\n", t[2] == *(t + 2));
    return 0;
}
Affichage
10 20 30 40
1

📍 Le cours : comprendre les pointeurs

Les pointeurs font la réputation du C : ils permettent ses plus belles optimisations et provoquent ses pires bugs. Retiens l'idée simple : chaque variable vit quelque part en mémoire, à une adresse. Un pointeur est une variable qui contient une telle adresse.

La mémoire en schéma

adresse d'exemple      contenu           nom
0x7ffd1000          +------------+
                    |     10     |      x
                    +------------+
0x7ffd1008          +------------+
                    | 0x7ffd1000 |      p   (pointe vers x)
                    +------------+

Les adresses réelles changent à chaque exécution, et leur valeur exacte n'a aucune importance : ce qui compte, c'est la relation « p contient l'adresse de x ».

Déclarer, pointer, déréférencer

  • Dans une déclaration, int *p se lit « p est un pointeur vers un int ».
  • &x est l'adresse de x (opérateur « adresse de »).
  • Dans une expression, *p désigne la valeur située à l'adresse p (opérateur de déréférencement). On peut la lire (y = *p) ou la modifier (*p = 3).
  • Le type compte : un int * ne pointe que vers des int, un double * vers des double.

Tu utilises déjà des pointeurs sans le savoir : dans scanf("%d", &n), on donne à scanf l'adresse de n pour qu'elle puisse y écrire la valeur lue.

Pourquoi s'en servir ?

  • Pour qu'une fonction modifie les variables de l'appelant (voir Fonctions).
  • Pour manipuler les tableaux sans les copier.
  • Pour la mémoire dynamique : malloc renvoie une adresse (voir Mémoire dynamique).
  • Pour construire des structures chaînées (listes, arbres).

Le pointeur NULL

NULL est l'adresse « qui ne désigne rien ». Un pointeur qu'on ne peut pas encore initialiser avec une vraie adresse doit valoir NULL, et on le teste avant de le déréférencer : lire ou écrire à l'adresse NULL fait planter le programme (*segmentation fault*).

null.c
#include <stdio.h>

int main(void) {
    int *p = NULL;

    if (p != NULL) {
        printf("%d\n", *p);
    } else {
        printf("p ne pointe vers rien\n");
    }
    return 0;
}
Affichage
p ne pointe vers rien

Plusieurs résultats grâce aux adresses

Donner l'adresse d'une variable à une fonction lui permet d'y écrire un résultat. Pour la division euclidienne, deux résultats : le quotient et le reste.

divise.c
#include <stdio.h>

void divise(int a, int b, int *quotient, int *reste) {
    *quotient = a / b;
    *reste = a % b;
}

int main(void) {
    int q, r;

    divise(17, 5, &q, &r);
    printf("17 = 5 x %d + %d\n", q, r);
    return 0;
}
Affichage
17 = 5 x 3 + 2

Pointeurs et tableaux

t[i] est une écriture abrégée de *(t + i). Quand une fonction reçoit un tableau, elle reçoit en réalité un pointeur vers sa première case : int t[] et int *t sont équivalents en paramètre. Le parcours peut donc se faire avec un pointeur qui avance jusqu'à l'adresse située juste après la dernière case.

somme_ptr.c
#include <stdio.h>

int somme(const int *t, int n) {
    int s = 0;
    const int *fin = t + n;          /* juste après la dernière case */

    for (const int *p = t; p < fin; p++) {
        s += *p;
    }
    return s;
}

int main(void) {
    int t[] = {4, 8, 15, 16};
    printf("%d\n", somme(t, 4));
    return 0;
}
Affichage
43
const et pointeurs

const int *p signifie « je ne modifierai pas la valeur pointée » : le compilateur refuse *p = …. C'est la bonne façon de déclarer un paramètre qu'une fonction ne fait que lire.

⚠️ Erreurs fréquentes

Utiliser un pointeur non initialisé

À éviter
faux.c
int *p;        /* contient une adresse quelconque */
*p = 5;        /* écrit n'importe où en mémoire */
Correct
correct.c
int x;
int *p = &x;   /* p pointe vers une vraie variable */
*p = 5;

Confondre p et *p

À éviter
faux.c
p = 10;     /* change l'adresse contenue dans p ! */
Correct
correct.c
*p = 10;    /* change la valeur à l'adresse p */

Déréférencer NULL ou un pointeur invalide

Un *p quand p vaut NULL, ou quand p pointe vers une variable qui n'existe plus (variable locale d'une fonction terminée, mémoire déjà libérée), provoque un plantage ou, pire, un résultat faux sans message d'erreur. C'est un pointeur fou (*dangling pointer*).

Sortir des limites avec l'arithmétique

p + i ne vérifie pas que le résultat reste dans le tableau. Lire ou écrire après la dernière case est un comportement indéfini, comme avec un indice trop grand.

Une étoile qui ne s'applique qu'à une variable

À éviter
faux.c
int* a, b;     /* a est un int*, mais b est un simple int ! */
Correct
correct.c
int *a, *b;    /* deux pointeurs */

Afficher un pointeur avec %d

Utilise %p avec un cast (void *) pour une adresse. %d est réservé aux int : sur une machine 64 bits une adresse ne tient pas dans un int.

📋 Aide-mémoire

  • int *p; : pointeur vers int · &x : adresse de x · *p : valeur à l'adresse p.
  • p = &x; fait pointer p vers x ; *p = 3; modifie x.
  • NULL : ne pointe vers rien ; à tester avant tout *p.
  • t[i] ≡ *(t + i) ; p + 1 avance d'un élément ; un tableau passé à une fonction devient un pointeur.
  • const int *p : lecture seule via p ; %p + (void *) pour afficher une adresse.
  • Un pointeur doit toujours pointer vers quelque chose de valide, ou valoir NULL.