📍 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*).
#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;
}
Affichagep 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.
#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;
}
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.
#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;
}
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.