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

StackOverflow https://stackoverflow.com/questions/5727

  •  08-06-2019
  •  | 
  •  

Question

Pourquoi les pointeurs sont-ils un tel facteur de confusion pour de nombreux nouveaux et même anciens étudiants de niveau universitaire 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 des variables, des fonctions et au-delà ?

Quelles sont les bonnes pratiques qui peuvent être mises en œuvre pour amener quelqu'un au niveau « Ah-hah, j'ai compris », sans l'enliser dans le concept global ?Fondamentalement, explorez des scénarios similaires.

Était-ce utile?

La solution

Les pointeurs sont un concept qui, pour beaucoup, peut prêter à confusion au début, en particulier lorsqu'il s'agit de copier des valeurs de pointeur tout en faisant référence au même bloc de mémoire.

J'ai trouvé que la meilleure analogie est de considérer le pointeur comme un morceau de papier sur lequel figure l'adresse d'une maison et le bloc de mémoire auquel il fait référence comme étant la maison réelle.Toutes sortes d’opérations peuvent ainsi s’expliquer facilement.

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 de la même manière des problèmes tels que les fuites de mémoire.

Si vous souhaitez uniquement apprendre le concept de haut niveau des pointeurs, vous devez alors ignorer les parties intitulées « Disposition de la mémoire » dans l'explication ci-dessous.Ils sont destinés à donner des exemples de ce à quoi 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 comment fonctionnent réellement les dépassements de tampon, il était important que j'ajoute ces diagrammes.

Clause de non-responsabilité:À toutes fins utiles, cette explication et l'exemple de dispositions de mémoire sont largement simplifiées.Il y a plus de frais généraux et beaucoup plus de détails vous auriez besoin de savoir si vous avez besoin de gérer la mémoire à bas niveau.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 une certaine surcharge associée à l'allocation de la maison, je vais illustrer cela ci-dessous comme ceci :

---[ttttNNNNNNNNNN]---
     ^   ^
     |   |
     |   +- the FName array
     |
     +- overhead

La zone "tttt" est une surcharge, il y en aura généralement plus pour différents types d'exécution et de langages, 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 principales du système, sinon vous risquez de faire 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 savoir où allouer, mais trouvera un endroit approprié avec suffisamment d'espace et communiquera l'adresse à la mémoire allouée.

Autrement dit, c’est l’entrepreneur qui choisira le lieu.

THouse.Create('My house');

Disposition de la mémoire :

---[ttttNNNNNNNNNN]---
    1234My house

Conserver 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 vous ne pouvez pas trouver la maison, à moins d'y être déjà.

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

Disposition de la mémoire :

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

Copier la valeur du pointeur

Écrivez simplement l'adresse sur une nouvelle feuille de papier.Vous disposez désormais de deux morceaux de papier qui vous mèneront à la même maison, et non à deux maisons distinctes.Toute tentative de suivre l'adresse d'un journal et de réorganiser les meubles dans cette maison donnera l'impression que l'autre maison a été modifié de la même manière, à moins que vous ne puissiez détecter explicitement qu'il ne s'agit en réalité que d'une seule maison.

Note C'est généralement le concept que j'ai le plus de mal à expliquer aux gens, deux pointeurs ne signifient pas deux objets ou blocs mémoire.

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

Libérer la mémoire

