Quels sont les obstacles à la compréhension des pointeurs et que peut-on faire pour les surmonter? [fermé]


449

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.


20
La thèse de cette question est que les pointeurs sont difficiles à comprendre. La question n'apporte aucune preuve que les pointeurs sont plus difficiles à comprendre qu'autre chose.
bmargulies

14
Peut-être que je manque quelque chose (parce que je code dans des langages GCC) mais j'ai toujours pensé si les pointeurs en mémoire étaient une structure Key-> Value. Comme il est coûteux de faire circuler de grandes quantités de données dans un programme, vous créez la structure (valeur) et passez son pointeur / référence (clé) car la clé est une représentation beaucoup plus petite de la structure plus grande. La partie difficile est lorsque vous devez comparer deux pointeurs / références (comparez-vous les clés ou les valeurs), ce qui nécessite plus de travail pour pénétrer les données contenues dans la structure (valeur).
Evan Plaice

2
@ Wolfpack'08 "Il me semble qu'une mémoire d'adresse sera toujours un int." - Ensuite, il devrait vous sembler que rien n'a de type, car ce ne sont que des bits en mémoire. "En fait, le type du pointeur est le type du var vers lequel le pointeur pointe" - Non, le type du pointeur est le pointeur vers le type du var vers lequel le pointeur pointe - ce qui est naturel et devrait être évident.
Jim Balter

2
Je me suis toujours demandé ce qui est si difficile à comprendre dans le fait que les variables (et fonctions) ne sont que des blocs de mémoire et les pointeurs sont des variables stockant des adresses mémoire. Ce modèle de pensée peut-être trop pratique pourrait ne pas impressionner tous les fans de concepts abstraits, mais il aide parfaitement à comprendre comment fonctionnent les pointeurs.
Christian Rau

8
En un mot, les étudiants ne comprennent probablement pas parce qu'ils ne comprennent pas correctement, ou pas du tout, comment la mémoire d' un ordinateur en général, et en particulier le « modèle de mémoire » C fonctionne. Ce livre Programmation à partir du sol donne une très bonne leçon sur ces sujets.
Abbafei

Réponses:


745

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:

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

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:

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

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.

THouse.Create('My house');

Disposition de la mémoire:

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

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à.

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 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.

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é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.

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

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:

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

    h (pointe maintenant nulle part) <- +
                                + - après gratuit
---------------------- | (notez que la mémoire peut 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. 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).

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

L'utilisation haprè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.

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

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

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.

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 remplacé le contenu de la hvariable 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:

    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 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:

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 nos variables où 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 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.

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, grâce à la référence h1, et bien qu'elle ait h1été effacée également, h2elle 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:

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 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.

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

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 NextHouseréférence, 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é NextHouse en tant que lien dans l'objet, noté avec les quatre LLLL dans le diagramme ci-dessous):

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

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:

    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]
    1234Home 0028 5678Cabine 0000
                   | ^ |
                   + -------- + * (pas de lien)

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.


59
Le dépassement de tampon est hilarant. "Le voisin rentre à la maison, ouvre son crâne en glissant sur votre camelote et vous poursuit en oubli."
gtd

12
C'est une belle explication du concept, bien sûr. Le concept n'est PAS la chose que je trouve confuse à propos des pointeurs, donc tout cet essai a été un peu gaspillé.
Breton

10
Mais juste pour avoir demandé, que trouvez- vous déroutant sur les pointeurs?
Lasse V. Karlsen

11
J'ai revu ce post plusieurs fois depuis que vous avez écrit la réponse. Votre clarification avec le code est excellente, et je vous remercie de l'avoir revu pour ajouter / affiner d'autres réflexions. Bravo Lasse!
David McGraw

3
Il n'y a aucun moyen qu'une seule page de texte (quelle que soit sa longueur) puisse résumer toutes les nuances de la gestion de la mémoire, des références, des pointeurs, etc. Puisque 465 personnes ont voté pour cela, je dirais que cela sert assez bien page de départ sur les informations. Y a-t-il plus à apprendre? Bien sûr, quand n'est-ce pas?
Lasse V. Karlsen

153

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.


