Quels sont les obstacles à la compréhension des indicateurs et que peut-on faire pour les surmonter?

Pourquoi les pointeurs sont-ils un facteur de confusion important pour de nombreux nouveaux et même anciens étudiants de niveau C ou C ++? Existe-t-il des outils ou des processus de reflection qui vous ont aidé à comprendre comment les pointeurs fonctionnent au niveau de la variable, de la fonction et au-delà?

Quelles sont les bonnes pratiques qui peuvent être mises en œuvre pour amener quelqu’un au niveau “Ah-hah, je l’ai compris”, sans pour autant les enliser dans le concept global? Fondamentalement, percez des scénarios similaires.

Les pointeurs sont un concept qui, pour beaucoup, peut être déroutant au début, en particulier lorsqu’il s’agit de copier des valeurs de pointeur autour du même bloc de mémoire et de s’y référer.

J’ai trouvé que la meilleure analogie est de considérer le pointeur comme un morceau de papier sur lequel se trouve une adresse de maison, et le bloc de mémoire fait référence à la maison réelle. Toutes sortes d’opérations peuvent ainsi être facilement expliquées.

J’ai ajouté du code Delphi ci-dessous, et quelques commentaires le cas échéant. J’ai choisi Delphi car mon autre langage de programmation principal, C #, ne présente pas les mêmes choses que les memory leaks.

Si vous souhaitez uniquement apprendre le concept de pointeur de haut niveau, vous devez ignorer les parties intitulées “Disposition de la mémoire” dans l’explication ci-dessous. Ils sont destinés à donner des exemples de ce à quoi la mémoire pourrait ressembler après les opérations, mais ils sont de nature moins complexe. Cependant, afin d’expliquer avec précision le fonctionnement des dépassements de tampon, il était important d’append ces diagrammes.

Disclaimer: À toutes fins utiles, cette explication et l’exemple de disposition de la mémoire sont grandement simplifiés. Il y a plus de frais généraux et plus de détails à connaître si vous devez gérer la mémoire à un niveau inférieur. Cependant, pour expliquer la mémoire et les pointeurs, il est suffisamment précis.


Supposons que la classe THouse utilisée ci-dessous ressemble à ceci:

type THouse = class private FName : array[0..9] of Char; public constructor Create(name: PChar); end; 

Lorsque vous initialisez l’object house, le nom donné au constructeur est copié dans le champ privé FName. Il y a une raison pour laquelle il est défini comme un tableau de taille fixe.

En mémoire, il y aura des frais généraux associés à l’allocation de la maison, je vais illustrer ceci ci-dessous comme ceci:

 --- [ttttNNNNNNNNNN] ---
      ^ ^
      |  |
      |  + - le tableau FName
      |
      + - frais généraux

La zone “tttt” est surchargée, il y en aura généralement plus pour différents types d’exécutions et de langages, comme 8 ou 12 octets. Il est impératif que les valeurs stockées dans cette zone ne soient modifiées par rien d’autre que l’allocateur de mémoire ou les routines du système principal, ou vous risquez de planter le programme.


Allouer de la mémoire

Demandez à un entrepreneur de construire votre maison et de vous donner l’adresse de la maison. Contrairement au monde réel, l’allocation de mémoire ne peut pas être définie sur l’endroit où allouer, mais trouvera un emplacement approprié avec suffisamment de place et restituera l’adresse à la mémoire allouée.

En d’autres termes, l’entrepreneur choisira l’endroit.

 THouse.Create('My house'); 

Disposition de la mémoire:

 --- [ttttNNNNNNNNNN] ---
     1234Ma maison

Garder une variable avec l’adresse

Écrivez l’adresse de votre nouvelle maison sur un morceau de papier. Ce papier servira de référence à votre maison. Sans ce morceau de papier, vous êtes perdu et vous ne pouvez pas trouver la maison, à moins que vous y soyez déjà.

 var h: THouse; begin h := THouse.Create('My house'); ... 

Disposition de la mémoire:

     h
     v
 --- [ttttNNNNNNNNNN] ---
     1234Ma maison

Copier la valeur du pointeur

Écrivez simplement l’adresse sur un nouveau morceau de papier. Vous avez maintenant deux morceaux de papier qui vous mèneront à la même maison, pas deux maisons séparées. Toute tentative de suivre l’adresse à partir d’un papier et de réorganiser les meubles dans cette maison donnera l’impression que l’autre maison a été modifiée de la même manière, à moins que vous ne puissiez détecter explicitement qu’il ne s’agit que d’une seule maison.

Note C’est généralement le concept que j’ai le plus de problèmes à expliquer aux gens, deux pointeurs ne signifient pas deux objects ou blocs de mémoire.

 var h1, h2: THouse; begin h1 := THouse.Create('My house'); h2 := h1; // copies the address, not the house ... 
     h1
     v
 --- [ttttNNNNNNNNNN] ---
     1234Ma maison
     ^
     h2

