Pourquoi les pointeurs sont-ils un facteur de confusion aussi important pour de nombreux nouveaux et même anciens étudiants de niveau collégial en C ou C ++? Existe-t-il des outils ou des processus de réflexion 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 faites pour amener quelqu'un au niveau de "Ah-hah, je l'ai compris", sans les enliser dans le concept global? Fondamentalement, percez comme des scénarios.
Réponses:
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 et de référencer toujours le même bloc de mémoire.
J'ai trouvé que la meilleure analogie est de considérer le pointeur comme un morceau de papier avec une adresse de maison et le bloc de mémoire auquel il fait référence comme 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 des choses comme les fuites de mémoire de la même manière.
Si vous ne souhaitez apprendre que le concept de haut niveau des pointeurs, vous devez ignorer les parties étiquetées "Disposition de la mémoire" dans l'explication ci-dessous. Ils sont destinés à donner des exemples de ce à quoi pourrait ressembler la mémoire après les opérations, mais ils sont de nature plus bas niveau. Cependant, afin d'expliquer avec précision le fonctionnement réel des dépassements de mémoire tampon, il était important que j'ajoute ces diagrammes.
Avis de non-responsabilité: à toutes fins utiles, cette explication et les exemples de dispositions de mémoire sont considérablement simplifiés. Il y a plus de frais généraux et beaucoup plus de détails que vous devez savoir si vous devez gérer la mémoire à un niveau bas. 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:
Lorsque vous initialisez l'objet maison, 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 maison, je vais l'illustrer ci-dessous comme ceci:
La zone "tttt" est en surcharge, il y en aura généralement plus pour différents types de runtimes et de langues, comme 8 ou 12 octets. Il est impératif que les valeurs stockées dans cette zone ne soient jamais modifiées par autre chose que l'allocateur de mémoire ou les routines du système principal, sinon vous risquez de planter le programme.
Allouer de la mémoire
Demandez à un entrepreneur de construire votre maison et donnez-vous l'adresse de la maison. Contrairement au monde réel, l'allocation de mémoire ne peut pas être indiquée où allouer, mais trouvera un endroit approprié avec suffisamment de place et rendra l'adresse à la mémoire allouée.
En d'autres termes, l'entrepreneur choisira l'endroit.
Disposition de la mémoire:
Gardez une variable avec l'adresse
Écrivez l'adresse de votre nouvelle maison sur un morceau de papier. Ce document vous servira de référence pour votre maison. Sans ce morceau de papier, vous êtes perdu et ne pouvez pas trouver la maison, sauf si vous y êtes déjà.
Disposition de la mémoire:
Copier la valeur du pointeur
Écrivez simplement l'adresse sur une nouvelle feuille 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 d'un papier et de réorganiser les meubles de cette maison donnera l'impression que l'autre maison a été modifiée de la même manière, sauf si vous pouvez détecter explicitement qu'il s'agit en fait d'une seule maison.
Remarque C'est généralement le concept que j'ai le plus de mal à expliquer aux gens, deux pointeurs ne signifie pas deux objets ou blocs de mémoire.
Libérer la mémoire
Démolissez la maison. Vous pouvez ensuite plus tard réutiliser le papier pour une nouvelle adresse si vous le souhaitez, ou l'effacer pour oublier l'adresse de la maison qui n'existe plus.
Ici, je construis d'abord la maison et 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:
Pointeurs pendants
Vous dites à votre entrepreneur de détruire la maison, mais vous oubliez d'effacer l'adresse de votre feuille de papier. Quand plus tard vous regardez le morceau de papier, vous avez oublié que la maison n'est plus là et allez la visiter, avec des résultats infructueux (voir aussi la partie sur une référence invalide ci-dessous).
L'utilisation
h
après l'appel à.Free
pourrait fonctionner, mais ce n'est que pure chance. Très probablement, il échouera, chez un client, au milieu d'une opération critique.Comme vous pouvez le voir, h pointe toujours vers les restes des données en mémoire, mais comme il n'est peut-être pas complet, l'utiliser comme avant peut échouer.
Fuite de mémoire
Vous perdez le morceau de papier et ne pouvez pas trouver la maison. Cependant, la maison est toujours debout quelque part, et lorsque vous souhaitez plus tard construire une nouvelle maison, vous ne pouvez pas réutiliser cet endroit.
Ici, nous avons remplacé le contenu de la
h
variable par 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 restera debout. En d'autres termes, la mémoire allouée restera allouée jusqu'à la fermeture de l'application, point auquel le système d'exploitation la supprimera.Disposition de la mémoire après la première allocation:
Disposition de la mémoire après la deuxième allocation:
Une façon plus courante d'obtenir cette méthode consiste simplement à oublier de libérer quelque chose, au lieu de l'écraser comme ci-dessus. En termes Delphi, cela se produira avec la méthode suivante:
Une fois cette méthode exécutée, il n'y a pas de place dans nos variables où l'adresse de la maison existe, mais la maison est toujours là.
Disposition de la mémoire:
Comme vous pouvez le voir, les anciennes données sont laissées intactes en mémoire et ne seront pas réutilisées par l'allocateur de mémoire. L'allocateur garde une trace des zones de mémoire qui ont été utilisées et ne les réutilisera que si vous le libérez.
Libérer la mémoire mais conserver une référence (désormais invalide)
Démolissez la maison, effacez l'un des morceaux de papier mais vous avez également un autre morceau de papier avec l'ancienne adresse dessus, lorsque 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 initialement reçu l'adresse, et donc toute tentative de l'utiliser comme si elle vous appartenait pourrait échouer horriblement.
Parfois, vous pourriez même trouver qu'une adresse voisine a une maison assez grande 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 à 3 adresses comme une petite maison unique pourrait également échouer horriblement.
Ici, la maison a été démolie, grâce à la référence
h1
, et bien qu'elle aith1
été effacée également,h2
elle a toujours l'ancienne adresse obsolète. L'accès à la maison qui n'est plus debout peut ou non fonctionner.Il s'agit d'une variante du pointeur suspendu ci-dessus. Voir son agencement de mémoire.
Dépassement de tampon
Vous déplacez plus de choses dans la maison que vous n'en avez peut-être, en déversant dans la maison ou la cour voisine. Lorsque le propriétaire de la 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 sera, 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. De plus, ils sont attribués côte à côte.
Ainsi, ce code:
Disposition de la mémoire après la première allocation:
Disposition de la mémoire après la deuxième allocation:
La partie qui provoquera le plus souvent un crash est lorsque vous écrasez des parties importantes des données que vous avez stockées qui ne devraient vraiment pas être modifiées au hasard. Par exemple, cela pourrait ne pas être un problème si des parties du nom de la maison h1 ont été modifiées, en termes de plantage du programme, mais l'écrasement de la surcharge de l'objet se bloquera très probablement lorsque vous essayez d'utiliser l'objet cassé, comme le fera écraser les liens qui sont stockés vers d'autres objets dans l'objet.
Listes liées
Lorsque vous suivez une adresse sur un morceau de papier, vous arrivez dans une maison, et dans cette maison, il y a un autre morceau de papier avec une nouvelle adresse, pour la prochaine maison de la chaîne, etc.
Ici, nous créons un lien entre notre maison et notre cabine. Nous pouvons suivre la chaîne jusqu'à ce qu'une maison n'ait aucune
NextHouse
référence, ce qui signifie que c'est la dernière. Pour visiter toutes nos maisons, nous pourrions utiliser le code suivant:Disposition de la mémoire (ajouté NextHouse en tant que lien dans l'objet, noté avec les quatre LLLL dans le diagramme ci-dessous):
En termes de base, qu'est-ce qu'une adresse mémoire?
Une adresse mémoire n'est en termes de base qu'un nombre. Si vous considérez la mémoire comme un grand tableau d'octets, le tout premier octet a l'adresse 0, le suivant l'adresse 1 et ainsi de suite. C'est simplifié, mais assez bon.
Donc, cette disposition de la mémoire:
Pourrait avoir ces deux adresses (la plus à gauche - est l'adresse 0):
Ce qui signifie que notre liste de liens ci-dessus pourrait ressembler à ceci:
Il est typique de stocker une adresse qui "ne pointe nulle part" comme une adresse zéro.
En termes de base, qu'est-ce qu'un pointeur?
Un pointeur n'est qu'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 exécutions essaient de cacher le fait qu'il y a un nombre en dessous, simplement parce que le nombre lui-même n'a pas vraiment de sens pour vous. Il est préférable de considérer un pointeur comme une boîte noire, c'est-à-dire. vous ne savez pas ou ne vous souciez pas vraiment de la façon dont il est réellement mis en œuvre, tant qu'il fonctionne.
la source
Dans ma première classe de Comp Sci, nous avons fait l'exercice suivant. Certes, c'était une salle de conférence avec environ 200 étudiants dedans ...
Le professeur écrit au tableau:
int john;
John se lève
Le professeur écrit:
int *sally = &john;
Sally se lève et pointe John
Professeur:
int *bill = sally;
Bill se lève, montre John
Professeur:
int sam;
Sam se lève
Professeur:
bill = &sam;
Bill pointe maintenant Sam.
Je pense que vous avez l'idée. Je pense que nous avons passé environ une heure à faire cela, jusqu'à ce que nous passions en revue les bases de l'affectation des pointeurs.
la source
Une analogie que j'ai trouvée utile pour expliquer les pointeurs 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 pointeront tous deux vers la même page Web d'origine. Si vous allez modifier cette page d'origine, puis suivez l'un de ces liens (pointeurs), vous obtiendrez cette nouvelle page mise à jour.
la source
int *a = b
ne fait pas deux copies de*b
).La raison pour laquelle les pointeurs semblent dérouter tant de gens 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 façon 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 le bytecode (dans la langue de leur choix, python fonctionne très bien pour cela) avec un jeu d'instructions axé sur les opérations de pointeur (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 impliquera des pointeurs et ils seront sûrs de l'obtenir.
la source
Le concept d'un espace réservé pour une valeur - variables - correspond à quelque chose qu'on nous 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 chose jusqu'à ce qu'il traite des choses de bas niveau - au niveau des communications C / C ++ / octet .
Adresse les boîtes. Je me souviens quand j'apprenais à programmer BASIC sur des micro-ordinateurs, il y avait ces jolis livres avec des jeux, et parfois vous deviez insérer des valeurs dans des adresses particulières. Ils avaient une photo d'un tas de boîtes, étiquetées progressivement 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 en avait jusqu'à 65535! Ils étaient côte à côte, et ils avaient tous une adresse.
Pour une perceuse? Faites une structure:
Même exemple que ci-dessus, sauf en C:
Production:
Peut-être que cela explique certaines des bases par l'exemple?
la source
La raison pour laquelle j'ai eu du mal à comprendre les pointeurs, 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 le problème. Lorsque vous utilisez un paramètre de pointeur, vous passez toujours par valeur; mais la valeur se trouve être une adresse plutôt que, disons, un int.
Quelqu'un d'autre a déjà lié ce didacticiel, mais je peux souligner le moment où j'ai commencé à comprendre les pointeurs:
Un tutoriel sur les pointeurs et les tableaux en C: Chapitre 3 - Pointeurs et chaînes
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 du pointeur.
Même si vous êtes un développeur VB .NET ou C # (comme je le suis) et que vous n'utilisez jamais de code dangereux, cela vaut toujours la peine de comprendre comment fonctionnent les pointeurs, ou vous ne comprendrez pas comment fonctionnent les références d'objet. Ensuite, vous aurez la notion courante mais erronée que le passage d'une référence d'objet à une méthode copie l'objet.
la source
J'ai trouvé le "Tutoriel sur les pointeurs et les tableaux en C" de Ted Jensen une excellente ressource pour en apprendre davantage sur les pointeurs. Il est divisé en 10 leçons, commençant par une explication de ce que sont les pointeurs (et à quoi ils servent) et se terminant par des pointeurs de fonction. http://home.netcom.com/~tjensen/ptr/cpoint.htm
À partir de là, le Guide de Beej sur la programmation réseau enseigne l'API des sockets Unix, à partir de laquelle vous pouvez commencer à faire des choses vraiment amusantes. http://beej.us/guide/bgnet/
la source
La complexité des pointeurs va au-delà de ce que nous pouvons facilement enseigner. Faire pointer les élèves les uns vers les autres et utiliser des morceaux de papier avec des adresses de domicile sont tous deux d'excellents outils d'apprentissage. Ils font un excellent travail pour introduire les concepts de base. En effet, l'apprentissage des concepts de base est vital pour réussir à utiliser des pointeurs. Cependant, dans le code de production, il est courant d'entrer dans des scénarios beaucoup plus complexes que ces simples démonstrations ne 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 à vous retrouver avec un code comme celui-ci:
cela peut devenir déroutant très rapidement (imaginez beaucoup plus de lignes et potentiellement plus de niveaux). Ajoutez des tableaux de pointeurs et des pointeurs de noeud à noeud (arbres, listes liées) et cela 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 des développeurs qui comprenaient très bien les bases.
Les structures complexes de pointeurs n'indiquent pas nécessairement un mauvais codage non plus (bien qu'elles le puissent). La composition est un élément essentiel d'une bonne programmation orientée objet, et dans les langages avec des pointeurs bruts, elle conduira inévitablement à une indirection multicouche. De plus, les systèmes doivent souvent utiliser des bibliothèques tierces avec des structures qui ne correspondent pas entre elles par leur style ou leur technique. Dans de telles situations, la complexité va naturellement surgir (même si nous devons certainement la combattre autant que possible).
Je pense que la meilleure chose que les collèges puissent faire pour aider les étudiants à apprendre les pointeurs est d'utiliser de bonnes démonstrations, combinées à des projets qui nécessitent l'utilisation d'un pointeur. Un projet difficile fera plus pour la compréhension des pointeurs que mille démonstrations. Les démonstrations peuvent vous donner une compréhension superficielle, mais pour saisir profondément les pointeurs, vous devez vraiment les utiliser.
la source
J'ai pensé ajouter une analogie à cette liste que j'ai trouvée très utile pour expliquer les pointeurs (à l'époque) en tant que tuteur en informatique; commençons par:
Préparez le terrain :
Considérons un parking de 3 places, ces places sont numérotées:
D'une certaine manière, c'est comme des emplacements de mémoire, ils sont séquentiels et contigus. Un peu comme un tableau. À l'heure actuelle, il n'y a pas de voitures, c'est comme un tableau vide (
parking_lot[3] = {0}
).Ajoutez les données
Un parking ne reste jamais vide longtemps ... s'il le faisait, ce serait inutile et personne n'en construirait. Alors disons que le jour avance, le lot se remplit de 3 voitures, une voiture bleue, une voiture rouge et une voiture verte:
Ces voitures sont du même type (voiture) donc une façon de penser à ce que nos voitures sont une sorte de données (disons un
int
) , mais ils ont des valeurs différentes (blue
,red
,green
, qui pourrait être une couleurenum
)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 l'utilisez pour pointer vers une voiture bleue à l'endroit 1. C'est comme prendre un pointeur et l'attribuer à une adresse mémoire (
int *finger = parking_lot
)Votre doigt (le pointeur) n'est pas la réponse à ma question. Vous cherchez à votre doigt me dit rien, mais si je regarde où vous doigt pointez vers (déréférencement le pointeur), je peux trouver la voiture (les données) que je cherchais.
Réaffectation du pointeur
Maintenant, je peux vous demander de trouver une voiture rouge à la place et vous pouvez rediriger 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 parking où se trouve la voiture rouge) du même type (la voiture).
Le pointeur n'a pas changé physiquement, c'est toujours votre doigt, juste les données qu'il me montrait ont changé. (l'adresse "place de parking")
Double pointeurs (ou un pointeur vers un pointeur)
Cela fonctionne également avec plusieurs pointeurs. 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 vers le deuxième doigt, vers la voiture (les données).
Le pointeur pendant
Supposons maintenant que vous vous sentiez très bien comme une statue et que vous vouliez tenir votre main pointée vers la voiture rouge indéfiniment. Et si cette voiture rouge partait?
Votre pointeur pointe toujours vers l' endroit où la voiture rouge était mais n'est plus. Disons qu'une nouvelle voiture arrive là-bas ... une voiture Orange. Maintenant, si je vous demande à nouveau, "où est la voiture rouge", vous pointez toujours là, mais maintenant vous vous trompez. Ce n'est pas une voiture rouge, c'est orange.
Arithmétique du pointeur
Ok, donc vous pointez toujours la deuxième place de parking (maintenant occupée par la voiture Orange)
Eh bien, j'ai une nouvelle question maintenant ... Je veux connaître la couleur de la voiture dans la prochaine place de parking. Vous pouvez voir que vous pointez sur le point 2, il vous suffit donc d'ajouter 1 et vous pointez sur le point suivant. (
finger+1
), maintenant que je voulais savoir quelles étaient les données, vous devez vérifier cet endroit (pas seulement le doigt) pour pouvoir déférer le pointeur (*(finger+1)
) pour voir qu'il y a une voiture verte là-bas (les données à cet endroit) )la source
"without getting them bogged down in the overall concept"
comme une compréhension de haut niveau. Et à votre point:"I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"
- vous seriez très surpris du nombre de personnes qui ne comprennent pas les pointeurs même à ce niveauJe ne pense pas que les pointeurs en tant que concept soient particulièrement délicats - la plupart des modèles mentaux des élèves correspondent à quelque chose comme ça et quelques croquis rapides peuvent aider.
La difficulté, du moins celle que j'ai connue dans le passé et que j'ai vue avec d'autres, est que la gestion des pointeurs en C / C ++ peut être inutilement compliquée.
la source
Un exemple de tutoriel avec un bon ensemble de diagrammes aide grandement à la compréhension des pointeurs .
Joel Spolsky fait quelques bons points sur la compréhension des pointeurs dans son article Guerrilla Guide to Interviewing :
la source
Le problème avec les pointeurs n'est pas le concept. C'est l'exécution et le langage impliqués. Une confusion supplémentaire se produit lorsque les enseignants supposent que c'est le CONCEPT de pointeurs qui est difficile, et non le jargon, ou le désordre alambiqué C et C ++ fait du concept. Des efforts énormes sont nécessaires pour expliquer le concept (comme dans la réponse acceptée à cette question) et c'est à peu près gaspillé pour quelqu'un comme moi, car je comprends déjà tout cela. Cela explique simplement la mauvaise partie du problème.
Pour vous donner une idée d'où je viens, je suis quelqu'un qui comprend parfaitement les pointeurs et je peux les utiliser avec compétence en langage assembleur. Parce que dans le langage assembleur, ils ne sont pas appelés pointeurs. Ils sont appelés adresses. En ce qui concerne la programmation et l'utilisation des pointeurs en C, je fais beaucoup d'erreurs et je suis vraiment confus. Je n'ai toujours pas réglé cela. Laisse moi te donner un exemple.
Quand une API dit:
que veut-il?
il pourrait vouloir:
un nombre représentant une adresse vers un tampon
(Pour lui donner ça, est-ce que je dis
doIt(mybuffer)
, oudoIt(*myBuffer)
?)un nombre représentant l'adresse à une adresse à un tampon
(est-ce
doIt(&mybuffer)
oudoIt(mybuffer)
oudoIt(*mybuffer)
?)un nombre représentant l'adresse à l'adresse à l'adresse au tampon
(peut-être que c'est
doIt(&mybuffer)
. ou est-cedoIt(&&mybuffer)
? ou mêmedoIt(&&&mybuffer)
)et ainsi de suite, et le langage impliqué ne le rend pas aussi clair car il implique les mots "pointeur" et "référence" qui n'ont pas autant de sens et de clarté pour moi que "x détient l'adresse de y" et " cette fonction nécessite une adresse à y ". La réponse dépend également de ce que le "mybuffer" est pour commencer, et de ce qu'il a l'intention de faire avec. Le langage ne prend pas en charge les niveaux d'imbrication rencontrés dans la pratique. Comme quand je dois remettre un "pointeur" à une fonction qui crée un nouveau tampon, et il modifie le pointeur pour pointer vers le nouvel emplacement du tampon. Veut-il vraiment le pointeur, ou un pointeur sur le pointeur, donc il sait où aller pour modifier le contenu du pointeur. La plupart du temps, je dois juste deviner ce que l'on entend par "
"Pointer" est tout simplement trop surchargé. Un pointeur est-il une adresse vers une valeur? ou est-ce une variable qui contient une adresse à une valeur. Lorsqu'une fonction veut un pointeur, veut-elle l'adresse que contient la variable de pointeur ou veut-elle l'adresse de la variable de pointeur? Je suis confus.
la source
double *(*(*fn)(int))(char)
, alors le résultat de l'évaluation*(*(*fn)(42))('x')
sera adouble
. Vous pouvez supprimer des couches d'évaluation pour comprendre quels doivent être les types intermédiaires.(*(*fn)(42))('x')
alors?x
) où, si vous évaluez*x
, vous obtenez un double.fn
est en plus en termes de ce que vous pouvez faire avecfn
Je pense que le principal obstacle à la compréhension des pointeurs est les mauvais enseignants.
Presque tout le monde apprend des mensonges sur les pointeurs: qu'ils ne sont rien de plus que des adresses mémoire ou qu'ils vous permettent de pointer vers des emplacements arbitraires .
Et bien sûr, ils sont difficiles à comprendre, dangereux et semi-magiques.
Rien de tout cela n'est vrai. Les pointeurs sont en fait des concepts assez simples, tant que vous vous en tenez à ce que le langage C ++ a à dire à leur sujet et que vous ne leur donnez pas d'attributs qui se révèlent "généralement" fonctionner en pratique, mais qui ne sont néanmoins pas garantis par le langage. , et ne font donc pas partie du concept réel d'un pointeur.
J'ai essayé d'écrire une explication à ce sujet il y a quelques mois dans ce billet de blog - j'espère que cela aidera quelqu'un.
(Remarque, avant que quiconque ne se moque de moi, oui, la norme C ++ dit que les pointeurs représentent des adresses de mémoire. adresses ". La distinction est importante)
la source
Je pense que ce qui rend les pointeurs difficiles à apprendre, c'est que jusqu'à ce que les pointeurs soient à l'aise avec l'idée que "à cet emplacement mémoire se trouve un ensemble de bits qui représentent un int, un double, un caractère, peu importe".
Lorsque vous voyez un pointeur pour la première fois, vous n'obtenez pas vraiment le contenu de cet emplacement de mémoire. "Que voulez-vous dire, il contient une adresse ?"
Je ne suis pas d'accord avec l'idée que "vous les obtenez ou vous ne les avez pas".
Ils deviennent plus faciles à comprendre lorsque vous commencez à leur trouver de véritables utilisations (comme ne pas passer de grandes structures en fonctions).
la source
La raison pour laquelle il est si difficile à comprendre n'est pas parce que c'est un concept difficile mais parce que la syntaxe est incohérente .
Vous apprenez d'abord que la partie la plus à gauche d'une création de variable définit le type de la variable. La déclaration de pointeur ne fonctionne pas comme ceci en C et C ++. Au lieu de cela, ils disent que la variable pointe vers le type à gauche. Dans ce cas:
*
mypointer pointe sur un int.Je n'ai pas complètement saisi les pointeurs jusqu'à ce que j'essaie de les utiliser en C # (avec dangereux), ils fonctionnent exactement de la même manière mais avec une syntaxe logique et cohérente. Le pointeur est un type lui-même. Ici mypointer est un pointeur vers un int.
Ne me lancez même pas sur les pointeurs de fonction ...
la source
int *p;
a une signification simple:*p
est un entier.int *p, **pp
signifie:*p
et**pp
sont des entiers.*p
et ne**pp
sont pas des entiers, car vous n'avez jamais initialisép
oupp
ou*pp
pour pointer vers quoi que ce soit. Je comprends pourquoi certaines personnes préfèrent s'en tenir à la grammaire sur celle-ci, en particulier parce que certains cas marginaux et cas complexes vous obligent à le faire (bien que, vous pouvez toujours trivialement contourner cela dans tous les cas que je connaisse) ... mais je ne pense pas que ces cas soient plus importants que le fait que l'enseignement de l'alignement à droite soit trompeur pour les débutants. Sans parler de laide! :)Je pouvais travailler avec des pointeurs quand je ne connaissais que le C ++. Je savais en quelque sorte quoi faire dans certains cas et ce qu'il ne fallait pas faire par essais / erreurs. Mais ce qui m'a donné une compréhension complète, c'est le langage d'assemblage. Si vous effectuez un débogage sérieux au niveau des instructions avec un programme en langage assembleur que vous avez écrit, vous devriez être en mesure de comprendre beaucoup de choses.
la source
J'aime l'analogie avec l'adresse de la maison, mais j'ai toujours pensé que l'adresse était celle de la boîte aux lettres elle-même. De cette façon, vous pouvez visualiser le concept de déréférencement du pointeur (ouverture de la boîte aux lettres).
Par exemple, en suivant une liste liée: 1) commencez par votre papier avec l'adresse 2) Allez à l'adresse sur le papier 3) Ouvrez la boîte aux lettres pour trouver un nouveau morceau de papier avec l'adresse suivante dessus
Dans une liste liée linéaire, la dernière boîte aux lettres ne contient rien (fin de la liste). Dans une liste liée circulaire, la dernière boîte aux lettres contient l'adresse de la première boîte aux lettres.
Notez que l'étape 3 est l'endroit où la déréférence se produit et où vous vous plantez ou vous trompez lorsque l'adresse n'est pas valide. En supposant que vous puissiez vous rendre à la boîte aux lettres d'une adresse non valide, imaginez qu'il y a un trou noir ou quelque chose là-dedans qui transforme le monde à l'envers :)
la source
Je pense que la principale raison pour laquelle les gens ont du mal à le faire est que ce n'est généralement pas enseigné de manière intéressante et engageante. J'aimerais voir un conférencier obtenir 10 volontaires de la foule et leur donner une règle de 1 mètre chacun, les amener à rester dans une certaine configuration et utiliser les règles pour se pointer. Ensuite, montrez l'arithmétique des pointeurs en déplaçant les gens (et où ils pointent leurs dirigeants). Ce serait une manière simple mais efficace (et surtout mémorable) de montrer les concepts sans trop s'enliser dans la mécanique.
Une fois que vous arrivez en C et C ++, cela semble devenir plus difficile pour certaines personnes. Je ne sais pas si c'est parce qu'ils mettent enfin en pratique la théorie qu'ils ne comprennent pas correctement ou parce que la manipulation du pointeur est intrinsèquement plus difficile dans ces langues. Je ne me souviens pas très bien de ma propre transition, mais je connaissais des pointeurs en Pascal, puis je suis passé à C et je me suis totalement perdu.
la source
Je ne pense pas que les pointeurs eux-mêmes prêtent à confusion. La plupart des gens peuvent comprendre le concept. Maintenant, à combien de pointeurs pouvez-vous penser ou à combien de niveaux d'indirection êtes-vous à l'aise? Il ne faut pas trop pour mettre les gens au bord du gouffre. Le fait qu'ils puissent être modifiés accidentellement par des bogues dans votre programme peut également les rendre très difficiles à déboguer lorsque les choses tournent mal dans votre code.
la source
Je pense que cela pourrait en fait être un problème de syntaxe. La syntaxe C / C ++ pour les pointeurs semble incohérente et plus complexe qu'elle ne devrait l'être.
Ironiquement, la chose qui m'a réellement aidé à comprendre les pointeurs était de rencontrer le concept d'un itérateur dans la bibliothèque de modèles standard c ++ . C'est ironique car je ne peux que supposer que les itérateurs ont été conçus comme une généralisation du pointeur.
Parfois, vous ne pouvez tout simplement pas voir la forêt avant d'apprendre à ignorer les arbres.
la source
(*p)
elle l'avait été(p->)
, et donc nous aurionsp->->x
au lieu de l'ambiguïté*p->x
a->b
signifie simplement(*a).b
.* p->x
signifie* ((*a).b)
alors que*p -> x
signifie(*(*p)) -> x
. Le mélange des opérateurs de préfixe et de suffixe provoque une analyse ambiguë.1+2 * 3
devrait être 9.La confusion vient des multiples couches d'abstraction mélangées dans le concept de "pointeur". Les programmeurs ne sont pas déroutés par les références ordinaires en Java / Python, mais les pointeurs sont différents en ce qu'ils exposent les caractéristiques de l'architecture de mémoire sous-jacente.
C'est un bon principe de séparer proprement les couches d'abstraction, et les pointeurs ne le font pas.
la source
foo[i]
signifie aller à un certain endroit, avancer d'une certaine distance et voir ce qui s'y trouve. Ce qui complique les choses, c'est la couche d'abstraction supplémentaire beaucoup plus compliquée qui a été ajoutée par la norme uniquement au profit du compilateur, mais modélise les choses d'une manière qui ne convient pas aux besoins du programmeur et aux besoins du compilateur.J'ai aimé l'expliquer en termes de tableaux et d'index - les gens ne sont peut-être pas familiers avec les pointeurs, mais ils savent généralement ce qu'est un index.
Je dis donc imaginez que la RAM est un tableau (et vous n'avez que 10 octets de RAM):
Ensuite, un pointeur sur une variable n'est vraiment que l'index de (le premier octet de) cette variable dans la RAM.
Donc, si vous avez un pointeur / index
unsigned char index = 2
, alors la valeur est évidemment le troisième élément, ou le nombre 4. Un pointeur vers un pointeur est l'endroit où vous prenez ce nombre et l'utilisez comme un index lui-même, commeRAM[RAM[index]]
.Je dessinerais un tableau sur une liste de papier, et je l'utiliserais simplement pour montrer des choses comme de nombreux pointeurs pointant vers la même mémoire, l'arithmétique du pointeur, le pointeur vers le pointeur, etc.
la source
Numéro de boîte postale.
C'est une information qui vous permet d'accéder à autre chose.
(Et si vous faites de l'arithmétique sur les numéros de boîte postale, vous pouvez avoir un problème, car la lettre va dans la mauvaise case. Et si quelqu'un passe dans un autre état - sans adresse de transfert - alors vous avez un pointeur pendant. d'autre part - si le bureau de poste transfère le courrier, alors vous avez un pointeur vers un pointeur.)
la source
Pas une mauvaise façon de le saisir, via les itérateurs .. mais continuez à regarder, vous verrez Alexandrescu commencer à se plaindre à leur sujet.
De nombreux développeurs ex-C ++ (qui n'ont jamais compris que les itérateurs sont un pointeur moderne avant de vider le langage) sautent en C # et croient toujours qu'ils ont des itérateurs décents.
Hmm, le problème est que tout ce que sont les itérateurs est en totale contradiction avec ce que les plates-formes d'exécution (Java / CLR) tentent d'atteindre: une nouvelle utilisation simple, tout le monde est un développeur. Ce qui peut être bien, mais ils l'ont dit une fois dans le livre violet et ils l'ont dit avant et avant C:
Indirection.
Un concept très puissant mais jamais si vous le faites à fond. Les itérateurs sont utiles car ils aident à l'abstraction des algorithmes, un autre exemple. Et la compilation est le lieu d'un algorithme, très simple. Vous connaissez le code + les données, ou dans cette autre langue C #:
IEnumerable + LINQ + Massive Framework = 300 Mo de pénalité d'exécution indirection de moche, en faisant glisser les applications via des tas d'instances de types de référence.
"Le Pointer est bon marché."
la source
Certaines réponses ci-dessus ont affirmé que "les pointeurs ne sont pas vraiment difficiles", mais n'ont pas poursuivi directement où "les pointeurs sont difficiles!" vient de. Il y a quelques années, j'ai enseigné aux étudiants de première année de CS (pendant seulement un an, car je l'avais clairement aspiré) et il était clair pour moi que l' idée de pointeur n'était pas difficile. Ce qui est difficile, c'est de comprendre pourquoi et quand vous voulez un pointeur .
Je ne pense pas que vous puissiez dissocier cette question - pourquoi et quand utiliser un pointeur - d'expliquer des problèmes plus larges d'ingénierie logicielle. Pourquoi chaque variable ne devrait pas être une variable globale, et pourquoi on devrait factoriser du code similaire en fonctions (cela, obtenez ceci, utilisez des pointeurs pour spécialiser leur comportement vers leur site d'appel).
la source
Je ne vois pas ce qui est si déroutant au sujet des pointeurs. Ils pointent vers un emplacement en mémoire, c'est-à-dire qu'il stocke l'adresse mémoire. En C / C ++, vous pouvez spécifier le type vers lequel pointe le pointeur. Par exemple:
Dit que my_int_pointer contient l'adresse d'un emplacement contenant un int.
Le problème avec les pointeurs est qu'ils pointent vers un emplacement en mémoire, il est donc facile de se retrouver dans un emplacement dans lequel vous ne devriez pas être. passé la limite attribuée).
la source
Juste pour confondre un peu plus les choses, il faut parfois travailler avec des poignées au lieu de pointeurs. Les poignées sont des pointeurs vers des pointeurs, de sorte que le back-end peut déplacer des éléments en mémoire pour défragmenter le tas. Si le pointeur change à la mi-routine, les résultats sont imprévisibles, vous devez donc d'abord verrouiller la poignée pour vous assurer que rien ne va nulle part.
http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 en parle un peu plus de manière cohérente que moi. :-)
la source
Chaque débutant C / C ++ a le même problème et ce problème ne se produit pas parce que "les pointeurs sont difficiles à apprendre" mais "qui et comment cela est expliqué". Certains apprenants le recueillent verbalement certains visuellement et la meilleure façon de l'expliquer est d'utiliser l' exemple "train" (convient pour l'exemple verbal et visuel).
Où "locomotive" est un pointeur qui ne peut rien contenir et "wagon" est ce que "locomotive" essaie de tirer (ou de pointer). Après, vous pouvez classer le «wagon» lui-même, peut-il contenir des animaux, des plantes ou des personnes (ou un mélange d'entre eux).
la source