5
Je ne pense pas m'être trompé. Mon intention était de changer la valeur de la variable pointée de John à Sam. C'est un peu plus difficile à représenter avec les gens, car il semble que vous modifiez la valeur des deux pointeurs.
Tryke

24
Mais la raison pour laquelle cela est déroutant, c'est que ce n'est pas comme si John s'est levé de son siège, puis Sam s'est assis, comme on pourrait l'imaginer. C'est plus comme si Sam est venu et a collé sa main dans John et a cloné la programmation de Sam dans le corps de John, comme le tissage d'Hugo dans une matrice rechargée.
Breton

59
Plus comme Sam prend le siège de John, et John flotte dans la pièce jusqu'à ce qu'il tombe sur quelque chose de critique et provoque une faute de segmentation.
just_wes

2
Personnellement, je trouve cet exemple inutilement compliqué. Mon prof m'a dit de pointer une lumière et m'a dit "ta main est le pointeur vers l'objet lumineux".
Celeritas

Le problème avec ce type d'exemples est que le pointeur vers X et X n'est pas le même. Et cela ne se dépeint pas avec les gens.
Isaac Nequittepas

124

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.


15
J'aime vraiment ça. Il n'est pas difficile de voir que l'écriture d'un hyperlien deux fois ne fait pas apparaître deux sites Web (tout comme int *a = bne fait pas deux copies de *b).
detly

4
C'est en fait très intuitif et quelque chose que tout le monde devrait pouvoir comprendre. Bien qu'il existe de nombreux scénarios où cette analogie s'effondre. Idéal pour une introduction rapide cependant. +1
Brian Wigginton

Un lien vers une page ouverte deux fois crée généralement deux instances presque entièrement indépendantes de cette page Web. Je pense qu'un hyperlien pourrait plutôt être une bonne analogie avec un constructeur peut-être, mais pas avec un pointeur.
Utkan Gezer

@ThoAppelsin Pas nécessairement vrai, si vous accédez à une page Web html statique par exemple, vous accédez à un seul fichier sur le serveur.
dramzy

5
Vous y pensez trop. Les hyperliens pointent vers des fichiers sur le serveur, c'est l'étendue de l'analogie.
dramatique

48

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.


2
Intéressant. Mais je ne sais pas par où commencer. Des ressources à partager?
Karolis

1
Je suis d'accord. Par exemple, j'ai appris à programmer en assembleur avant C et sachant comment fonctionnent les registres, l'apprentissage des pointeurs était facile. En fait, il n'y avait pas beaucoup d'apprentissage, tout est venu très naturellement.
Milan Babuškov

Prenez un processeur de base, dites quelque chose qui fait fonctionner des tondeuses à gazon ou des lave-vaisselle et mettez-le en œuvre. Ou un sous-ensemble très très basique d'ARM ou de MIPS. Les deux ont un ISA très simple.
Daniel Goldberg

1
Il convient de souligner que cette approche éducative a été préconisée / pratiquée par Donald Knuth lui-même. L'art de la programmation informatique de Knuth décrit une architecture hypothétique simple et demande aux étudiants d'implémenter des solutions pour mettre en pratique des problèmes dans un langage d'assemblage hypothétique pour cette architecture. Une fois que cela est devenu pratiquement possible, certains étudiants lisant les livres de Knuth implémentent réellement son architecture en tant que machine virtuelle (ou utilisent une implémentation existante), et exécutent réellement leurs solutions. OMI, c'est une excellente façon d'apprendre, si vous avez le temps.
WeirdlyCheezy

4
@Luke Je ne pense pas que ce soit aussi facile à comprendre les gens qui ne peuvent tout simplement pas saisir les pointeurs (ou, pour être plus exact, l'indirection en général). Vous supposez essentiellement que les personnes qui ne comprennent pas les pointeurs en C pourraient commencer à apprendre l'assemblage, comprendre l'architecture sous-jacente de l'ordinateur et revenir à C avec une compréhension des pointeurs. Cela peut être vrai pour beaucoup, mais selon certaines études, il semble que certaines personnes par nature ne peuvent pas saisir l'indirection, même en principe (je trouve cela très difficile à croire, mais j'ai peut-être juste eu de la chance avec mes "étudiants"). ").
Luaan