Libérer la mémoire

Démolir la maison. Vous pourrez ensuite réutiliser le papier pour une nouvelle adresse si vous le souhaitez, ou le supprimer pour oublier l’adresse de la maison qui n’existe plus.

 var h: THouse; begin h := THouse.Create('My house'); ... h.Free; h := nil; 

Ici, je construis d’abord la maison et je saisis son adresse. Ensuite, je fais quelque chose à la maison (utilisez-le, le code, laissé comme exercice pour le lecteur), puis je le libère. Enfin, j’efface l’adresse de ma variable.

Disposition de la mémoire:

     h <- +
     v + - avant gratuit
 --- [ttttNNNNNNNNNN] --- |
     1234Ma maison <- +

     h (pointe maintenant nulle part) <- +
                                 + - après gratuit
 ---------------------- |  (note, la mémoire pourrait encore
     xx34Ma maison <- + contient des données)

Pointeurs pendants

Vous dites à votre entrepreneur de détruire la maison, mais vous oubliez d'effacer l'adresse de votre feuille de papier. Lorsque vous regardez plus tard le morceau de papier, vous avez oublié que la maison n'est plus là et va la visiter, avec des résultats ratés (voir également la partie concernant une référence non valide ci-dessous).

 var h: THouse; begin h := THouse.Create('My house'); ... h.Free; ... // forgot to clear h here h.OpenFrontDoor; // will most likely fail 

Utiliser h après l'appel à .Free pourrait fonctionner, mais ce n'est que de la chance. Il est fort probable que cela échouera chez un client, en pleine opération critique.

     h <- +
     v + - avant gratuit
 --- [ttttNNNNNNNNNN] --- |
     1234Ma maison <- +

     h <- +
     v + - après gratuit
 ---------------------- |
     xx34Ma maison <- +

Comme vous pouvez le constater, h pointe toujours vers les rests de données en mémoire, mais comme il risque de ne pas être complet, son utilisation peut échouer.


Fuite de mémoire

Vous perdez le morceau de papier et vous ne pouvez pas trouver la maison. La maison est toujours debout quelque part et quand vous voulez construire plus tard une nouvelle maison, vous ne pouvez pas réutiliser cet endroit.

 var h: THouse; begin h := THouse.Create('My house'); h := THouse.Create('My house'); // uh-oh, what happened to our first house? ... h.Free; h := nil; 

Ici, nous avons écrasé le contenu de la variable h avec l'adresse d'une nouvelle maison, mais l'ancienne est toujours debout ... quelque part. Après ce code, il n'y a aucun moyen d'atteindre cette maison, et elle sera laissée en suspens. En d'autres termes, la mémoire allouée restra allouée jusqu'à la fermeture de l'application, à quel point le système d'exploitation va le détruire.

Disposition de la mémoire après la première allocation:

     h
     v
 --- [ttttNNNNNNNNNN] ---
     1234Ma maison

Disposition de la mémoire après la deuxième allocation:

                        h
                        v
 --- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
     1234Ma maison 5678Ma maison

Une méthode plus courante pour obtenir cette méthode consiste simplement à oublier de libérer quelque chose au lieu de la remplacer comme ci-dessus. En termes Delphi, cela se produira avec la méthode suivante:

 procedure OpenTheFrontDoorOfANewHouse; var h: THouse; begin h := THouse.Create('My house'); h.OpenFrontDoor; // uh-oh, no .Free here, where does the address go? end; 

Une fois cette méthode exécutée, il n'y a pas de place dans les variables pour que l'adresse de la maison existe, mais la maison est toujours là.

Disposition de la mémoire:

     h <- +
     v + - avant de perdre le pointeur
 --- [ttttNNNNNNNNNN] --- |
     1234Ma maison <- +

     h (pointe maintenant nulle part) <- +
                                 + - après avoir perdu le pointeur
 --- [ttttNNNNNNNNNN] --- |
     1234Ma maison <- +

Comme vous pouvez le constater, les anciennes données sont conservées en mémoire et ne seront pas réutilisées par l’allocateur de mémoire. L'allocateur conserve la trace des zones de mémoire utilisées et ne les réutilise que si vous les libérez.


Libérer la mémoire mais conserver une référence (désormais invalide)

Démolissez la maison, effacez une des feuilles de papier, mais vous avez aussi une autre feuille avec l’ancienne adresse, quand vous allez à l’adresse, vous ne trouverez pas de maison, mais vous pourriez trouver quelque chose qui ressemble aux ruines. d'un.

Peut-être trouverez-vous même une maison, mais ce n’est pas la maison à laquelle vous avez été donné l’adresse et, par conséquent, toute tentative de l’utiliser comme si elle vous appartenait pourrait échouer horriblement.

