Pointeur : modèle d'adresse et accès indirect
R — Texte original
"Puisque l'on peut trouver la cellule variable souhaitée par son adresse, on peut dire que l'adresse pointe vers cette cellule. […] Par conséquent, l'adresse est appelée de manière imagée un 'pointeur'. Cela signifie que l'on peut trouver, grâce à elle, l'unité mémoire pour laquelle elle est l'adresse." — Chapitre 8 §8.1 Qu'est-ce qu'un pointeur
"Il faut d'abord comprendre précisément la signification du pointeur. […] Une fois que l'on a compris que 'pointeur signifie adresse', les choses deviennent plus faciles à comprendre, et de nombreux problèmes se résolvent d'eux-mêmes." — Chapitre 8 §8.9 Résumé sur les pointeurs (1)
"Il faut distinguer le pointeur et la variable pointeur. Le pointeur est l'adresse, tandis que la variable pointeur est la variable utilisée pour stocker l'adresse." — Chapitre 8 §8.9 Résumé sur les pointeurs (3)
"Il faut noter que la valeur de p égale à NULL et le fait de ne pas avoir assigné p sont deux concepts différents. Le premier a une valeur (0) et ne pointe vers aucune variable ; le second, bien que p n'ait pas été assigné, ne signifie pas que p n'a pas de valeur, seulement que sa valeur est imprévisible. […] Par conséquent, avant de déréférencer une variable pointeur, il faut lui avoir assigné une valeur." — Chapitre 8 §8.9 Résumé sur les pointeurs (8)
I — Présentation personnelle
La difficulté des pointeurs vient du fait qu'ils impliquent simultanément deux choses : l'adresse (un numéro d'emplacement) et le type de données stockées à cet emplacement. En séparant ces deux couches, tout le chapitre devient clair.
Première couche : l'adresse. Chaque variable dans un programme se voit attribuer plusieurs octets de mémoire à la compilation, chaque octet ayant un numéro appelé adresse — comme un numéro de chambre d'hôtel. En trouvant le numéro de chambre, on trouve la chambre, donc "l'adresse pointe vers cette unité", et on appelle l'adresse un "pointeur". Voici la phrase la plus importante du livre : le pointeur est l'adresse.
Il existe deux façons d'accéder à une variable : l'accès direct se fait par le nom de la variable (le compilateur a une table de correspondance "nom de variable → adresse" en interne, donc en donnant le nom, on trouve l'adresse) ; l'accès indirect consiste d'abord à stocker l'adresse dans une autre variable, puis à l'utiliser pour atteindre la cible — cette "variable spécialisée pour stocker des adresses" est la variable pointeur.
Deuxième couche : le type. Une adresse seule ne suffit pas. Supposons que l'on vous dise "prenez les données à partir de l'unité 2000". Vous ne saurez pas s'il faut prendre 1 octet (caractère), 2 octets (entier court) ou 4 octets (entier), ni s'il faut interpréter en complément à deux ou en format flottant. C'est pourquoi l'adresse en C est une adresse typée : elle ne contient pas seulement la position, mais aussi "le type de données pointé". C'est pour cela que, lors de la définition d'une variable pointeur, vous devez spécifier le type de base (le int dans int *p) : il détermine combien d'octets *p lit, selon quel format il les interprète, et aussi si p+1 avance de 1 octet ou de 4 octets.
De là découlent deux points essentiels concernant le déréférencement *p : *p représente "la variable pointée par p", et on peut à la fois la lire et y écrire (*p=3 stocke 3 dans l'unité pointée par p, pas dans p lui-même). La valeur de p est l'adresse ; modifier p change seulement "qui est pointé".
Les trois concepts les plus faciles à confondre doivent être strictement séparés :
- Pointeur = adresse (une valeur, comme 2000) ;
- Variable pointeur = variable qui stocke l'adresse (sa valeur est cette adresse) ;
- Type de base = le type de données pointé.
On peut donc dire "le pointeur de la variable i est 2000", mais on ne peut pas dire "la variable pointeur de i est 2000" — la variable pointeur est p elle-même, pas 2000.
Il n'y a qu'une seule règle absolue pour une utilisation sûre : avant d'utiliser un pointeur, il doit pointer vers un objet valide. Si une variable pointeur n'est pas initialisée après sa définition, sa valeur est un résidu aléatoire de la mémoire, et peut pointer n'importe où ; dans ce cas, *p = 3 modifie aléatoirement une zone mémoire sans rapport, et les symptômes peuvent n'apparaître que bien plus tard. Il faut distinguer trois états : ① non assigné (valeur sale, dangereux) ; ② valeur NULL (pointe explicitement vers rien, état valide et testable) ; ③ pointe vers un objet valide (le seul état autorisant le déréférencement). NULL est défini comme 0 dans stdio.h, et le système garantit que l'unité 0 n'est pas utilisée à d'autres fins.
Les véritables utilisations des pointeurs sont au nombre de trois (résumé à la fin de §8.9) : améliorer l'efficacité du programme ; permettre à une fonction de modifier les variables de l'appelant et de renvoyer plusieurs résultats ; réaliser l'allocation dynamique de mémoire (listes chaînées, etc.). Le livre met également en garde : le pointeur est "trop flexible", et les erreurs sont souvent cachées et peuvent corrompre tout le programme ; il faut être très prudent.
A1 — Exemples du manuel
- Exemple 8.2 Échange des points de deux pointeurs (résultat fourni par le manuel) : Saisir a=5, b=9, après
if(a<b){p=p1;p1=p2;p2=p;}, les valeurs de a et b n'ont pas été échangées (elles restent 5 et 9), mais les points dep1etp2ont été échangés, donc l'affichage de*p1,*p2donne 9 et 5. Citation du manuel : "Les valeurs de a et b n'ont pas été échangées, elles conservent leurs valeurs d'origine, mais les valeurs de p1 et p2 ont changé." Cet exemple distingue précisément entre "changer le point d'un pointeur" et "changer la valeur de la variable cible". - Exemple 8.3 swap avec passage de pointeur pour échanger les valeurs cibles (résultat fourni par le manuel) : Les paramètres formels sont
int *p1, int *p2, et dans la fonctiontemp=*p1; *p1=*p2; *p2=temp;ce qui est modifié est*p1et*p2, donc a et b sont vraiment échangés. Ces deux exemples, l'un échangeant le point (ex. 8.2) et l'autre échangeant les valeurs (ex. 8.3), sont le meilleur contraste pour comprendre la différence entrepet*p. - §8.1 Différence entre adresse et contenu (souligné par le manuel) : Supposons que la variable int i soit allouée à 2000-2003, j à 2004-2007, k à 2008-2011, alors
k=i+jsignifie "prendre i à partir de 2000, prendre j à partir de 2004, additionner et envoyer le résultat à partir de 2008". Avecscanf,scanf("%d",&i)envoie la valeur à l'unité commençant à 2000. Ce passage explique en détail le processus complet "nom de variable → adresse → lecture selon le type".
A2 — Quand déclencher
L'utilisateur est dans ces situations :
- Il bloque en apprenant les pointeurs et ne peut pas dire ce que sont
*p,&a, etp; - Une fonction souhaite modifier une variable de l'appelant, et quelqu'un lui dit "il faut passer un pointeur", mais il ne comprend pas pourquoi ;
- Le programme plante aléatoirement, ou une variable sans rapport est modifiée mystérieusement ;
- Il ne distingue pas les termes "pointeur" et "variable pointeur" ;
- Il se demande si le
*dansint *pet le*dans*psont la même chose ; - Il y a des pointeurs partout dans les tableaux, les chaînes, les listes chaînées, et il ne voit pas ce qui se passe en profondeur ;
- Il a utilisé un pointeur sans l'initialiser et a un défaut difficile à reproduire.
Signaux linguistiques : "Qu'est-ce qu'un pointeur en fait" "Comment comprendre qu'un pointeur est une adresse" "Différence entre *p et &a" "Quelle est la différence entre variable pointeur et pointeur" "Pourquoi faut-il spécifier le type de base" "Pourquoi p+1 ajoute 4 et pas 1" "Pointeur sauvage / pointeur nul" "Segmentation fault" "Pourquoi passer un pointeur à une fonction modifie l'argument réel".
Distinction avec les compétences adjacentes : Cette compétence couvre "modèle d'adresse + sémantique des pointeurs + règles de sécurité". Si le problème est "comment parcourir un tableau avec un pointeur" → utilisez d'abord array-and-string puis revenez ici ; si c'est "comment construire une liste chaînée" → utilisez la carte de compétence Struct et liste chaînée du même package ; si c'est "comment écrire les paramètres d'une fonction" → utilisez function-and-recursion (la partie mécanisme de passage de paramètres est complémentaire).
E — Étapes d'exécution
Contrat d'entrée : Besoin de ① le point de confusion actuel de l'utilisateur (concept flou / comportement inattendu d'un code / crash) ; ② le fragment de code concerné ; ③ si crash, les conditions de reproduction et le message d'erreur. Une simple description vague comme "je ne maîtrise pas les pointeurs" doit d'abord amener l'utilisateur à donner un code ou une question spécifique, sinon on ne peut que parler de concepts sans localiser le problème.
Contrat de sortie : Pour un scénario conceptuel, livrer "une décomposition en trois couches (adresse / variable pointeur / type de base) + un exemple de code exécutable en contraste" ; pour un scénario de débogage, livrer "classification du problème (pointeur sauvage / type non correspondant / entier utilisé comme adresse / confusion pointeur et variable pointeur) → cause → correction".
Étapes :
- Séparer les deux couches : D'abord confirmer si l'utilisateur bloque sur la "couche adresse" ou la "couche type". Critère de complétion : pouvoir dire quelle phrase précise l'utilisateur ne comprend pas.
- Établir le modèle d'adresse : Utiliser des métaphores comme "numéro de chambre", "clé de tiroir dans un autre tiroir" pour expliquer l'accès direct vs indirect. Critère de complétion : l'utilisateur peut lui-même dire ce qu'est
&aet ce que faitp=&a. - Distinguer les trois concepts : Clarifier "pointeur (adresse, une valeur) / variable pointeur (variable qui stocke l'adresse) / type de base (ce vers quoi on pointe)". Critère de complétion : l'utilisateur peut répondre correctement aux trois questions : "Quelle est la valeur de p ?", "Qu'est-ce que *p ?", "Quel est le type de p ?".
- Expliquer la double signification de
*(point de contrôle) : Le*dans une déclarationint *p;est juste un marqueur "c'est une variable pointeur" ; ce n'est que dans l'expression*pqu'il signifie "déréférencement". Si l'utilisateur confond les deux → revenir à l'étape 3 et recommencer, ne pas avancer. - Vérifier la concordance des types de base : Un
int *pne peut accepter que l'adresse d'une variableint; assigner l'adresse d'unfloatàint*interprétera les octets dans le mauvais format. Critère de complétion : les types des deux côtés de l'opérateur d'assignation sont cohérents. - Vérification de sécurité (obligatoire) : Confirmer que chaque pointeur déréférencé ① a été assigné à un objet valide, ou ② a été explicitement mis à
NULLavec un test de nullité. Critère de complétion : ne trouver aucun chemin où un pointeur est utilisé avec*psans avoir été initialisé. - Donner un exemple exécutable en contraste : Utiliser le contraste "échange de points vs échange de valeurs" (ex. 8.2 / 8.3) pour montrer la différence entre
pet*p. Critère de complétion : l'utilisateur peut prédire la sortie de chaque code.
B — Limites
- Non applicable : Si l'utilisateur veut simplement "parcourir un tableau avec un pointeur" sans se soucier de la théorie, donnez-lui directement la syntaxe, sans insister sur le modèle d'adresse complet.
- Portée de la mémoire dynamique à distinguer (mentionné dans A2 mais avec des limites) :
- Couvert :
mallocalloue,freelibère, oublierfreeprovoque une fuite mémoire (le programme occupe de plus en plus de mémoire en tournant), le système d'exploitation libère toute la mémoire du processus à la sortie (donc une petite fuite n'affecte pas la prochaine exécution, mais un service long va épuiser la mémoire). - Non couvert : Modèle de propriété mémoire (qui doit libérer), pointeurs pendants (utilisation du pointeur après
free), doublefree, problèmes d'optimisation dus aux alias de pointeurs,restrict, règles strictes d'alias, accès concurrents à un pointeur partagé entre threads. - Si l'utilisateur demande "peut-on encore utiliser ce pointeur après free ?" → cette carte peut seulement répondre "non, c'est un comportement indéfini, il faut le mettre à NULL", mais pour une conception plus profonde de la propriété, il faut préciser que cela dépasse le cadre du livre.
- Couvert :
- Angle mort de l'auteur (critique au stade 0) : L'explication des pointeurs dans ce livre utilise la métaphore "adresse = numéro de chambre", qui est très efficace pour comprendre
&/*, mais ne couvre pas la propriété mémoire, la durée de vie, les pointeurs pendants, les problèmes d'optimisation liés aux alias de pointeurs. Si l'utilisateur rencontre en projet réel l'utilisation aprèsfree, ou les pointeurs partagés entre threads, cette carte ne peut donner qu'un jugement de base, et il faut préciser que cela dépasse le cadre. - Confusions fréquentes : ①
*a un sens différent dans une déclaration et dans une expression ; ②&signifie à la fois "prendre l'adresse" et "ET bit à bit", à distinguer selon le contexte ; ③NULL(a une valeur, 0) ≠ non assigné (pas de valeur, valeur sale) ; ④ le nom d'un tableau est l'adresse du premier élément (un pointeur), mais n'est pas une variable pointeur — on ne peut pas l'assigner (a = pest illégal). - Mode d'échec : Considérer le pointeur comme une "technique avancée" pour frimer, en écrivant des expressions illisibles comme
*(*(p+i)+j). Le livre est clair : au début, "il ne faut pas trop tôt utiliser les détails du langage C qui peuvent facilement causer des erreurs", il faut privilégier un code clair et lisible. - Ligne rouge de sécurité : Ne jamais suggérer à l'utilisateur de déréférencer ou d'écrire via un pointeur sans avoir confirmé qu'il est valide ; pour les adresses système sensibles (comme l'unité 0 ou des adresses matérielles), expliquer seulement le principe sans donner d'exemple exécutable d'écriture.