27

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 langage C / C ++?

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 .

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à?

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.

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.

Pour une perceuse? Faites 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);

Production:

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 l'exemple?


+1 pour "sans comprendre comment la mémoire est physiquement établie". Je suis arrivé en C dans un contexte de langage d'assemblage et le concept de pointeurs était très naturel et facile; et j'ai vu des gens avec seulement une formation linguistique de niveau supérieur avoir du mal à le comprendre. Pour aggraver la syntaxe est source de confusion (pointeurs de fonction!), Donc apprendre le concept et la syntaxe en même temps est une recette pour des ennuis.
Brendan

4
Je sais que c'est un ancien message, mais ce serait formidable si la sortie du code fourni était ajoutée au message.
Josh

Oui, c'est similaire à l'algèbre (bien que les algèbres ont un point de compréhension supplémentaire en ayant leurs "variables" immuables). Mais environ la moitié des gens que je connais n'ont aucune compréhension de l'algèbre dans la pratique. Il ne calcule tout simplement pas pour eux. Ils connaissent toutes ces «équations» et prescriptions pour arriver au résultat, mais ils les appliquent de façon un peu aléatoire et maladroite. Et ils ne peuvent pas les étendre à leurs propres fins - c'est juste une boîte noire inchangeable et incomposable pour eux. Si vous comprenez l'algèbre et êtes capable de l'utiliser efficacement, vous avez déjà une longueur d'avance - même parmi les programmeurs.
Luaan

24

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

int puts(const char *s);

Pour le moment, ignorez le const.paramètre Le paramètre transmis à puts()est un pointeur, c'est-à-dire la valeur d'un pointeur (puisque tous les paramètres de C sont transmis par valeur), et la valeur d'un pointeur est l'adresse vers laquelle il pointe, ou, tout simplement , une adresse. Ainsi, lorsque nous écrivons 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 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.


Me laisse me demander quel est l'intérêt d'avoir un pointeur. Dans la plupart des blocs de code que je rencontre, un pointeur n'est visible que dans sa déclaration.
Wolfpack'08

@ Wolfpack'08 ... quoi ?? Quel code regardez-vous ??
Kyle Strand

@KyleStrand Laissez-moi jeter un œil.
Wolfpack'08 du

19

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/


1
J'appuie le tutoriel de Ted Jensen. Il décompose les pointeurs à un niveau de détail, ce n'est pas trop détaillé, aucun livre que j'ai lu ne le fait. Extrêmement utile! :)
Dave Gallagher

12

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:

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

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.


10

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:

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

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:

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

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 couleur enum)


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?

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

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)

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

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) )


N'utilisez simplement pas le mot "double pointeur". Les pointeurs peuvent pointer vers n'importe quoi, donc vous pouvez évidemment avoir des pointeurs pointant vers d'autres pointeurs. Ce ne sont pas des pointeurs doubles.
gnasher729

Je pense que cela passe à côté du fait que les «doigts» eux-mêmes, pour continuer votre analogie, chacun «occupe une place de parking». Je ne suis pas sûr que les gens aient du mal à comprendre les pointeurs au niveau d'abstraction élevé de votre analogie, c'est comprendre que les pointeurs sont des choses mutables qui occupent des emplacements de mémoire, et comment cela est utile, qui semble échapper aux gens.
Emmet

1
@Emmet - Je ne suis pas en désaccord avec le fait qu'il y a beaucoup plus à faire dans les pointeurs WRT, mais j'ai lu la question: "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 niveau
Mike

Y a-t-il un intérêt à étendre l'analogie entre les doigts de la voiture et une personne (avec un ou plusieurs doigts - et une anomalie génétique qui peut permettre à chacun de pointer dans n'importe quelle direction!) Était assise dans l'une des voitures pointant vers une autre voiture (ou penché sur le terrain vague à côté du terrain comme un "pointeur non initialisé", ou une main entière écartée pointant sur une rangée d'espaces comme un "tableau de pointeurs de taille fixe [5]" ou recourbée dans la paume "pointeur nul" qui pointe vers un endroit où l'on sait qu'il n'y a JAMAIS de voiture) ... 8-)
SlySven