Parfois, vous pouvez même trouver une adresse assez grande sur une adresse voisine qui occupe trois adresses (rue principale 1-3), et votre adresse va au milieu de la maison. Toute tentative de traiter cette partie de la grande maison à trois adresses comme une seule petite maison pourrait aussi échouer horriblement.

 var h1, h2: THouse; begin h1 := THouse.Create('My house'); h2 := h1; // copies the address, not the house ... h1.Free; h1 := nil; h2.OpenFrontDoor; // uh-oh, what happened to our house? 

Ici, la maison a été démolie, à travers la référence en h1 , et tandis que h1 été effacé, h2 toujours l'ancienne adresse, désuète. L'access à la maison qui n'est plus debout pourrait ou non fonctionner.

Ceci est une variation du pointeur suspendu ci-dessus. Voir sa disposition en mémoire.


Saturation de la mémoire tampon

Vous déplacez plus de choses dans la maison que vous ne pouvez le faire, en vous répandant dans la maison ou la cour des voisins. Lorsque le propriétaire de cette maison voisine rentrera plus tard à la maison, il trouvera toutes sortes de choses qu'il considérera comme les siennes.

C'est la raison pour laquelle j'ai choisi un tableau de taille fixe. Pour préparer le terrain, supposons que la deuxième maison que nous allouons soit, pour une raison quelconque, placée avant la première en mémoire. En d'autres termes, la deuxième maison aura une adresse inférieure à la première. En outre, ils sont alloués les uns à côté des autres.

Ainsi, ce code:

 var h1, h2: THouse; begin h1 := THouse.Create('My house'); h2 := THouse.Create('My other house somewhere'); ^-----------------------^ longer than 10 characters 0123456789 <-- 10 characters 

Disposition de la mémoire après la première allocation:

                         h1
                         v
 ----------------------- [ttttNNNNNNNNNN]
                         5678Ma maison

Disposition de la mémoire après la deuxième allocation:

     h2 h1
     vv
 --- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
     1234Mon autre maison quelque part
                         ^ --- + - ^
                             |
                             + - écrasé

La partie qui causera le plus souvent un crash est lorsque vous écrasez des parties importantes des données que vous avez stockées et qui ne devraient pas être modifiées de manière aléatoire. Par exemple, cela ne pose peut-être pas de problème que certaines parties du nom de h1-house aient été modifiées, en écrasant le programme, mais écrasant le surcoût de l'object lorsque vous essayez d'utiliser l'object cassé. écraser les liens stockés dans d'autres objects de l'object.


Listes liées

Lorsque vous suivez une adresse sur une feuille de papier, vous arrivez à une maison et, à cette maison, il y a une autre feuille de papier sur laquelle est inscrite une nouvelle adresse, la prochaine maison de la chaîne, etc.

 var h1, h2: THouse; begin h1 := THouse.Create('Home'); h2 := THouse.Create('Cabin'); h1.NextHouse := h2; 

Ici, nous créons un lien de notre maison à notre cabine. Nous pouvons suivre la chaîne jusqu'à ce qu'une maison n'ait pas de référence NextHouse , ce qui signifie que c'est la dernière. Pour visiter toutes nos maisons, nous pourrions utiliser le code suivant:

 var h1, h2: THouse; h: THouse; begin h1 := THouse.Create('Home'); h2 := THouse.Create('Cabin'); h1.NextHouse := h2; ... h := h1; while h <> nil do begin h.LockAllDoors; h.CloseAllWindows; h := h.NextHouse; end; 