Démolissez la maison.Vous pourrez ensuite 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 je récupère son adresse.Ensuite, je fais quelque chose à la maison (l'utiliser, le...code, laissé en exercice au lecteur), puis je le libère.Enfin, j'efface l'adresse de ma variable.

Disposition de la mémoire :

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after free
----------------------          | (note, memory might still
    xx34My house             <--+  contain some data)

Pointeurs pendants

Vous dites à votre entrepreneur de détruire la maison, mais vous oubliez d'effacer l'adresse de votre 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 ratés (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

En utilisant h après l'appel à .Free pourrait travail, mais ce n'est que de la pure chance.Il est fort probable qu'il échouera chez le client, au milieu d'une opération critique.

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h                        <--+
    v                           +- after free
----------------------          |
    xx34My house             <--+

Comme vous pouvez le voir, H pointe toujours les restes des données en mémoire, mais comme il peut ne pas être complet, l'utiliser comme auparavant pourrait échouer.


Fuite de mémoire

Vous perdez le morceau de papier et ne trouvez pas 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 écrasé le contenu du h variable avec l'adresse d'une nouvelle maison, mais l'ancienne est toujours debout...quelque part.Après ce code, il n’y a aucun moyen d’atteindre cette maison et elle restera debout.En d’autres termes, la mémoire allouée restera allouée jusqu’à la fermeture de l’application, après quoi le système d’exploitation la supprimera.

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

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

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

                       h
                       v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

Une manière 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 aucune place dans nos variables où l'adresse de la maison existe, mais la maison est toujours là.

Disposition de la mémoire :

    h                        <--+
    v                           +- before losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

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 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 un des morceaux de papier mais vous avez aussi un autre morceau de papier avec l'ancienne adresse dessus, quand vous allez à l'adresse, vous ne trouverez pas de maison, mais vous pourriez trouver quelque chose qui ressemble aux ruines d'un.

Peut-être trouverez-vous même une maison, mais ce n’est pas la maison dont 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 constater qu'une adresse voisine a une maison assez grande qui occupe trois adresses (Main Street 1-3), et votre adresse va au milieu de la maison.Toute tentative visant à traiter cette partie de la grande maison à 3 adresses comme une seule petite maison 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, d'après la référence dans h1, et tandis que h1 a également été effacé, h2 a toujours l’ancienne adresse obsolète.L’accès à la maison qui n’est plus debout peut fonctionner ou non.

Il s’agit d’une variante du pointeur suspendu ci-dessus.Voir sa disposition en mémoire.


Dépassement de tampon

Vous déplacez plus de choses dans la maison que vous ne pouvez en contenir, ce qui se répand dans la maison ou la cour du voisin.Lorsque le propriétaire de la maison voisine reviendra plus tard chez lui, il trouvera toutes sortes de choses qu'il considérera comme 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 répartis les uns à côté des autres.

Ainsi, ce code :

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

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

                        h1
                        v
-----------------------[ttttNNNNNNNNNN]
                        5678My house

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

    h2                  h1
    v                   v
---[ttttNNNNNNNNNN]----[ttttNNNNNNNNNN]
    1234My other house somewhereouse
                        ^---+--^
                            |
                            +- overwritten

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, il pourrait ne pas être un problème que certaines parties du nom de la maison H1 ont été modifiées, en termes de crash du programme, mais d'écraser la surcharge de l'objet se bloquera très probablement lorsque vous essayez d'utiliser l'objet cassé, comme cela Écraser des liens stockés à d'autres objets de l'objet.


Listes liées

Lorsque vous suivez une adresse sur un morceau de papier, vous arrivez à une maison, et dans cette maison il y a un autre morceau de papier avec une nouvelle adresse dessus, pour la maison suivante dans la chaîne, et ainsi de suite.

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 cabane.Nous pouvons suivre la chaîne jusqu'à ce qu'une maison n'ait plus NextHouse ré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 de Nexthouse comme lien dans l'objet, noté avec les quatre llll dans le diagramme ci-dessous):

    h1                      h2
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home       +        5678Cabin      +
                   |        ^              |
                   +--------+              * (no link)

En termes simples, qu’est-ce qu’une adresse mémoire ?

En termes simples, une adresse mémoire n’est qu’un nombre.Si vous considérez la mémoire comme un grand tableau d'octets, le tout premier octet a l'adresse 0, la suivante l'adresse 1 et ainsi de suite.C'est simplifié, mais suffisant.

Donc cette disposition de la mémoire :

    h1                 h2
    v                  v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

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

  • h1 = 4
  • h2 = 23

Ce qui signifie que notre liste chaînée ci-dessus pourrait ressembler à ceci :

    h1 (=4)                 h2 (=28)
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home      0028      5678Cabin     0000
                   |        ^              |
                   +--------+              * (no link)

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


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

Un pointeur est simplement 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 temps d'exécution essaient de cacher le fait qu'il y a un nombre en dessous, simplement parce que le nombre lui-même ne vous a pas vraiment de sens.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 que cela fonctionne.

Autres conseils

Lors de mon premier cours de Comp Sci, nous avons fait l'exercice suivant.Certes, il s'agissait d'une salle de conférence avec environ 200 étudiants...

Le professeur écrit au tableau : int john;

Jean se lève

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

Sally se lève, montre John du doigt.

Professeur: int *bill = sally;

Bill se lève et montre John du doigt.

Professeur: int sam;

Sam se lève

Professeur: bill = &sam;

Bill montre maintenant Sam.

Je pense que vous voyez l'idée.Je pense que nous avons passé environ une heure à faire cela, jusqu'à ce que nous ayons passé en revue les bases de l'affectation des pointeurs.

Une analogie que j’ai trouvée utile pour expliquer les pointeurs est celle des 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 ce lien hypertexte, ils pointeront tous les deux vers la même page Web d'origine.Si vous modifiez cette page originale, puis suivez l'un de ces liens (pointeurs), vous obtiendrez cette nouvelle page mise à jour.

La raison pour laquelle les pointeurs semblent dérouter tant de gens est qu’ils viennent pour la plupart avec peu ou pas d’expérience 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 machine virtuelle simple basée sur le bytecode (dans n'importe quel langage qu'ils ont choisi, 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.

Pourquoi les pointeurs sont-ils un tel facteur de confusion pour de nombreux étudiants, nouveaux et même anciens, de niveau universitaire en langage C/C++ ?

Le concept d'espace réservé pour une valeur - les variables - correspond à quelque chose que nous apprenons à l'école - l'algèbre.Il n'existe aucun parallèle que vous puissiez établir sans comprendre comment la mémoire est physiquement disposée dans un ordinateur, et personne ne pense à ce genre de chose tant qu'il n'a pas affaire à 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 des variables, des fonctions et au-delà ?

Boîtes d'adresses.Je me souviens que lorsque j'apprenais à programmer BASIC dans des micro-ordinateurs, il y avait ces jolis livres contenant des jeux, et parfois il fallait 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 cases, et il y en avait beaucoup - certains ordinateurs en avaient jusqu'à 65535 !Ils étaient côte à côte et avaient tous une adresse.

Quelles sont les bonnes pratiques qui peuvent être mises en œuvre pour amener quelqu'un au niveau « Ah-hah, j'ai compris », sans l'enliser dans le concept global ?Fondamentalement, explorez des scénarios similaires.

Pour une perceuse ?Créez 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);

Sortir:

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

Peut-être que cela explique certaines des bases à travers un exemple ?

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 que brouiller les pistes.Lorsque vous utilisez un paramètre de pointeur, vous êtes toujours passer par valeur ;mais la valeur se trouve être une adresse plutôt que, disons, un int.

Quelqu'un d'autre a déjà créé un lien vers ce tutoriel, 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. Le paramètre passé à puts() est un pointeur, c'est la valeur d'un pointeur (puisque tous les paramètres en C sont transmis par valeur), et la valeur d'un pointeur est l'adresse vers laquelle il pointe, ou, simplement, une adresse. Ainsi quand on écrit puts(strA); comme nous l'avons vu, nous transmettons 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.

Même si vous êtes un développeur VB .NET ou C# (comme moi) et que vous n'utilisez jamais de code dangereux, cela vaut toujours la peine de comprendre comment fonctionnent les pointeurs, sinon vous ne comprendrez pas comment fonctionnent les références d'objet.Vous aurez alors l’idée courante mais erronée selon laquelle passer une référence d’objet à une méthode copie l’objet.

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 les pointeurs de fonction. http://home.netcom.com/~tjensen/ptr/cpoint.htm

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

La complexité des indicateurs va au-delà de ce que nous pouvons facilement enseigner.Demander aux élèves de se montrer du doigt et utiliser des morceaux de papier avec des adresses personnelles sont deux excellents outils d'apprentissage.Ils font un excellent travail en introduisant les concepts de base.En effet, apprendre les concepts de base est vital pour utiliser avec succès les pointeurs.Cependant, dans le code de production, il est courant de se retrouver dans des scénarios beaucoup plus complexes que ce que ces simples démonstrations peuvent résumer.

J'ai été impliqué dans des systèmes dans lesquels 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 indicateurs 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 très vite déroutant (imaginez beaucoup plus de lignes et potentiellement plus de niveaux).Ajoutez des tableaux de pointeurs et des pointeurs de nœud à nœud (arbres, listes chaîné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.

Des structures complexes de pointeurs n’indiquent pas nécessairement non plus un mauvais codage (bien qu’elles puissent le faire).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 dont les structures ne correspondent pas en termes de style ou de technique.Dans de telles situations, la complexité va naturellement surgir (même si nous devrions 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 nécessitant l'utilisation de pointeurs.Un projet difficile fera plus pour la compréhension des pointeurs qu'un millier de démonstrations.Les démonstrations peuvent vous donner une compréhension superficielle, mais pour comprendre en profondeur les indicateurs, vous devez vraiment les utiliser.

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 ;d'abord, commençons :


Préparer 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 mémoire, ils sont séquentiels et contigus.un peu comme un tableau.À l'heure actuelle, il n'y a aucune voiture à l'intérieur, donc c'est comme un tableau vide (parking_lot[3] = {0}).


Ajouter les données

Un parking ne reste jamais vide longtemps...si c’était le cas, cela ne servirait à rien et personne n’en construirait.Disons qu'au fur et à mesure que la journée avance, le parking 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 toutes du même type (voiture), donc une façon d'y penser est que nos voitures sont une sorte de données (disons un int) mais ils ont des valeurs différentes (blue, red, green;ça 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 au point 1.C'est comme prendre un pointeur et lui attribuer une adresse mémoire (int *finger = parking_lot)

Votre doigt (le pointeur) n'est pas la réponse à ma question.Regarder à ton doigt ne me dit rien, mais si je regarde où est ton doigt pointant vers (déréférençant 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 qu'avant) m'affiche de nouvelles données (l'emplacement de stationnement où se trouve la voiture rouge) du même type (la voiture).

Le pointeur n'a pas changé physiquement, il est toujours ton doigt, seules les données qu'il me montrait ont changé.(l'adresse "place de parking")


Pointeurs doubles (ou un pointeur vers un pointeur)

Cela fonctionne également avec plusieurs pointeurs.Je peux demander où se trouve le pointeur qui pointe vers la voiture rouge et vous pouvez utiliser votre autre main et pointer du doigt le premier doigt.(c'est comme int **finger_two = &finger)

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


Le pointeur qui pend

Supposons maintenant que vous vous sentiez comme une statue et que vous souhaitiez garder indéfiniment votre main pointée vers la voiture rouge.Et si cette voiture rouge s'en allait ?

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

Votre pointeur pointe toujours vers l'endroit où se trouve la voiture rouge était mais ne l'est plus.Disons qu'une nouvelle voiture arrive là-bas...une voiture Orange.Maintenant, si je vous demande encore « 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 (désormais 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 le suivant place de stationnement.Vous pouvez voir que vous pointez vers le point 2, donc vous ajoutez simplement 1 et vous pointez vers 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érencer le pointeur (*(finger+1)) pour voir qu'il y a une voiture verte présente là-bas (les données à cet endroit)

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

La difficulté, du moins celle que j'ai rencontrée dans le passé et que d'autres ont rencontrée, est que la gestion des pointeurs en C/C++ peut être inutilement alambiquée.

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

Joel Spolsky fait valoir quelques bons arguments sur la compréhension des indicateurs dans son Guide de guérilla pour les entretiens article:

Pour une raison quelconque, la plupart des gens semblent naître sans la partie du cerveau qui comprend les indicateurs.C’est une question d’aptitude, pas de compétence – cela nécessite une forme complexe de pensée doublement indirecte que certaines personnes ne peuvent tout simplement pas faire.

Je pense que le principal obstacle à la compréhension des indicateurs réside dans les mauvais enseignants.

Presque tout le monde apprend des mensonges sur les pointeurs :Qu'ils sont rien de plus que des adresses mémoire, ou qu'ils vous permettent de pointer vers 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 réalité des concepts assez simples, tant que vous vous en tenez à ce que le langage C++ a à dire à leur sujet et ne leur imprégnez pas d'attributs qui "généralement" s'avèrent efficaces dans la pratique, mais qui ne sont néanmoins pas garantis par le langage et ne font donc pas partie du concept réel de pointeur.

J'ai essayé d'écrire une explication à ce sujet il y a quelques mois dans ce billet de blog -- j'espère que ça aidera quelqu'un.

(Remarque, avant que quiconque ne devienne pédant sur moi, oui, la norme C++ dit que les pointeurs représenter adresses mémoire.Mais il ne dit pas que "les pointeurs sont des adresses mémoire, et rien d'autre que des adresses mémoire et peuvent être utilisés ou considérés de manière interchangeable avec les adresses mémoire".La distinction est importante)

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 des pointeurs qui est difficile, et non le jargon, ou le désordre alambiqué que C et C++ créent avec le concept.De gros efforts sont consacrés à expliquer le concept (comme dans la réponse acceptée à cette question) et c'est pratiquement inutile 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.Lorsqu'il s'agit de programmer et d'utiliser des pointeurs en C, je fais beaucoup d'erreurs et je suis vraiment confus.Je n'ai toujours pas réglé ce problème.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 à 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

(c'est peut-être 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 qu'est "mybuffer" au départ et de ce que doIt a l'intention d'en faire.Le langage ne prend pas en charge les niveaux d’imbrication rencontrés dans la pratique.Comme lorsque je dois remettre un "pointeur" à une fonction qui crée un nouveau tampon, et qu'elle modifie le pointeur pour pointer vers le nouvel emplacement du tampon.Veut-il vraiment le pointeur, ou un pointeur vers le pointeur, pour savoir où aller pour modifier le contenu du pointeur.La plupart du temps, je dois juste deviner ce que l'on entend par « pointeur » et la plupart du temps, je me trompe, quelle que soit l'expérience que j'ai en matière de devinettes.

"Pointeur" 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 pointeur, ou veut-elle l'adresse de la variable pointeur ?Je suis confus.

Je pense que ce qui rend les pointeurs difficiles à apprendre, c'est que jusqu'aux pointeurs, vous êtes à 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 ne comprenez pas vraiment ce qu'il y a à cet emplacement mémoire."Comment ça, il détient un adresse?"

Je ne suis pas d'accord avec l'idée selon laquelle "soit vous les obtenez, soit vous ne les obtenez pas".

Ils deviennent plus faciles à comprendre lorsque vous commencez à leur trouver de véritables utilisations (comme ne pas transmettre de grandes structures en fonctions).

La raison pour laquelle c'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 de la création d'une variable définit le type de la variable.La déclaration de pointeur ne fonctionne pas ainsi en C et C++.Au lieu de cela, ils disent que la variable pointe sur le type à gauche.Dans ce cas: *mon pointeur pointe sur un int.

Je n'ai pas bien compris les pointeurs jusqu'à ce que j'essaye de les utiliser en C# (avec unsafe), 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 mon pointeur est un pointeur vers un int.

  int* mypointer;

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

Je pouvais travailler avec des pointeurs alors que je ne connaissais que le C++.Je savais en quelque sorte quoi faire dans certains cas et quoi ne pas faire par essais/erreurs.Mais ce qui m'a donné une compréhension complète est le langage assembleur.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 capable de comprendre beaucoup de choses.

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 chaînée :1) Commencez par votre papier avec l'adresse 2) Accédez à l'adresse sur le papier 3) Ouvrez la boîte aux lettres pour trouver un nouveau papier avec l'adresse suivante

Dans une liste chaînée linéaire, la dernière boîte aux lettres ne contient rien (fin de la liste).Dans une liste chaîné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ù le déréférencement se produit et où vous allez planter ou vous tromper lorsque l'adresse n'est pas valide.En supposant que vous puissiez vous diriger vers la boîte aux lettres d'une adresse invalide, imaginez qu'il y a un trou noir ou quelque chose là-dedans qui bouleverse le monde :)

Je pense que la principale raison pour laquelle les gens ont du mal avec cela est que ce n'est généralement pas enseigné d'une manière intéressante et engageante.J'aimerais voir un conférencier recruter 10 volontaires parmi la foule et leur donner une règle de 1 mètre chacun, les amener à se tenir debout dans une certaine configuration et à utiliser les règles pour se pointer les uns vers les autres.Ensuite, montrez l'arithmétique du pointeur en déplaçant les gens (et où ils pointent leurs règles).Ce serait une façon simple mais efficace (et surtout mémorable) de montrer les concepts sans trop s'embourber dans la mécanique.

Une fois arrivés au C et au C++, cela semble devenir plus difficile pour certaines personnes.Je ne sais pas si c'est parce qu'ils mettent enfin en pratique une théorie qu'ils ne maîtrisent pas correctement ou parce que la manipulation du pointeur est intrinsèquement plus difficile dans ces langages.Je ne me souviens pas très bien de ma propre transition, mais je savait pointeurs en Pascal puis déplacés vers C et se sont totalement perdus.

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 avec combien de niveaux d’indirection êtes-vous à l’aise.Il n’en faut pas beaucoup pour mettre les gens à l’écart.Le fait qu'ils puissent être modifiés accidentellement par des bugs dans votre programme peut également les rendre très difficiles à déboguer lorsque des choses tournent mal dans votre code.

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, ce qui m'a réellement aidé à comprendre les pointeurs a été de rencontrer le concept d'itérateur en C++. Bibliothèque de modèles standards.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 tant que vous n'avez pas appris à ignorer les arbres.

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 dans le sens où ils exposent les caractéristiques de l'architecture 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 façon dont j'aimais l'expliquer était 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 que 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 vers une variable n'est en réalité que l'index (du 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 chiffre 4.Un pointeur vers un pointeur est l'endroit où vous prenez ce numéro et l'utilisez comme index lui-même, comme RAM[RAM[index]].

Je dessinerais un tableau sur une liste de papier et je l'utiliserais simplement pour afficher des choses comme de nombreux pointeurs pointant vers la même mémoire, l'arithmétique du pointeur, un pointeur vers un pointeur, etc.

Numéro de boîte postale.

C'est une information qui permet d'accéder à autre chose.

(Et si vous faites du calcul sur les numéros de boîte postale, vous pourriez avoir un problème, car la lettre va dans la mauvaise boîte.Et si quelqu’un déménage dans un autre État – sans adresse de réexpédition – alors vous avez un pointeur en suspens.D'un autre côté, si le bureau de poste transmet le courrier, vous disposez alors d'un pointeur vers un pointeur.)

Ce n'est pas une mauvaise façon de l'appréhender, via les itérateurs.mais continuez à chercher, vous verrez Alexandrescu commencer à se plaindre d'eux.

De nombreux anciens développeurs C++ (qui n'ont jamais compris que les itérateurs sont un pointeur moderne avant d'abandonner le langage) passent au C# et croient toujours avoir 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'accomplir :utilisation nouvelle et 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 on le fait jusqu'au bout..Les itérateurs sont utiles car ils facilitent l'abstraction des algorithmes, un autre exemple.Et au moment de la compilation, c'est le lieu idéal pour un algorithme, très simple.Vous connaissez le code + les données, ou dans cet autre langage C# :

IEnumerable + LINQ + Massive Framework = pénalité d'exécution de 300 Mo pour une indirection d'applications moche, en faisant glisser des applications via des tas d'instances de types de référence.

"Le Pointer n'est pas cher."

Certaines réponses ci-dessus ont affirmé que "les pointeurs ne sont pas vraiment difficiles", mais n'ont pas continué à aborder directement où "le pointeur est dur!" vient de.Il y a quelques années, j'ai donné des cours particuliers à des étudiants de première année d'informatique (pendant un an seulement, car j'étais clairement nul en la matière) et il était clair pour moi que le idée du pointeur n'est pas difficile.Ce qui est difficile c'est de comprendre pourquoi et quand voudriez-vous un pointeur.