votre explication était appréciable et bonne pour un débutant.
Yatendra Rathore

9

Je 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.


9

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 :

Pour une raison quelconque, la plupart des gens semblent nés sans la partie du cerveau qui comprend les pointeurs. C'est une chose d'aptitude, pas une chose de compétence - cela nécessite une forme complexe de pensée doublement indirecte que certaines personnes ne peuvent tout simplement pas faire.


8

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:

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

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), ou doIt(*myBuffer)?)

un nombre représentant l'adresse à une adresse à un tampon

(est-ce doIt(&mybuffer)ou doIt(mybuffer)ou doIt(*mybuffer)?)

un nombre représentant l'adresse à l'adresse à l'adresse au tampon

(peut-être que c'est doIt(&mybuffer). ou est-ce doIt(&&mybuffer)? ou même doIt(&&&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.


Je l'ai vu expliqué comme ceci: si vous voyez une déclaration de pointeur comme double *(*(*fn)(int))(char), alors le résultat de l'évaluation *(*(*fn)(42))('x')sera a double. Vous pouvez supprimer des couches d'évaluation pour comprendre quels doivent être les types intermédiaires.
Bernd Jendrissek

@BerndJendrissek Je ne suis pas sûr de suivre. Quel est le résultat de l'évaluation (*(*fn)(42))('x') alors?
Breton

vous obtenez une chose (appelons-la x) où, si vous évaluez *x, vous obtenez un double.
Bernd Jendrissek

@BerndJendrissek Est-ce censé expliquer quelque chose sur les pointeurs? Je ne comprends pas. À quoi veux-tu en venir? J'ai supprimé une couche et n'ai obtenu aucune nouvelle information sur les types intermédiaires. Qu'est-ce que cela explique sur ce qu'une fonction particulière va accepter? Qu'est-ce que cela a à voir avec quoi que ce soit?
Breton

Peut - être que le message dans cette explication (et ce n'est pas le mien, je voudrais pouvoir trouver où je l' ai vu) est de penser moins en termes de ce qui fn est en plus en termes de ce que vous pouvez faire avecfn
Bernd Jendrissek

8

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)


Après tout, un pointeur nul ne pointe pas vers une adresse nulle dans la mémoire, même si sa "valeur" C est nulle. C'est un concept entièrement distinct, et si vous le traitez mal, vous pourriez finir par aborder (et déréférencer) quelque chose auquel vous ne vous attendiez pas. Dans certains cas, il peut même s'agir d'une adresse nulle dans la mémoire (en particulier maintenant que l'espace d'adressage est généralement plat), mais dans d'autres, il peut être omis comme comportement non défini par un compilateur d'optimisation, ou accéder à une autre partie de la mémoire associée avec "zéro" pour le type de pointeur donné. L'hilarité s'ensuit.
Luaan

Pas nécessairement. Vous devez être en mesure de modéliser l'ordinateur dans votre tête pour que les pointeurs aient un sens (et pour déboguer d'autres programmes également). Tout le monde ne peut pas faire ça.
Thorbjørn Ravn Andersen

5

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).


5

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 .

   int *mypointer;

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.

  int* mypointer;

Ne me lancez même pas sur les pointeurs de fonction ...


2
En fait, vos deux fragments sont valides C. C'est une question de beaucoup d'années de style C que le premier est plus courant. Le second est un peu plus courant en C ++, par exemple.
RBerteig

1
Le deuxième fragment ne fonctionne pas vraiment bien avec des déclarations plus complexes. Et la syntaxe n'est pas aussi "incohérente" une fois que vous réalisez que la partie droite d'une déclaration de pointeur vous montre ce que vous devez faire au pointeur pour obtenir quelque chose dont le type est le spécificateur de type atomique à gauche.
Bernd Jendrissek

2
int *p;a une signification simple: *pest un entier. int *p, **ppsignifie: *pet **ppsont des entiers.
Miles Rout

@MilesRout: Mais c'est exactement le problème. *pet ne**pp sont pas des entiers, car vous n'avez jamais initialisé pou ppou *pppour 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! :)
Courses de légèreté en orbite

@LightnessRacesinOrbit L'enseignement de l'alignement à droite est loin d'être trompeur. C'est la seule façon correcte de l'enseigner. NE PAS l'enseigner est trompeur.
Miles Rout