Disposition de la mémoire (ajout de NextHouse en tant que lien dans l'object, noté avec les quatre LLLL dans le diagramme ci-dessous):

     h1 h2
     vv
 --- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
     1234Accueil + 5678Cabine +
                    |  ^ |
                    + -------- + * (pas de lien)

En termes simples, quelle est une adresse mémoire?

Une adresse mémoire est simplement un nombre. Si vous considérez la mémoire comme un grand tableau d'octets, le tout premier octet a l'adresse 0, la suivante l'adresse 1 et ainsi de suite. Ceci est simplifié, mais suffisant.

Donc, cette disposition de mémoire:

     h1 h2
     vv
 --- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
     1234Ma maison 5678Ma maison

Pourrait avoir ces deux adresses (la plus à gauche - est l'adresse 0):

  • h1 = 4
  • h2 = 23

Ce qui signifie que notre liste de liens ci-dessus pourrait ressembler à ceci:

     h1 (= 4) h2 (= 28)
     vv
 --- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
     1234Accueil 0028 5678Cabine 0000
                    |  ^ |
                    + -------- + * (pas de lien)

Il est courant de stocker une adresse qui "ne pointe nulle part" comme une adresse zéro.


En termes simples, qu'est-ce qu'un pointeur?

Un pointeur est juste une variable contenant une adresse mémoire. Vous pouvez généralement demander au langage de programmation de vous donner son numéro, mais la plupart des langages de programmation et des runtimes tentent de cacher le fait qu'il y a un nombre en dessous, simplement parce que le numéro lui-même n'a aucune signification. Il est préférable de penser à un pointeur comme une boîte noire, c.-à-d. vous ne savez pas ou ne vous souciez pas vraiment de la façon dont cela est réellement mis en œuvre, aussi longtemps que cela fonctionne.

Dans mon premier cours de Compi Sci, nous avons fait l’exercice suivant. Certes, il s’agissait d’une salle de conférence avec environ 200 étudiants dans …

Le professeur écrit au tableau: int john;

John se lève

Le professeur écrit: int *sally = &john;

Sally se lève, pointe sur John

Professeur: int *bill = sally;

Bill se lève, pointe sur John

Professeur: int sam;

Sam se lève

Professeur: bill = &sam;

Bill pointe maintenant vers Sam.

Je pense que vous avez l’idée. Je pense que nous avons passé environ une heure à le faire, jusqu’à ce que nous passions aux bases de l’affectation des pointeurs.

Une analogie que j’ai trouvée utile pour expliquer les indicateurs est les hyperliens. La plupart des gens peuvent comprendre qu’un lien sur une page Web pointe vers une autre page sur Internet, et si vous pouvez copier et coller cet hyperlien, ils indiqueront tous deux la même page Web d’origine. Si vous allez modifier cette page originale, puis suivez l’un de ces liens (pointeurs), vous obtiendrez cette nouvelle page mise à jour.

La raison pour laquelle les pointeurs semblent confondre tant de personnes, c’est qu’ils viennent pour la plupart avec peu ou pas de connaissances en architecture informatique. Étant donné que beaucoup ne semblent pas avoir une idée de la manière dont les ordinateurs (la machine) sont réellement implémentés, travailler en C / C ++ semble étranger.

Un exercice consiste à leur demander d’implémenter une simple machine virtuelle basée sur un bytecode (dans n’importe quel langage, python fonctionne parfaitement) avec un jeu d’instructions axé sur les opérations de pointage (chargement, stockage, adressage direct / indirect). Demandez-leur ensuite d’écrire des programmes simples pour ce jeu d’instructions.

Tout ce qui nécessite un peu plus qu’un simple ajout va impliquer des pointeurs et ils sont sûrs de l’obtenir.

Pourquoi les pointeurs sont-ils un facteur de confusion majeur pour de nombreux nouveaux étudiants, voire anciens, du niveau C / C ++?

Le concept d’un espace réservé pour une valeur – variables – correspond à quelque chose qu’on enseigne à l’école – l’algèbre. Il n’y a pas de parallèle existant que vous pouvez dessiner sans comprendre comment la mémoire est physiquement disposée dans un ordinateur, et personne ne pense à ce genre de choses avant d’avoir affaire à des niveaux bas – au niveau des communications C / C ++ / octets .

Existe-t-il des outils ou des processus de reflection qui vous ont aidé à comprendre comment les pointeurs fonctionnent au niveau de la variable, de la fonction et au-delà?

Boîtes d’adresses Je me souviens quand j’apprenais à programmer BASIC dans des micro-ordinateurs, il y avait ces jolis livres avec des jeux, et parfois il fallait introduire des valeurs dans des adresses particulières. Ils avaient une image d’un groupe de boîtes, étiquetées de manière incrémentielle avec 0, 1, 2 … et il a été expliqué qu’une seule petite chose (un octet) pouvait tenir dans ces boîtes, et il y en avait beaucoup – certains ordinateurs eu autant que 65535! Ils étaient côte à côte et ils avaient tous une adresse.

Quelles sont les bonnes pratiques qui peuvent être mises en œuvre pour amener quelqu’un au niveau “Ah-hah, je l’ai compris”, sans pour autant les enliser dans le concept global? Fondamentalement, percez des scénarios similaires.

Pour une perceuse? Faire une structure:

 struct { char a; char b; char c; char d; } mystruct; mystruct.a = 'r'; mystruct.b = 's'; mystruct.c = 't'; mystruct.d = 'u'; char* my_pointer; my_pointer = &mystruct.b; cout << 'Start: my_pointer = ' << *my_pointer << endl; my_pointer++; cout << 'After: my_pointer = ' << *my_pointer << endl; my_pointer = &mystruct.a; cout << 'Then: my_pointer = ' << *my_pointer << endl; my_pointer = my_pointer + 3; cout << 'End: my_pointer = ' << *my_pointer << endl; 

Même exemple que ci-dessus, sauf en C:

 // Same example as above, except in C: struct { char a; char b; char c; char d; } mystruct; mystruct.a = 'r'; mystruct.b = 's'; mystruct.c = 't'; mystruct.d = 'u'; char* my_pointer; my_pointer = &mystruct.b; printf("Start: my_pointer = %c\n", *my_pointer); my_pointer++; printf("After: my_pointer = %c\n", *my_pointer); my_pointer = &mystruct.a; printf("Then: my_pointer = %c\n", *my_pointer); my_pointer = my_pointer + 3; printf("End: my_pointer = %c\n", *my_pointer); 

Sortie:

 Start: my_pointer = s After: my_pointer = t Then: my_pointer = r End: my_pointer = u 

Peut-être que cela explique certaines des bases par exemple?

La raison pour laquelle j’ai eu du mal à comprendre les indicateurs, au début, est que de nombreuses explications incluent beaucoup de conneries sur le passage par référence. Tout cela ne fait qu’embrouiller la question. Lorsque vous utilisez un paramètre de pointeur, vous passez toujours par valeur; mais la valeur se trouve être une adresse plutôt que, par exemple, un int.

Quelqu’un d’autre a déjà lié ce tutoriel, mais je peux souligner le moment où j’ai commencé à comprendre les pointeurs:

Un tutoriel sur les pointeurs et les tableaux dans C: Chapitre 3 – Pointeurs et chaînes

 int puts(const char *s); 

Pour le moment, ignorez le const. Le paramètre passé à puts() est un pointeur, c’est-à-dire la valeur d’un pointeur (puisque tous les parameters de C sont passés par valeur), et la valeur d’un pointeur est l’adresse à laquelle il pointe ou simplement une adresse . Ainsi, lorsque nous écrivons des puts(strA); comme nous l’avons vu, nous passons l’adresse de strA [0].

Au moment où j’ai lu ces mots, les nuages ​​se sont séparés et un rayon de soleil m’a enveloppé de compréhension de pointeur.

Même si vous êtes un développeur VB .NET ou C # (comme moi) et n’utilisez jamais de code non sécurisé, il est toujours utile de comprendre comment fonctionnent les pointeurs, ou vous ne comprendrez pas le fonctionnement des références d’objects. Ensuite, vous aurez la notion commune mais erronée selon laquelle le passage d’une référence d’object à une méthode copie l’object.

J’ai trouvé que “Tutorial on Pointers and Arrays in C” de Ted Jensen était une excellente ressource pour apprendre les pointeurs. Il est divisé en 10 leçons, en commençant par une explication de ce que sont les pointeurs (et à quoi ils servent) et en terminant par des pointeurs de fonction. http://home.netcom.com/~tjensen/ptr/cpoint.htm

À partir de là, le Guide de programmation réseau de Beej présente l’API des sockets Unix, à partir de laquelle vous pouvez commencer à faire des choses vraiment amusantes. http://beej.us/guide/bgnet/

Les complexités des indicateurs vont au-delà de ce que nous pouvons facilement enseigner. Avoir des élèves qui se pointent les uns les autres et utiliser des morceaux de papier avec des adresses de maison sont deux excellents outils d’apprentissage. Ils font un excellent travail en introduisant les concepts de base. En effet, apprendre les concepts de base est essentiel pour réussir à utiliser des pointeurs. Cependant, dans le code de production, il est courant de se retrouver dans des scénarios beaucoup plus complexes que ces simples démonstrations peuvent encapsuler.

J’ai été impliqué dans des systèmes où nous avions des structures pointant vers d’autres structures pointant vers d’autres structures. Certaines de ces structures contenaient également des structures intégrées (plutôt que des pointeurs vers des structures supplémentaires). C’est là que les pointeurs deviennent vraiment déroutants. Si vous avez plusieurs niveaux d’indirection, et que vous commencez avec du code comme celui-ci:

 widget->wazzle.fizzle = fazzle.foozle->wazzle; 

cela peut être très compliqué rapidement (imaginez beaucoup plus de lignes, et potentiellement plus de niveaux). Ajoutez des tableaux de pointeurs et des pointeurs de nœud à nœud (arborescences, listes chaînées) et la situation empire encore. J’ai vu de très bons développeurs se perdre une fois qu’ils ont commencé à travailler sur de tels systèmes, même les développeurs qui comprenaient très bien les bases.

Les structures complexes de pointeurs n’indiquent pas nécessairement un mauvais codage (même si elles le peuvent). La composition est un élément essentiel de la programmation orientée object et, dans les langages avec des pointeurs bruts, elle conduira inévitablement à une indirection multi-couches. De plus, les systèmes doivent souvent utiliser des bibliothèques tierces avec des structures qui ne correspondent pas entre elles en termes de style ou de technique. Dans de telles situations, la complexité va naturellement se produire (même si, bien sûr, nous devrions le combattre autant que possible).

Je pense que la meilleure chose que les collèges peuvent faire pour aider les étudiants à apprendre des conseils est d’utiliser de bonnes démonstrations, combinées à des projets nécessitant une utilisation de pointeur. Un projet difficile fera plus pour comprendre les pointeurs que mille manifestations. Les démonstrations peuvent vous amener à une compréhension superficielle, mais pour bien saisir les indications, vous devez vraiment les utiliser.

Je ne pense pas que les pointeurs en tant que concept soient particulièrement délicats – la plupart des modèles mentaux des étudiants correspondent à quelque chose comme cela et des croquis rapides peuvent aider.

La difficulté, du moins celle que j’ai connue par le passé et que j’ai vue avec les autres, est que la gestion des pointeurs en C / C ++ peut être compliquée de façon inhabituelle.

Un exemple de tutoriel avec un bon ensemble de diagrammes facilite grandement la compréhension des pointeurs .

Joel Spolsky fait de bonnes remarques sur la compréhension des indications dans son article Guerrilla Guide to Interviewing :

Pour une raison quelconque, la plupart des gens semblent être nés sans la partie du cerveau qui comprend les pointeurs. C’est une chose d’aptitude et non une compétence – il faut une forme complexe de pensée doublement indirecte que certaines personnes ne peuvent tout simplement pas faire.

Je pensais append une analogie à cette liste que j’ai trouvé très utile pour expliquer des conseils (de retour dans la journée) en tant que tuteur en informatique. Tout d’abord, allons:


Mettre en scène :

Considérons un parking avec 3 espaces, ces espaces sont numérotés:

 ------------------- | | | | | 1 | 2 | 3 | | | | | 

D’une certaine manière, c’est comme les emplacements de mémoire, ils sont séquentiels et contigus … un peu comme un tableau. En ce moment, il n’y a pas de voitures dans ces voitures, c’est comme un tableau vide ( parking_lot[3] = {0} ).


Ajouter les données

Un parking ne rest jamais longtemps vide … si tel était le cas, ce serait inutile et personne n’en construirait. Alors que la journée avance, le lot se remplit de 3 voitures, une voiture bleue, une voiture rouge et une voiture verte:

  1 2 3 ------------------- | o=o | o=o | o=o | | |B| | |R| | |G| | | oo | oo | oo | 

Ces voitures sont du même type (voiture), une façon de penser est que nos voitures sont une sorte de données (disons un int ), mais elles ont des valeurs différentes ( blue , red , green ; cela pourrait être une couleur).


Entrez le pointeur

Maintenant, si je vous emmène dans ce parking et vous demande de me trouver une voiture bleue, vous tendez un doigt et utilisez-le pour pointer une voiture bleue au point 1. C’est comme prendre un pointeur et l’assigner à une adresse mémoire. ( int *finger = parking_lot )

Votre doigt (le pointeur) n’est pas la réponse à ma question. Regarder votre doigt ne me dit rien, mais si je regarde où vous pointez le doigt (déréférencement du pointeur), je peux trouver la voiture (les données) que je cherchais.


Réaffecter le pointeur

Maintenant, je peux vous demander de trouver une voiture rouge à la place et vous pouvez redirect votre doigt vers une nouvelle voiture. Maintenant, votre pointeur (le même que précédemment) me montre de nouvelles données (la place de stationnement où la voiture rouge peut être trouvée) du même type (la voiture).

Le pointeur n’a pas changé physiquement, c’est toujours votre doigt, juste les données qu’il m’a montrées modifiées. (l’adresse “place de stationnement”)


Double pointeurs (ou un pointeur sur un pointeur)

Cela fonctionne avec plus d’un pointeur aussi. Je peux demander où est le pointeur, qui pointe vers la voiture rouge et vous pouvez utiliser votre autre main et pointer du doigt vers le premier doigt. (c’est comme int **finger_two = &finger )

Maintenant, si je veux savoir où se trouve la voiture bleue, je peux suivre la direction du premier doigt jusqu’au deuxième doigt, vers la voiture (les données).


Le pointeur suspendu

Maintenant, disons que vous vous sentez comme une statue et que vous voulez tenir votre main sur la voiture rouge indéfiniment. Et si cette voiture rouge partait?

  1 2 3 ------------------- | o=o | | o=o | | |B| | | |G| | | oo | | oo | 

Votre pointeur pointe toujours vers la voiture rouge mais n’est plus. Disons qu’une nouvelle voiture y amène une voiture orange. Maintenant, si je vous le demande encore, “où est la voiture rouge”, vous pointez toujours là, mais maintenant vous avez tort. Ce n’est pas une voiture rouge, c’est orange.


Arithmétique de pointeur

Ok, donc vous pointez toujours vers la deuxième place de stationnement (maintenant occupée par la voiture Orange)

  1 2 3 ------------------- | o=o | o=o | o=o | | |B| | |O| | |G| | | oo | oo | oo | 

Eh bien, j’ai une nouvelle question maintenant … Je veux connaître la couleur de la voiture dans la prochaine place de stationnement. Vous pouvez voir que vous pointez sur le point 2, donc vous ajoutez simplement 1 et vous pointez au prochain point. ( finger+1 ), maintenant que je voulais savoir quelles étaient les données, vous devez vérifier cet endroit (pas seulement le doigt) afin de pouvoir déférer le pointeur ( *(finger+1) ) pour voir qu’il y a un vert voiture présente là (les données à cet endroit)

I think the main barrier to understanding pointers is bad teachers.

Almost everyone are taught lies about pointers: That they are nothing more than memory addresses , or that they allow you to point to arbitrary locations .

And of course that they are difficult to understand, dangerous and semi-magical.

None of which is true. Pointers are actually fairly simple concepts, as long as you stick to what the C++ language has to say about them and don’t imbue them with atsortingbutes that “usually” turn out to work in practice, but nevertheless aren’t guaranteed by the language, and so aren’t part of the actual concept of a pointer.

I sortinged to write up an explanation of this a few months ago in this blog post — hopefully it’ll help someone.

(Note, before anyone gets pedantic on me, yes, the C++ standard does say that pointers represent memory addresses. But it does not say that “pointers are memory addresses, and nothing but memory addresses and may be used or thought of interchangeably with memory addresses”. The distinction is important)

The problem with pointers is not the concept. It’s the execution and language involved. Additional confusion results when teachers assume that it’s the CONCEPT of pointers that’s difficult, and not the jargon, or the convoluted mess C and C++ makes of the concept. So vast amounts of effort are poored into explaining the concept (like in the accepted answer for this question) and it’s pretty much just wasted on someone like me, because I already understand all of that. It’s just explaining the wrong part of the problem.

To give you an idea of where I’m coming from, I’m someone who understands pointers perfectly well, and I can use them competently in assembler language. Because in assembler language they are not referred to as pointers. They are referred to as addresses. When it comes to programming and using pointers in C, I make a lot of mistakes and get really confused. I still have not sorted this out. Let me give you an example.

When an api says:

 int doIt(char *buffer ) //*buffer is a pointer to the buffer 

what does it want?

it could want:

a number representing an address to a buffer

(To give it that, do I say doIt(mybuffer) , or doIt(*myBuffer) ?)

a number representing the address to an address to a buffer

(is that doIt(&mybuffer) or doIt(mybuffer) or doIt(*mybuffer) ?)

a number representing the address to the address to the address to the buffer

(maybe that’s doIt(&mybuffer) . or is it doIt(&&mybuffer) ? or even doIt(&&&mybuffer) )

and so on, and the language involved doesn’t make it as clear because it involves the words “pointer” and “reference” that don’t hold as much meaning and clarity to me as “x holds the address to y” and “this function requires an address to y”. The answer additionally depends on just what the heck “mybuffer” is to begin with, and what doIt intends to do with it. The language doesn’t support the levels of nesting that are encountered in practice. Like when I have to hand a “pointer” in to a function that creates a new buffer, and it modifies the pointer to point at the new location of the buffer. Does it really want the pointer, or a pointer to the pointer, so it knows where to go to modify the contents of the pointer. Most of the time I just have to guess what is meant by “pointer” and most of the time I’m wrong, regardless of how much experience I get at guessing.

“Pointer” is just too overloaded. Is a pointer an address to a value? or is it a variable that holds an address to a value. When a function wants a pointer, does it want the address that the pointer variable holds, or does it want the address to the pointer variable? Je suis confus.

I think that what makes pointers sortingcky to learn is that until pointers you’re comfortable with the idea that “at this memory location is a set of bits that represent an int, a double, a character, whatever”.

When you first see a pointer, you don’t really get what’s at that memory location. “What do you mean, it holds an address ?”

I don’t agree with the notion that “you either get them or you don’t”.

They become easier to understand when you start finding real uses for them (like not passing large structures into functions).

The reason it’s so hard to understand is not because it’s a difficult concept but because the syntax is inconsistent .

  int *mypointer; 

You are first learned that the leftmost part of a variable creation defines the type of the variable. Pointer declaration does not work like this in C and C++. Instead they say that the variable is pointing on the type to the left. In this case: * mypointer is pointing on an int.

I didn’t fully grasp pointers until i sortinged using them in C# (with unsafe), they work in exact same way but with logical and consistent syntax. The pointer is a type itself. Here mypointer is a pointer to an int.

  int* mypointer; 

Don’t even get me started on function pointers…

I could work with pointers when I only knew C++. I kind of knew what to do in some cases and what not to do from sortingal/error. But the thing that gave me complete understanding is assembly language. If you do some serious instruction level debugging with an assembly language program you’ve written, you should be able to understand a lot of things.

I like the house address analogy, but I’ve always thought of the address being to the mailbox itself. This way you can visualize the concept of dereferencing the pointer (opening the mailbox).

For instance following a linked list: 1) start with your paper with the address 2) Go to the address on the paper 3) Open the mailbox to find a new piece of paper with the next address on it