Je ne pense pas que l'on puisse dissocier cette question - pourquoi et quand utiliser un pointeur - de l'explication de problèmes plus larges de génie logiciel.Pourquoi chaque variable devrait pas être une variable globale, et pourquoi on devrait prendre en compte un code similaire dans des fonctions (qui, comprenez ceci, utilisent pointeurs pour spécialiser leur comportement sur leur site d'appel).

Je ne vois pas ce qu'il y a de si déroutant dans les pointeurs.Ils pointent vers un emplacement en mémoire, c'est-à-dire qu'ils stockent 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 qui contient un int.

Le problème avec les pointeurs est qu’ils pointent vers un emplacement en mémoire, il est donc facile de se diriger vers un emplacement où vous ne devriez pas vous trouver.Pour preuve, regardez les nombreuses failles de sécurité dans les applications C/C++ dues au débordement de tampon (incrémentation du pointeur au-delà de la limite allouée).

Juste pour confondre un peu plus les choses, vous devez 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 puisse déplacer des éléments en mémoire pour défragmenter le tas.Si le pointeur change au milieu d'une 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 de manière un peu plus cohérente que moi.:-)

Chaque débutant en 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 rassemblent verbalement, d'autres visuellement et la meilleure façon de l'expliquer est d'utiliser exemple de "train" (convient pour l'exemple verbal et visuel).

"locomotive" est un pointeur qui ne peut pas tenir n'importe quoi et "wagon" c'est ce que la "locomotive" essaie de tirer (ou de pointer).Ensuite, vous pouvez classer le « wagon » lui-même, peut-il contenir des animaux, des plantes ou des personnes (ou un mélange de ceux-ci).

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top