5

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.


4

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 :)


Une complication désagréable avec l'analogie du numéro de boîte aux lettres est que, tandis que le langage inventé par Dennis Ritchie définit le comportement en termes d'adresses des octets et des valeurs stockées dans ces octets, le langage défini par la norme C invite les implémentations "d'optimisation" à utiliser un comportement modèle qui est plus compliqué mais définit divers aspects du modèle de manière ambiguë, contradictoire et incomplète.
supercat

3

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.


2

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.


2

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.


Le problème se situe principalement dans la syntaxe de déclaration C. Mais l'utilisation du pointeur serait certainement plus facile si (*p)elle l'avait été (p->), et donc nous aurions p->->xau lieu de l'ambiguïté*p->x
MSalters

@MSalters Oh mon dieu, tu plaisantes, non? Il n'y a aucune incohérence. a->bsignifie simplement (*a).b.
Miles Rout

@Miles: En effet, et par cette logique * p->xsignifie * ((*a).b)alors que *p -> xsignifie (*(*p)) -> x. Le mélange des opérateurs de préfixe et de suffixe provoque une analyse ambiguë.
MSalters

@MSalters non, car les espaces blancs ne sont pas pertinents. C'est comme dire que cela 1+2 * 3devrait être 9.
Miles Rout

2

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.


1
Ce qui est intéressant, c'est que les pointeurs C n'exposent en fait aucune caractéristique de l'architecture de mémoire sous-jacente. Les seules différences entre les références Java et les pointeurs C sont que vous pouvez avoir des types complexes impliquant des pointeurs (par exemple, int *** ou char * ( ) (void * )), il existe une arithmétique de pointeur pour les tableaux et pointeurs vers les membres de structure, la présence du vide *, et la dualité tableau / pointeur. A part ça, ils fonctionnent tout de même.
jpalecek

Bon point. C'est l'arithmétique du pointeur et la possibilité de débordements de tampon - sortir de l'abstraction en sortant de la zone de mémoire actuellement pertinente - qui le font.
Joshua Fox

@jpalecek: Il est assez facile de comprendre comment les pointeurs fonctionnent sur les implémentations qui documentent leur comportement en termes d'architecture sous-jacente. Dire 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.
supercat

2

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):

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

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, comme RAM[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.


1

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.)


1

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é."


4
Qu'est-ce que cela a à voir avec quoi que ce soit?
Neil Williams

... qu'essayez-vous de dire, à part "la liaison statique est la meilleure chose qui soit" et "je ne comprends pas comment quelque chose de différent de ce que j'ai appris fonctionne auparavant"?
Luaan

Luaan, vous ne pouviez pas savoir ce que l'on peut apprendre en démontant le JIT en 2000, n'est-ce pas? Qu'il se retrouve dans une table de saut, à partir d'une table de pointeurs, comme illustré en 2000 dans ASM, donc ne rien comprendre de différent peut prendre un autre sens: lire attentivement est une compétence essentielle, essayez à nouveau.
rama-jka toti

1

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).


0

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:

int* my_int_pointer;

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).


0

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. :-)


-1: Les poignées ne sont pas des pointeurs vers des pointeurs; ce ne sont en aucun cas des pointeurs. Ne les confondez pas.
Ian Goldby

"Ce ne sont en aucun cas des pointeurs" - euh, je vous prie de différer.
SarekOfVulcan

Un pointeur est un emplacement mémoire. Une poignée est un identifiant unique. Ce pourrait être un pointeur, mais il pourrait tout aussi bien être un index dans un tableau, ou toute autre chose d'ailleurs. Le lien que vous avez donné n'est qu'un cas spécial où la poignée est un pointeur, mais ce n'est pas obligatoire. Voir également parashift.com/c++-faq-lite/references.html#faq-8.8
Ian Goldby

Ce lien ne prend pas en charge votre affirmation selon laquelle ils ne sont en aucun cas des pointeurs - "Par exemple, les poignées pourraient être Fred **, où les pointeurs Fred * pointés ..." Je ne pense pas le -1 était juste.
SarekOfVulcan

0

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).

"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).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.