In a linear linked list, the last mailbox has nothing in it (end of the list). In a circular linked list, the last mailbox has the address of the first mailbox in it.

Note that step 3 is where the dereference occurs and where you’ll crash or go wrong when the address is invalid. Assuming you could walk up to the mailbox of an invalid address, imagine that there’s a black hole or something in there that turns the world inside out 🙂

I think that the main reason that people have trouble with it is because it’s generally not taught in an interesting and engaging manner. I’d like to see a lecturer get 10 volunteers from the crowd and give them a 1 meter ruler each, get them to stand around in a certain configuration and use the rulers to point at each other. Then show pointer arithmetic by moving people around (and where they point their rulers). It’d be a simple but effective (and above all memorable) way of showing the concepts without getting too bogged down in the mechanics.

Once you get to C and C++ it seems to get harder for some people. I’m not sure if this is because they are finally putting theory that they don’t properly grasp into practice or because pointer manipulation is inherently harder in those languages. I can’t remember my own transition that well, but I knew pointers in Pascal and then moved to C and got totally lost.

I don’t think that pointers themselves are confusing. Most people can understand the concept. Now how many pointers can you think about or how many levels of indirection are you comfortable with. It doesn’t take too many to put people over the edge. The fact that they can be changed accidently by bugs in your program can also make them very difficult to debug when things go wrong in your code.

I think it might actually be a syntax issue. The C/C++ syntax for pointers seems inconsistent and more complex than it needs to be.

Ironically, the thing that actually helped me to understand pointers was encountering the concept of an iterator in the c++ Standard Template Library . It’s ironic because I can only assume that iterators were conceived as a generalization of the pointer.

Sometimes you just can’t see the forest until you learn to ignore the trees.

The confusion comes from the multiple abstraction layers mixed together in the “pointer” concept. Programmers don’t get confused by ordinary references in Java/Python, but pointers are different in that they expose characteristics of the underlying memory-architecture.

It is a good principle to cleanly separate layers of abstraction, and pointers do not do that.

The way I liked to explain it was in terms of arrays and indexes – people might not be familiar with pointers, but they generally know what an index is.

So I say imagine that the RAM is an array (and you have only 10-bytes of RAM):

 unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 }; 

Then a pointer to a variable is really just the index of (the first byte of) that variable in the RAM.

So if you have a pointer/index unsigned char index = 2 , then the value is obviously the third element, or the number 4. A pointer to a pointer is where you take that number and use it as an index itself, like RAM[RAM[index]] .

I would draw an array on a list of paper, and just use it to show things like many pointers pointing to the same memory, pointer arithmetic, pointer to pointer, and so on.

I don’t see what is so confusing about pointers. They point to a location in memory, that is it stores the memory address. In C/C++ you can specify the type the pointer points to. Par exemple:

 int* my_int_pointer; 

Says that my_int_pointer contains the address to a location that contains an int.

The problem with pointers is that they point to a location in memory, so it is easy to trail off into some location you should not be in. As proof look at the numerous security holes in C/C++ applications from buffer overflow (incrementing the pointer past the allocated boundary).

Post office box number.

It’s a piece of information that allows you to access something else.

(And if you do arithmetic on post office box numbers, you may have a problem, because the letter goes in the wrong box. And if somebody moves to another state — with no forwarding address — then you have a dangling pointer. On the other hand — if the post office forwards the mail, then you have a pointer to a pointer.)

Not a bad way to grasp it, via iterators.. but keep looking you’ll see Alexandrescu start complaining about them.

Many ex-C++ devs (that never understood that iterators are a modern pointer before dumping the language) jump to C# and still believe they have decent iterators.

Hmm, the problem is that all that iterators are is in complete odds at what the runtime platforms (Java/CLR) are trying to achieve: new, simple, everyone-is-a-dev usage. Which can be good, but they said it once in the purple book and they said it even before and before C:

Indirection.

A very powerful concept but never so if you do it all the way.. Iterators are useful as they help with abstraction of algorithms, another example. And comstack-time is the place for an algorithm, very simple. You know code + data, or in that other language C#:

IEnumerable + LINQ + Massive Framework = 300MB runtime penalty indirection of lousy, dragging apps via heaps of instances of reference types..

“Le Pointer is cheap.”

Some answers above have asserted that “pointers aren’t really hard”, but haven’t gone on to address directly where “pointer are hard!” comes from. Some years back I tutored first year CS students (for only one year, since I clearly sucked at it) and it was clear to me that the idea of pointer is not hard. What’s hard is understanding why and when you would want a pointer .

I don’t think you can divorce that question – why and when to use a pointer – from explaining broader software engineering issues. Why every variable should not be a global variable, and why one should factor out similar code into functions (that, get this, use pointers to specialize their behaviour to their call site).

Just to confuse things a bit more, sometimes you have to work with handles instead of pointers. Handles are pointers to pointers, so that the back end can move things in memory to defragment the heap. If the pointer changes in mid-routine, the results are unpredictable, so you first have to lock the handle to make sure nothing goes anywhere.

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 talks about it a bit more coherently than me. 🙂

Every C/C++ beginner has the same problem and that problem occurs not because “pointers are hard to learn” but “who and how it is explained”. Some learners gather it verbally some visually and the best way of explaining it is to use “train” example (suits for verbal and visual example).

Where “locomotive” is a pointer which can not hold anything and “wagon” is what “locomotive” sortinges pull (or point to). After, you can classify the “wagon” itself, can it hold animals,plants or people (or a mix of them).